学ぶ › JavaScript
アルゴリズム · 29
累積和と尺取り法
区間の和を引き算1回で求め、二つの位置を前にだけ動かして区間を探します。
累積和は、前から足してきた値を表に書いておくことです。
尺取り法は、リストの上の二つの位置を動かしながら答えを探す方法です。
下の例は、6個の数について「l番目から r番目までの和」という質問3つに答え、和が 16 以上になるいちばん短い連続した区間の長さを求めます。
n, q, s = map(int, input().split())a = list(map(int, input().split())) # prefix[i] = 앞에서 i개의 합 (prefix[0] = 0)prefix = [0] * (n + 1)for i in range(n): prefix[i + 1] = prefix[i] + a[i] for _ in range(q): l, r = map(int, input().split()) print(prefix[r] - prefix[l - 1]) # l번째부터 r번째까지의 합 # 합이 s 이상인 가장 짧은 이어진 구간: left와 right는 앞으로만 간다best = 0left = 0window = 0for right in range(n): window += a[right] while window >= s: if best == 0 or right - left + 1 < best: best = right - left + 1 window -= a[left] left += 1print(best)const t = require("fs").readFileSync(0, "utf8").trim().split(/\s+/).map(Number);const [n, q, s] = t;const a = t.slice(3, 3 + n); // prefix[i] = 앞에서 i개의 합 (prefix[0] = 0)const prefix = new Array(n + 1).fill(0);for (let i = 0; i < n; i++) prefix[i + 1] = prefix[i] + a[i]; for (let k = 0; k < q; k++) { const l = t[3 + n + 2 * k], r = t[4 + n + 2 * k]; console.log(prefix[r] - prefix[l - 1]); // l번째부터 r번째까지의 합} // 합이 s 이상인 가장 짧은 이어진 구간: left와 right는 앞으로만 간다let best = 0, left = 0, window = 0;for (let right = 0; right < n; right++) { window += a[right]; while (window >= s) { if (best === 0 || right - left + 1 < best) best = right - left + 1; window -= a[left]; left++; }}console.log(best);import { readFileSync } from "node:fs";const t: number[] = readFileSync(0, "utf8").trim().split(/\s+/).map(Number);const [n, q, s] = t;const a: number[] = t.slice(3, 3 + n); // prefix[i] = 앞에서 i개의 합 (prefix[0] = 0)const prefix: number[] = new Array(n + 1).fill(0);for (let i = 0; i < n; i++) prefix[i + 1] = prefix[i] + a[i]; for (let k = 0; k < q; k++) { const l = t[3 + n + 2 * k], r = t[4 + n + 2 * k]; console.log(prefix[r] - prefix[l - 1]); // l번째부터 r번째까지의 합} // 합이 s 이상인 가장 짧은 이어진 구간: left와 right는 앞으로만 간다let best = 0, left = 0, window = 0;for (let right = 0; right < n; right++) { window += a[right]; while (window >= s) { if (best === 0 || right - left + 1 < best) best = right - left + 1; window -= a[left]; left++; }}console.log(best);import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner in = new Scanner(System.in); int n = in.nextInt(), q = in.nextInt(); long s = in.nextLong(); long[] a = new long[n]; for (int i = 0; i < n; i++) a[i] = in.nextLong(); // prefix[i] = 앞에서 i개의 합 (prefix[0] = 0). 합은 int를 넘을 수 있어 long long[] prefix = new long[n + 1]; for (int i = 0; i < n; i++) prefix[i + 1] = prefix[i] + a[i]; for (int k = 0; k < q; k++) { int l = in.nextInt(), r = in.nextInt(); System.out.println(prefix[r] - prefix[l - 1]); // l번째부터 r번째까지의 합 } // 합이 s 이상인 가장 짧은 이어진 구간: left와 right는 앞으로만 간다 int best = 0, left = 0; long window = 0; for (int right = 0; right < n; right++) { window += a[right]; while (window >= s) { if (best == 0 || right - left + 1 < best) best = right - left + 1; window -= a[left]; left++; } } System.out.println(best); }}using System; class Program{ static void Main() { string[] first = Console.ReadLine().Split(); int n = int.Parse(first[0]), q = int.Parse(first[1]); long s = long.Parse(first[2]); long[] a = Array.ConvertAll(Console.ReadLine().Split(), long.Parse); // prefix[i] = 앞에서 i개의 합 (prefix[0] = 0). 합은 int를 넘을 수 있어 long long[] prefix = new long[n + 1]; for (int i = 0; i < n; i++) prefix[i + 1] = prefix[i] + a[i]; for (int k = 0; k < q; k++) { string[] lr = Console.ReadLine().Split(); int l = int.Parse(lr[0]), r = int.Parse(lr[1]); Console.WriteLine(prefix[r] - prefix[l - 1]); // l번째부터 r번째까지의 합 } // 합이 s 이상인 가장 짧은 이어진 구간: left와 right는 앞으로만 간다 int best = 0, left = 0; long window = 0; for (int right = 0; right < n; right++) { window += a[right]; while (window >= s) { if (best == 0 || right - left + 1 < best) best = right - left + 1; window -= a[left]; left++; } } Console.WriteLine(best); }}#include <iostream>#include <vector> int main() { int n, q; long long s; std::cin >> n >> q >> s; std::vector<long long> a(n); for (int i = 0; i < n; i++) std::cin >> a[i]; // prefix[i] = 앞에서 i개의 합 (prefix[0] = 0). 합은 int를 넘을 수 있어 long long std::vector<long long> prefix(n + 1, 0); for (int i = 0; i < n; i++) prefix[i + 1] = prefix[i] + a[i]; for (int k = 0; k < q; k++) { int l, r; std::cin >> l >> r; std::cout << prefix[r] - prefix[l - 1] << '\n'; // l번째부터 r번째까지의 합 } // 합이 s 이상인 가장 짧은 이어진 구간: left와 right는 앞으로만 간다 int best = 0, left = 0; long long window = 0; for (int right = 0; right < n; right++) { window += a[right]; while (window >= s) { if (best == 0 || right - left + 1 < best) best = right - left + 1; window -= a[left]; left++; } } std::cout << best << '\n';}#include <stdio.h> long long a[10000], prefix[10001]; int main(void) { int n, q; long long s; scanf("%d %d %lld", &n, &q, &s); for (int i = 0; i < n; i++) scanf("%lld", &a[i]); // prefix[i] = 앞에서 i개의 합 (prefix[0] = 0). 합은 int를 넘을 수 있어 long long for (int i = 0; i < n; i++) prefix[i + 1] = prefix[i] + a[i]; for (int k = 0; k < q; k++) { int l, r; scanf("%d %d", &l, &r); printf("%lld\n", prefix[r] - prefix[l - 1]); // l번째부터 r번째까지의 합 } // 합이 s 이상인 가장 짧은 이어진 구간: left와 right는 앞으로만 간다 int best = 0, left = 0; long long window = 0; for (int right = 0; right < n; right++) { window += a[right]; while (window >= s) { if (best == 0 || right - left + 1 < best) best = right - left + 1; window -= a[left]; left++; } } printf("%d\n", best); return 0;}첫줄 = コンソール.数読み();n = 첫줄[0];q = 첫줄[1];s = 첫줄[2];a = コンソール.数読み(); // prefix[i] = 앞에서 i개의 합 (prefix[0] = 0)prefix = 新リスト(n + 1, 0);繰り返す (i = 0; i < n; i++){ prefix[i + 1] = prefix[i] + a[i];} 繰り返す (k = 0; k < q; k++){ lr = コンソール.数読み(); l = lr[0]; r = lr[1]; コンソール.書く(prefix[r] - prefix[l - 1]); // l번째부터 r번째까지의 합} // 합이 s 이상인 가장 짧은 이어진 구간: left와 right는 앞으로만 간다best = 0;left = 0;window = 0;繰り返す (right = 0; right < n; right++){ window += a[right]; 間 (window >= s) { もし (best == 0 || right - left + 1 < best) { best = right - left + 1; } window -= a[left]; left++; }}コンソール.書く(best);6 3 16 5 1 9 2 7 4 1 6 2 4 3 3
28 12 9 3
ポイント
- 質問のたびに最初から足し直すと、同じ足し算を繰り返すことになります。
そこでprefix[i]に「前から i個の和」をあらかじめ書いておきます。
数が5 1 9 2 7 4なら、表は0 5 6 15 17 24 28です。 - l番目から r番目までの和は
prefix[r] - prefix[l - 1]です。
前から r個の和から前の l − 1個を取り除くと、そのあいだだけが残ります。
表は1回だけ作り、質問ごとに引き算1回で済みます。 - 表を1マス長く(n + 1マス)作って
prefix[0] = 0にしておくと、l が 1 のときも同じ式がそのまま使えます。 - 尺取り法は、区間の左端
leftと右端rightを持ちます。rightを1つ進めて数を入れ、和が s 以上のあいだは長さを記録してleftを1つ進め、数を抜きます。 - どちらの位置も後ろに戻らないので、それぞれ多くても n回しか動きません。
すべての区間を一つずつ試さなくてよいのは、数がすべて正なので、区間を伸ばすと和が増え、縮めると減るからです。 - 値が10億くらいになると、3個足しただけで32ビット整数(約21億まで)を超えます。
和を入れる変数と表は64ビットにします。
言語ごとの書き方
| Python | prefix = [0] * (n + 1) を作って prefix[i + 1] = prefix[i] + a[i]。list(itertools.accumulate(a, initial=0)) でも同じ表ができます。整数に大きさの制限はありません |
|---|---|
| JavaScript | new Array(n + 1).fill(0)。数は 2^53(約9000兆)まで正確なので、10億くらいの値が数万個あっても和はそのまま入ります |
| TypeScript | JavaScript と同じです。 表の型は number[] |
| Java | long[] prefix = new long[n + 1]; — 新しい配列は 0 で埋まっています。和を入れる変数も long |
| C# | long[] prefix = new long[n + 1]; — 新しい配列は 0 で埋まっています。和を入れる変数も long |
| C++ | std::vector<long long> prefix(n + 1, 0);。和を入れる変数も long long |
| C | long long prefix[10001]; を関数の外に置くと 0 で埋まっています。表示は %lld |
| Craykon | prefix = 新リスト(n + 1, 0);。数は 2^53 まで正確です。 区間を縮める繰り返しは 間 (window >= s) { } |
よくある間違い
prefix[r] - prefix[l] で引いた
a = [5, 1, 9, 2, 7, 4]
prefix = [0, 5, 6, 15, 17, 24, 28]
l, r = 2, 4
print(prefix[r] - prefix[l])→ 11 — 2番目の数 1 まで引いてしまいました。正しい答えは 12 です
引くのは prefix[l - 1] です。
l番目の数は残したいので、その前の l − 1個だけを取り除きます。
区間を縮めるのに while ではなく if を使った
const a = [1, 1, 1, 10], s = 10;
let best = 0, left = 0, window = 0;
for (let right = 0; right < a.length; right++) {
window += a[right];
if (window >= s) {
if (best === 0 || right - left + 1 < best) best = right - left + 1;
window -= a[left];
left++;
}
}
console.log(best);→ 4 — 正しい答えは、10 だけの区間の 1 です。1周に1つしか縮めないので、left が追いつきませんでした
和が s 以上のあいだ縮め続ける必要があるので、while で書きます。
累積和を int に入れたJava · C# · C++ · C
int[] a = {1000000000, 1000000000, 1000000000};
int[] prefix = new int[4];
for (int i = 0; i < 3; i++) prefix[i + 1] = prefix[i] + a[i];
System.out.println(prefix[3]);→ -1294967296 — 30億が int(約21億まで)に入らず、あふれました
表と和の変数を long(C・C++ は long long)にします。
値一つ一つは int に入っても、和はあふれることがあります。
言語ごとの違い
累積和は、値が変わらないリストに対して和を何度も聞かれるときに使います。
尺取り法で和を見るこの方法は、数がすべて正のときだけ正しく動きます。
負の数が混ざると区間を伸ばしても和が減ることがあり、
番号を 0 から数えるのか 1 から数えるのかを先に決め、小さな例を手でたどってみましょう。
累積和は、値が変わらないリストに対して和を何度も聞かれるときに使います。
尺取り法で和を見るこの方法は、数がすべて正のときだけ正しく動きます。
負の数が混ざると区間を伸ばしても和が減ることがあり、
-1 5 で和が 5 以上の区間を探すと、5 一つの区間を見逃して 0 を出します。番号を 0 から数えるのか 1 から数えるのかを先に決め、小さな例を手でたどってみましょう。