B - 本棚の並び順 / Order of Books on the Bookshelf 解説 by admin
Claude 4.6 Opus (Thinking)概要
長さ \(N\) の数列 \(A\) の中に、長さ \(M\) の数列 \(B\) と完全に一致する連続部分列が存在するかを判定し、存在すれば最小の開始位置を求める問題です。いわゆる「部分文字列検索」の数列版です。
考察
素朴なアプローチとその問題点
最も単純な方法は、\(A\) のすべての開始位置 \(i\)(\(1 \le i \le N - M + 1\))について、\(A_i, A_{i+1}, \dots, A_{i+M-1}\) と \(B_1, B_2, \dots, B_M\) を1要素ずつ比較することです。
- 開始位置の候補は最大 \(N - M + 1\) 個
- 各候補について最大 \(M\) 回の比較
よって最悪計算量は \(O(N \times M)\) となります。\(N, M\) がともに \(5 \times 10^5\) の場合、最大 \(2.5 \times 10^{11}\) 回の演算が必要になり、TLE(時間超過) になります。
高速化のアイデア:ローリングハッシュ
そこで Rabin-Karp 法(ローリングハッシュ) を使います。基本的なアイデアは次の通りです:
- 数列 \(B\) 全体のハッシュ値を計算する
- \(A\) の長さ \(M\) の連続区間のハッシュ値を、スライドさせながら \(O(1)\) で更新する
- ハッシュ値が一致した場合のみ、実際に要素を1つずつ比較して確認する
ハッシュが一致しなければ即座にスキップできるので、ほとんどの位置を \(O(1)\) で棄却でき、全体として高速に動作します。
アルゴリズム
ローリングハッシュの仕組み
数列 \(X_1, X_2, \dots, X_M\) に対して、基数 \(b\) と法 \(p\) を用いたハッシュ値を次のように定義します:
\[H(X) = (X_1 \cdot b^{M-1} + X_2 \cdot b^{M-2} + \cdots + X_M \cdot b^0) \mod p\]
スライド更新:\(A_i, \dots, A_{i+M-1}\) のハッシュから \(A_{i+1}, \dots, A_{i+M}\) のハッシュへの更新は次の式で \(O(1)\) です:
\[H_{\text{new}} = (H_{\text{old}} \times b + A_{i+M} - A_i \times b^M) \mod p\]
ハッシュ衝突への対策
1つのハッシュだけでは異なる数列が同じハッシュ値を持つ(衝突する)可能性があります。本コードでは 2つの独立なハッシュ(異なる基数・法)を使い、衝突確率を極めて小さくしています:
- ハッシュ1:\(b = 131\), \(p = 2^{61} - 1\)(メルセンヌ素数)
- ハッシュ2:\(b = 137\), \(p = 2^{31} - 1\)(メルセンヌ素数)
両方のハッシュが一致した場合にのみ、実際の要素比較で最終確認を行います。
処理の流れ
- \(B\) のハッシュ値(2種類)を計算
- \(A\) の最初の長さ \(M\) の区間のハッシュ値を計算し、\(B\) と比較
- 開始位置を1つずつ右にずらしながらハッシュを \(O(1)\) で更新し、一致判定
- ハッシュが一致したら要素を直接比較し、本当に一致すればその位置を出力
- 最後まで見つからなければ \(-1\) を出力
具体例
\(A = [3, 1, 4, 1, 5]\), \(B = [4, 1, 5]\)(\(N=5, M=3\))の場合:
| 開始位置 \(i\) | 区間 | ハッシュ一致? | 結果 |
|---|---|---|---|
| 1 | \([3,1,4]\) | No | スキップ |
| 2 | \([1,4,1]\) | No | スキップ |
| 3 | \([4,1,5]\) | Yes → 要素比較も一致 | 答え: 3 |
計算量
- 時間計算量: \(O(N + M)\)(平均的な場合。ハッシュ衝突がほぼ起きないため、要素比較のコストは無視できる。最悪でも \(O(NM)\) だが実質発生しない)
- 空間計算量: \(O(N + M)\)(入力の数列を保持するため)
実装のポイント
法にメルセンヌ素数を使用: \(2^{61}-1\) や \(2^{31}-1\) は素数であり、剰余演算の性質が良く衝突が起きにくい
ダブルハッシュ: 2つの独立なハッシュを使うことで、衝突確率を \(O(1/p_1 \cdot 1/p_2)\) 程度まで下げている
ハッシュ一致時の実比較: ハッシュだけでは正しさが保証されないため、一致した場合は必ず全要素を直接比較する(安全策)
入力の高速読み込み:
sys.stdin.buffer.read()を使い、Python での入力ボトルネックを軽減している\(b^M \mod p\) の事前計算: ローリング更新で毎回使う \(b^M\) を
pow(BASE, M, MOD)で事前に求めておくソースコード
import sys
def solve():
input_data = sys.stdin.buffer.read().split()
idx = 0
N = int(input_data[idx]); idx += 1
M = int(input_data[idx]); idx += 1
A = input_data[idx:idx+N]; idx += N
B = input_data[idx:idx+M]; idx += M
if M > N:
print(-1)
return
# Rabin-Karp style matching with rolling hash
# Convert to integer arrays
# Use two hashes to reduce collision probability
MOD1 = (1 << 61) - 1
BASE1 = 131
MOD2 = (1 << 31) - 1
BASE2 = 137
def mod_mul(a, b, mod):
return a * b % mod
# Compute hash of B
hash_b1 = 0
hash_b2 = 0
for j in range(M):
v = int(B[j])
hash_b1 = (hash_b1 * BASE1 + v) % MOD1
hash_b2 = (hash_b2 * BASE2 + v) % MOD2
# Compute BASE1^M mod MOD1 and BASE2^M mod MOD2
pow1 = pow(BASE1, M, MOD1)
pow2 = pow(BASE2, M, MOD2)
# Compute hash of A[0..M-1]
hash_a1 = 0
hash_a2 = 0
A_int = [int(x) for x in A]
for j in range(M):
hash_a1 = (hash_a1 * BASE1 + A_int[j]) % MOD1
hash_a2 = (hash_a2 * BASE2 + A_int[j]) % MOD2
if hash_a1 == hash_b1 and hash_a2 == hash_b2:
# Verify
match = True
for j in range(M):
if A_int[j] != int(B[j]):
match = False
break
if match:
print(1)
return
B_int = [int(x) for x in B]
for i in range(1, N - M + 1):
hash_a1 = (hash_a1 * BASE1 + A_int[i + M - 1] - A_int[i - 1] * pow1) % MOD1
hash_a2 = (hash_a2 * BASE2 + A_int[i + M - 1] - A_int[i - 1] * pow2) % MOD2
if hash_a1 == hash_b1 and hash_a2 == hash_b2:
match = True
for j in range(M):
if A_int[i + j] != B_int[j]:
match = False
break
if match:
print(i + 1)
return
print(-1)
solve()
この解説は claude4.6opus-thinking によって生成されました。