最近我一直在努力解决以下问题:
给定一个整数数组,找到一个总和至少为k的最小(最短长度)子数组。
显然,这可以在O(n ^ 2)中轻松完成。我能够编写一个算法,在自然数的线性时间内解决它,但我无法弄清楚它是否为整数。
我最近的尝试是:
def find_minimal_length_subarr_z(arr, min_sum):
found = False
start = end = cur_end = cur_sum = 0
for cur_start in range(len(arr)):
if cur_end <= cur_start:
cur_end, cur_sum = cur_start, arr[cur_start]
else:
cur_sum -= arr[cur_start-1]
# Expand
while cur_sum < min_sum and cur_end < len(arr)-1:
cur_end += 1
cur_sum += arr[cur_end]
# Contract
while cur_end > cur_start:
new_sum = cur_sum - arr[cur_end]
if new_sum >= min_sum or new_sum >= cur_sum:
cur_end -= 1
cur_sum = new_sum
else:
break
if cur_sum >= min_sum and (not found or cur_end-cur_start < end-start):
start, end, found = cur_start, cur_end, True
if found:
return start, end
例如:
[8, -7, 5, 5, 4], 12 => (2, 4)
然而,它失败了:
[-12, 2, 2, -12, 2, 0], 4
正确的结果是(1, 2)
,但算法找不到它。
这完全可以在线性时间内完成(最好是空间复杂度恒定)吗?
答案 0 :(得分:7)
这是一个线性时间,也是线性空间。额外的空间来自可以增长到线性尺寸的双端队列。 (还有第二个数组可以保持累积总和,但可以很容易地删除。)
from collections import deque
def find_minimal_length_subarr(arr, k):
# assume k is positive
sumBefore = [0]
for x in arr: sumBefore.append(sumBefore[-1] + x)
bestStart = -1
bestEnd = len(arr)
startPoints = deque()
start = 0
for end in range(len(arr)):
totalToEnd = sumBefore[end+1]
while startPoints and totalToEnd - sumBefore[startPoints[0]] >= k: # adjust start
start = startPoints.popleft()
if totalToEnd - sumBefore[start] >= k and end-start < bestEnd-bestStart:
bestStart,bestEnd = start,end
while startPoints and totalToEnd <= sumBefore[startPoints[-1]]: # remove bad candidates
startPoints.pop()
startPoints.append(end+1) # end+1 is a new candidate
return (bestStart,bestEnd)
双端队列从左到右保存一系列候选起始位置。关键不变量是双端队列中的位置也按“sumBefore”的增加值排序。
要了解原因,请考虑x和y两个位置,x> y,并假设sumBefore [x]&lt; = sumBefore [y]。那么x是比y更严格的起始位置(对于以x或更晚结尾的段),所以我们不需要再考虑y。
进一步解释:
想象一个看起来像这样的天真算法:
for end in 0..N-1
for start in 0..end
check the segment from start to end
我试图改进内循环,只考虑某些起点而不是所有可能的起点。那么我们何时才能从进一步的考虑中消除一个特定的起点?在两种情况下。考虑两个起点S0和S1,其中S0位于S1的左侧。
首先,如果我们发现S1开始符合条件的段(即,一个段总和至少为k),我们就可以消除S0。这就是第一个while循环所做的,其中start是S0,startPoints [0]是S1。即使我们从S0开始发现一些未来符合条件的段,它也会比我们从S1开始找到的段长。
其次,如果从S0到S1-1的元素之和是&lt; = 0(或者等效地,如果S0之前的元素之和> = S1之前的元素之和),则可以消除S0。这是第二个while循环所做的,其中S0是startPoints [-1]而S1是end + 1。修剪从S0到S1-1的元素总是有意义的(对于S1或更高的终点),因为它会缩短段而不减少其总和。
实际上,还有第三种情况我们可以消除S0:当从S0到end的距离大于到目前为止发现的最短段的长度。我没有实施这个案例,因为不需要它。
答案 1 :(得分:1)
这里有一个伪代码,可以提供您正在寻找的解决方案。
curIndex = 0
while (curIndex <= endIndex)
{
if(curSum == 0)
{
startIndex = curIndex
}
curSum = curSum + curVal
curTot = curTot + 1
if(curSum >= targetVal AND curTot < minTotSofar)
{
maxSumSofar = curSum
maxStartIndex = startIndex
maxEndIndex = curIndex
minTotSofar = curTot
if(curTot == 1)
{
exit_loop
}
curSum = 0
curTot = 0
curIndex = startIndex
}
else if(curIndex == endIndex)
{
if(maxSumSofar == 0 AND curSum >= targetValue)
{
maxSumSofar = curSum
maxStartIndex = startIndex
maxEndIndex = curIndex
minTotSofar = curTot
}
else if(curSum < targetValue AND startIndex < endIndex)
{
curSum = 0
curTot = 0
curIndex = startIndex
}
}
curIndex = curIndex + 1
}
------------ JWPAT7建议后更新
INPUTS:整数数组,索引从0到endIndex
。目标值(k)与(targetVal
)进行比较。
输出:最终添加所选子集(maxSumSoFar
),子集的起始索引(maxStartIndex
),子集的结束索引(maxEndIndex
),元素总数子集(minTotSofar
)。