問題
長さ $N$ の数列 $a_1,\dots,a_N$ が与えられる($N$ は2べき)。Batcherのビトニックソートネットワークを用いてこの数列を昇順に整列せよ。
ビトニックソートネットワークは「比較器(compare-exchange)」だけを固定の順序で並べた回路であり、入力の値によらず同じ比較器列を適用すれば必ずソートされる(並列ハードウェアでのソートに応用される)。
出力として (1) 使用した比較器の総数、(2) ソート後の数列、の2つを求めよ。
入力形式
N
a_1 a_2 ... a_N
制約
$N$ は2べき
$1 \le N \le 4096$
$-10^9 \le a_i \le 10^9$
入出力例
入力例1
4
5 3 8 1
出力例1
6
1 3 5 8
比較器の総数は $\frac{N}{4}\log_2 N(\log_2 N+1) = \frac{4}{4}\times2\times3=6$ と一致する。
概念図: N=4 のソーティングネットワーク配線図
ヒント(段階的開示)
ヒント1: 方向性
「ビトニック数列」(前半が単調増加、後半が単調減少になっている数列)は、特別な比較の仕方で$O(\log N)$段の比較器だけで完全にソートできる。任意の数列を2つのビトニック数列の半分ずつに分けて再帰処理し、最後に1回のビトニックマージで合体する構造を考えよ。
ヒント2: アプローチ
bitonic_sort(lo,cnt,dir)は前半を昇順・後半を降順に再帰ソートしてビトニック数列を作り、最後にbitonic_mergeで方向dirに整列する。bitonic_merge(lo,cnt,dir)は前半iと後半i+cnt/2を比較器でペアにし、その後前半・後半をそれぞれ再帰的にマージする。比較器総数は$T(N)=\frac{N}{4}\log_2N(\log_2N+1)$。ヒント3: 誘導(コード骨格)
def bitonic_merge(a, lo, cnt, direction):
if cnt > 1:
k = cnt // 2
for i in range(lo, lo + k):
compare_exchange(a, i, i + k, direction)
bitonic_merge(a, lo, k, direction)
bitonic_merge(a, lo + k, k, direction)
def bitonic_sort(a, lo, cnt, direction):
if cnt > 1:
k = cnt // 2
bitonic_sort(a, lo, k, True)
bitonic_sort(a, lo + k, k, False)
bitonic_merge(a, lo, cnt, direction)
模範解答 (Python)
import sys
def solve():
data = sys.stdin.read().split()
n = int(data[0])
a = [int(x) for x in data[1:1 + n]]
ops = 0
def compare_exchange(i, j, direction):
nonlocal ops
ops += 1
if (a[i] > a[j]) == direction:
a[i], a[j] = a[j], a[i]
def bitonic_merge(lo, cnt, direction):
if cnt > 1:
k = cnt // 2
for i in range(lo, lo + k):
compare_exchange(i, i + k, direction)
bitonic_merge(lo, k, direction)
bitonic_merge(lo + k, k, direction)
def bitonic_sort(lo, cnt, direction):
if cnt > 1:
k = cnt // 2
bitonic_sort(lo, k, True)
bitonic_sort(lo + k, k, False)
bitonic_merge(lo, cnt, direction)
bitonic_sort(0, n, True)
print(ops)
print(' '.join(map(str, a)))
solve()
計算量: 比較器総数 $T(N)=\frac{N}{4}\log_2N(\log_2N+1)=O(N\log^2N)$。データ依存分岐なしで並列実行可能。
Step-by-Step 解説
1ビトニック数列とは
単調増加後に単調減少する(あるいはその巡回シフト)数列。特別な比較の当て方で$O(\log N)$段だけで完全ソート可能。
単調増加後に単調減少する(あるいはその巡回シフト)数列。特別な比較の当て方で$O(\log N)$段だけで完全ソート可能。
2bitonic_mergeの仕組み
前半・後半を同じオフセットでペアにして比較器を当てると、両方が半分の長さのビトニック数列になり前半≤後半(または逆)が保証される(ビトニックマージ定理)。
前半・後半を同じオフセットでペアにして比較器を当てると、両方が半分の長さのビトニック数列になり前半≤後半(または逆)が保証される(ビトニックマージ定理)。
3bitonic_sortの分割統治
前半を昇順・後半を降順に再帰ソートすると全体が1つのビトニック数列になり、あとはbitonic_mergeで目的方向に整列する。
前半を昇順・後半を降順に再帰ソートすると全体が1つのビトニック数列になり、あとはbitonic_mergeで目的方向に整列する。
4再帰の停止条件と比較器総数
cnt==1で停止。漸化式$T(N)=2T(N/2)+\frac{N}{2}\log_2N$を解くと$T(N)=\frac{N}{4}\log_2N(\log_2N+1)$。5ソーティングネットワークとしての意義
比較する添字ペアが入力値に依存せず事前決定できる(データ非依存)ため、並列ハードウェアや秘密計算でのソートに応用される。
比較する添字ペアが入力値に依存せず事前決定できる(データ非依存)ため、並列ハードウェアや秘密計算でのソートに応用される。
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| 前半・後半を両方昇順でソート | ビトニック数列にならずbitonic_mergeの前提が崩れる | 前半はTrue、後半はFalseで再帰する |
| $N$が2べきでない入力にそのまま適用 | 分割が整数比で半分にならない | $N$が2べきであることを前提とする(一般化は番兵挿入で対応) |
| 比較器のカウントをbitonic_mergeの外側だけで数える | 再帰内部の比較器も数える必要がある | compare_exchangeの呼び出し自体でカウンタをインクリメントする |
| directionの引き継ぎを誤って固定にする | bitonic_merge内の再帰呼び出しは同じdirectionを渡す設計 | 再帰呼び出しでも同じdirectionをそのまま渡す |
次のステップ
- 発展: $N$が2べきでない場合の拡張(番兵挿入、または奇偶マージソートによる一般化)
- 次回予告: Zobrist Hashing(盤面差分XORハッシュ・重複状態検出)