メインコンテンツまでスキップ

累積和

累積和とは

累積和とは文字の通りで、配列の前・後ろから順に配列の値を足し合わせていくことをいう。 累積和を取ることで配列の区間和を高速で求めることが可能となる。

イメージ図

上記の図において配列番号 3 \sim 7 の和が知りたい場合を考えると

赤色の配列で考えると配列番号 3 \sim 7 を足し合わせるので23 + 7 + 1 + 7 + 12 = 50のように合計 5 回配列にアクセスする必要がある。

しかし、青色の配列であれば 7 番目の配列までを足し合わせた78、2 番目の配列までを足しわせた28を取り出し、78 - 28 = 50とすることで 配列番号 3 \sim 7 までの和を求めることができる。

イメージ図

上記の下図のように、長い線から短い線を引くことで緑の線を求めることができる。

いもす法

累積和は他にも使い方がある。それがいもす法と呼ばれるイベントを管理する方法である。

例えば、店に来店した時刻と退店した時刻が分かっていた際に店内に人が一番いた時間帯を求めたといった場合に、いもす法を用いることで求めることができる。

イメージ図

店内にいた時間を横線で表すと上記の図のようになる。線が一番重なっている時間帯が一番店内に人がいた時間帯であると言える。上記の図では 13 分の時に 4 人おり、一番人がいたと言える。 これをいもす法(累積和)を使うと求めることができる。

イメージ図

上記の赤色四角のように入店した時間に+1、退店した時間+1 分の時間に-1 を入れ、前から(1 から)順に累積和をとることで青色四角のように線表されたものを表現することができる。

2 次元

累積和は 2 次元に拡張することができる。2 次元の場合は線ではなく面となる。例えば以下のような図の任意の 4 点に囲まれた値の和(赤い色になったエリア)を出したいとする。

イメージ図

足し合わせる数が 9 個と少ないので、1 つ 1 つ取り出し足し合わせても今回であれば問題ない。しかし、例えば以下のように同じ図の中で他のエリアの和も求める場合を考えると求めるエリアの数が多くなると時間がかかりすぎてしまう。

イメージ図

こういった場合にも累積和を用いることができる。まずは、右方向に累積和をとる。

イメージ図

続いて下方向にも累積和を取っていく。そうすると、右下に全ての値を足した値が格納される。

イメージ図

この累積和を取った図から赤色エリアの和を求めるには、1 次元の時と同じ様に全体から一部を引き算していき、求めたいエリアの和を求める。 ここで赤色エリアの右下(右 5, 下 4 のインデック番号)の70というのは累積和を取ったので右 0 \sim 5, 下 0 \sim 4 の全ての和であることがわかる。

イメージ図

上記の図の薄い青色のエリアから不必要なエリアを引き算することで求めたい赤色エリアの和を求めることができそうであることがわかる。 計算に必要そうなエリアに色付けを行うと以下の様になる。

イメージ図

薄い青色以外にもオレンジ, 緑, 紫の 3 色のエリアが存在する。それぞれのエリアを以下の様に計算することで赤色エリアを求めることができる。 オレンジと緑のエリアが青色エリアから不必要なので引き算で取り除き、紫のエリアは引きすぎてしまった分加算して帳尻合わせを行っている。

イメージ図

これを計算で行うには各色のエリアの右下の値を使い、

702617+8=3570 - 26 - 17 + 8 = 35

となる。実際に赤色エリアを

3+3+3+4+4+4+4+5+5=353 + 3 + 3 + \\ 4 + 4 + 4 + \\ 4 + 5 + 5 = 35

となり一致している。この様に累積和は 2 次元にも応用することができる。同じ様に拡張することで 3 次元でも累積和を使用することができる。

また、面でデータを保持できるので、以下の図のようなデータの保持も簡単に行うことができる。

イメージ図

図の状態をプログラムで表現すると以下の様になる。

イメージ図

上記の図のように数値を持つことができれば、面を表すことができる。しかし、対応する面に 1 ずつ値を代入していくと時間がかかりすぎる場合がある。 累積和を使うと効率よく値を代入することができる。

イメージ図

上記のようにデータを保持すると、右方向に累積和を取り、下方向に累積和を取るという処理(下方向から行って良い)を行うと面の情報を保持できる。

イメージ図

このように 2 次元の累積和を活用すると面の情報を簡単に求めることができる。

アルゴリズム

1 次元

cumulative-sum.py
class CumulativeSum:

def __init__(self, box: list) -> None:
self.sum_box = [0]
for i in range(len(box)):
self.sum_box.append(self.sum_box[-1] + box[i])

def query(self, l: int, r: int):
"""l ~ rの和を返す

Args:
l (int): 開始点
r (int): 終了点
"""
# 配列番号を元の配列番号に合わせる
if l > r:
l, r = r, l
r += 1
assert l >= 0
assert r < len(self.sum_box)
return self.sum_box[r] - self.sum_box[l]

2 次元

cumulative-sum.py
class CumulativeSum:
def __init__(self, box: list[list]) -> None:
self.sum_box = [[0] * (len(box[0]) + 1) for _ in range(len(box)+1)]

for i in range(len(box)):
for j in range(len(box[i])):
self.sum_box[i+1][j+1] += box[i][j] + self.sum_box[i+1][j]
for i in range(len(box)):
for j in range(len(box[i])):
self.sum_box[i+1][j+1] += self.sum_box[i][j+1]

def query(self, sx: int, sy: int, gx: int, gy: int):
"""l ~ rの和を返す

Args:
l (int): 開始点
r (int): 終了点
"""
# 配列番号を元の配列番号に合わせる
if sx > gx:
sx, gx = gx, sx
if sy > gy:
sy, gy = gy, sy
gx += 1
gy += 1
assert sx >= 0
assert sy >= 0
assert gx < len(self.sum_box[0])
assert gy < len(self.sum_box)
ret = self.sum_box[gy][gx]
ret -= self.sum_box[gy][sx]
ret -= self.sum_box[sy][gx]
ret += self.sum_box[sy][sx]
return ret

面に値を入れから累積和を行うと表したい図形を表すことができる。

cumulative-sum.py
box = [[0] * 10 for _ in range(11)]
a = [(1, 1, 3, 3), (3, 5, 6, 6), (5, 1, 5, 7)]
for i in range(len(a)):
sx, sy, gx, gy = a[i]
box[sx][sy] += 1
box[sx][gy+1] -= 1
box[gx+1][sy] -= 1
box[gx+1][gy+1] += 1

for i in range(len(box)):
for j in range(len(box[i])-1):
box[i][j+1] += box[i][j]

for i in range(len(box)-1):
for j in range(len(box[i])):
box[i+1][j] += box[i][j]