この記事の第 1 部 は、競プロ Advent Calender 2 日目として公開されています。
第 1 部 2021-12-02 投稿
第 2 部 2023-12-14 投稿
第 1 部:問題
Library Checker – Chromatic Number
Library Checker
Library Checker
問題文中の N N N は、ここでは代わりに n n n と表します。頂点全体の集合を V V V とします。 ∣ V ∣ = n \vert V \vert = n ∣ V ∣ = n です。 タイトルのように、この問題を解く計算量 O ( 2 n n ) O(2^n n) O ( 2 n n ) や O ( 2 n ) O(2^n) O ( 2 n ) のアルゴリズムを実装し、その定数倍高速化をしました。
グラフの彩色数を用いた解法がある問題の例: (1-1:cp-unspoiler)
また、同じアルゴリズムを用いて和集合に関するある問題を高速に解けます。例: (2-1)
第 1 部:そもそも、そのアルゴリズムは何
頂点集合の部分集合 s s s について、 k k k 色で塗ることができるかどうかの真偽値を d p k [ s ] \mathrm{dp}_k[s] dp k [ s ] とします。実際は添え字は非負整数を用いて、ビット列で集合を表現します。 d p 1 \mathrm{dp}_1 dp 1 を求めておきます。
bitwise OR convolution を用いる O ( 2 n n 2 ) O(2^n n^2) O ( 2 n n 2 )
テーブル X X X をゼータ変換して得られるテーブルを X ^ \hat{X} X ^ と表します。
頂点集合 s s s を k k k -彩色でき、頂点集合 t t t を l l l -彩色できるとき、 s ∪ t s \cup t s ∪ t は ( k + l ) (k+l) ( k + l ) -彩色可能です。よって、 2 2 2 つのテーブル d p k \mathrm{dp}_k dp k と d p l \mathrm{dp}_l dp l から、 bitwise OR convolution を用いて d p k + l \mathrm{dp}_{k+l} dp k + l を求められます。 ここで、 d p k [ V ] \mathrm{dp}_k[V] dp k [ V ] ( V V V は頂点全体の集合、 1 ≤ k ≤ n 1 \leq k \leq n 1 ≤ k ≤ n ) に注目すると、 k k k -彩色が判定でき、よって彩色数が求まります。この時点では計算量は O ( 2 n n 2 ) O(2^n n^2) O ( 2 n n 2 ) 、ダブリングと二分探索を用いると O ( 2 n n log n ) O(2^n n \log n) O ( 2 n n log n ) となります。
1 1 1 色塗るごとに計算量が指数的に減少 O ( 2 n n ) O(2^n n) O ( 2 n n )
彩色後のグラフについて、任意の k k k 頂点を選ぶとそれらは k k k 色以下で塗られていることが明らかです。ここで、頂点 n − k , n − k + 1 , … , n − 1 n-k,n-k+1, \ldots ,n-1 n − k , n − k + 1 , … , n − 1 を始めの k k k 色で塗ることにします。色に色 0 0 0 、色 1 1 1 、……と番号を振るなら、頂点 n − k n-k n − k の色を色 x x x として x ≤ k − 1 x \leq k-1 x ≤ k − 1 が成り立ちます。すると、色 0 , 1 , … , k 0,1, \ldots ,k 0 , 1 , … , k を塗る頂点の集合を決めた後は k k k 個の頂点を無視できることになります。したがって、テーブル d p k \mathrm{dp}_k dp k のサイズは色を塗るごとに半分にしてもよく、つまり k k k の増加とともに指数的に小さくなります。これで計算量 O ( 2 n n ) O(2^n n) O ( 2 n n ) が達成できます。
ちなみに、この方法では次の紹介する方法に比べて具体的な彩色の構築が容易です。
適当な値で割った余りを用いる O ( 2 n n ) O(2^n n) O ( 2 n n )
以上で説明したアルゴリズムでは、 bitwise OR convolution の結果を真偽値に変換するため、 bitwise OR convolution の累乗を高速に求める方法(計算量 O ( 2 n n ) O(2^n n) O ( 2 n n ) ) を適用できません。しかし、真偽値に変換せずに適当な値 P P P で割ったあまりを用いることで、出力が正しい保証と引き換えに高速な累乗を可能にします。
bitwise OR convolution は以下の手順で行われます。
高速ゼータ変換
各点積
高速メビウス変換
まず、 d p 1 ^ \hat{\mathrm{dp}_1} dp 1 ^ の各点を k k k 乗します。興味があるのは d p k [ V ] \mathrm{dp}_k[V] dp k [ V ] のみなので、高速メビウス変換の代わりに O ( 2 n ) O(2^n) O ( 2 n ) で値を求められます。 d p k [ V ] ≠ 0 \mathrm{dp}_k[V] \neq 0 dp k [ V ] = 0 ならグラフは k k k 彩色可能です。 k = 1 , 2 , … , n k=1,2, \ldots ,n k = 1 , 2 , … , n と順に求めると (各点の k k k 乗は順に求まるので) O ( 2 n n ) O(2^n n) O ( 2 n n ) 時間になります。このアルゴリズムが(Library Checker の提出で)よく用いられています。
出力が正しい保証がないと言いましたが、高々 n n n 個の値のどれかが P P P の倍数かつ正になって初めて失敗しうるので、出力が正しくない可能性はかなり低いです。
調べた限りでは、この方法で定数倍が小さいものが Library Checker によく提出されています。
以上をまとめて O ( 2 n ) O(2^n) O ( 2 n )
以上の 2 2 2 つの考えを合わせて計算量を O ( 2 n ) O(2^n) O ( 2 n ) にできます。つまり、 d p k ^ \hat{\mathrm{dp}_k} dp k ^ は、 P P P で割った余りを計算しながら、色を塗るごとにサイズを半分にします。
d p 1 \mathrm{dp}_1 dp 1 を高速ゼータ変換したテーブルは、場合分けを用いて計算量 O ( 2 n ) O(2^n) O ( 2 n ) で直接計算できます( Nyaan さんの提出 を参考にしました )。定数倍高速化の項で改めて解説します。
P ← 1 0 9 + 9 P \leftarrow 10^9+9 P ← 1 0 9 + 9 T [ i ] ← 1 T[i] \leftarrow 1 T [ i ] ← 1 ( i = 0 , 1 , … 2 n − 1 ) (i=0,1,\ldots 2^n-1) ( i = 0 , 1 , … 2 n − 1 ) A ← d p 1 ^ A \leftarrow \hat{\mathrm{dp}_1} A ← dp 1 ^ T [ i ] ← T [ i ] ⋅ ( − 1 ) popcount ( i ) m o d 2 ( i = 0 , 1 , … 2 n − 1 ) T[i] \leftarrow T[i] \cdot (-1)^{\text{popcount}(i) \bmod 2}\ (i=0,1,\ldots 2^n-1) T [ i ] ← T [ i ] ⋅ ( − 1 ) popcount ( i ) mod 2 ( i = 0 , 1 , … 2 n − 1 ) for k ← 1 , 2 , … , n \text{for }k \leftarrow 1,2, \ldots ,n for k ← 1 , 2 , … , n z ← 2 n − k \hspace{20px}z \leftarrow 2^{n-k} z ← 2 n − k T [ i ] ← ( T [ i ] A [ i ] ) m o d P ( i = 0 , 1 , … 2 z − 1 ) \hspace{20px}T[i] \leftarrow (T[i]A[i]) \bmod P\ (i=0,1,\ldots 2z-1) T [ i ] ← ( T [ i ] A [ i ]) mod P ( i = 0 , 1 , … 2 z − 1 ) T [ i ] ← ( T [ i ] + T [ i + z ] ) m o d P ( i = 0 , 1 , … z − 1 ) \hspace{20px}T[i] \leftarrow (T[i]+T[i+z]) \bmod P\ (i=0,1,\ldots z-1) T [ i ] ← ( T [ i ] + T [ i + z ]) mod P ( i = 0 , 1 , … z − 1 ) if ( ∑ i = 0 z − 1 T [ i ] ) m o d P = 0 \hspace{20px}\text{if }(\sum_{i=0}^{z-1}T[i]) \bmod P =0 if ( ∑ i = 0 z − 1 T [ i ]) mod P = 0 return k \hspace{40px}\text{return }k return k
いったい何を数え上げているんだ…?
適当な値で割った余りを用いる O ( 2 n n ) O(2^n n) O ( 2 n n ) では、
各頂点を複数の色で塗ってよいとしたときの塗り方の個数
を数え上げることになります。
以上をまとめて O ( 2 n ) O(2^n) O ( 2 n ) では、
各頂点を複数の色で塗ってよいが、頂点 n − 1 − i n-1-i n − 1 − i は番号が i i i 以下の色しか塗ってはいけないときの塗り方の個数
を数え上げることになります。
第 1 部:これを定数倍高速化します
概要
出力が 1 1 1 の場合は自明なので、別で判定し、テーブルの最大サイズを 2 n 2^n 2 n から 2 n − 1 2^{n-1} 2 n − 1 に減らします。つまり、各テーブルは初めから頂点 n − 1 n-1 n − 1 を塗る前提で構築します。
詳細
まず変数を定義します。
非負整数 E i E_i E i の j j j ビット目が立つのは、頂点 i i i に頂点 j j j が隣接しているときである。
n n n 以下の正整数 k k k 、 { 0 , 1 , 2 , … , n − 1 − k } \{0,1,2,\ldots ,n-1-k\} { 0 , 1 , 2 , … , n − 1 − k } の部分集合 d d d について、 d p 2 k [ d ] \mathrm{dp2}_k[d] dp2 k [ d ] が 0 0 0 でないのは、 d d d で表される頂点集合に頂点 n − k , n − k + 1 , … n − 1 n-k,n-k+1, \ldots n-1 n − k , n − k + 1 , … n − 1 を足した頂点集合は、 k k k 色で塗れるとき。ある k k k についてこのテーブルをまとめて d p 2 k \mathrm{dp2}_k dp2 k と呼ぶ。
実際は 1 1 1 つの非負整数を bitset として d d d に用いるため、この意味で非負整数と頂点集合を同一視する場合がある。
P P P で割った余りを用いて高速化するときはこの値が間違っている可能性がある。
d p 2 k ^ \hat{\mathrm{dp2}_k} dp2 k ^ は d p 2 k \mathrm{dp2}_k dp2 k をゼータ変換したもの。
d p 1 ^ \hat{\mathrm{dp}_1} dp 1 ^ の計算
d p 1 ^ [ d ] \hat{\mathrm{dp}_1}[d] dp 1 ^ [ d ] の値は、
d d d の部分集合であって 1 1 1 色で塗れるものの個数
と解釈できます。頂点 h h h を塗るかどうかで場合分けすると、 2 h ≤ d < 2 h + 1 2^h \leq d \lt 2^{h+1} 2 h ≤ d < 2 h + 1 のとき
頂点 h h h を塗らない場合の個数は d p 1 ^ [ d ∩ { h } ‾ ] \hat{\mathrm{dp}_1}[d \cap \overline{\{h\}}] dp 1 ^ [ d ∩ { h } ]
頂点 h h h を塗る場合、 h h h に隣接する頂点は塗れないので、求める個数は d p 1 ^ [ d ∩ { h } ‾ ∩ E h ‾ ] \hat{\mathrm{dp}_1}[d \cap \overline{\{h\}} \cap \overline{E_h}] dp 1 ^ [ d ∩ { h } ∩ E h ]
として、より小さい d d d についての結果から計算でき、 d p 1 ^ [ 0 ] = 1 \hat{\mathrm{dp}_1}[0]=1 dp 1 ^ [ 0 ] = 1 と合わせると動的計画法でテーブル全体を計算できます。
d p 2 1 ^ \hat{\mathrm{dp2}_1} dp2 1 ^ の計算
同様に場合分けし、動的計画法を導きます。
d p 2 1 ^ [ 0 ] = 1 \hat{\mathrm{dp2}_1}[0]=1 dp2 1 ^ [ 0 ] = 1
頂点 h h h を塗らない場合の個数は d p 1 ^ [ d ∩ { h } ‾ ] \hat{\mathrm{dp}_1}[d \cap \overline{\{h\}}] dp 1 ^ [ d ∩ { h } ]
頂点 h h h を塗る場合、
頂点 h h h が頂点 n − 1 n-1 n − 1 に隣接する場合、求める個数は 0 0 0 。
隣接しない場合、 h h h に隣接する頂点は塗れないので、求める個数は d p 1 ^ [ d ∩ { h } ‾ ∩ E h ‾ ] \hat{\mathrm{dp}_1}[d \cap \overline{\{h\}} \cap \overline{E_h}] dp 1 ^ [ d ∩ { h } ∩ E h ]
オーバーフローの吟味
(2023/01/20 更新)
実は、 32 32 32 ビット整数で計算すると、ゼータ変換したテーブルどうしで各要素を掛け合わせたときに最大で 2 n 2n 2 n ビット程度の値が現れ、オーバーフローします。ここで、 C++ の符号なし 32 32 32 ビット整数型は加減乗算を m o d 2 32 \bmod2^{32} mod 2 32 で合同な値を用いて計算することを用いて正しく計算します。
(以降、 2023/01/20 追記) この部分は当時の勘違いにもとづいて書かれており、結果的には誤りではなかったものの誤解を招いた可能性がありました。申し訳ございません。
n = 20 n=20 n = 20 の場合、 bitwize OR convolution の計算結果に表れる値の上界に関して 3 20 ≤ 3.5 × 1 0 9 < 2 32 3^{20}\leq 3.5\times 10^9\lt 2^{32} 3 20 ≤ 3.5 × 1 0 9 < 2 32 が成り立つので、正しい結果が得られることが示せますが、例えば n = 26 n=26 n = 26 などとすると間違う可能性を否定できません。 64 64 64 ビット整数型、より一般に 2 n 2n 2 n ビット以上の表現で計算すれば安全になります。
全体をもう一度
除数 P P P を用いない計算量 O ( 2 n n ) O(2^n n) O ( 2 n n ) のアルゴリズムは次のようになります。
A ← d p 1 ^ A \leftarrow \hat{\mathrm{dp}_1} A ← dp 1 ^ T ← d p 2 1 ^ T \leftarrow \hat{\mathrm{dp2}_1} T ← dp2 1 ^ if E i = ∅ ( i = 0 , 1 , … n − 1 ) \text{if }E_i=\emptyset\ (i=0,1,\ldots n-1) if E i = ∅ ( i = 0 , 1 , … n − 1 ) return 1 \hspace{20px}\text{return }1 return 1 for k ← 2 , 3 , … , n \text{for }k \leftarrow 2,3, \ldots ,n for k ← 2 , 3 , … , n z ← 2 n − k \hspace{20px}z \leftarrow 2^{n-k} z ← 2 n − k T [ i ] ← T [ i ] A [ i ] ( i = 0 , 1 , … 2 z − 1 ) \hspace{20px}T[i] \leftarrow T[i]A[i]\ (i=0,1,\ldots 2z-1) T [ i ] ← T [ i ] A [ i ] ( i = 0 , 1 , … 2 z − 1 ) T [ i ] ← − T [ i ] + T [ i + z ] ( i = 0 , 1 , … z − 1 ) \hspace{20px}T[i] \leftarrow -T[i]+T[i+z]\ (i=0,1,\ldots z-1) T [ i ] ← − T [ i ] + T [ i + z ] ( i = 0 , 1 , … z − 1 ) T ← first half of T \hspace{20px}T \leftarrow \text{first half of }T T ← first half of T T ← M o ¨ bius transformed T \hspace{20px}T \leftarrow \text{Möbius transformed }T T ← M o ¨ bius transformed T if ( ∑ i = 0 z − 1 T [ i ] ) m o d P = 0 \hspace{20px}\text{if }(\sum_{i=0}^{z-1}T[i]) \bmod P =0 if ( ∑ i = 0 z − 1 T [ i ]) mod P = 0 return k \hspace{40px}\text{return }k return k T ← zeta transformed T \hspace{20px}T \leftarrow \text{zeta transformed }T T ← zeta transformed T
除数 P P P を用いる計算量 O ( 2 n ) O(2^n) O ( 2 n ) のほうは次のようになります。
P ← 1 0 9 + 9 P \leftarrow 10^9+9 P ← 1 0 9 + 9 A ← d p 1 ^ A \leftarrow \hat{\mathrm{dp}_1} A ← dp 1 ^ T ← d p 2 1 ^ T \leftarrow \hat{\mathrm{dp2}_1} T ← dp2 1 ^ T [ i ] ← T [ i ] ⋅ ( − 1 ) popcount ( i ) m o d 2 ( i = 0 , 1 , … 2 n − 1 ) T[i] \leftarrow T[i] \cdot (-1)^{\text{popcount}(i) \bmod 2}\ (i=0,1,\ldots 2^n-1) T [ i ] ← T [ i ] ⋅ ( − 1 ) popcount ( i ) mod 2 ( i = 0 , 1 , … 2 n − 1 ) if E i = ∅ ( i = 0 , 1 , … n − 1 ) \text{if }E_i=\emptyset\ (i=0,1,\ldots n-1) if E i = ∅ ( i = 0 , 1 , … n − 1 ) return 1 \hspace{20px}\text{return }1 return 1 for k ← 2 , 3 , … , n \text{for }k \leftarrow 2,3, \ldots ,n for k ← 2 , 3 , … , n z ← 2 n − k \hspace{20px}z \leftarrow 2^{n-k} z ← 2 n − k T [ i ] ← ( T [ i ] A [ i ] ) m o d P ( i = 0 , 1 , … 2 z − 1 ) \hspace{20px}T[i] \leftarrow (T[i]A[i]) \bmod P\ (i=0,1,\ldots 2z-1) T [ i ] ← ( T [ i ] A [ i ]) mod P ( i = 0 , 1 , … 2 z − 1 ) T [ i ] ← T [ i ] + T [ i + z ] ( i = 0 , 1 , … z − 1 ) \hspace{20px}T[i] \leftarrow T[i]+T[i+z]\ (i=0,1,\ldots z-1) T [ i ] ← T [ i ] + T [ i + z ] ( i = 0 , 1 , … z − 1 ) if ( ∑ i = 0 z − 1 T [ i ] ) m o d P = 0 \hspace{20px}\text{if }(\sum_{i=0}^{z-1}T[i]) \bmod P =0 if ( ∑ i = 0 z − 1 T [ i ]) mod P = 0 return k \hspace{40px}\text{return }k return k
第 1 部:完成したものがこちら
(2024-01-15 コンパイルエラー修正)
第 2 部:決定性 O ( 2 n ) O(2^n) O ( 2 n ) 時間へ
アルゴリズム
「以上をまとめて O ( 2 n ) O(2^n) O ( 2 n ) 」のアルゴリズムにおいて、 m o d P {}\bmod P mod P を取らずに多倍長整数で計算することを考えます。実はこれが O ( 2 n ) O(2^n) O ( 2 n ) 時間の deterministic アルゴリズムになります。(!?)
高速ゼータ変換をした後の配列に含まれる値は 2 n 2^n 2 n 以下です。よって、 bitwise OR convolution で k k k 乗した配列を高速ゼータ変換した列に含まれる値は ( 2 n ) k (2^n)^k ( 2 n ) k 以下です。これを k k k ワードで管理します。
k k k 色目を塗る処理にかかる計算量は O ( k 2 n − k ) O(k2^{n-k}) O ( k 2 n − k ) 時間と表せるので、全体の計算量は
∑ k = 1 n k 2 n − k ≤ 2 n ∑ k = 1 ∞ k 2 − k = 2 n ∑ s = 1 ∞ 2 − s + 1 = 2 n 2 2 \sum_{k=1}^{n}k2^{n-k}\leq 2^n \sum_{k=1}^{\infty}k2^{-k} =2^n\sum_{s=1}^{\infty}2^{-s+1}=2^n 2^2 k = 1 ∑ n k 2 n − k ≤ 2 n k = 1 ∑ ∞ k 2 − k = 2 n s = 1 ∑ ∞ 2 − s + 1 = 2 n 2 2
と評価して O ( 2 n ) O(2^n) O ( 2 n ) です。
実装
実装しました。 (2024-01-15 修正)
第 1 部での制作と比較すると、オーバーフローや除数による不正確さといった問題点が取り除かれたうえで、処理速度を維持することができました。
この実装では、正しく動作する範囲として 1 ≤ n ≤ 31 1\leq n\leq 31 1 ≤ n ≤ 31 を想定しています。
実装のポイント
メモリの戦略
第 1 部では一貫して、番号の大きい頂点から取り除くことで、アクセスする配列の範囲を狭めることを優先しました。今回は多倍長整数を扱うので 1 1 1 要素あたりのメモリが一定ではありません。
このため、頂点を取り除くときは番号の小さい頂点から順に取り除くことにして、配列を間引くように空間を減らすようにします。これにより、 k k k 頂点取り除いた後は 1 1 1 要素あたり 2 k 2^k 2 k ワードを使えるようになり、必要な k + 1 k+1 k + 1 ワードを確保できます。
多倍長整数の演算
ゼータ変換の長さを半分にするステップでは、
1 1 1 ワードの非負整数値 a , b a,b a , b と、 k k k ワードの非負整数値 x , y x,y x , y が与えられたとき、
k + 1 k+1 k + 1 ワードの非負整数値 a x − b y ax-by a x − b y を計算する
という計算が繰り返されます。 a x ax a x と b y by b y でそれぞれ繰り上がりを計算してから単に引き算することにより、引き算の繰り上がりの処理が簡潔になるようにしました。
また、彩色可能か判定するためにテーブルの値を足し引きするときは、符号・桁ごとに足し合わせてから繰り上がりを処理し、正の成分と負の成分それぞれの総和が一致するかどうか判定するように実装しました。
おわりに
第 1 部では地道な高速化で差を付けることにしていましたが、第 2 部では競プロでよく用いられる方法と比べて確かに高速な解法プログラムを作ることができました。ソースコード長もそんなに大きくありません。 Library Checker の問題設定において、競プロ用ライブラリとしてはかなり良いところに落ち着いているのではないでしょうか。