如何解决最大子数组问题的变化,使总和应该在两个边界之间?

Posted

技术标签:

【中文标题】如何解决最大子数组问题的变化,使总和应该在两个边界之间?【英文标题】:How to solve a variation to the maximum subarray problem, so that the sum should be between two bounds? 【发布时间】:2021-05-16 16:18:13 【问题描述】:

假设我有一个整数数组,表示股票价格的每日变化,例如以下数组:

[3, -1, -4, 1, 5, -9, 2, 6]. 

我如何找到在两个值之间求和的子数组的数量(下限和上限,所以l <= s <= u),例如-1(=lower)和0(=upper)?在这种情况下,答案是 5。您可以拥有子数组

[3, -1, -4, 1]
[-1]
[-1, -4, 1, 5, -9, 2, 6]
[1, 5, -9, 2]
[-9, 2, 6]

另一个示例数组是:

[4, 2, 2, -6, 7] 

下限3,上限4。答案是3。我尝试了以下非常幼稚的方法,我确信有许多更快的替代方法。我想知道如何通过分而治之或可能通过动态编程更快地解决这个问题。

public class Stock

   public int sequenceLength;
   public int[] prices;
   public int lowerBound;
   public int upperBound;
   public int count = 0;
   public Stock()
   
      sequenceLength = Int32.Parse(Console.ReadLine());
      prices = new int[sequenceLength];
      var split = Console.ReadLine();
      var splitSpace = split.Split(' ');
      for (int i = 0; i < sequenceLength; i++)
         prices[i] = Int32.Parse(splitSpace[i]);
      lowerBound = Int32.Parse(Console.ReadLine());
      upperBound = Int32.Parse(Console.ReadLine());
   

用法

static void Main(string[] args)


   int testcases = Int32.Parse(Console.ReadLine());
   Stock[] stock = new Stock[testcases];

   for (int i = 0; i < testcases; i++)
      stock[i] = new Stock();

   int count = 0;
   for (int i = 0; i < stock.Length; i++)
   
      for (int j = 0; j < stock[i].sequenceLength - 1; j++)
      
         int sum = stock[i].prices[j];
         if (sum >= stock[i].lowerBound && sum <= stock[i].upperBound)
            count++;
         for (int k = j + 1; k < stock[i].sequenceLength; k++)
         
            sum += stock[i].prices[k];
            if (sum >= stock[i].lowerBound && sum <= stock[i].upperBound)
               count++;
         
      
      if (stock[i].prices[stock[i].sequenceLength - 1] >= stock[i].lowerBound && stock[i].prices[stock[i].sequenceLength - 1] <= stock[i].upperBound)
         count++;
      stock[i].count = count;
      count = 0;
   

   Console.Clear();
   for (int i = 0; i < stock.Length; i++)
      Console.WriteLine(stock[i].count);

【问题讨论】:

【参考方案1】:

已经有了O(N^2) 复杂性的答案,我将提出O(NlogN) 解决方案。

创建一个数组sums,其中sums[0] = array[0]sums[i] = sums[i-1]+array[i]。现在,对于sums 中的每个索引i,您需要找到索引j 的数量,使得sums[i] - sums[j][lower, upper] 范围内。但是如何找到索引数量j

创建平衡二叉搜索树(AVL 树)。在其中插入sums[0]。从左到右开始处理节点。处理完一个节点后,将其添加到树中。您可以在O(logN) 复杂度中搜索[lower, upper] 范围内的索引数,同样适用于插入。这将使您的总时间复杂度为O(NlogN)

【讨论】:

【参考方案2】:

如果我理解问题(并且陪审团出局)

前提是,第一个循环在数组中运行。第二个循环负责保存和检查范围,然后产生结果

显然这是 O(n2) 时间复杂度

给定

public static IEnumerable<int[]> GetSubs(int[] source, int lower, int upper)

   for (var i = 0; i < source.Length; i++)
   for (int j = i, sum = 0; j < source.Length; j++)
   
      sum += source[j];
      if (sum >= lower && sum <= upper)
         yield return source[i..(j+1)];
   

用法

var array = new[]  -5, -4, -3, -2, -1, 0, 2, 3, 4, 5, 6, 7, 8, 9 ;

foreach (var sequence in GetSubs(array,2,5))
    Console.WriteLine($"sequence.Sum() : [string.Join(", ", sequence)]");
   

输出

5 : [-5, -4, -3, -2, -1, 0, 2, 3, 4, 5, 6]
4 : [-4, -3, -2, -1, 0, 2, 3, 4, 5]
3 : [-3, -2, -1, 0, 2, 3, 4]
2 : [-2, -1, 0, 2, 3]
4 : [-1, 0, 2, 3]
2 : [0, 2]
5 : [0, 2, 3]
2 : [2]
5 : [2, 3]
3 : [3]
4 : [4]
5 : [5]

Full Demo Here

注意:您可以在 linq 中使用 Enumerable.Range 执行此操作,但这很容易理解

如果你只是想要计数,你可以一起删除迭代器,当 if 条件为真时增加一个计数器

public static int GetSubs(int[] source, int lower, int upper)

   var result = 0;
   for (var i = 0; i < source.Length; i++)
   for (int j = i, sum = 0; j < source.Length; j++)
   
      sum+= source[j];
      if (sum >= lower && sum <= upper)
         result++;
   
   return result;

【讨论】:

以上是关于如何解决最大子数组问题的变化,使总和应该在两个边界之间?的主要内容,如果未能解决你的问题,请参考以下文章

如何将列表中的两个最大元素相加?

获得最大和的子矩阵?

Java算法之边界为1的最大子矩阵

c_cpp 从正整数数组中找出子序列的最大总和,其中任意两个子序列彼此不相邻i

最大子数组问题

最大和子数组 - 返回子数组和总和 - 分而治之