この記事は夏のブログリレー 12日目の記事です
概要
bitset を使ったワードサイズ倍の高速化は、ほかの方法では計算量を改善しにくい問題で有用です。N × N サイズの 0 -1 行列 A をサイズ N の bitset N 個により保持しているとしましょう。j を固定して A i , j に関する何らかの情報を得るためには、自明な実装ではワードサイズ倍の高速化を行えません。
しかし、転置を行うことにより単一のビット列の場合に帰着して、高速化を行うことができるようになります。この記事では、転置を高速に行うアルゴリズムおよび、転置を必要とせず、より高速に行える場合を紹介します。
以下では簡単のため正方行列のみを扱いますが、そうでない行列にも容易に適用可能です。
記法
添字は 0 -indexed とします。
N × N の 0 -1 行列 A の第 i 行を A i とし、N ビットのビット列として扱います。
ビット列 x の第 j ビットを x j と書きます。ビット列に対する ∧ , ∨ , ⊕ , ¬ は、それぞれ各ビットごとの AND、OR、XOR、NOT を表します。
word size を w とおいて w = Θ ( log N ) と仮定します。
説明を簡単にするため N を 2 冪と仮定し、l = log 2 N とします。
多重集合の和を ⊎ で表します。
目次
1. 高速な転置
問題
N × N の 0 -1 行列 A が与えられる。
各行は長さ N のビット列として保持されているものとする。
この行列の転置 A T を求めよ。
解法
転置では、( r , c ) にあるビットを ( c , r ) に移せばよいです。
行番号と列番号を 2 進数で見ると、これはそれぞれのビットを入れ替えることに対応します。
次のアルゴリズムが正当です。
アルゴリズムの正当性
最初、( r , c ) にあったビットの動きを観察しましょう。
r , c を 2 進数に展開して、r l − 1 r l − 2 … r 0 および c l − 1 c l − 2 … c 0 とします。
i = k の場合、ビットは次のように動きます。
[1] r k = c k のとき
変わりません。
[2] r k = 0 , c k = 1 のとき
このとき、r k = 1 , c k = 0 になります。また、r k = 0 , c k = 1 なので、r + 2 k , c − 2 k では繰り上げ、繰り下げが起こらず、r k , c k 以外は変わりません。
[3] r k = 1 , c k = 0 のとき
[2] で述べた交換のもう一方の要素となるため、r k = 0 , c k = 1 になります。
以上から最終的に r i , c i は全て swap されて転置されます。
具体例
N = 8 の場合の転置の過程を示しておきます。
クリックして開く
i = 2
( 000 , 000 ) ( 001 , 000 ) ( 010 , 000 ) ( 011 , 000 ) ( 100 , 000 ) ( 101 , 000 ) ( 110 , 000 ) ( 111 , 000 ) ( 000 , 001 ) ( 001 , 001 ) ( 010 , 001 ) ( 011 , 001 ) ( 100 , 001 ) ( 101 , 001 ) ( 110 , 001 ) ( 111 , 001 ) ( 000 , 010 ) ( 001 , 010 ) ( 010 , 010 ) ( 011 , 010 ) ( 100 , 010 ) ( 101 , 010 ) ( 110 , 010 ) ( 111 , 010 ) ( 000 , 011 ) ( 001 , 011 ) ( 010 , 011 ) ( 011 , 011 ) ( 100 , 011 ) ( 101 , 011 ) ( 110 , 011 ) ( 111 , 011 ) ( 000 , 100 ) ( 001 , 100 ) ( 010 , 100 ) ( 011 , 100 ) ( 100 , 100 ) ( 101 , 100 ) ( 110 , 100 ) ( 111 , 100 ) ( 000 , 101 ) ( 001 , 101 ) ( 010 , 101 ) ( 011 , 101 ) ( 100 , 101 ) ( 101 , 101 ) ( 110 , 101 ) ( 111 , 101 ) ( 000 , 110 ) ( 001 , 110 ) ( 010 , 110 ) ( 011 , 110 ) ( 100 , 110 ) ( 101 , 110 ) ( 110 , 110 ) ( 111 , 110 ) ( 000 , 111 ) ( 001 , 111 ) ( 010 , 111 ) ( 011 , 111 ) ( 100 , 111 ) ( 101 , 111 ) ( 110 , 111 ) ( 111 , 111 )
⇓
( 000 , 000 ) ( 001 , 000 ) ( 010 , 000 ) ( 011 , 000 ) ( 000 , 100 ) ( 001 , 100 ) ( 010 , 100 ) ( 011 , 100 ) ( 000 , 001 ) ( 001 , 001 ) ( 010 , 001 ) ( 011 , 001 ) ( 000 , 101 ) ( 001 , 101 ) ( 010 , 101 ) ( 011 , 101 ) ( 000 , 010 ) ( 001 , 010 ) ( 010 , 010 ) ( 011 , 010 ) ( 000 , 110 ) ( 001 , 110 ) ( 010 , 110 ) ( 011 , 110 ) ( 000 , 011 ) ( 001 , 011 ) ( 010 , 011 ) ( 011 , 011 ) ( 000 , 111 ) ( 001 , 111 ) ( 010 , 111 ) ( 011 , 111 ) ( 100 , 000 ) ( 101 , 000 ) ( 110 , 000 ) ( 111 , 000 ) ( 100 , 100 ) ( 101 , 100 ) ( 110 , 100 ) ( 111 , 100 ) ( 100 , 001 ) ( 101 , 001 ) ( 110 , 001 ) ( 111 , 001 ) ( 100 , 101 ) ( 101 , 101 ) ( 110 , 101 ) ( 111 , 101 ) ( 100 , 010 ) ( 101 , 010 ) ( 110 , 010 ) ( 111 , 010 ) ( 100 , 110 ) ( 101 , 110 ) ( 110 , 110 ) ( 111 , 110 ) ( 100 , 011 ) ( 101 , 011 ) ( 110 , 011 ) ( 111 , 011 ) ( 100 , 111 ) ( 101 , 111 ) ( 110 , 111 ) ( 111 , 111 )
i = 1
( 000 , 000 ) ( 001 , 000 ) ( 010 , 000 ) ( 011 , 000 ) ( 000 , 100 ) ( 001 , 100 ) ( 010 , 100 ) ( 011 , 100 ) ( 000 , 001 ) ( 001 , 001 ) ( 010 , 001 ) ( 011 , 001 ) ( 000 , 101 ) ( 001 , 101 ) ( 010 , 101 ) ( 011 , 101 ) ( 000 , 010 ) ( 001 , 010 ) ( 010 , 010 ) ( 011 , 010 ) ( 000 , 110 ) ( 001 , 110 ) ( 010 , 110 ) ( 011 , 110 ) ( 000 , 011 ) ( 001 , 011 ) ( 010 , 011 ) ( 011 , 011 ) ( 000 , 111 ) ( 001 , 111 ) ( 010 , 111 ) ( 011 , 111 ) ( 100 , 000 ) ( 101 , 000 ) ( 110 , 000 ) ( 111 , 000 ) ( 100 , 100 ) ( 101 , 100 ) ( 110 , 100 ) ( 111 , 100 ) ( 100 , 001 ) ( 101 , 001 ) ( 110 , 001 ) ( 111 , 001 ) ( 100 , 101 ) ( 101 , 101 ) ( 110 , 101 ) ( 111 , 101 ) ( 100 , 010 ) ( 101 , 010 ) ( 110 , 010 ) ( 111 , 010 ) ( 100 , 110 ) ( 101 , 110 ) ( 110 , 110 ) ( 111 , 110 ) ( 100 , 011 ) ( 101 , 011 ) ( 110 , 011 ) ( 111 , 011 ) ( 100 , 111 ) ( 101 , 111 ) ( 110 , 111 ) ( 111 , 111 )
⇓
( 000 , 000 ) ( 001 , 000 ) ( 000 , 010 ) ( 001 , 010 ) ( 000 , 100 ) ( 001 , 100 ) ( 000 , 110 ) ( 001 , 110 ) ( 000 , 001 ) ( 001 , 001 ) ( 000 , 011 ) ( 001 , 011 ) ( 000 , 101 ) ( 001 , 101 ) ( 000 , 111 ) ( 001 , 111 ) ( 010 , 000 ) ( 011 , 000 ) ( 010 , 010 ) ( 011 , 010 ) ( 010 , 100 ) ( 011 , 100 ) ( 010 , 110 ) ( 011 , 110 ) ( 010 , 001 ) ( 011 , 001 ) ( 010 , 011 ) ( 011 , 011 ) ( 010 , 101 ) ( 011 , 101 ) ( 010 , 111 ) ( 011 , 111 ) ( 100 , 000 ) ( 101 , 000 ) ( 100 , 010 ) ( 101 , 010 ) ( 100 , 100 ) ( 101 , 100 ) ( 100 , 110 ) ( 101 , 110 ) ( 100 , 001 ) ( 101 , 001 ) ( 100 , 011 ) ( 101 , 011 ) ( 100 , 101 ) ( 101 , 101 ) ( 100 , 111 ) ( 101 , 111 ) ( 110 , 000 ) ( 111 , 000 ) ( 110 , 010 ) ( 111 , 010 ) ( 110 , 100 ) ( 111 , 100 ) ( 110 , 110 ) ( 111 , 110 ) ( 110 , 001 ) ( 111 , 001 ) ( 110 , 011 ) ( 111 , 011 ) ( 110 , 101 ) ( 111 , 101 ) ( 110 , 111 ) ( 111 , 111 )
i = 0
( 000 , 000 ) ( 001 , 000 ) ( 000 , 010 ) ( 001 , 010 ) ( 000 , 100 ) ( 001 , 100 ) ( 000 , 110 ) ( 001 , 110 ) ( 000 , 001 ) ( 001 , 001 ) ( 000 , 011 ) ( 001 , 011 ) ( 000 , 101 ) ( 001 , 101 ) ( 000 , 111 ) ( 001 , 111 ) ( 010 , 000 ) ( 011 , 000 ) ( 010 , 010 ) ( 011 , 010 ) ( 010 , 100 ) ( 011 , 100 ) ( 010 , 110 ) ( 011 , 110 ) ( 010 , 001 ) ( 011 , 001 ) ( 010 , 011 ) ( 011 , 011 ) ( 010 , 101 ) ( 011 , 101 ) ( 010 , 111 ) ( 011 , 111 ) ( 100 , 000 ) ( 101 , 000 ) ( 100 , 010 ) ( 101 , 010 ) ( 100 , 100 ) ( 101 , 100 ) ( 100 , 110 ) ( 101 , 110 ) ( 100 , 001 ) ( 101 , 001 ) ( 100 , 011 ) ( 101 , 011 ) ( 100 , 101 ) ( 101 , 101 ) ( 100 , 111 ) ( 101 , 111 ) ( 110 , 000 ) ( 111 , 000 ) ( 110 , 010 ) ( 111 , 010 ) ( 110 , 100 ) ( 111 , 100 ) ( 110 , 110 ) ( 111 , 110 ) ( 110 , 001 ) ( 111 , 001 ) ( 110 , 011 ) ( 111 , 011 ) ( 110 , 101 ) ( 111 , 101 ) ( 110 , 111 ) ( 111 , 111 )
⇓
( 000 , 000 ) ( 000 , 001 ) ( 000 , 010 ) ( 000 , 011 ) ( 000 , 100 ) ( 000 , 101 ) ( 000 , 110 ) ( 000 , 111 ) ( 001 , 000 ) ( 001 , 001 ) ( 001 , 010 ) ( 001 , 011 ) ( 001 , 100 ) ( 001 , 101 ) ( 001 , 110 ) ( 001 , 111 ) ( 010 , 000 ) ( 010 , 001 ) ( 010 , 010 ) ( 010 , 011 ) ( 010 , 100 ) ( 010 , 101 ) ( 010 , 110 ) ( 010 , 111 ) ( 011 , 000 ) ( 011 , 001 ) ( 011 , 010 ) ( 011 , 011 ) ( 011 , 100 ) ( 011 , 101 ) ( 011 , 110 ) ( 011 , 111 ) ( 100 , 000 ) ( 100 , 001 ) ( 100 , 010 ) ( 100 , 011 ) ( 100 , 100 ) ( 100 , 101 ) ( 100 , 110 ) ( 100 , 111 ) ( 101 , 000 ) ( 101 , 001 ) ( 101 , 010 ) ( 101 , 011 ) ( 101 , 100 ) ( 101 , 101 ) ( 101 , 110 ) ( 101 , 111 ) ( 110 , 000 ) ( 110 , 001 ) ( 110 , 010 ) ( 110 , 011 ) ( 110 , 100 ) ( 110 , 101 ) ( 110 , 110 ) ( 110 , 111 ) ( 111 , 000 ) ( 111 , 001 ) ( 111 , 010 ) ( 111 , 011 ) ( 111 , 100 ) ( 111 , 101 ) ( 111 , 110 ) ( 111 , 111 )
ビット演算による高速化
さらに、ビット演算により各 i , r に対しての交換をまとめて行うことができます。
具体的には、次のような疑似コードで記述することができます。
for i = l - 1 to 0
s = 1 << i
m = ...
for r = 0 to N - 1
if (r >> i) & 1 :
continue
a = bs[r]
b = bs[r + s]
t = ((a >> s) ^ b) & m
a ^= t << s
b ^= t
bs[r] = a
bs[r + s] = b
特に、
t = ((a >> s) ^ b) & m
a ^= t << s
b ^= t
の部分の正当性について確認しておきます。
j の i ビット目が 0 であるとします。この時、交換すべきビットの組は a j + s と b j です。
ここで、a を s だけ右シフトしていることに注意して、t j = a j + s ⊕ b j となります。(& m はコード中の m の定義から干渉しないことに注意してください)
x = a j + s , y = b j とすると、次のように更新されており、a j + s と b j が swap されていることが確認できます。
a j + s → x ⊕ ( x ⊕ y ) = y
b j → y ⊕ ( x ⊕ y ) = x
交換する必要のないビットは & m により t の対応するビットが 0 となるため、値が変化しません。
以上からこのアルゴリズムは正当です。
計算量解析
ループは全体で O ( Nl ) 回実行され、ループ内で行われる操作はいずれも長さ N のビット列に対する演算であるため、1 回あたり O ( w N ) 時間です。
以上から、このアルゴリズムの計算量は O ( w N 2 l ) です。
詳しくは記述しませんが、w × w ごとに分割して処理することで O ( w N 2 l o g w ) 時間にすることもできます。
2. 転置なしの高速化
転置を行えば列方向の情報をビット列として扱えますが、
欲しい情報によっては転置そのものを行わず、より高速に処理できる場合があります。
2.1 find_first
問題
N × N の 0 -1 行列 A 、長さ N の整数列 ( p 0 , p 1 , … , p N − 1 ) が与えられる。ただし、0 ≤ p j ≤ N − 1 とする。
各行は長さ N のビット列として保持されているものとする。
各 j ( 0 ≤ j ≤ N − 1 ) に対して A i , j = 1 なる最小の i ( ≥ p j ) を求めよ。存在しなければ、それを報告せよ。
解法
i = 0 , 1 , … , N − 1 の順に、答えが i になるような j ( 0 ≤ j ≤ N − 1 ) を発見して、それを答えに反映することにします。
i を処理する時点で、まだ答えが決まっておらず、かつ p j ≤ i であるような j に対して S j = 1 となる 0 -1 列 S を持つことにします。最初は S = 0 として、i を処理する直前に、p j = i である全ての j について S j = 1 とします。
T = S ∧ A i としましょう。T j = 1 であることは、j の答えがまだ決まっておらず、p j ≤ i であり、かつ A i , j = 1 であることと同値であるため、j に対する答えは i であることを示しています。
従って、T j = 1 である全ての j について答えを i として、S ← S ∧ ¬ T とすればよいです。
T のビットが立っている位置を 1 つ探すのは O ( w N ) 時間でできます。(例えば、GCC なら std::bitset の _Find_first を使えます)各 j について T の j ビット目が立つのは高々 1 回ですから、T のビットが立っている位置を探すパートは O ( w N 2 ) 時間で可能です。T を計算するのも、N 回 0 -1 列のビット演算をするだけですから、結局 O ( w N 2 ) 時間です。
2.2 popcount
問題
N × N の 0 -1 行列 A が与えられる。
各行は長さ N のビット列として保持されているものとする。
各 j = 0 , 1 , … , N − 1 について、A i , j = 1 なる i の個数を求めよ。
解法
各列の popcount だけが欲しいのであれば転置する必要はなく、Carry-Save Adder という手法で、より高速に解くことができます。
まずは、A が N × w の 0 -1 行列である場合を解きます。
w 個の列の答えをそれぞれ独立に持つ代わりに、w 列をまとめて管理してみましょう。
l = log 2 N として、j = 0 , 1 , … , l に対して、w ビット整数を高々 2 個持つ多重集合 bucket 0 , bucket 1 , … , bucket l を用意します。
i = 0 , 1 , … , N − 1 の順に A i を追加して、順次 bucket を更新することにします。A 0 , A 1 , … , A i − 1 を追加した時点で、次の条件が満たされるようにしておきます。
r = 0 ∑ i − 1 A r , k = j ∑ 2 j x ∈ bucket j ∑ x k
が成立する。
各 j について、∣ bucket j ∣ ≤ 2 が成立する。
一つも追加していない場合は、bucket j = ∅ とすれば条件を満たします。
X = A i を追加するとき、まず bucket 0 ← bucket 0 ⊎ { X } としたうえで、modify ( 0 ) を行います。
一般に modify ( j ) を次のように定義します。
modify ( j )
∣ bucket j ∣ ≤ 2 のとき
∣ bucket j ∣ = 3 のとき
bucket j の要素を a , b , c とします。
low = a ⊕ b ⊕ c および high = ( a ∧ b ) ∨ ( b ∧ c ) ∨ ( c ∧ a ) とします。
bucket j ← { low } とし、bucket j + 1 ← bucket j + 1 ⊎ { high } として、modify ( j + 1 ) を行います。
アルゴリズムの正当性
まずは、 modify ( j ) によって、∑ j 2 j ( ∑ x ∈ bucket j x k ) が変化しないことを確認します。
一般に x , y , z ∈ { 0 , 1 } に対して、
x + y + z = ( x ⊕ y ⊕ z ) + 2 (( x ∧ y ) ∨ ( y ∧ z ) ∨ ( z ∧ x ))
が成立します。つまり、各 k ( 0 ≤ k < w ) について、
a k + b k + c k = low k + 2 high k
が成立します。
よって、(∣ bucket j ∣ = 3 であった時に) bucket j にあった a , b , c による 2 j ( a k + b k + c k ) という寄与は、2 j low k + 2 j + 1 high k に置き換わりますが、両者は等しいです。
したがって、modify ( j ) は ∑ j 2 j ( ∑ x ∈ bucket j x k ) を変えません。
A i を bucket 0 に追加すると ∑ j 2 j ( ∑ x ∈ bucket j x k ) に A i , k が加わり、また modify ( 0 ) を行っても上に示した事実から∑ j 2 j ( ∑ x ∈ bucket j x k ) は変化しないため、処理後には
r = 0 ∑ i A r , k = j ∑ 2 j x ∈ bucket j ∑ x k
が成立しています。
また、確かに modify が終了した時点では全ての j について ∣ bucket j ∣ ≤ 2 が成立しています。
最後に、bucket は bucket l まで持っておけば十分であることを示します。
S = ∑ j 2 j ∣ bucket j ∣ とします。A i まで追加した時点では S = i + 1 です。また、modify の途中でも S は変化しません。したがって処理中の任意の時点で S ≤ N です。
よって、bucket j = ∅ なら 2 j ≤ S ≤ N であり j ≤ l が成立するため、bucket l まで持っておけば十分です。
以上から、このアルゴリズムが正しく動くことが分かりました。
計算量解析
計算量を解析します。A i を bucket 0 に追加するたびに ∑ j ∣ bucket j ∣ は 1 増加します。一方、∣ bucket j ∣ = 3 のときの modify ( j ) では、∑ j ∣ bucket j ∣ は 1 減少します。
∑ j ∣ bucket j ∣ は最初は 0 であり、常に 0 以上です。また、増加する回数はちょうど N 回なので、∣ bucket j ∣ = 3 の場合の処理が行われる回数は高々 N 回です。
modify ( j ) が呼ばれるのは modify ( j − 1 ) によるものか、A i を追加した時に呼ばれる modify ( 0 ) のどちらかです。前者は先の議論により高々 N 回、後者は明らかに N 回ですから、modify が呼ばれる回数は高々 2 N 回です。
各更新で行われる演算はいずれも w ビット整数のビット演算であるため、O ( 1 ) 時間で実行できます。従って、上の処理全体は O ( N ) 時間で行えます。
処理後は、各列 k の答えを
j = 0 ∑ l 2 j x ∈ bucket j ∑ x k
として復元できます。これは O ( wl ) 時間で行えます。
以上から、∣ A i ∣ = w の場合の計算量は O ( N + wl ) = O ( N + w log N ) です。
∣ A i ∣ = N の場合は N / w 個のブロックに分割してそれぞれにこのアルゴリズムを適用すれば、全体で O ( w N 2 + Nl ) 時間のアルゴリズムが得られます。
w = Θ ( log N ) という仮定の下では、O ( w N 2 ) 時間です。
3. 適用例
https://atcoder.jp/contests/abc471/tasks/abc471_g
概要
0 , 1 , … , K − 1 の各記号について母音かどうかが定められており、長さ N の記号列 A が与えられます。
各 k = 0 , 1 , … , K − 1 について、全ての A i を ( A i + k ) mod K に置き換えたときの、母音からなる極大な連続区間の個数を求めよ。
解法
母音を 1 、子音を 0 に対応させます。
0 -1 列 B の 1 の極大区間の個数は 2 [ B 0 = 1 ] + [ B N − 1 = 1 ] + # { B i = B i + 1 } です。第 1 項、第 2 項は容易に数えられるので、第 3 項を考えます。
0 , 1 , … , K − 1 が母音かどうかの 0 -1 列を V とし、b i , k = V ( A i + k ) mod K として長さ K の 0 -1 列 b i を定義します。b i は V の巡回シフトです。従って、b i ⊕ b i + 1 を O ( w K ) 時間で計算できます。
固定された k に対して、第 3 項は、ビット列 b 0 ⊕ b 1 , b 1 ⊕ b 2 , … であって、k ビット目が立っているものの個数と一致します。
従って、転置を用いるアルゴリズムにより O ( w N K l o g w ) 時間、Carry-Save Adder を用いるアルゴリズムにより O ( w N K + K l ) 時間で計算することができます。w = Θ ( log N ) という仮定の下では、O ( w N K ) 時間です。
終わりに
明日の投稿者は@Alt--er さんです