【问题标题】:Maximum subarray sum modulo M最大子数组和模 M
【发布时间】:2015-09-15 19:36:12
【问题描述】:

我们大多数人都熟悉maximum sum subarray problem。我遇到了这个问题的一个变体,它要求程序员输出所有子数组和以某个数 M 为模的最大值。

解决此变体的简单方法是找到所有可能的子数组和(其数量级为 N^2,其中 N 是数组的大小)。当然,这还不够好。问题是——我们怎样才能做得更好?

示例:让我们考虑以下数组:

6 6 11 15 12 1

令 M = 13。在这种情况下,子数组 6 6(或 12 或 6 6 11 15 或 11 15 12)将产生最大和( = 12 )。

【问题讨论】:

  • M有上限吗?
  • 让我们假设数 M 的上限等于数组中的最大数。
  • O(n*M) 是微不足道的,通过查找以 i 结尾的存在子数组并求和(以模数为单位)到 k,对于每个索引 i 和每个 k[0,M)(在 DP 中完成)
  • @amit 我们希望我们的复杂性独立于模 M。

标签: algorithm binary-search modulo kadanes-algorithm


【解决方案1】:

这是针对此问题的 Java 解决方案的一种实现,它使用 Java 中的 TreeSet 来优化解决方案!

public static long maximumSum2(long[] arr, long n, long m)
{
    long x = 0;
    long prefix = 0;
    long maxim = 0;
    TreeSet<Long> S = new TreeSet<Long>();
    S.add((long)0);

    // Traversing the array.
    for (int i = 0; i < n; i++)
    {

    // Finding prefix sum.
    prefix = (prefix + arr[i]) % m;

    // Finding maximum of prefix sum.
    maxim = Math.max(maxim, prefix);

    // Finding iterator poing to the first
    // element that is not less than value
    // "prefix + 1", i.e., greater than or
    // equal to this value.
    long it = S.higher(prefix)!=null?S.higher(prefix):0;
    // boolean isFound = false;
    // for (long j : S)
    // {
    //     if (j >= prefix + 1)
    //     if(isFound == false) {
    //         it = j;
    //         isFound = true;
    //     }
    //     else {
    //         if(j < it) {
    //             it = j;
    //         }
    //     }
    // }
    if (it != 0)
    {
        maxim = Math.max(maxim, prefix - it + m);
    }

    // adding prefix in the set.
    S.add(prefix);
    }
    return maxim;
}

【讨论】:

    【解决方案2】:

    在java中使用treeset实现...

    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.util.TreeSet;
    

    公共类主{

    public static void main(String[] args) throws IOException {
        BufferedReader read = new BufferedReader(new InputStreamReader(System.in)) ;
        String[] str = read.readLine().trim().split(" ") ;
        int n = Integer.parseInt(str[0]) ;
        long m = Long.parseLong(str[1]) ;
        str = read.readLine().trim().split(" ") ;
        long[] arr = new long[n] ;
        for(int i=0; i<n; i++) {
            arr[i] = Long.parseLong(str[i]) ;
        }
    
        long maxCount = 0L ;
        TreeSet<Long> tree = new TreeSet<>() ;
        tree.add(0L) ;
        long prefix = 0L ;
        for(int i=0; i<n; i++) {
            prefix = (prefix + arr[i]) % m ;
            maxCount = Math.max(prefix, maxCount) ;
    
            Long temp = tree.higher(prefix) ;
            System.out.println(temp);
            if(temp != null) {
                maxCount = Math.max((prefix-temp+m)%m, maxCount) ;
            } 
            
            //System.out.println(maxCount);
            tree.add(prefix) ;
        }
    
        System.out.println(maxCount);
    }
    

    }

    【讨论】:

      【解决方案3】:

      这里已经列出了一堆很棒的解决方案,但我想添加一个运行时间为 O(nlogn) 的解决方案,而不使用 Python 标准库中没有的平衡二叉树。这个解决方案不是我的主意,但我不得不考虑一下它为什么起作用。代码如下,解释如下:

      def maximumSum(a, m):
          prefixSums = [(0, -1)]
          for idx, el in enumerate(a):
              prefixSums.append(((prefixSums[-1][0] + el) % m, idx))
          
          prefixSums = sorted(prefixSums)
          maxSeen = prefixSums[-1][0]
          
          for (a, a_idx), (b, b_idx) in zip(prefixSums[:-1], prefixSums[1:]):
              if a_idx > b_idx and b > a:
                  maxSeen = max((a-b) % m, maxSeen)
                  
          return maxSeen
      

      与其他解决方案一样,我们首先计算前缀和,但这次我们还跟踪前缀和的索引。然后我们对前缀和进行排序,因为我们想找到前缀和之间的最小差异模 m - 排序让我们只查看相邻元素,因为它们具有最小的差异。

      此时您可能会认为我们忽略了问题的一个重要部分——我们希望前缀和之间的差异最小,但较大的前缀和需要出现在较小的前缀和之前(意味着它具有较小的索引) .在使用树的解决方案中,我们通过将前缀和一一添加并重新计算最佳解决方案来确保。

      但是,事实证明,我们可以查看相邻元素,而忽略不满足索引要求的元素。这让我困惑了一段时间,但关键的实现是最佳解决方案总是来自两个相邻的元素。我将通过一个矛盾来证明这一点。假设最优解来自两个不相邻的前缀和 x 和 z,索引为 i 和 k,其中 z > x(它已排序!)并且 k > i:

      x ... z
      k ... i
      

      让我们考虑 x 和 z 之间的一个数字,我们称它为 y,索引为 j。由于列表已排序,x

      x ... y ... z
      k ... j ... i
      

      前缀和 y 必须具有索引 j

      【讨论】:

        【解决方案4】:

        从您的问题来看,您似乎已经创建了一个数组来存储累积和(前缀和数组),并将子数组arr[i:j] 的总和计算为(sum[j] - sum[i] + M) % M。 (arr 和 sum 分别表示给定数组和前缀 sum 数组)

        计算每个子数组的总和得出O(n*n) 算法。

        出现的问题是——

        我们真的需要考虑每个子数组的总和才能达到所需的最大值吗?

        不!

        对于j 的值,当sum[i] 刚好大于sum[j] 或差值为M - 1 时,(sum[j] - sum[i] + M) % M 的值将是最大值。

        这会将算法简化为O(nlogn)

        你可以看看这个解释! https://www.youtube.com/watch?v=u_ft5jCDZXk

        【讨论】:

          【解决方案5】:

          我觉得我的想法与已经发布的内容一致,但以防万一 - Kotlin O(NlogN) 解决方案:

          val seen = sortedSetOf(0L)
          var prev = 0L
          
          return max(a.map { x ->
              val z = (prev + x) % m
              prev = z
              seen.add(z)
              seen.higher(z)?.let{ y ->
                  (z - y + m) % m
              } ?: z
          })
          

          【讨论】:

            【解决方案6】:

            我的几点看法可能有助于人们更好地理解问题。

            1. 你不需要在模计算中加上+M,如前所述,% 运算符很好地处理负数,所以a % M = (a + M) % M

            2. 如前所述,诀窍是构建代理总和表,以便

            proxy[n] = (a[1] + ... a[n]) % M
            

            这样就可以将maxSubarraySum[i, j] 表示为

            maxSubarraySum[i, j] = (proxy[j] - proxy[j]) % M
            

            实现技巧是在我们遍历元素时构建代理表,而不是先预先构建它然后使用。这是因为对于数组a[i] 中的每个新元素,我们要计算proxy[i] 并找到大于但尽可能接近proxy[i]proxy[i](理想情况下要大1,因为这会导致提醒M - 1)。为此,我们需要使用一个聪明的数据结构来构建proxy 表,同时保持它的排序和 能够快速找到最接近proxy[i] 的更大元素。 bisect.bisect_right 在 Python 中是个不错的选择。

            请参阅下面的 Python 实现(希望这会有所帮助,但我知道这可能不一定像其他人的解决方案那样简洁):

            def maximumSum(a, m):
                prefix_sum = [a[0] % m]
                prefix_sum_sorted = [a[0] % m]
                current_max = prefix_sum_sorted[0]
                for elem in a[1:]:
                    prefix_sum_next = (prefix_sum[-1] + elem) % m
                    prefix_sum.append(prefix_sum_next)
                    idx_closest_bigger = bisect.bisect_right(prefix_sum_sorted, prefix_sum_next)
                    if idx_closest_bigger >= len(prefix_sum_sorted):
                        current_max = max(current_max, prefix_sum_next)
                        bisect.insort_right(prefix_sum_sorted, prefix_sum_next)
                        continue
                    if prefix_sum_sorted[idx_closest_bigger] > prefix_sum_next:
                        current_max = max(current_max, (prefix_sum_next - prefix_sum_sorted[idx_closest_bigger]) % m)
                        bisect.insort_right(prefix_sum_sorted, prefix_sum_next)
                return current_max
            

            【讨论】:

              【解决方案7】:

              正如您在Wikipedia 中看到的那样,存在一种称为 Kadane 算法的解决方案,该算法计算最大子数组和,观察所有位置 i 的最大子数组,该最大子数组结束于位置 i > 通过在数组上迭代一次。然后这解决了运行时复杂度 O(n) 的问题。

              不幸的是,我认为当存在多个解决方案时,Kadane 的算法无法找到所有可能的解决方案。

              Java 中的一个实现,我没有测试过:

              public int[] kadanesAlgorithm (int[] array) {
                      int start_old = 0;
                      int start = 0;
                      int end = 0;
                      int found_max = 0;
              
                      int max = array[0];
              
                      for(int i = 0; i<array.length; i++) {
                          max = Math.max(array[i], max + array[i]);
                          found_max = Math.max(found_max, max);
                          if(max < 0)
                              start = i+1;
                          else if(max == found_max) {
                              start_old=start;
                              end = i;
                              }
                      }
              
                      return Arrays.copyOfRange(array, start_old, end+1);
                  }
              

              【讨论】:

                【解决方案8】:

                对我来说,这里的所有解释都很糟糕,因为我没有得到搜索/排序部分。我们如何搜索/排序,尚不清楚。

                我们都知道需要构建prefixSum,意思是sum of all elems from 0 to i with modulo m

                我想,我们正在寻找的东西很明确。 知道subarray[i][j] = (prefix[i] - prefix[j] + m) % m(表示从索引 i 到 j 的模和),当给定前缀 [i] 时,我们的最大值始终是前缀 [j],它尽可能接近前缀 [i],但稍大。

                例如对于 m = 8,prefix[i] 为 5,我们正在寻找 5 之后的下一个值,它在我们的 prefixArray 中。

                为了高效搜索(二分搜索),我们对前缀进行排序。

                我们不能做的是,先构建prefixSum,然后再从0迭代到n,在排序后的前缀数组中寻找索引,因为我们可以找到小于我们的startIndex的endIndex,这是不好的。

                因此,我们所做的就是从 0 迭代到 n,指示我们潜在的最大子数组总和的 endIndex,然后查看我们的排序前缀数组(开头为空),其中包含在 0 和 endIndex 之间排序的前缀。

                def maximumSum(coll, m):
                    n = len(coll)
                    maxSum, prefixSum = 0, 0
                    sortedPrefixes = []
                
                    for endIndex in range(n):
                        prefixSum = (prefixSum + coll[endIndex]) % m
                        maxSum = max(maxSum, prefixSum)
                
                        startIndex = bisect.bisect_right(sortedPrefixes, prefixSum)
                        if startIndex < len(sortedPrefixes): 
                            maxSum = max(maxSum, prefixSum - sortedPrefixes[startIndex] + m)
                
                        bisect.insort(sortedPrefixes, prefixSum)
                
                    return maxSum
                

                【讨论】:

                • "我想,我们要找的很清楚。知道子数组[i][j] = (prefix[i] - prefix[j] + m) % m(表示模和从索引 i 到 j)"。这个等式是从哪里来的,我不清楚?
                • @Ghos3t 基本上我们只是减去两个前缀和,得到 i 和 j 之间段的前缀和。由于 prefix(i) 可以是 0 和 m 之间的任何值,通过减去 prefix(j) 我们可以得到一个负数(如果 prefix(i) prefix(j)),最终结果将大于 m,这就是我们执行 %m 操作的原因。没什么花哨的,只是模算术
                【解决方案9】:

                根据@Pham Trung 建议的解决方案添加 STL C++11 代码。可能很方便。

                #include <iostream>
                #include <set>
                
                int main() {
                    int N;
                    std::cin>>N;
                    for (int nn=0;nn<N;nn++){
                        long long n,m;
                        std::set<long long> mSet;
                        long long maxVal = 0; //positive input values
                        long long sumVal = 0;
                
                        std::cin>>n>>m;
                        mSet.insert(m);
                        for (long long q=0;q<n;q++){
                            long long tmp;
                
                            std::cin>>tmp;
                            sumVal = (sumVal + tmp)%m;
                            auto itSub = mSet.upper_bound(sumVal);
                            maxVal = std::max(maxVal,(m + sumVal - *itSub)%m);
                            mSet.insert(sumVal);                
                        }
                        std::cout<<maxVal<<"\n";
                    }
                }
                

                【讨论】:

                【解决方案10】:

                这是最大子数组和模的Java代码。我们处理在树中找不到严格大于 s[i] 的最小元素的情况

                public static long maxModulo(long[] a, final long k) {
                    long[] s = new long[a.length];
                    TreeSet<Long> tree = new TreeSet<>();
                
                    s[0] = a[0] % k;
                    tree.add(s[0]);
                    long result = s[0];
                
                    for (int i = 1; i < a.length; i++) {
                
                        s[i] = (s[i - 1] + a[i]) % k;
                
                        // find least element in the tree strictly greater than s[i]
                        Long v = tree.higher(s[i]);
                
                        if (v == null) {
                            // can't find v, then compare v and s[i]
                            result = Math.max(s[i], result);
                        } else {
                            result = Math.max((s[i] - v + k) % k, result);
                        }
                        tree.add(s[i]);
                    }
                    return result;
                 }
                

                【讨论】:

                  【解决方案11】:

                  修改Kadane algorithm 以跟踪#occurrence。下面是代码。

                  #python3
                  #source: https://github.com/harishvc/challenges/blob/master/dp-largest-sum-sublist-modulo.py  
                  #Time complexity: O(n)
                  #Space complexity: O(n)
                  def maxContiguousSum(a,K):
                      sum_so_far =0
                      max_sum = 0
                      count = {} #keep track of occurrence
                      for i in range(0,len(a)):
                              sum_so_far += a[i]
                              sum_so_far = sum_so_far%K
                              if sum_so_far > 0:
                                      max_sum = max(max_sum,sum_so_far)
                                      if sum_so_far in count.keys():
                                              count[sum_so_far] += 1
                                      else:
                                              count[sum_so_far] = 1
                              else:
                                      assert sum_so_far < 0 , "Logic error"
                                      #IMPORTANT: reset sum_so_far
                                      sum_so_far = 0
                      return max_sum,count[max_sum]
                  
                    a = [6, 6, 11, 15, 12, 1]
                    K = 13
                    max_sum,count = maxContiguousSum(a,K)
                    print("input >>> %s max sum=%d #occurrence=%d" % (a,max_sum,count))
                  

                  【讨论】:

                    【解决方案12】:

                    用 O(n*log(n)) 实现的全部 java 实现

                    import java.io.BufferedReader;
                    import java.io.InputStreamReader;
                    import java.util.TreeSet;
                    import java.util.stream.Stream;
                    
                    public class MaximizeSumMod {
                    
                        public static void main(String[] args) throws Exception{
                    
                            BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
                            Long times = Long.valueOf(in.readLine());
                    
                            while(times --> 0){
                                long[] pair = Stream.of(in.readLine().split(" ")).mapToLong(Long::parseLong).toArray();
                                long mod = pair[1];            
                                long[] numbers = Stream.of(in.readLine().split(" ")).mapToLong(Long::parseLong).toArray();
                                printMaxMod(numbers,mod);
                            }
                        }
                    
                        private static void printMaxMod(long[] numbers, Long mod) {
                    
                            Long maxSoFar = (numbers[numbers.length-1] + numbers[numbers.length-2])%mod;
                            maxSoFar = (maxSoFar > (numbers[0]%mod)) ? maxSoFar : numbers[0]%mod;
                            numbers[0] %=mod;
                            for (Long i = 1L; i < numbers.length; i++) {
                                long currentNumber = numbers[i.intValue()]%mod;            
                                maxSoFar = maxSoFar > currentNumber ? maxSoFar : currentNumber;
                                numbers[i.intValue()] = (currentNumber + numbers[i.intValue()-1])%mod;
                                maxSoFar = maxSoFar > numbers[i.intValue()] ? maxSoFar : numbers[i.intValue()];
                            }
                    
                            if(mod.equals(maxSoFar+1) || numbers.length == 2){
                                System.out.println(maxSoFar);
                                return;
                            }
                    
                            long previousNumber = numbers[0];
                            TreeSet<Long> set = new TreeSet<>();
                            set.add(previousNumber);
                    
                            for (Long i = 2L; i < numbers.length; i++) {
                                Long currentNumber = numbers[i.intValue()];
                                Long ceiling = set.ceiling(currentNumber);
                                if(ceiling == null){
                                    set.add(numbers[i.intValue()-1]);            
                                    continue;
                                }
                    
                                if(ceiling.equals(currentNumber)){
                                    set.remove(ceiling);
                                    Long greaterCeiling = set.ceiling(currentNumber);
                                    if(greaterCeiling == null){
                                        set.add(ceiling);
                                        set.add(numbers[i.intValue()-1]);            
                                        continue;
                                    }
                                    set.add(ceiling);                    
                                    ceiling = greaterCeiling;
                                }
                                Long newMax = (currentNumber - ceiling + mod);
                                maxSoFar = maxSoFar > newMax ? maxSoFar :newMax;
                                set.add(numbers[i.intValue()-1]);            
                            }
                    
                            System.out.println(maxSoFar);
                    
                        }
                    
                    }
                    

                    【讨论】:

                      【解决方案13】:

                      我们可以这样做:

                      维护一个数组sum,它位于索引ith,它包含从0到ith的模和。

                      对于每个索引ith,我们需要找到以该索引结尾的最大子和:

                      对于每个子数组 (start + 1 , i ),我们知道这个子数组的 mod sum 是

                      int a = (sum[i] - sum[start] + M) % M

                      因此,如果sum[start] 大于sum[i] 并尽可能接近sum[i],我们只能实现大于sum[i] 的子和。

                      如果您使用二叉搜索树,这可以轻松完成。

                      伪代码:

                      int[] sum;
                      sum[0] = A[0];
                      Tree tree;
                      tree.add(sum[0]);
                      int result = sum[0];
                      for(int i = 1; i < n; i++){
                          sum[i] = sum[i - 1] + A[i];
                          sum[i] %= M;
                          int a = tree.getMinimumValueLargerThan(sum[i]);
                          result = max((sum[i] - a + M) % M, result);
                          tree.add(sum[i]);
                      }
                      print result;
                      

                      时间复杂度:O(n log n)

                      【讨论】:

                      • 不错。您也可以通过仅在树中插入不同的和来使其成为 O(n log min(n, M))。
                      • 第 5 行的结果应该是 sum[0]%m,而不是 sum[0]
                      • 看着这个,对我来说,这似乎不可能是一个解决方案,因为它甚至没有引用除 A[0] 之外的任何 A 元素。少了点什么
                      • 为什么我们在 (sum[i] - sum[start] + M) % M 中有 +M。想不通。
                      • 因为 sum[i] - sum[start] 可以是负数,因此我们添加 M 并取 M 的模得到正余数。此外,添加 M 的任何倍数都不会改变余数。 1%7 == (1 + 7)%7 == (1+2*7)%7 等
                      【解决方案14】:

                      A 成为我们的输入数组,索引从零开始。我们可以在不改变结果的情况下以 M 为模减少 A

                      首先,让我们通过计算一个表示A的前缀和的数组P,以M为模,将问题简化为一个稍微简单的问题>:

                      A = 6 6 11 2 12 1
                      P = 6 12 10 12 11 12
                      

                      现在让我们按降序处理解决方案子数组的可能左边界。这意味着我们将首先确定从索引 n - 1 开始的最优解,然后确定从索引 n - 2 开始的最优解,以此类推。

                      在我们的示例中,如果我们选择 i = 3 作为左边界,则可能的子数组和由后缀 P[3..n-1] 表示加上一个常数a = A[i] - P[i]:

                      a = A[3] - P[3] = 2 - 12 = 3 (mod 13)
                      P + a = * * * 2 1 2
                      

                      全局最大值也将出现在某一点。由于我们可以从右到左插入后缀值,我们现在将问题简化为:

                      给定一组值S和整数xM,求S + x的最大值模M

                      这个很简单:只需使用平衡二叉搜索树来管理S的元素。给定一个查询x,我们希望找到S中小于M - x的最大值(即没有溢出的情况添加 x 时发生)。如果没有这个值,就用S的最大值。两者都可以在 O(log |S|) 时间内完成。

                      此解决方案的总运行时间:O(n log n)

                      这里有一些计算最大和的 C++ 代码。它还需要一些小的调整才能返回最优子数组的边界:

                      #include <bits/stdc++.h>
                      using namespace std;
                      
                      int max_mod_sum(const vector<int>& A, int M) {
                          vector<int> P(A.size());
                          for (int i = 0; i < A.size(); ++i)
                              P[i] = (A[i] + (i > 0 ? P[i-1] : 0)) % M;
                          set<int> S;
                          int res = 0;
                          for (int i = A.size() - 1; i >= 0; --i) {
                              S.insert(P[i]);
                              int a = (A[i] - P[i] + M) % M;
                              auto it = S.lower_bound(M - a);
                              if (it != begin(S))
                                  res = max(res, *prev(it) + a);
                              res = max(res, (*prev(end(S)) + a) % M);
                          }
                          return res;
                      }
                      
                      int main() {
                          // random testing to the rescue
                          for (int i = 0; i < 1000; ++i) {
                              int M = rand() % 1000 + 1, n = rand() % 1000 + 1;
                              vector<int> A(n);
                              for (int i = 0; i< n; ++i)
                                  A[i] = rand() % M;
                              int should_be = 0;
                              for (int i = 0; i < n; ++i) {
                                  int sum = 0;
                                  for (int j = i; j < n; ++j) {
                                      sum = (sum + A[j]) % M;
                                      should_be = max(should_be, sum);
                                  }
                              }
                              assert(should_be == max_mod_sum(A, M));
                          }
                      }
                      

                      【讨论】:

                      • 我觉得你的解释中有一个非明确的假设,关于 S + x mod M 在 S = M - 1 - x 处达到最大值。如果 S 和 x 可以是任何值,则 S = M - 1 - x + y * M 也是有效的解决方案。在您的树中,您只存储其中一个。我认为这行得通,因为 x 和 S 都在 [0,M[.
                      • 是的,我们只考虑规范代表 mod M。因此两个代表的总和在 (0, 2M(
                      猜你喜欢
                      • 1970-01-01
                      • 2014-09-04
                      • 2015-02-09
                      • 2019-11-20
                      • 2017-12-28
                      • 2016-09-26
                      • 2021-01-21
                      • 2020-01-22
                      • 1970-01-01
                      相关资源
                      最近更新 更多