Official

H - Nim Counting Editorial by Nyaan


別解:DPの高速化

penciI さんの公式解説へのリンク

上の解説では \(M = 2^{16} \geq \max(A)\) として \(\mathrm{O}(M(\log M + \log N) )\) 解法を紹介していますが、ここではもう \(1\) つの想定解として DPの高速化 による\(\mathrm{O}(M \log M \log N )\) 解法の解説を行います。 また、石取りゲーム (Nim) の勝利条件の関する知識はすでに持っていると仮定します。

ナイーブな DP

まず、愚直な解法として次の DP を考えます。

  • \(dp(i, j) := \)\(i\) まで石の個数が確定していて、石の個数の xor が \(j\) である場合の数

するとこの DP は、山 \(i\) の石の個数で場合分けを行うと次の遷移で計算できます。 ( \(\oplus\) は xor (排他的論理和) を意味する記号です。 )

\[ dp(i, j) = \begin{cases} 1 & i = 0, j = 0 \\ 0 & i = 0, j \neq 0 \\ \displaystyle \sum_{a \in A} dp(i-1,j \oplus a) & \mathrm{otherwise} \end{cases} \]

求める答えは、山の個数 \(i\)\(1 \leq i \leq N\) で、排他的論理和 \(j\)\(0\) でない場合の \(dp(i, j)\) を全て足したものになります。

この DP により \(\mathrm{O}(N M K)\) で答えを求めることができますが、これでは実行時間には間に合いません。 \(N,M,K\) はいずれも \(10^5\) 程度なので、ここから \(N,M,K\) の 3 つのうち 2 つの実行時間への寄与を線形より高速にする必要があります。

xor 畳み込みを利用した高速化

xor 畳み込み (xor convolution) とは、 2 つの長さ \(M\) の列 \(f, g\) があった時に

\[ h_k = \sum_{i \oplus j = k} f_i \times g_j\]

を満たす長さ \(M\) の列 \(h\) を計算することを言います。 これはアダマール変換を利用すると \(\mathrm{O}(M \log M)\) で高速に計算できることが知られています。 ( アダマール変換に関する解説はここでは割愛します。詳しくは実装をお読みください。 )

上記のDP は xor 畳み込みを利用すると計算量から \(K\) を落とすことが出来ます。より具体的には、 DP を \(1\) 行ごとに遷移する式に変形することで xor 畳み込みを適用できる形になります。

\(F(i)\) を長さ \(M\) の 0-indexed の列 \( (dp(i,0), dp(i, 1), \dots, dp(i, M-1) )\) として定めます。
また、列 \(G\)\(a\)\(A\) に入っているときは \(a\) 番目の要素が \(1\) で、それ以外の場合は \(a\) 番目の要素が \(0\) である列として定めます。すると\(i \leq 0\) に対して

\[F(i+1)_u = \sum_{s \oplus t = u} F(i)_s \times G_t\]

と表せて、 xor畳み込みを利用すれば \(F(i)\) から \(F(i+1)\)\(\mathrm{O}(M \log M)\) でまとめて計算することが出来ます。

これで計算量は \(\mathrm{O}(N M \log M)\) まで落とせましたがこれではまだ速度が足りません。xor 畳み込みを使うと \(M\) は計算量から落とせなさそうなので、次は \(N\) を落とす方法を考えてみましょう。

二分累乗法を利用したさらなる高速化

上記の DP は \(F(0)\)\(G\)\(\mathrm{O}(N)\) 回畳み込み続けるという DP を行っています。このような単純な構造の DP では 二分累乗法 を利用した高速化を適用すると、遷移の回数を \(\mathrm{O}(\log N)\) 回に抑えることができます。

長さ \(M\) の列 \( S(i) \) を次のように定めます。

  • \(S(i)_j :=\) 山の個数が \(1\) 個以上 \(i\) 個以下で、石の個数の xor が \(j\) である場合の数

このとき\(S(i)\)\(F(1),\dots,F(i)\) の和として表せます。より厳密には、

\[S(i)_j = \sum_{s=1}^i F(s)_j\]

が成り立ちます。また、このとき答えは \(\sum_{j=1}^{M-1} S(N)_j\) になります。

ここで二分累乗法の成立条件を振り返ると、次の 2 つの遷移を高速に計算できればよいとわかります。

  • \(S(k), F(k) \to S(k + 1), F(k + 1)\)
  • \(S(k), F(k) \to S(2k), F(2k)\)

考えます。もしもこの 2 つが高速に計算できれば、二分累乗(繰り返し自乗法)の要領で \(S(N), F(N)\)\(\mathrm{O}(\log N)\) で計算できます。

前者は今までの DP を利用すると高速に計算できるので後者を考えます。 これは \(S(2k), F(2k)\)

  • 「 山 \(1\) から 山 \(k\) の石の置き方の場合の数」\(\times\) 「 山 \(k+1\) から 山 \(2k\) の石の置き方の場合の数」

と解釈すると \(F(k), S(k)\) の係数の積に分割することが出来るので、次の式が成り立つことが示せます。

\[S(2k)_u = S(k)_u + \sum_{s \oplus t = u} S(k)_s \times F(k)_t\]

\[F(2k)_u = \sum_{s \oplus t = u} F(k)_s \times F(k)_t\]

以上の式を利用して二分累乗法を行うことで、遷移 1 回あたり \(\mathrm{O}(M \log M)\) の遷移を \(\mathrm{O}(\log N)\) 回行うことで \(S(N)\) を計算することができました。計算量は全体で \(\mathrm{O}(M \log M \log N)\) となり十分高速に計算することができます。

解答例 (C++, 215ms)

posted:
last update: