rabinKarp - 拉宾 - 卡普算法 o(n)匹配字符串
0 拉宾-卡普算法
来自wiki
在计算机科学中,拉宾-卡普算法(英語:Rabin–Karp algorithm)或卡普-拉宾算法(Karp–Rabin algorithm),是一种由理查德·卡普与迈克尔·拉宾于1987年提出的、使用散列函数以在文本中搜寻单个模式串的字符串搜索算法单次匹配。该算法先使用旋转哈希以快速筛出无法与给定串匹配的文本位置,此后对剩余位置能否成功匹配进行检验。此算法可推广到用于在文本搜寻单个模式串的所有匹配或在文本中搜寻多个模式串的匹配。
1 算法本身主要思想
- 1 朴素匹配: text = “abcdefabc”长度为n,去匹配长度为m的模式串abc,
- 具体做法则是找出所有长度为m的字串,然后为每一个m字串去匹配模式串abc,复杂度为O(nm)
- 2 使用拉宾-卡普算法改进:
- 2.1 找出了所有长度为m的字串,那么能不能在O(1)的时间内去判断两个长度为m的字串是否相等?
- 2.2 很自然的想到hash,那么如何计算一个字串的hash?比如abc,使用26进制编码(因为text中只有小写字母)即可: hash(abc) = 26^2 * (‘a’ - ‘a’) + 26^1 * (‘b’ - ‘a’) + 26^0 * (‘c’ - ‘a’);
- 2.3 很容易注意到上面,若字符串很长,比如100,那么hash值就有26^100,显然太大,需要取模,取模后带来问题,造成hash碰撞,则需要散列,常用的有拉宾指纹,我们可以使用两个不同模算出来一对hash值当做为一个整体hash,降低hash碰撞的概率
- 2.4 那么计算hash明明需要读取模式串,复杂度为O(m)啊?
- 那是因为对于text,我们只需要计算第一个长度为m的字串的hash,后面的字串hash都可以通过O(1)时间获取:
- 直接看图:,图片来源
- 解释: ft_i为第i个字串的hash,在o(1)时间内得到ft_i+1的方案就如图所示,或者看如下例子的代码的check函数
例子
1044. 最长重复子串 longestDupSubstring
1 题目
https://leetcode-cn.com/problems/longest-duplicate-substring/
2 解题思路
- 0 使用官方思路: rabin-karp + binarySearch
- 1 这里需要使用rabin-karp算法,在长度为n的text中寻找长度为m的模式串,其复杂度为o(m),然后用二分法去确定最长字符串的长度,故整体复杂度为O(n logn),拉宾-卡普算法参考:https://xychen5.github.io/2021/12/28/rabinKarp/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
typedef pair<long long, long long> pll;
class Solution {
public:
static constexpr int big = 1000000006;
// cal: a^m % mod, when m = 1000, a = 26, there will be overflow
long long pow(int a, int m, int mod) {
long long ans = 1;
long long curNum = a;
while(m > 0) {
if(m % 2 == 1) {
ans = ans * curNum % mod;
// overflow
if(ans < 0) {
ans += mod;
}
}
curNum = curNum * curNum % mod;
// overflow
if(curNum < 0) {
curNum += mod;
}
m /= 2;
}
return ans;
}
// return st of substr with len = len
int check(vector<int>& arr, int len, int a1, int a2, int mod1, int mod2) {
int n = arr.size();
long long powA1 = pow(a1, len, mod1);
long long powA2 = pow(a2, len, mod2);
long long hashA1 = 0, hashA2 = 0;
// cout << "d2.5" << endl;
// cal hash of the first substr
// hashA1 = arr[0] * a1^(len-1) + arr[1] * a1^(len-2) + ... + arr[len-1]
for(int i = 0; i < len; ++i) {
hashA1 = (hashA1 * a1 % mod1 + arr[i]) % mod1;
hashA2 = (hashA2 * a2 % mod2 + arr[i]) % mod2;
hashA1 += hashA1 >= 0 ? 0 : mod1;
hashA2 += hashA2 >= 0 ? 0 : mod2;
}
// cout << "d3" << endl;
// calculate all substr's hash with len = len
set<pll> seen;
seen.emplace(hashA1, hashA2);
for(int st = 1; st <= n - len; ++st) {
// cout << "d4" << endl;
// O(1) to cal next hash
hashA1 = (hashA1 * a1 % mod1 - arr[st - 1] * powA1 % mod1 + arr[st + len - 1]) % mod1;
hashA2 = (hashA2 * a2 % mod2 - arr[st - 1] * powA2 % mod2 + arr[st + len - 1]) % mod2;
hashA1 += hashA1 >= 0 ? 0 : mod1;
hashA2 += hashA2 >= 0 ? 0 : mod2;
// before cursubstr, there is a same one
if(seen.count(make_pair(hashA1, hashA2))) {
return st;
}
seen.emplace(hashA1, hashA2);
}
return -1;
}
string longestDupSubstring(string s) {
int n = s.size();
// code the string
vector<int> arr(n);
for(int i = 0; i < n; ++i) {
arr[i] = s[i] - 'a';
}
// two random base and mod
srand((unsigned)time(NULL));
int a1 = random()%75 + 26;
int a2 = random()%75 + 26;
int mod1 = random()%(INT_MAX - big) + big;
int mod2 = random()%(INT_MAX - big) + big;
// bin search the length of longest dup substr
int l = 1, r = n - 1;
int finalSt = -1, finalLen = -1;
while(l <= r) {
// m represents target len
// int m = (l + r) / 2;
int m = l + (r - l + 1) / 2;
// cout << "d1" << endl;
int st = check(arr, m, a1, a2, mod1, mod2);
// cout << "d2" << endl;
if(st != -1) {
finalLen = m;
l = m + 1;
finalSt = st;
} else {
r = m - 1;
}
}
return finalLen == -1 ? "" : s.substr(finalSt, finalLen);
}
}