競プロやっていると、そのうちアルゴリズムの勉強が必要になってきます。今回はその中でも有名な累積和を扱います。
そこまで難しいものでもないので、すぐに実践で使えるようになるはずです。
累積和とは
累積和は「数列のある区間の和を高速に計算するアルゴリズム」です。
「数列 \(a_n\) の第 \(n\) 項までの和を \(S_n\) とする。」なんて数学でやりますが、まさにこれと同じです。
例えば \(\sum_{k=15}^{50}a_n\) を求める時、ループを回して計算してもいいですが、求める値は \(S_{50}-S_{14}\) とも表せます。前者は \(35\) 回程度の計算が必要ですが、後者は \(1\) 回の計算で済みます。(これについてはまた後述します。)
配列の準備
では、実装の話をしましょう。
数列 \(a_0,a_1,\cdots ,a_n\) を配列 \(A\) に入れておきます。\(A[0]=a_0,A[1]=a_1,\cdots\) です。
配列 \(S\) を考えます。\(S[i]\) は「\(A\)の先頭から \(i\) 個の要素の和」とします。「数列の第 \(i\) 項までの和」とも言えますね。(数列は第 \(1\) 項, 第 \(2\) 項 \(\cdots\) のように数えてください。)
ただし、\(S[0]=0\) とします。(前者の定義で考えると自明。)


つまり、\(S\) は次のように計算しておけばいいでしょう。(\(S\) の要素数は、\(A\)の要素数\(+1\) だけ必要です。)
vector<int> S(n+1);
for(int i = 0;i < n;i++) S[i+1] = S[i]+A[i];
vectorを使う場合は初期値が \(0\) になっているのでこれでいいですが、静的配列を使う場合は \(S[0] = 0\) を忘れないようにしましょう。
任意の区間の和を求める
\(A[l]\) から \(A[r]\) の和を「区間 \([l,r]\) の和」のように書くとします。これは \((l-1,r+1)\) のように開区間でも書けるとします。
上のように \(S\) を準備すると、\([i,j)\ (=[i,j-1])\) の和は \(S[j]-S[i]\) と書けることが分かります。


例を挙げれば、
- \([5,23]\) の和は \(S[24]-S[5]\)
- \([0,i]\)の和は \(S[i+1]-S[0]\)
- 数列の第 \(2\) 項から第 \(7\) 項までの和は \(S[7]-S[1]\)
特に数列の第 \(i\) 項 の番号と、配列 \(A\) のインデックスはずれるので注意しましょう。(このような場合はあまりないですが。)
実践
例題です。
以下が提出コードです。重りの番号は \(0,1,\cdots\) として考えると混乱しにくいです。
#include <bits/stdc++.h>
#define rep(i,n) for(int i = 0;i < (n);i++)
using namespace std;
int main(){
int n;
cin >> n;
vector<int> w(n);
rep(i,n) cin >> w[i];
vector<int> s(n+1);
rep(i,n) s[i+1] = s[i]+w[i];
int ans = 100000;
rep(t,n-1){
int s1 = s[t+1];
int s2 = s[n] - s[t+1];
ans = min(ans,abs(s1-s2));
}
cout << ans << endl;
return 0;
}
問題文の通りシミュレーションすれば解けます。
区間の和を取るところでループを回して計算すると、\(\mathcal{O}(N^2)\) になりますが、累積和を使えば \(\mathcal{O}(N)\) となります。
\(S\) の計算で \(\mathcal{O}(N)\)、区間の和の計算で \(\mathcal{O}(1)\) ですね。これが累積和を使うと嬉しいところです。
終わりに
累積和は結構いろいろなところで出てきます。様々なアルゴリズムを覚えて、解ける問題を増やしていきましょう。
コメント