転倒数とBinary Indexed Tree
転倒数とBITについて知ったのでまとめておく.
以下では自然数配列と普通の大小関係について考える.転倒数とは,組
の個数,すなわち項よりも左にあるより大きい項の数の和[1].これを高速に求めたい.
解
二重ループで定義通りに計算:
let count_inv a =
let open Array in
let a = mapi (fun i v -> i,v) a in
fold_left (fun sum (i,v) ->
fold_left (fun sum (j,w) ->
if j<i && w>v then
(a.(j) <- j,v; a.(i) <- i,w; sum+1)
else sum) sum a) 0 a
見てわかるとおりこの計算量は.
ところが実際はこの問題はで解くことができる.必要なのは元配列の区間について「値より大きい要素の数」を求めるクエリに答える機能.
BITについて
これを実現する方法の一つにBinary Indexed Tree (Fenwick Tree) の利用がある.BITは次の操作をで可能とするデータ構造である(以下の参考文献):
- を求める(もとして求まる)
これはサイズの配列を用意することで実装可能.原理については参考文献がわかりやすいので基本的に省略するが,LSBがで求まることについて脚注で軽く説明しておく[2].
具体的な実装は次[3]:
let rec fix f x = f (fix f) x
module BIT = struct
let lsb i = i land -i
let sum i b = (* 0-indexed; Σv[0,i-1] *)
fix (fun f i s ->
if i>0 then f (i - lsb i) (s + b.(i)) else s) i 0
let sum_closed i = sum (i+1) (* 0-indexed; Σv[0,i] *)
let sum_interval l r b =
(* 0-indexed; Σv[l,r] = Σv[0,r+1-1] - Σv[0,l-1] *)
if l>r then 0 else sum (r+1) b - sum l b
let add i x b = (* 0-indexed; v[i]+=x *)
fix (fun f i -> if i<Array.length b then (
b.(i) <- b.(i) + x; f (i + lsb i))) (i+1)
let of_array v = let b = Array.make (Array.length v + 1) 0 in
Array.iteri (fun i v -> add i v b) v; b
let make n = Array.make (n+1) 0
let reconst b = Array.init (Array.length b-1) (
fun i -> sum_interval i i b)
end
なおadd
の対象は元配列のほうなのでデバッグ時に混乱しないように注意(私はデバッグ用にからを復元する関数reconst
を作っている).
BITの利用
今回答えるべきクエリはに対して「に現れる値より大きい要素の数」だった.ここである配列の番目にその値の出現回数を記録しておくことを考えると,は以上以下の値の出現回数の和となっている.したがってを見ることで「より大きい要素数」を求めることができる.
BITを用いるとこれが,すなわちで計算できる.の追加とこの計算をに対して繰り返すことで,時間計算量で転倒数を得ることが可能.
が大きいときはBITの空間計算量が厳しくなるが,を座標圧縮しておけば空間計算量かつ時間計算量にできる.
例
の転倒数を求める.BITは(0-indexed).
- を追加:..
- を追加:..
- を追加:..[4]
- を追加:..
- を追加:..
以上より転倒数は5となる.コード(座標圧縮なし):
let a = [|3;1;5;3;2|]
let mx = Array.fold_left max 0 a
let b = BIT.make (mx+1)
let () = print_int @@
Array.fold_left (fun sum v ->
BIT.add v 1 b;
sum + BIT.sum_interval (v+1) mx b) 0 a
ジャッジ
競技プログラミングサイトAtCoderで: Chokudai SpeedRun 001: J - 転倒数.座標圧縮しなくても通る.座標圧縮版.
wikipediaでは右にある小さい項の数を数えているが,視点を逆にすれば同じこと. ↩︎
2の補数を思い出せばは(のビットを反転させたもの+1)となる.以下ビット反転をと書く.のLSBを桁目(0-indexed)とすると,は桁目までは0, 桁目は1となっている.よっては桁目まで1, 桁目で0となる.ここに1を足せばが求まる:1を足すと繰り上がりが起こり,桁目まで0,桁目が1となる.これととの論理積を取る:桁目まではともに0であるから0.桁目も1で等しいから1.桁目以降のはと変わらないから0となる.したがって0....010...0の形となり,10進法でであるからLSBが求まる.図にすると次:
↩︎x = 011001001110000 ~x = 100110110001111 -x = 100110110001111 +1 = 100110110010000 x&-x = 000000000010000
定義が面倒なのでfix関数を使ってしまったものの,少なくとも他の例題ではきちんと再帰関数を定義したほうが速かった. ↩︎
このようにとなる場合のために領域確保しておく. ↩︎