Leetcode1044. 最长重复子串(二分查找 + Rabin-Karp 字符串编码)思路分享-日常总结
思路:
刚看到这题的时候脑子里只有暴力做法(bushi)
后来看了一下各路大佬的题解 发现并看不懂 (字符串哈希+二分)
说明前置知识还不够
然后我学习了一点点前置知识如下:
Rabin-Karp算法概述
关于Rabin-Karp算法
Rabin-Karp是用来解决字符串匹配(查重)的问题的。这个问题如下表达:
Input : 字符串p,和字符串q
Output:如果p中包含q,则输出True;如果p中不包含q,则输出False
时间复杂度:O(m+n)
关于字符串的比较以及hashing字符串
1.如果想要比较两个字符串是否相同,需要依次比较每个位置对应的字符是否相同,则时间复杂度为O(n)
2.如果我们将字符串以一个特定的函数H(·),将字符串转换成一个数字,那么我们只需要比较两个字符串的哈希值,就能够判断它们是否相同,时间复杂度为O(1)。
3.Hashing字符串一般用到如下公式:
其中,代表的是S的定义域大小,比如说如果S全是英文字母,那么的值为26,因为英文字母就只有26个。然后这个函数是一个映射函数,映射S的定义域中的每一个字符到数字的函数。
常规Brute Forch算法(暴力解法)
-
假设字符串p的长度为m,字符串q的长度为n
-
在字符串p上放一个长度为n的窗口,缓慢滑动这个窗口,每滑动一次就与字符串q比较一次
-
当比较结果一致时返回True,若直到最后依然不一致,则返回False
分析:
1. 最坏情况下,窗口滑动至末尾,一共有(n-m+1)次滑动。-- O(m)
2. 每次滑动字符串都得进行比较。-- O(n)
3. 综上,时间复杂度为O(m*n)
Rabin-Karp算法
-
基本思想与暴力解法一致,但比较的是两个字符串的哈希值。
-
由于哈希值为数字,因此比较的时间复杂度为O(1)
-
比较两个字符串的哈希值前需要先进行计算。若字符串q长度为n,则计算q的哈希值的时间为O(n)。
接下来,就是这个算法的神奇之处了:
1)首先我们看一下字符串哈希值的计算公式:
2)若我们已经知道上一个窗口的哈希值时,则我们可以在此基础之上计算当前窗口的哈希值(减法-乘法-加法),原理如下:
3)因此,在已知上一个窗口的哈希值时,计算当前窗口的哈希值的时间复杂度为O(1)
- 重新计算一次时间复杂度:
时间复杂度 = 窗口滑动的最坏情况x字符串比较时间+哈希值的计算时间=m+n
以上为理论,可能一开始看不懂,于是我先从一道简单题入手。
AcWing 841.字符串哈希
#include<iostream>
#include<stdio.h>
#include<string.h>
using namespace std;
typedef unsigned long long ULL;
const int N=1e5+10,P=131;
ULL h[N],p[N];
//h[i] 前i个字符的hash值
//字符串变成一个p进制数字,体现了字符+顺序,(需要确保不同的字符串对应不同的数字)
//P=131或13331 Q=2^64 (在99%的情况下不会出现冲突)
//使用场景:两个字符串的子串是否相同
ULL query(int l,int r){
return h[r]-h[l-1]*p[r-l+1];
}
int main(){
int n,m;
cin>>n>>m; //n为字符串长度 m为询问次数
string x;
cin>>x;
//字符串从1开始编号,h[1]为前一个字符的哈希值
p[0]=1;
h[0]=0;
for(int i=0;i<n;i++){
p[i+1]=p[i]*P;
h[i+1]=h[i]*P+x[i]; //前缀求整个字符串的哈希值
}
while(m--){
int l1,r1,l2,r2;
cin>>l1>>r1>>l2>>r2;
if(query(l1,r1)==query(l2,r2))puts("Yes");
else puts("No");
}
return 0;
}
现在已经学会了最基本的字符串哈希,然后接下来做一道leetcode中等题练习一下二分与字符串哈希的应用。
Leetcode718.最长重复子数组
//二分+字符串哈希
typedef unsigned long long ULL;
const int N=1010;
const int P=131;
class Solution {
public:
int n,m;
//ha[i]中存的是A中 长度为i的 前缀子字符串的哈希值 i~[1,n],ha[0]没有实际意义
ULL ha[N],hb[N],p[N];
int findLength(vector<int>& A, vector<int>& B) {
p[0]=1;
ha[0]=0;
hb[0]=0;
n=A.size(),m=B.size();
for(int i=0;i<n;i++){
ha[i+1]=ha[i]*P+A[i];//前缀求整个字符串A的哈希值
p[i+1]=p[i]*P;
}
for(int i=0;i<m;i++){
hb[i+1]=hb[i]*P+B[i];//前缀求整个字符串B的哈希值
}
int l=0,r=min(n,m);//l:相同子串最短可能长度 r:相同子串最大可能长度,不超过 min(n, m)
while(l<r){
int mid=l+r+1>>1; // 先查找长度为mid的子串 是否有匹配的. 注意 l = mid, 所以要 + 1
// check(mid) 的作用是看 A,B 里是否有 长度为mid的子字符串相同
if (check(mid)) l = mid; // 如果有长度为 mid 的相同子串, 那么最短长度l=mid
else r = mid - 1; // 如果没有 mid 长度的,最长也不会超过mid-1, 即r=mid-1
}
return r;
}
// check(len)的作用是看 A,B 里是否有 mid 长度的子字符串相同
bool check(int len){// check(len)的作用是看 A,B 里是否有 mid 长度的子字符串相同
unordered_set<ULL> hash;
//注意 A 字符串的下标范围: [0, n - 1], ha 的[l, r]范围是[1, n], 下标存在1的差值
//长度len的子串下标范围: [i+1, i+len]
for(int i=0;i+len-1<n;i++){
hash.insert(getHash(ha,i+1,i+len));
}
for(int i=0;i+len-1<m;i++){
if(hash.count(getHash(hb,i+1,i+len))){
return true;
}
}
return false;
}
ULL getHash(ULL h[],int l,int r){
return h[r]-h[l-1]*p[r-l+1];
}
};
然后回归正题,
Leetcode1044.最长重复子串
做法和前面一道题差不多,只是check函数内部有一些简单的变换,最后终于写出来了~
const int N=3*1e4+10;
typedef unsigned long long ULL;
const int P=131;
class Solution {
public:
string ans="";
ULL h[N],p[N];
int n;
int pos,length;
string longestDupSubstring(string s) {
h[0]=0;
p[0]=1;
n=s.size();
for(int i=0;i<n;i++){
h[i+1]=h[i]*P+s[i];//求前缀和整个字符串的哈希值
p[i+1]=p[i]*P;
}
int l=0,r=n;
while(l<r){
int mid=(l+r+1)>>1;
if(check(mid))l=mid;
else r=mid-1;
}
return s.substr(pos,r);
}
bool check(int len){
unordered_set<ULL> hash;
for(int i=0;i<n;i++){
if(hash.count(getHash(h,i+1,i+len))){
pos=i;
return true;
}
hash.insert(getHash(h,i+1,i+len));
}
return false;
}
ULL getHash(ULL h[],int l,int r){
return h[r]-h[l-1]*p[r-l+1];
}
};
执行用时比较长内存消耗也比较大,说明还有很多不足之处~
最后贴上官方题解与思路
typedef pair<long long, long long> pll;
class Solution {
public:
long long pow(int a, int m, int mod) {
long long ans = 1;
long long contribute = a;
while (m > 0) {
if (m % 2 == 1) {
ans = ans * contribute % mod;
if (ans < 0) {
ans += mod;
}
}
contribute = contribute * contribute % mod;
if (contribute < 0) {
contribute += mod;
}
m /= 2;
}
return ans;
}
int check(const vector<int> & arr, int m, int a1, int a2, int mod1, int mod2) {
int n = arr.size();
long long aL1 = pow(a1, m, mod1);
long long aL2 = pow(a2, m, mod2);
long long h1 = 0, h2 = 0;
for (int i = 0; i < m; ++i) {
h1 = (h1 * a1 % mod1 + arr[i]) % mod1;
h2 = (h2 * a2 % mod2 + arr[i]) % mod2;
if (h1 < 0) {
h1 += mod1;
}
if (h2 < 0) {
h2 += mod2;
}
}
// 存储一个编码组合是否出现过
set<pll> seen;
seen.emplace(h1, h2);
for (int start = 1; start <= n - m; ++start) {
h1 = (h1 * a1 % mod1 - arr[start - 1] * aL1 % mod1 + arr[start + m - 1]) % mod1;
h2 = (h2 * a2 % mod2 - arr[start - 1] * aL2 % mod2 + arr[start + m - 1]) % mod2;
if (h1 < 0) {
h1 += mod1;
}
if (h2 < 0) {
h2 += mod2;
}
// 如果重复,则返回重复串的起点
if (seen.count(make_pair(h1, h2))) {
return start;
}
seen.emplace(h1, h2);
}
// 没有重复,则返回-1
return -1;
}
string longestDupSubstring(string s) {
srand((unsigned)time(NULL));
// 生成两个进制
int a1 = random()%75 + 26;
int a2 = random()%75 + 26;
// 生成两个模
int mod1 = random()%(INT_MAX - 1000000006) + 1000000006;
int mod2 = random()%(INT_MAX - 1000000006) + 1000000006;
int n = s.size();
// 先对所有字符进行编码
vector<int> arr(n);
for (int i = 0; i < n; ++i) {
arr[i] = s[i] - 'a';
}
// 二分查找的范围是[1, n-1]
int l = 1, r = n - 1;
int length = 0, start = -1;
while (l <= r) {
int m = l + (r - l + 1) / 2;
int idx = check(arr, m, a1, a2, mod1, mod2);
if (idx != -1) {
// 有重复子串,移动左边界
l = m + 1;
length = m;
start = idx;
} else {
// 无重复子串,移动右边界
r = m - 1;
}
}
return start != -1 ? s.substr(start, length) : "";
}
};
作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/longest-duplicate-substring/solution/zui-chang-zhong-fu-zi-chuan-by-leetcode-0i9rd/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
【宫水三叶】「字符串哈希 + 二分]
三叶大佬的思路真的很清晰,一看就懂。
java代码:
class Solution {
long[] h, p;
public String longestDupSubstring(String s) {
int P = 1313131, n = s.length();
h = new long[n + 10]; p = new long[n + 10];
p[0] = 1;
for (int i = 0; i < n; i++) {
p[i + 1] = p[i] * P;
h[i + 1] = h[i] * P + s.charAt(i);
}
String ans = "";
int l = 0, r = n;
while (l < r) {
int mid = l + r + 1 >> 1;
String t = check(s, mid);
if (t.length() != 0) l = mid;
else r = mid - 1;
ans = t.length() > ans.length() ? t : ans;
}
return ans;
}
String check(String s, int len) {
int n = s.length();
Set<Long> set = new HashSet<>();
for (int i = 1; i + len - 1 <= n; i++) {
int j = i + len - 1;
long cur = h[j] - h[i - 1] * p[j - i + 1];
if (set.contains(cur)) return s.substring(i - 1, j);
set.add(cur);
}
return "";
}
}
作者:AC_OIer
链接:https://leetcode-cn.com/problems/longest-duplicate-substring/solution/gong-shui-san-xie-zi-fu-chuan-ha-xi-ying-hae9/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。