【问题标题】:Maximum Contiguous Subsequence Sum of At Least Length L至少长度为 L 的最大连续子序列和
【发布时间】:2011-12-13 06:24:42
【问题描述】:

所以对于以下数组,其中 L = 3

-5 -1 2 -3 0 -3 3

长度至少为 3 的最佳可能总和为 0,其中子序列是最后三个元素 (0, -3, 3)

如何以比 O(NL)(如果 L==0 时有效地为 O(N^2))时间更快地计算任何数组的总和?

【问题讨论】:

  • @Felix:这是作业吗?如果是,请添加作业标签。
  • @MK.,我认为情况正好相反。 L=N 是最好的情况,可以在 O(N) 中轻松计算,因为您只有一个选择。但是在 L=0 的情况下,任何子序列都是可能的,直接的解决方案是 O(N^2)。
  • @svick:L=N 的情况实际上甚至是 O(1) :) 对于 L=0,有一个简单的 O(N) 解决方案。
  • @jdv-Jan de Vaan 不,这不是家庭作业。
  • @svick 啊,对不起,我的速度很慢 :(

标签: algorithm dynamic-programming arrays


【解决方案1】:

我相信无论选择使用修改版的Kadane's algorithm,您都可以在 O(n) 时间内完成此操作。

要了解它是如何工作的,让我们考虑 L = 0 的情况。在这种情况下,我们想要找到原始序列的最大和子数组。这可以通过 Kadane 的算法来解决,这是一个聪明的dynamic programming 解决方案,其工作原理如下。这个想法是跟踪在数组中每个位置之前和之后结束的最大权重子数组的权重。这些数组中总和最大的就是总和最大的子数组。设原数组为A,以k位置结束的最大和数组为数组M。那么Kadane的算法是这样的:

  • 设置 M(0) = 0。任何在第一个数组条目之前结束的子数组都不能包含任何内容,因此总和为零。
  • 对于每个数组索引 k,按顺序设置 M(k + 1) = max(0, M(k) + A(k))。这里的想法是,在该位置之前结束的最佳子数组要么通过将前一个位置的最佳数组扩展单个元素来形成,要么完全丢弃该数组并仅选择该位置之前的空子数组。

一旦你填写了这个表 M,你就可以扫描它来找到整体的最大值,这给你最大权重子数组的权重。

但是我们如何适应 L ≠ 0 的情况呢?幸运的是,这还不算太糟糕。看看 Kadane 算法的递归。这个想法是,在每一点我们都可以将数组扩展一步,或者我们可以重置回空数组。但是,如果我们对子数组的大小有一个下限,我们可以不同地思考这个问题:长度至少为 L 的最大权重子数组恰好在位置 k + 1 之前结束,或者通过扩展至少长度的最佳数组来形成L 在位置 k 之前结束一个元素,或者通过丢弃该数组并获取在位置 k 之前结束的 L 元素子数组。这为我们提供了一个新版本的 Kadane 算法,如下所示:

  • 设置 M(L) 等于数组的前 L 个元素的总和。
  • 对于每个数组索引 k ≥ L,依次将 M(k + 1) 设置为 M(k) + A(k) 的最大值(我们通过扩展数组得到的值)和 L 的和位置 k + 1 之前的元素(我们只取最后 k 个元素得到的值)。

如果我们运行这个,我们将把表 M 的值从 L 填充到数组的长度。该范围内的最大值是长度至少为 L 的子数组的最大总和子数组值。

但这不会在线性时间内运行!特别是,它在 O(nL) 中运行,因为计算的每次迭代都必须查看数组的前 L 个元素。但是,通过做一些额外的预计算,我们可以将其降低到 O(n)。我们的想法是,我们可以在 O(n) 时间内建立一个包含每个数组索引之前的 L 元素总和的表,如下所示。首先,对数组的前 L 个元素求和,并将其存储为 S(L)。这是位置 L 之前的 L 个元素的总和。现在,如果我们想得到索引 L + 1 之前的 L 个元素的总和,wr 可以通过将数组的前 L 个元素相加来做 s,添加下一个数组元素,然后减去第一个数组元素。这可以通过计算 S(L + 1) = S(L) + A(L) - A(0) 在 O(1) 时间内完成。然后我们可以使用类似的技巧来计算 S(L + 2) = S(L + 1) + A(L + 1) - A(1)。更一般地,我们可以使用递归在 O(n) 时间内填写这张部分和表

  • S(L) = A(0) + A(1) + ... + A(L - 1)。
  • S(L + k + 1) = S(L + k) + A(L + k) - A(k)。

这在 O(n) 时间内运行。如果我们预先计算了这个表,我们可以通过使用上面的递归找到长度至少为 L 的最大权重子数组:

  • M(L) = S(L)
  • M(L + k + 1) = max(M(L + k) + A(L + k), S(L + k))

然后我们可以扫描 M 数组以找到最大值。整个过程在 O(n) 时间内运行:我们需要 O(n) 时间来计算 S 数组,O(n) 时间来计算 M 数组,以及 O(L) = O(n) 时间来找到最大值.它也需要 O(L) 空间,因为我们需要存储 M 和 S 数组。

但是我们可以通过将内存使用减少到 O(1) 来做得更好!诀窍是要注意,在每一点我们都不需要整个 M 和 S 数组。只是最后一个学期。因此,我们可以只存储 M 和 S 的最后一个值,这仅占用 O(1) 内存。在每一点,我们还将跟踪我们在 M 数组中看到的最大值,因此我们不需要在填充后保存 M 数组。然后给出以下 O(n) -时间,O(1)-空间算法解决问题:

  • 将 S 设置为前 L 个数组元素的总和。
  • 设置 M = S。
  • 设置最佳 = M
  • 对于 k = L + 1 到 n,数组的长度:
    • 设置 S = S + A(k) - A(k - L)
    • 设置 M = max(M + A(k), S)
    • 设置最佳 = max(Best, M)
  • 输出最佳

例如,下面是 L = 3 的原始数组上的算法跟踪:

        -5    -1    2      -3    0    -3    3
S                      -4     -2   -1    -6    0  
M                      -4     -2   -1    -4    0
Best                   -4     -2   -1    -1    0

所以输出为0。

或者,在 L = 2 的不同数组上:

        0   5    -3    -1    2   -4   -1    7   8
S              5     2    -4   1    -2   -5   6   15
M              5     2     1   3    -1   -2   6   15
Best           5     5     5   5     5    5   6   15

所以输出是 15。

希望这会有所帮助!这是一个非常酷的问题!

编辑:如果您有兴趣查看解决方案的一些实际代码,我有一个此算法的C++ implementation可用。

【讨论】:

  • 上述解决方案精确计算 l 长度最大子数组。在 1. 最多 L 2. 至少 L 的情况下我们能做什么
【解决方案2】:

这可以在 O(n) 中使用动态规划来实现。

1.) 将数组中每个索引 i 的部分总和存储到 i

2.) 将最小总和的索引存储到 i

3.) 为数组中的每个索引 i 存储直到 i 的最大值,这是直到 i 的部分总和减去在步骤 2 中确定的索引的部分总和,即 Min(Sum(k)) k

所有这些都可以在一个循环中在 O(n) 内完成。

现在您已经有了数组中每个索引 i 的最大总和,您可以确定连续子序列的最大总和以及该子序列的结束索引。当您拥有结束索引时,您可以向后走,直到达到最大总和。这两个操作也是 O(n)。

C# 中的示例实现:

int [] values = {-5, -1, 2, -3, 0, -3, 3};
int L = 3;

int[] sumUpTo = new int [values.Length];
int[] minUpTo = new int[values.Length];
int[] maxUpTo = new int[values.Length];

for (int i = 0; i < values.Length; i++)
{
    sumUpTo[i] = values[i];
    minUpTo[i] = i;
    if (i > 0)
    {
        sumUpTo[i] += sumUpTo[i - 1];
        minUpTo[i] = sumUpTo[i] < sumUpTo[i - 1] ? i : minUpTo[i - 1];
    }
    maxUpTo[i] = sumUpTo[i] - ((i >= L && sumUpTo[minUpTo[i - L]] < 0) ? sumUpTo[minUpTo[i - L]] : 0);
}


int maxSum = int.MinValue;
int endIndex = -1;

for (int i = L-1 ; i < values.Length; i++)
    if(maxUpTo[i] > maxSum)
    {
        endIndex = i;
        maxSum = maxUpTo[i];
    }

//Now walk backwards from maxIndex until we have reached maxSum
int startIndex = endIndex;
int currentSum = values[startIndex];

while (currentSum != maxSum || (endIndex - startIndex < L-1))
{
    startIndex--;
    currentSum += values[startIndex];

}

Console.WriteLine("value of maximum sub sequence = {0}, element indexes {1} to {2}", maxSum, startIndex, endIndex);

【讨论】:

    【解决方案3】:

    无用的案例和定义等。我的解决方案是自然的。 首先,请记住这一点,我们正在寻找整数数组的连续片段的最大和,该片段具有多个或恰好 L 个元素。让我们将 A 命名为初始数组。出于与 Kadane 算法相同的原因,我们考虑一个辅助数组 REZ,它有 N 个元素,如 A,REZ[i] 表示 A 的连续片段的最大和,至少包含 L 个元素并恰好在 i 结束-第一个位置。当然,REZ[1]、RZ[2]、REZ[L-1] 都等于零或 -INFINITY 值。 REZ[L]=A[1]+A[2]+...+A[L]。 对于 REZ 中的其余值,从 i 从 L+1 增长到 N,要计算 REZ[i],我们必须在两种情况之间选择最大值:

    1. 恰好是 L 值并包含 A[i] 的片段
    2. 具有超过 L 个值且包含 A[i] 的片段

    第一种情况的结果可以立即用部分和数组(S[i]=A[1]+A[2]+...+A[i])计算,S[i]-S [iL]。第二种情况的结果是 REZ[i-1]+A[i]。 所以,

    • REZ[i]=-INFINITY,如果 1
    • REZ[i]=S[i],如果 i=L
    • REZ[i]=max(S[i]-S[i-L], REZ[i-1]+A[i]),如果 i>L。

    REZ 建成后,我们必须计算它的最大值。 让我们考虑以下示例:

    N=7

    A -5 -1 2 -3 0 -3 3

    L=3

    S -5 -6 -4 -7 -7 -10 -7

    REZ:-INF -INF -4

    REZ[4]=max(S[4]-S[4-3],REZ[3]+A[4])=max(-2, -7)=-2

    REZ:-INF -INF -4 -2

    REZ[5]=max(S[5]-S[5-3],REZ[4]+A[5])=max(-1,-2)=-1

    REZ:-INF -INF -4 -2 -1

    REZ[6]=max(S[6]-S[6-3], REZ[5]+A[6])=max(-6,-4)=-4

    REZ:-INF -INF -4 -2 -1 -4

    REZ[7]=max(S[7]-S[7-3],REZ[6]+A[7])=max(0,-1)= 0

    REZ:-INF -INF -4 -2 -1 -4 0

    REZ 中的最大值为 0,这就是整个问题的答案。

    我希望我的英语足够好。当结果必须最多有 L 个连续元素时,我正在寻找类似问题的解决方案。当我意识到上述方法实际上是针对至少具有 L 个元素的解决方案时,我感到非常失望。

    【讨论】:

      【解决方案4】:

      下面是我的 Java 实现。

      public static int maxSumSubsequenceGivenLength(int[] array, int l) {
      
          if (null == array || array.length < l) {
              return -1;
          }
      
          int previousSequenceSum = 0;
      
          for (int i = 0; i < l; i++) {
              previousSequenceSum += array[i];
          }
      
          int maxSum = previousSequenceSum;
          int currentSum = 0;
          int startIndexFinal = 0;
          int endIndexFinal = l - 1;
      
          for (int i = l; i < array.length; i++) {
              currentSum = previousSequenceSum + array[i] - array[i - l];
              if (currentSum > maxSum) {
                  maxSum = currentSum;
                  endIndexFinal = i;
                  startIndexFinal = i - l + 1;
              }
              previousSequenceSum = currentSum;
          }
      
          System.out.println("start index:" + startIndexFinal + " end index: " + endIndexFinal);
          return maxSum;
      }
      

      【讨论】:

        【解决方案5】:

        只是为了发表 TLDR 评论: 上述算法遵循与 Kadane 相同的逻辑。要获得真正直观的感觉,您必须以这种方式开始推理:

        Kadane 版本:“数组中最大和的子数组在哪里?我们不知道。但它必须在某个索引 i 处结束。考虑 DP[i] 我们可以通过一个结束于的子数组获得的最佳和索引 i。那么在索引 i+1 处完成的大小至少为 K 的最佳子数组必须使用在索引 i 处完成的最佳子数组或不使用它:DP[i+1] = max(DP[i]+nums[i +1],nums[i+1])".

        这个问题的 Kadane 变体:“这个数组中大小至少为 K 的最大和子数组在哪里?我们不知道。但是这个子数组必须在某个索引 i 处结束。考虑 DP[i] 是最好的总和我们可以得到一个大小至少为 K 且在索引 i 处结束的子数组,那么在索引 i+1 处结束的最佳子数组必须使用在索引 i 处结束的最佳子数组,或者不使用它:DP[i+1] = max(DP[i]+nums[i+1],sum(nums[i+1-k]...nums[i+1]) "

        一旦你以这种方式提出你的推理,问题就会变得非常直观。

        【讨论】:

        • “上面的算法”?帖子可以根据投票更改顺序,您可以根据不同的条件对它们进行排序。更加详细一些。链接到您所指的答案。
        【解决方案6】:
        Here is the JAVA version : 
        
        Note : Credit goes to @templatetypedef. That explanation is awesome.
        
        
         public static int max_sum_in_subarray_of_minimum_length(int [] array, int min_length){
        
            int running_sum=0, max_sum_up_to_here=0, max_sum=0;
        
            int begin=0, end=0, max_start=0;
        
          /* max_sum_up_here = sum of all elements in array up to length L */
        
            for(int i=0;i<min_length;i++){
        
                max_sum_up_to_here+=array[i];
            }
        
          /* running sum and max sum = max_sum_up_here */
        
            running_sum = max_sum_up_to_here;
            max_sum= running_sum;
        
          /* Iterate through all elements starting from L i.e minimum length */
        
            for(int i=min_length;i<array.length;i++){
        
          /* min_sum_up_to_here = min_sum_up_to_here +
           next element in array - (i-L)th element in array */    
        
                max_sum_up_to_here+=array[i]-array[i-min_length];
        
          /* if running_sum + next element in array > max_sum_up_to here then 
                 running_sum = running_sum + next element in array 
             else running_sum = max_sum_up_to_here */
        
                if( (running_sum+array[i]) > max_sum_up_to_here ){
        
        
                    running_sum = running_sum+array[i];
                    max_start = i-min_length+1;
        
                 }else{
        
                    running_sum= max_sum_up_to_here;
                }
        
             /* if running sum > max_sum then max_sum = running sum */
        
                if( max_sum < running_sum ){
        
        
                    max_sum = running_sum;
        
                    begin =max_start;
        
                    end=i;
        
                }
        
            }
        
             /* max_sum gives sum of contiguous sub array of length L and begin and end gives indexes of the sub array*/  
        
            return max_sum;
        }
        

        【讨论】:

          猜你喜欢
          • 2019-08-22
          • 2023-03-27
          • 2015-02-03
          • 1970-01-01
          • 2014-10-31
          • 1970-01-01
          • 2019-10-13
          • 2023-03-04
          • 1970-01-01
          相关资源
          最近更新 更多