東大院試から学ぶローリングハッシュ
文字列検索で用いられるローリングハッシュというアルゴリズム[1]を、東大の院試問題(東京大学大学院情報理工学系研究科コンピュータ科学専攻 の 2020年度 専門科目I 問題3)をもとに紹介します。
問題のPDFは以下になります。[2]
この問題はローリングハッシュが題材になっているものの、アルゴリズムを事前に知らなくても解けるようになっており、むしろこの問題を通してローリングハッシュを理解することができます。その意味で、非常に教育的な出題と言えますし、これからローリングハッシュを知りたいという方にも有益な問題なのではないかと思います。[3]
では問題を見ていきましょう。
問題文[1]
いろいろ記号が出てきました。整理してみましょう。
文字列
文字
(例えば文字コードを想像してみれば良いでしょう。)
また、問題 FIND の説明も読んでみましょう。これは文字列検索の説明になります。例えば
PROBLEM
OBL
マッチせず
PROBLEM
OBL
マッチせず
PROBLEM
OBL
マッチ!
となります。マッチしたとき、
となり、
問題文[2]
さて、問題 FIND を解くアルゴリズム S が提示されました。このアルゴリズムが何をしているかを考えてみましょう。
for ループの中では
ここで
という流れであり、これは先ほどの
PROBLEM
OBL
マッチせず
PROBLEM
OBL
マッチせず
PROBLEM
OBL
マッチ!
と同じ、愚直な検索であることが分かります。
問題文[3]
(1) はアルゴリズム S の最悪計算量を求めるという問題です。最も時間がかかるのは当然ながら
問題文の冒頭より、1回の文字列比較
となります。(
問題文[4]
ようやくハッシュが登場しました。
元の式をじっと見てもよく分からないので、まずは具体例で考えてみましょう。
としてみます。
イメージしやすいように、
と展開できます。例えば
ですね。
問題文[5]
(2) です。
問題文の冒頭の設定を整理しておくと、
-
とs からi を求める演算s+i -
からc を求める演算numval(c) - 整数の加算・乗算・剰余
でした。
では、先ほどの具体例をもう一度振り返ってみましょう。
でした。ここから、
さらに
となります。
ここまで来ると、
加算・減算・乗算は剰余に閉じている[4]ので、
-
からh(s, 4) を引き、numval(s[0]) \cdot d^3 - それに
を掛け、d - それに
を足し、numval(s[4]) - 最後に
をとる\mod q
という操作をすれば OK です。
一般の
についても、
-
からh' = h(s + i, m) を引き、numval(s[i]) \cdot d^{m-1} = numval(s[i]) \cdot d_m - それに
を掛け、d - それに
を足し、numval(s[i+m]) - 最後に
をとる\mod q
という操作で
tmp = numval(s[i]);
tmp = tmp * d_m;
ans = h' + tmp * (-1);
ans = ans * d;
ans = ans + numval(s[i+m]);
ans = ans % q;
return ans;
numval()
, +
, *
, %
は
問題文[6]
さて、(3) は本題の、ローリングハッシュによる文字列検索アルゴリズムについて考える問題です。2つの問題が与えられていることに気をつけてください。
まず、前半の問題についてですが、(2) の誘導を活かすことを考えましょう。
したがって、
h(p, l(p)) をあらかじめ計算
h(s, l(p)) をあらかじめ計算
d_l(p) をあらかじめ計算
if (h(p, l(p)) == h(s, l(p)))
return 0;
for (i = 0; i <= l(s)-l(p)-1; i++)
h(s+i, l(p)) と d_l(p) から h(s+i+1, l(p)) を O(1) で計算
if (h(p, l(p)) == h(s+i+1, l(p)))
return i+1;
return -1;
ハッシュはただの整数値ですから、ハッシュ同士の比較は当然
残るは、
-
をあらかじめ計算h(p, l(p)) -
をあらかじめ計算h(s, l(p)) -
をあらかじめ計算d_{l(p)} = d^{l(p) - 1}
これらの部分の計算量を解析する必要があります。
まず、乗算が
また、
ans = numval(s[0]);
for (i = 1; i <= l(p)-1; i++)
ans = ans * d;
ans = ans + numval(s[i]);
ans = ans % q;
return ans;
というように求まります。
以上より、for 文以外の部分の計算量は
次に後半の問題ですが、これもちょっと考えればわかることです。このアルゴリズム
逆に言えば、異なる文字列に同じハッシュ値が対応してしまう場合は、前提が破綻してしまいます。極端な例ですが、剰余 Mod の
よって、この問題の回答としては 「ハッシュが衝突する場合」 などとしておけばよいでしょう。[5]
ハッシュが衝突しない限り、アルゴリズム
例えば
いかがでしたでしょうか。ローリングハッシュのアルゴリズムや計算量、ハッシュ衝突問題なども含めて、網羅的に学ぶことができましたね。
この問題はコンピュータ科学専攻の過去問の中では、やや易〜標準 位の難易度[6][7]だと思うので、「こんなの簡単じゃん!」と思った方は、ぜひ東京大学大学院情報理工学系研究科コンピュータ科学専攻を受験してみましょう!
-
正確にはラビン-カープ(Rabin-Karp)法というのが今回紹介する文字列検索アルゴリズムの正式名称で、ローリングハッシュはラビン-カープ法で使うハッシュのことを指すのだと思いますが、この記事ではローリングハッシュをアルゴリズムのこととして略記してしまいます。 ↩︎
-
過去問PDFが非公開になって見れなくなっているかもしれないので、その場合は archive.org 等の魚拓ツールで探してみてください。 ↩︎
-
もちろん既に競プロなどでローリングハッシュを知っている方にとっては、入試でこの問題を解くのは全くもって余裕だったでしょう。私は今年この専攻に合格したのですが、この過去問を解くまではローリングハッシュを知りませんでした...。ちなみに、コンピュータ科学専攻を受験される方は、文字列検索アルゴリズムとして他にKMPアルゴリズムやBMアルゴリズムも抑えておくと良いでしょう。 ↩︎
-
これが肌感覚で分かるには、競プロで 1000000007 で割った余りを求める経験がないといけないような気もしますが... ↩︎
-
競プロの世界では、Mod を
にすると安全だそうです。参考: https://qiita.com/keymoon/items/11fac5627672a6d6a9f6 ↩︎2^{61} - 1 -
実はこの後に小問がもう1つだけあるのですが、ローリングハッシュの紹介からはやや話が逸れるので省きました。 ↩︎
-
ただし、このような競プロ色の強い問題が出題されるのはごく一部で、入試全体としては "情報数学、数値計算、離散数学、アルゴリズムと計算量、形式言語、論理学、プログラミング言語論、コンピュータアーキテクチャ、オペレーティングシステム、デジタル回路、機械学習" の各分野から幅広く出題されます。合格した私の見解ですが、難問を解ける必要はなく、どの分野についても初歩的な問題をきちんと正答できれば大丈夫だと思います。 ↩︎
Discussion