私たちのほとんどは max sumum 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)を生成します。
これは次のように実行できます。
インデックスsum
に0〜ith
のモジュラス合計を含む配列ith
を維持します。
各インデックスith
について、このインデックスで終わる最大の小計を見つける必要があります。
各サブ配列(start + 1、i)について、このサブ配列のmod sumは
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)
[〜#〜] a [〜#〜]をゼロから始まるインデックス付きの入力配列とします。 [〜#〜] a [〜#〜]modulo[〜#〜] m [〜#〜]結果を変更せずに減らすことができます。
まず、配列[〜#〜] p [〜#〜]を計算して、問題を少し簡単に減らしましょう[〜#〜] a [〜#〜]のプレフィックスの合計を表します、モジュロ[〜#〜] m [〜#〜]:
A = 6 6 11 2 12 1
P = 6 12 10 12 11 12
それでは、ソリューションサブアレイの左境界線を降順に処理してみましょう。つまり、最初にインデックスn-1で始まる最適なソリューションを決定し、次にインデックスn-2などで始まる最適なソリューションを決定します。
この例では、左境界としてi =を選択した場合、可能なサブ配列の合計は、サフィックスP [3..n-1]と定数a = A [ i]-P [i]:
a = A[3] - P[3] = 2 - 12 = 3 (mod 13)
P + a = * * * 2 1 2
グローバルな最大値も1つのポイントで発生します。サフィックス値を右から左に挿入できるため、問題を次のように減らしました。
値のセット[〜#〜] s [〜#〜]と整数xおよび[〜#〜] m [〜#〜]が与えられた場合、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));
}
}
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;
}
Wikipedia で読むことができるように、Kadaneのアルゴリズムと呼ばれるソリューションが存在します。これは、すべての位置について、位置iで終わる最大サブ配列を監視する最大サブ配列合計を計算しますi配列を1回繰り返します。次に、実行時の複雑さO(n)の問題を解決します。
残念ながら、複数のソリューションが存在する場合、カダネのアルゴリズムはすべての可能なソリューションを見つけることができないと思います。
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);
}
合計Java O(n * log(n))を使用した実装)
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);
}
}
私にとっては、ここでの説明はすべてひどいものでした。検索/ソートの部分がなかったからです。どのように検索/ソートするのかは不明でした。
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 [i]にできるだけ近いprefix [j]ですが、わずかにより大きい。
例えば。 m = 8の場合、prefix [i]が5であるため、prefixArrayにある5の次の値を探しています。
効率的な検索(バイナリ検索)のために、プレフィックスを並べ替えます。
できないことは、最初にprefixSumを構築し、次に0からnに繰り返して、ソートされたプレフィックス配列でインデックスを探すことです。これは、startIndexよりも小さいendIndexを見つけることができるためです。
したがって、潜在的な最大サブ配列の合計のendIndexを示す0からnまで反復し、ソートされたプレフィックスを含むソートされたプレフィックス配列(最初は空です)を調べます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
_
@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";
}
}