4-7 文字列を華麗に扱う
文字列に対する動的計画法
単一文字列の場合
複数文字列の場合
文字列検索
長さ の文字列 に含まれる、長さ の文字列 の場所を探したり、含まれる回数を探したりすることを文字列検索という
ナイーブな解法では、 の開始位置を全て試し、一致しているかを調べる の解法が存在するが、ローリングハッシュではこれを で行う。
ローリングハッシュ
- ナイーブ解では一回の一致判定に であるが、これをハッシュ値を用いることにより で行うことで目指す
- そのままではハッシュ値の計算に かかるものの、直前の比較に用いたハッシュ値を利用することで計算を高速化 する
互いに素な適当な定数 を用意し、文字列 のハッシュ値を、
とする。すると、文字列 の 文字目からの 文字の部分文字列 に対するハッシュ値は、 文字目からの部分文字列 のハッシュ値により、以下のようにすぐ計算できる。
と定めると mod を取る操作を省く事が出来ることを利用して、
# ローリングハッシュ
B = 1000000007
H = 998244353
def rolling_hash(a: str, b: str) -> bool:
"""return b in a"""
n = len(a)
m = len(b)
powers = [1]
for _ in range(m):
powers.append(powers[-1] * B % H)
a_hash = sum(map(lambda i: ord(a[i]) * powers[m - i - 1], range(m))) % H
b_hash = sum(map(lambda i: ord(b[i]) * powers[m - i - 1], range(m))) % H
if a_hash == b_hash:
return True
for i in range(n - m):
a_hash = (a_hash * B + ord(a[i + m]) - ord(a[i]) * powers[m]) % H
if a_hash == b_hash:
return True
return False
(Apple M2 (macOS 14.4.1 (23E224)) での実行結果)
>>> import random, timeit
>>> n = 1_000_000
>>> m = 500_000
>>> s = "".join(chr(random.randint(ord("a"), ord("z"))) for _ in range(n))
>>> t = "".join(chr(random.randint(ord("a"), ord("z"))) for _ in range(m))
>>> s = s[: n // 3] + t + s[n // 3 + m :]
>>> assert(t in s and rolling_hash(s, t))
>>> timeit.timeit("t in s", globals=globals(), number=100)
0.08759437501430511
>>> timeit.timeit("rolling_hash(s, t)", globals=globals(), number=100)
14.977220457978547 # ??