最大子阵列和模M

时间:2015-06-29 11:01:39

标签: algorithm binary-search modulo kadanes-algorithm

我们大多数人都熟悉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)。

13 个答案:

答案 0 :(得分:22)

我们可以这样做:

维护索引为sum的数组ith,它包含从0到ith的模数和。

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

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

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

因此,如果sum[i]大于sum[start]并且尽可能接近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)

答案 1 :(得分:7)

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

首先,让我们通过计算表示 A 的前缀和,模数 M P 来将问题简化为更容易的问题。 >:

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 和整数 x M ,找到 S + x 的最大值modulo 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));
    }
}

答案 2 :(得分:3)

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

计算每个子数组的总和将产生O(n*n)算法。

出现的问题是-

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

不!

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

这会将算法减少到O(nlogn)

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

答案 3 :(得分:2)

这是最大子数组求和模的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;
 }

答案 4 :(得分:1)

使用O(n * log(n))

的总java实现
Object.keys

答案 5 :(得分:1)

根据@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";
    }
}

答案 6 :(得分:1)

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

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

我想,我们正在寻找的是明确的。 知道subarray[i][j] = (prefix[i] - prefix[j] + m) % m(表示从索引i到j的模和),给定prefix [i]时,我们的最大值始终是该prefix [j],该前缀与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

答案 7 :(得分:1)

这里已经列出了很多很棒的解决方案,但我想添加一个具有 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 < y < z.

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

前缀和 y 必须具有索引 j < i,否则它将成为带有 z 的更好解决方案的一部分。但是如果 j < i,则 j < k 和 y 和 x 形成比 z 和 x 更好的解决方案!所以 x 和 z 之间的任何元素都必须与两者之一形成更好的解,这与我们最初的假设相矛盾。因此最优解必须来自排序列表中相邻的前缀和。

答案 8 :(得分:0)

您可以在Wikipedia中看到一种解决方案,称为Kadane算法,该算法计算所有子数组 i <的最大子数组总和,并观察以子元素 i 结尾的最大子数组/ em>通过遍历数组一次。然后,这解决了运行时复杂度为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);
    }

答案 9 :(得分:0)

我觉得我的想法与已经发布的内容一致,但以防万一-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
})

答案 10 :(得分:0)

在java中使用treeset实现...

    handleChange = (event) => {
        this.setState({[event.target.name]: event.target.value});
    };

公共类 Main {

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

}

答案 11 :(得分:0)

这是一个在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;
}

答案 12 :(得分:-1)

修改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))