問題
バイト列 $S$(旧版)と $T$(新版)、パラメータ $mn, bits, mx$ が与えられる。$G[c]=\mathrm{sm}(c)$($c=0..255$、$\mathrm{sm}$ は splitmix64 の finalizer)、$\mathrm{MASK}$ を **上位 $bits$ ビットだけが 1** の 64 ビット値とし、バイト列 $D$(長さ $n$)を次の手順で塊(チャンク)に切る。
- 現在位置 $s$ から、残り $n-s\le mn$ なら残り全部を 1 塊にして終わる
- そうでなければ $fp=0$ とし、$i=s+mn,\ s+mn+1,\dots,\ \min(n,s+mx)-1$ の順に
$fp\leftarrow(2\cdot fp+G[D_i])\bmod 2^{64}$ を計算し、$fp\ \&\ \mathrm{MASK}=0$ となった最初の $i$ で $D[s..i]$ を 1 塊とする
- 見つからなければ長さ $\min(n-s,mx)$ で切る。次の $s$ は塊の直後
$S$ を切った塊をすべて保存済みとする。$T$ を切った塊を先頭から順に見て、保存済みの塊と内容が一致しないものを新たに保存する(保存したものは以後保存済み)。$S$ の塊数、$T$ の塊数、新たに保存したバイト数の合計を出力せよ。
入力形式
mn bits mx
S
T
制約
| # | 条件 |
|---|---|
| 1 | $0\le mn<mx\le 10^6$、$1\le bits\le 30$ |
| 2 | $1\le |S|,|T|\le 5\times10^5$、$S,T$ は ASCII の !〜~(空白を含まない印字可能文字)からなる |
入出力例
入力例1
4 2 16
djicfjhjbjaheiddhiihgcdcigabcjaeaehjgggjhcfbachdegegigjfijgj
djicfjhjbjaheiddhiihgcdciXgabcjaeaehjgggjhcfbachdegegigjfijgj出力例1
10 10 24($T$ は $S$ の 26 文字目に X を 1 文字挿入したもの。塊の長さは $S$: 5,5,5,11,7,5,5,5,10,2、$T$: 5,5,5,11,5,8,5,5,10,2。挿入を含む塊 dhiihgcdciX と、境界がずれた gabcj・aeaehjgg の 3 塊(11+5+8=24 バイト)だけが新規で、その後は gjhcf から境界が再び一致する。同じ入力を 6 バイト固定長で切ると、挿入位置以降が全部ずれて 61 バイト中 37 バイトが新規になる)
Gear ハッシュによる境界判定は Xia ら「FastCDC: a Fast and Efficient Content-Defined Chunking Approach for Data Deduplication」(USENIX ATC 2016、https://www.usenix.org/conference/atc16/technical-sessions/presentation/xia 、2026-09-25 参照)の Algorithm 1(最小長の読み飛ばし+上位ビットのマスク)を簡略化したもの。論文はオープンソースの Rabin 方式 CDC より約 10 倍速いと報告している。
概念図:内容定義チャンクと固定長の比較
ヒント(クリックで段階的に開示)
ヒント1(方向性)
固定長で切ると、先頭に 1 バイト入っただけで以降の全ての塊の中身がずれ、重複排除が全滅する。境界を「この位置の直前の数十バイトの内容」だけで決めれば、編集から離れた場所の境界は同じ内容の前で同じように引かれ、塊が一致に戻る。
ヒント2(アプローチ)
Gear ハッシュ $fp\leftarrow 2fp+G[c]$ では、64 ステップ前のバイトは左シフトで押し出される。よって $fp$ の 上位ビットは直近 64 バイト、下位 $t$ ビットは直近 $t$ バイトにしか依存しない(ローリングハッシュを 1 回のシフトと加算で実現)。上位 $bits$ ビットが全部 0 になる確率は $2^{-bits}$ なので、塊の長さはおよそ $mn+2^{bits}$。
ヒント3(誘導)
mask = ((1 << bits) - 1) << (64 - bits)
fp = 0; cut = end
for i in range(s + mn, end):
fp = ((fp << 1) + GEAR[data[i]]) & M64
if fp & mask == 0:
cut = i + 1; break
# 保存: set(bytes) に入れ、T の塊が無ければ len を足して追加模範解答 (Python)
模範解答を表示する(まず自力で解いてから)
import sys
M64 = (1 << 64) - 1
def splitmix64(x):
x = (x + 0x9E3779B97F4A7C15) & M64
x = ((x ^ (x >> 30)) * 0xBF58476D1CE4E5B9) & M64
x = ((x ^ (x >> 27)) * 0x94D049BB133111EB) & M64
return x ^ (x >> 31)
GEAR = [splitmix64(c) for c in range(256)]
def chunks(data, mn, bits, mx):
mask = ((1 << bits) - 1) << (64 - bits) # 上位 bits ビット(直近 64 バイトに依存)
n = len(data)
out = []
s = 0
while s < n:
rem = n - s
if rem <= mn:
out.append(data[s:]); break
end = s + min(rem, mx)
fp = 0
cut = end
for i in range(s + mn, end): # 先頭 mn バイトは読み飛ばす(cut-point skipping)
fp = ((fp << 1) + GEAR[data[i]]) & M64
if fp & mask == 0:
cut = i + 1
break
out.append(data[s:cut])
s = cut
return out
def main():
it = sys.stdin.buffer.read().split()
mn, bits, mx = int(it[0]), int(it[1]), int(it[2])
S, T = it[3], it[4]
cs, ct = chunks(S, mn, bits, mx), chunks(T, mn, bits, mx)
stored = set(cs)
new_bytes = 0
for c in ct:
if c not in stored:
new_bytes += len(c)
stored.add(c) # T の中で繰り返す塊も 1 回だけ保存
print(len(cs), len(ct), new_bytes)
main()
計算量: 時間 $O(|S|+|T|)$(各バイトを高々 1 回ハッシュに通す。塊の bytes を辞書に入れる費用も長さの総和)、空間 $O(|S|+|T|)$。実務では塊そのものではなく SHA-256 などの指紋を索引に入れる。
Step-by-Step 解説
固定長 $L$ では境界が $L,2L,\dots$ の 位置 に固定される。先頭に 1 バイト入ると全ての塊が 1 バイトずつずれ、中身が 1 つも一致しない。実測(CPython 標準ライブラリの .py を連結した 4 MB): 10 KiB 固定長で先頭 1 バイト挿入 → 新規 4,000,001 B(100%)。
境界の条件を「直近のバイト列のハッシュが特定の形」にすると、編集点から 64 バイト以上離れた場所の境界判定は旧版と同じ入力を見る。編集を含む塊と、その影響で位置がずれた 1〜2 塊が変わるだけで、その後は 同じ内容の前で同じ境界 に戻る(入力例1 の gjhcf)。実測で先頭 1 バイト挿入 → 新規 2,205 B(0.06%)。
Rabin 指紋は多項式の剰余で、1 バイトごとに「出て行くバイト」の寄与を引く表引きも要る。Gear は $fp=2fp+G[c]$ だけで、出て行くバイトは 左シフトで自然に消える(64 ビット整数の上から溢れる)。代わりに窓の長さは 64 バイト固定で、しかも 下位ビットほど窓が短い。
下位 13 ビットを見るマスクだと、境界は直近 13 バイトだけで決まる。ソースコードのように同じ 13 バイト(字下げの空白など)が繰り返すデータでは、条件を満たさない文脈がまとめて続き、塊が伸びる。
| マスク | 塊数 | 平均長 | 上限 64 KiB 到達 |
|---|---|---|---|
| 上位 13 ビット(本問、窓 64 バイト) | 384 | 10,417 B(期待 $mn+2^{13}=10{,}240$) | 0 |
| 下位 13 ビット(窓 13 バイト) | 276 | 14,493 B | 2 |
FastCDC 論文が「マスクの 1 を上位側に散らす(zero padding)」と書いているのはこの窓を広げるため。
各塊の先頭 $mn$ バイトはハッシュすら計算しない。平均 $mn+2^{bits}$ のうち $mn$ 分の計算が消えるので速い(本問の条件で 20%)。代わりに境界の決定が「塊の開始位置」に依存するようになり、厳密な内容定義性が少し落ちる。実測では再同期に支障は出なかった(Step 2 の 0.06%)。
迷った他案と採らなかった理由
| 案 | 採否 | 理由 |
|---|---|---|
| 固定長チャンク | 比較用 | 最速だが、挿入・削除で全滅(実測 99.7〜100% が新規) |
| Rabin 指紋の CDC(LBFS 2001、restic など) | 不採用 | 内容定義性は同等だが 1 バイトあたりの演算が多い。FastCDC は Rabin 系より約 10 倍速いと報告(論文値、本セッションで Rabin は実装していない) |
| Gear + 上位ビットマスク + 最小長の読み飛ばし(本解) | 採用 | 1 バイトあたりシフト・加算・AND 各 1 回 |
| FastCDC の正規化チャンキング(塊が短い間は厳しいマスク、長くなったら緩いマスク) | 扱わない | 塊の長さの分布を平均の周りに寄せ、重複排除率を保ったまま平均長を上げられる。本問の学習目的(再同期の原理)には不要。塊の長さのばらつきが索引サイズの問題になったら入れる |
| 差分(rsync のローリングチェックサム) | 不採用 | 2 版の間の差分転送には最適だが、多数の版・多数のファイルにまたがる重複排除の索引には向かない |
前提が変わったら: 平均長 $2^{bits}$ は「重複排除率」と「索引の大きさ」のトレードオフ。小さくすると細かく一致を拾えるが、塊の数(=指紋の数)が増える。何 TB を何 GB の索引で扱うか、という運用の制約から逆算して決める(本問の 10 KiB は 4 MB の実験用の値で、実運用の推奨値ではない)。
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| 下位ビットでマスクする | Gear の下位 $t$ ビットは直近 $t$ バイトにしか依存しない | 上位 $bits$ ビット ((1<<bits)-1) << (64-bits) |
fp を塊の途中で持ち越す/塊ごとに 0 にしない | 本問の定義は塊ごとに $fp=0$ から | 塊の開始で fp = 0 |
& M64 を忘れる | Python の int は溢れず、古いバイトが押し出されない(窓が無限になる) | 毎ステップ & M64 |
| 条件成立の位置 $i$ を含めずに切る | 定義は $D[s..i]$($i$ を含む) | cut = i + 1 |
| $T$ の中で繰り返す塊を毎回数える | 1 回保存すれば以後は保存済み | 新規を保存したら stored.add(c) |
次のステップ
- 発展1: 正規化チャンキング($mn\le$ 長さ $<$ 平均 では $bits+2$ ビット、平均以上では $bits-2$ ビット)を入れ、塊の長さの標準偏差と重複排除率を本問版と比べる
- 発展2: 100 箇所のランダム挿入で新規が 30.4%(実測)になった。平均長を 2 KiB/10 KiB/64 KiB と変えて、新規バイト数と塊数の積(索引コスト)の関係をグラフにする
- 扱わなかった範囲: 指紋の衝突(SHA-256 で実質無視)、塊の圧縮と暗号化、削除された塊のガベージコレクション
検証ログ(本セッションで実行した数値)
- 入力例1: 塊の長さは上記のとおり、出力
10 10 24。6 バイト固定長なら新規 37 バイト(手計算ではなくスクリプトで計算) - 定義どおりの素朴実装(各候補長について $fp$ を最初から計算し直す $O(L^2)$ 版)との一致(実測): ランダム 300 ケースで不一致 0
- 実データ(実測): CPython 3.14 標準ライブラリ直下の
.py155 本を連結した先頭 4,000,000 B、$mn=2048, bits=13, mx=65536$。塊 384、平均 10,417 B、中央値 7,568 B、最大 64,419 B、上限到達 0、0.40 s(9.9 MB/s) - 編集への強さ(実測): 先頭 1 バイト挿入 → 固定長 4,000,001 B(100.0%)vs CDC 2,205 B(0.06%)。ランダム 100 箇所に 1〜20 バイト挿入 → 固定長 3,990,772 B(99.7%)vs CDC 1,217,007 B(30.42%。1 編集あたり約 1.2 塊)
- 下位ビットマスク(実測): 塊 276、平均 14,493 B、上限到達 2(Step 4 の表)
- 出所ラベル: 塊数・バイト数・秒数は実測(Python 3.14.7)。「Rabin 系より約 10 倍速い」は FastCDC 論文の要旨の記述(本セッションで Rabin 版は実装・計測していない)
- 検証スクリプト: スクラッチパッドの
d160/q3_test.py