
去年も同じようなタイトルのブログを投稿してますね。
この記事では、いくつかの問題のネタバレが含まれます。
はじめに
この記事は、緋月ゆいさんの朝雑を聴きながら書いています。
とてもかわいい VTuber さんですね。私は大好きです。
本題
Codeforces Round 895 - E の解説の冒頭です。
However, of course, we do not expect participants in Div3 to have knowledge of these advanced data structures, so there is a simpler solution.
Div.3 に出る方々はセグ木や平方分割は知らんだろう、と書いてありますね。せっかくなので知っておきましょう。ちなみにいろいろ書いていたら長くなってしまったので、この問題に関する記述はないです。
セグ木はすでに記事がありますね~

お気持ち
Range Sum Query を考えます。
Segment Tree で一発です。ただ、今回は少し別のアプローチを考えてみましょう。
Segment Tree では、隣同士の 2 個の要素を合わせたブロックみたいなものを考えて、二分木チックな構造を作っていました。今回は、木のようなものは作らずに、 個の要素で つのブロックを構成するようにまとめてみましょう。
例えば、長さ の数列を考えると、上図のように つごとのブロックに分けることができますね。このようなブロックのことを「バケット」と呼びます、
次に、それぞれのバケットに対して総和を計算してあげます。
準備はこれでおしまいです。それでは各クエリを処理していきましょう。
- 1 : 配列の 番目を更新して、それが含まれるバケットの総和を更新します。
- 2 : 区間 の中に完全に含まれるバケットと、そうでないバケットに分けます。完全に含まれるバケットそれぞれの総和と、それに含まれていない各要素をすべて足し合わせたものを出力します。
クエリ 2 の処理が少しわかりにくいかもしれませんが、例えば の場合は次の図のようになります。
肝心なのは計算量ですね。クエリ 1 は自明に っぽいですね。では、クエリ 2 はどうでしょうか。
まず、先ほどの分け方から、存在するバケットの個数は 個です。そのため、バケットの総和として足される数は高々 個くらいというわけですね。
また、各要素を足し合わせる部分ですが、例えば先ほどの図で、黄色のバケットに含まれる各要素をすべて見るといったことは起こり得ません。もしそのような状況があれば、各要素を見るのではなく、そのバケットの総和を足すことになるからです。というわけで、黄色のバケットのうち見られる要素は 個未満ということになります。左の緑色のバケットに対しても同じことが言え、結果として各要素を足し合わせる部分では、 個以上の要素を見ることは起きません。
というわけで、単純に計算すると、クエリ 2 の計算量は ということが分かりました。以上より、このようにすると Range Sum Query が処理できました。
このように、 個の要素を 個の塊に分割してあげる、といったテクニックを平方分割といいます。
平方分割における遅延伝搬
Segment Tree に Lazy Segment Tree があったように、平方分割にも遅延伝搬を行う平方分割は存在します。とはいえ、大きなところは遅延セグ木とそんなに変わらないと思います。
今回の Update は可換でないので、遅延伝搬をしてあげましょう。
今回は、バケットごとに Lazy という値を持たせます。例えば、図のように を に変更させてみましょう。
丸で囲った部分が、値が変化した場所です。先ほどと同じように、バケットを完全に含む場所については Lazy に変化を保存させて、そうでないところは直接保存させてあげます。
これは でできそうですね。
では、この状態から、 を求めてみましょう。 が含まれているバケットは Lazy に の値を持っているので、この時点でバゲット内のすべての要素に対して反映してあげます。
この伝搬は、バケット内の要素の個数が なことから でできます。これを終えたら、 をそのまま出力すればよいです。
この問題でありうるもう一つのシチュエーションとしては、次の図のような状態で を に変更するようなものがあります。
はそのまま に変更すればよく、 は完全にバケットを含むので Lazy の値を に変更すればよいです。問題は、まだ伝搬を行っていないバケットを完全に含まないような区間になっている ( さん...) 場合ですね。
先述のように、Update は可換ではないので、先に黄色のブロックの 3 を伝搬させてしまってから、 を変更してあげればよいですね。伝搬は なので、このシチュエーションでも Update クエリは で行うことができます。
それ、セグ木でよくない?
Range ○○ Query みたいなのは世の中にいろいろあるわけですが、結局十分大きい について なので、計算クエリはセグ木・遅延セグ木のほうが速いから、計算量的にはセグ木でやったほうがよくない?と感じる方もいるでしょう。実は、計算量だけではわからない平方分割のありがたみがあります。
その 1 : 平方分割は、バケット上に重いデータ構造を乗せることが可能です。セグ木でやるとすべてのノード上に構造を乗せることになってしまって爆発しちゃいますが、平方分割だと空間計算量が で済んだりとか、そういう感じの恩恵を得ることができます。
その 2 : Segment Tree を使うためには必要な制約があって、この構造に持たせる要素はモノイドの元でなければなりません。
復習 : 集合 と二項演算 が与えられ、
- (結合律)
- (単位元の存在)
を満たす組 をモノイドといいます。
すると、Segment Tree に載るようなうまいモノイドが設計できないときに、くるし~ってなります。苦しいときには平方分割がたすけてくれるかもしれません。
モ~
問題の条件によっては、そのような場合に Mo's Algorithm が適用できる場合があります。名前の由来は牛さんの鳴き声です。嘘です。
Mo's Algorithm が適用できる条件は次の 3 つです :
- 要素が更新されない
- クエリの先読みが可能
- 区間 の結果から、区間 の結果を容易に計算できる。
試しに 1 問解いてみましょう。
モノイドが思いつきません。くるしいですね。Mo で解けないかやってみましょう。
問題の形式から、「1. 要素が更新されない」「2. クエリの先読みが可能」はわかりますね。では、3 がどうなるかを見てみましょう。
区間 における整数 の個数を とします。求める答えは です。
ここから、区間 に広げることを考えてみます。このとき、区間に が追加されます。すると、 の時に が奇数であれば答えは され、偶数であれば答えは変わりません。 についても同様に考えることができ、区間の長さを 1 変えたときの遷移は で行えそうだということが分かります。これで、3. の条件も満たすことが分かりました。
ここまでチェックできれば、Mo's Algorithm の出番です。平方分割っぽさがないなと思いながら読んでいたみなさま、お待たせいたしました。
まず、区間 を 個の区間に分割します。そして、それに基づいて 個のクエリをソートします。ソートの仕方としては、左端のブロックの位置 -> 右端の位置 の順でソートする感じです。
言葉だと表現が難しいですが、ソートのイメージはこんな感じです。
そして、尺取りみたいな感じで処理してあげます。やることはこれだけです。
時間計算量を見積もってみます。左端と右端がそれぞれ何回動くかを考えればよいです。
- 左端 : ブロックごとにまとめているので、1 回のクエリにつき 回動く。クエリは 回あるので 回動く。
- 右端 : ブロックごとに、 回動く。ブロックは 個あるので、 回動く。
つまり、1 回の遷移にかかる時間が であるとき、 でクエリをすべて処理できます。今回の場合、遷移には かかるので、 で解けました。