最初に大きなサイズの配列が与えられ、そのあと区間[a, b]の値にXを加えよとか、区間[c, d]の最大値を求めよとか、区間[e, f]の総和を求めよというクエリが大量に飛んできて、しかもそれらの結果を2秒以内に返せという問題は競技プログラミングではよくみかけます。競技プログラミングでなくてもこのような処理が必要になることは案外多いのではないでしょうか?

これに対応するものとして、今回は平方分割を扱います。

平方分割の考え方

長さ N の整数列 A の区間 A[l_i] … A[r_i] の最大の要素の値を K 回求めたいのですが、与えられる区間の要素をいちいち全て調べていては時間計算量にして最大で O(NK) かかってしまいます。

そこでこう考えます。

1. 長さ N の配列が与えられたら、N の平方根を求め、これを切り上げたものを X とします。配列を長さ x の配列に分割して、それぞれの配列について目的の値(最大値とか区間の総和とか)を調べておきます。N の平方根を切り上げたものが分割された配列の長さになるので、分割された最後の配列の長さは x よりも小さくなる場合があります。

2. 調べたい区間に完全に含まれている配列についての 1. で求めた値と、その配列以外の部分の値をすべて調べて目的の値を求める。

もし長さが100の配列 A が与えられたとしましょう。この場合、長さが10で10個の配列に分解されます。それぞれの配列のなかで最大値や総和を計算します。最大値を M[0] ~ M[9]、総和を S[0] ~ S[9] にそれぞれ格納します。もし区間[18, 31]の最大値を求めよというクエリが飛んできたら、A[18] から A[31] までを調べるのではなく、A[18], A[19], M[2], A[30], A[31] の5つのなかから最大値を求めます。これなら計算量を大幅に落とすことができそうです。

平方分割の実践例

たとえばこんな問題が出されたとしましょう。

点の幅 (paizaランク A 相当)

テストの返却中に暇だった paiza 君は 2人で遊ぶゲームを思いつきました。

2人はそれぞれ生徒番号 1 ~ N の全校生徒 N 人の中から生徒番号が連続するように好きな人数の生徒を選ぶ。その選んだ生徒達の得点の幅が大きい方、すなわちその生徒たちの (最高点 – 最低点) の値が大きい方が勝ち、同じだったら引き分けというものです。

学校の生徒数 N と試合の数 K , 各生徒の得点 S_1 … S_N と、i 番目の試合で対戦した A と B の 2 人が選んだ生徒の最小の生徒番号と最大の生徒番号が与えられます。各試合のジャッジをしてください。

入力される値

N K
S_1

S_N
A_{l_1} A_{r_1} B_{l_1} B_{r_1}

A_{l_K} A_{r_K} B_{l_K} B_{r_K}

1 ≦ N, K ≦ 10,000 ただし N は 平方数である)
0 ≦ S_i ≦ 100,000 (1 ≦ i ≦ N)
1 ≦ Al_i ≦ Ar_i ≦ N (1 ≦ i ≦ K)
1 ≦ Bl_i ≦ Br_i ≦ N (1 ≦ i ≦ K)

あいかわらず paiza 君は変なことばかり考えています。ただ鳩はAtCoderにも取り組んでいるのですが、その問題文のなかに登場する高橋君とすぬけ君コンビの奇行はさらに上を行っているように思えます。

これは平方分割とは関係ないのですが、こんな問題があります。

器物損壊!高橋君

こんな破壊活動を繰り返していては高橋君はそのうち警察に捕まります。

さて横道にそれた話をもとに戻して、審判役を任されたpaiza君を助けてあげることに全力を傾注しましょう。

平方分割で区間の最大値と最小値を素早く求める

さて N と K が最大で 10,000 なので普通にやっていては間に合いません。そこで平方分割を使います。

今後の課題

この問題では最初に与えられた配列の値が更新されるというクエリはありませんでした。しかし平方分割はこのようなクエリにも対応できます。また区間の値を更新するときもすべての要素をそのつど更新するのではなく、区間で処理をおこないます。この場合は遅延伝搬というテクニックが必須となります。この件については別の記事で書きます。