Leetcode1044. 最长重复子串(二分查找 + Rabin-Karp 字符串编码)思路分享-日常总结

Leetcode1044. 最长重复子串(二分查找 + Rabin-Karp 字符串编码)思路分享-日常总结

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bG1Sn8RL-1640247839729)(C:\Users\86182\AppData\Roaming\Typora\typora-user-images\image-20211223160333754.png)]

思路:

刚看到这题的时候脑子里只有暴力做法(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字符串一般用到如下公式:

img

其中,img代表的是S的定义域大小,比如说如果S全是英文字母,那么img的值为26,因为英文字母就只有26个。然后img这个函数是一个映射函数,映射S的定义域中的每一个字符到数字的函数。

常规Brute Forch算法(暴力解法)

  1. 假设字符串p的长度为m,字符串q的长度为n

  2. 在字符串p上放一个长度为n的窗口,缓慢滑动这个窗口,每滑动一次就与字符串q比较一次

  3. 当比较结果一致时返回True,若直到最后依然不一致,则返回False

分析:

1. 最坏情况下,窗口滑动至末尾,一共有(n-m+1)次滑动。-- O(m)

2. 每次滑动字符串都得进行比较。-- O(n)

3. 综上,时间复杂度为O(m*n)

Rabin-Karp算法

  1. 基本思想与暴力解法一致,但比较的是两个字符串的哈希值。

  2. 由于哈希值为数字,因此比较的时间复杂度为O(1)

  3. 比较两个字符串的哈希值前需要先进行计算。若字符串q长度为n,则计算q的哈希值的时间为O(n)。

接下来,就是这个算法的神奇之处了:

1)首先我们看一下字符串哈希值的计算公式:

img

2)若我们已经知道上一个窗口的哈希值时,则我们可以在此基础之上计算当前窗口的哈希值(减法-乘法-加法),原理如下:

img

3)因此,在已知上一个窗口的哈希值时,计算当前窗口的哈希值的时间复杂度为O(1)

  1. 重新计算一次时间复杂度:

时间复杂度 = 窗口滑动的最坏情况x字符串比较时间+哈希值的计算时间=m+n

以上为理论,可能一开始看不懂,于是我先从一道简单题入手。

AcWing 841.字符串哈希

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BFF22B19-1640247839733)(C:\Users\86182\AppData\Roaming\Typora\typora-user-images\image-20211223161127666.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xWq4rxPE-1640247839734)(C:\Users\86182\AppData\Roaming\Typora\typora-user-images\image-20211223161214306.png)]

#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)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值