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

平方分割

平方分割 とは

平方分割とはデータをN\lfloor\sqrt{N}\rfloor個の大きさのNN\lceil\frac{N}{\sqrt{N}}\rceilのグループに分割することで、データに対しての操作の計算量を落とす方法である。

アルゴリズム

流れ

例えば[5, 62, 2, 19, 10, 23, 0, 30, 1]の様な配列があったとする。クエリとしてl,rが与えられ、l \sim r の間で一番大きな数字を出力するということを考える。

配列は 9 個しかないので、毎回 l \sim r を検索して最大値を求めることをしても良いが、配列の個数が大きくなると毎回検索すると時間がかかってしまう。 平方分割では図のように配列を 3 個の大きさの 3 つのグループに分割する。分割した後各グループの最大値を保持する別の配列を用意する。

平方分割1

こうすることで、例えばl = 2, r = 7の最大値を求める場合に、グループ 1 の 3 \sim 5 が完全に内包されている様な状況だと、3 \sim 5 を探索する必要がなくグループの値を用いて最大値は23であるとわかる。そのため、2, 6, 7, グループ 1 の 4 つの値の最大の値を求めれば良くなり、確認する回数を少なくすることができる。

平方分割4

検索範囲にグループが含まれないl = 3, r = 4の様な場合はそのまま 3 \sim 4 の範囲を確認すれば良い。これは O(N\sqrt{N})で済むようになっている。なぜなら、N\lfloor\sqrt{N}\rfloor個ずつに区切っているのでグループが含まれる == 検索範囲の個数が2×N2 \times \lfloor\sqrt{N}\rfloor以上ということが言え、グループを含まない = 2×N2 \times \lfloor\sqrt{N}\rfloor 以下と言えるからである。

例えば、l = 2, r = 4 の場合はグループの個数以上だが、グループを含んでいないという場合がり、

平方分割5

必ずグループを含んでいると言える条件は以下となる。

rl>2×NorrNlN2r - l > 2\times \lfloor\sqrt{N}\rfloor \\ or \\ \lfloor \frac{r}{\sqrt{N}}\rfloor - \lfloor \frac{l}{\sqrt{N}}\rfloor \geqq 2

両方とも同じことを意味しており、グループ単位で見るか配列の個数で見るかによって条件が異なる。

グループ単位で見ると、差が22 \geqq であれば必ず 1 以上のグループを内包していると分かるため、rNlN2\lfloor \frac{r}{\sqrt{N}}\rfloor - \lfloor \frac{l}{\sqrt{N}}\rfloor \geqq 2 の条件となることがわかる。

配列の個数で見ると、グループ n の開始点からグループ n+1 の終了点の範囲の場合がグループ差が 1 となる最大の個数となる。そのため、それに+1 した rl1+2×Nr - l \geqq 1 + 2\times \lfloor\sqrt{N}\rfloorを満たす場合はグループ差が必ず 2 となる。この条件を整理するとrl>2×Nr - l > 2\times \lfloor\sqrt{N}\rfloorとなる。

平方分割2

上記の必ずグループを含んでいる条件を満たさない場合は、そのまま l \sim r を検索する。l \sim r で検索を行っても最大の個数が2×N2 \times \lfloor\sqrt{N}\rfloor個しか存在しないため、計算量はO(N)O(\sqrt{N})となる。

反対に条件を満たす場合は、グループを使用して検索個数を削減する。間に内包しているグループはグループで計算する。計算に用いるグループ番号は次の条件を満たしているものを使用すれば良い。

lN<n<rN\lfloor \frac{l}{\sqrt{N}}\rfloor < n < \lfloor \frac{r}{\sqrt{N}}\rfloor

グループに内包されていない範囲のものはそれぞれl(lN+1)×N1l \sim (\lfloor\frac{l}{\sqrt{N}}\rfloor + 1) \times \lfloor\sqrt{N}\rfloor - 1, rN×Nr\lfloor\frac{r}{\sqrt{N}}\rfloor \times \lfloor\sqrt{N}\rfloor \sim rとなる。(l,r が所属するグループの開始点から・終了点までを計算している)この個数は必ず2×N2 \times \lfloor\sqrt{N}\rfloor個以下しか存在しない。また、完全内包されているグループの個数は必ずN\lfloor\sqrt{N}\rfloor個以下なので、合わせて検索する個数は2×N+N2 \times \lfloor\sqrt{N}\rfloor + \lfloor\sqrt{N}\rfloorとなるので、計算量はO(N)O(\sqrt{N})となる。

平方分割3

両方の条件で計算量がO(N)O(\sqrt{N})となるので、平方分割のアルゴリズムの計算量は前処理に\O(N)\O(N), 検索にO(N)O(\sqrt{N})となる。

プログラム

square-division.py
import math
class SquareDivision():

def __init__(self, data_list, condition=max, initial=-1 << 64):
self.data = data_list
self.n = len(self.data)
self.sqrt_num = math.floor(math.sqrt(self.n))

self.condition = condition
print(self.condition)
self.initial = initial
self.division_list = [self.initial] * (self.n // self.sqrt_num + 1)
for i in range(self.n):
idx = i // self.sqrt_num
self.division_list[idx] = self.condition(self.division_list[idx], self.data[i])

def change(self, idx, val):
self.data[idx] = val
group_idx = idx // self.sqrt_num
self.division_list[group_idx] = self.condition(self.division_list[group_idx], val)

def query(self, l, r):
if r < l:
l, r = r, l
ret = self.initial
group_l = l // self.sqrt_num
group_r = r // self.sqrt_num
if group_r - group_l >= 2:
for i in range(group_l+1, group_r):
ret = self.condition(ret, self.division_list[i])
# 残り部分を比較
# 左側
ret = self.condition(ret, self._check(l, (group_l + 1) * self.sqrt_num - 1))
# 右側
ret = self.condition(ret, self._check(group_r * self.sqrt_num, r))
else:
ret = self._check(l, r)
return ret

def _check(self, l, r):
ret = self.initial
for i in range(l, r+1):
ret = self.condition(ret, self.data[i])
return ret