第一回日本最強プログラマー学生選手権決勝 F - Count Permutations Many Times
お題箱より。
F - Count Permutations Many Times
難しめの(とはいえ上級者には比較的知名度が高い)テクニックを複数使いこなす必要がある問題です。公式解説では多項式の係数に対応させて解いていますが、そうではない方法で説明しようと思います。
解法
まずは1つのクエリだけを考える
まずはこの問題を、あるクエリ(区間) 1つだけについて解くことを考えましょう。
「 である」よりも「 である(違反状態である)」のほうが数えやすいので、包除原理を適用します。 個の整数の集合を として、その部分集合 を考えます。 に含まれる整数が全て違反状態である(含まれない整数はどちらでもよい)ような場合の数を とすると、求めたい答えは包除原理から
と計算することができます。ここで は の要素数を示します。
の求め方を考えます。ある についてその要素を と書きます。このときクエリで指定された区間 に含まれている整数 の個数を とすると、これは以下のように考えることができます。
- まず を置く場所を順番に決めてしまう。このとき整数 が違反する置き場所として取れるものは 通りあり、これらの場所は異なる整数間で重複することはない。
- 先に上記の 個を置き終わったら、残っている 個の整数を残っている 個の場所に自由に置く。
ということで、 は以下のようになります。
これを全ての について個別に求めるのは当然できません。求めたい答えの計算において、サイズ が同じ部分集合には同じ係数 が掛かることに注目すると、残った をサイズが同じ集合ごとに合計したものが分かれば良さそうです。
ということで以下のDPを組みます。
整数 までのうち 個を含むような集合全てについて、それぞれの集合を としたときの の値を合計したもの。
これを最後まで計算したときの
の値が答えになります。
このDPにおける からの遷移は次の2通りです。
- 整数 を集合の要素として採用する。 を掛けて に足す。
- 整数 を集合の要素として採用しない。そのまま に足す。
これでクエリが1つの場合は のDPで解くことができました。このようにシンプルな遷移であることが後々効いてきます。
複数クエリではどうするか?
クエリとして区間が与えられるので、まず考えるのはセグメント木等です。ただこの問題の場合、2つの区間に対する計算結果(DPテーブル)をマージするのが難しいです。遷移の係数となっている が全部変わる可能性があるため、DPを最初から全部やりなおすことになります。
区間クエリの処理方法としてもう1つ重要テクニックがあります。Mo's Algorithm です。使える条件/やり方/計算量についてはリンク先を参照して欲しいのですが、これは
- オフラインクエリである(いわゆるクエリ先読みができる)
- 区間を1要素分伸ばす/1要素分縮めて、答えを更新するという操作が妥当な計算時間でできる
という条件を満たす時に使えるテクニックです。
伸び縮みの計算量を として全体計算量は 。 が大きいと として許せる計算量は や くらいなのですが、今回は制約が小さいので くらいならOKそうです。
要素を増やす/減らす処理を上手くやるには?
ある区間 について、先ほどのDPの最終結果が求められているとします。そこから区間の要素を1つだけ増やす/減らすという処理を上手くやる方法はあるでしょうか?
このとき増えた/減った要素の値を とすると、DP遷移の係数となっている のうち変化するのは だけです。そして先ほどのDPは、「戻すDP」というテクニックが適用できる形になっています。これは
- 今回の の整数のように、DPの計算において見ていく順番が変わっても問題ない。
- DPの遷移が単純で、 から を逆算することができる。
という条件を満たすときに使えるテクニックです。
今回のように 段階の遷移のうち1つの段階だけで係数の変化などがある時には、その遷移を古い係数で戻してから新しい係数で計算し直す…という手順で を更新することができます。この計算は でできます。
まとめ
以上で全ての材料が揃いました。
- まず包除原理を元に、1つのクエリ だけについてこの問題を解くDPを構成する。
- 与えられるクエリにMo's Algorithmを適用する。
- Mo's Algorithmの中で要求される区間の伸び縮みについては、先ほどのDPに「戻すDP」のテクニックを適用し、DPテーブルの値を更新する。
- 各クエリについて、その時のDPテーブルの値に係数を掛けて合計したものが答えとなる。
公式解説では多項式の係数と見なして解いていますが、多項式の掛け算がこの記事におけるDPを進める時の遷移に、割り算が戻す時の遷移にちょうど対応するので、本質的には同じ計算をしていることになります。