LeetCode字符串哈希合集

1,082 阅读3分钟

Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情

LeetCode字符串哈希合集

概述

字符串哈希是将原字符串看成是其长度位数的特定进制数,再通过取模映射到一定范围内的离散数值,字符串哈希中不考虑哈希碰撞的问题,因此进制和模数的选择就非常关键。生日悖论告诉我们,如果随机选择23个人,则其中至少有两人生日相同的概率大于50%,对于一般情况,如果我们在N中可重复的选择K个数,那么在K * K= O(N)的前提下,我们可以选择的K个数中存在重复的概率非常高,因此,对于O(N)的数据量,我们的模数选择需要远大于O(N2)O(N^2), 目前常用好写的方式是用unsigned long long自然溢出取模,通过选择合适的进制数减少哈希碰撞,进一步leetcode支持unsigned __int128,必要时可以采用减少哈希碰撞。自然模比较好卡,无符号长整型可以解决99%的leetcode字符串哈希问题,unsigned __int128可以解决剩下的1%,究极版本还可以采用双进制,双哈希加__int128类型。

字符串哈希可以用简单的方式解决很多难题,性价比极高,力扣上可以用字符串哈希解决的问题非常多,同时也不局限于字符串,结合例题说明。

字符串哈希解决回文串问题


T1: 214. 最短回文串

思路

最优解法为kmp,线性复杂度
仅能在字符串前面添加字符,因此可以将问题转化为求从首字母开头的最长回文子串,仅需要左右各哈希一遍即可。

Code

class Solution {
public:
    using ULL = unsigned long long;
    static const int N = 5e4 + 10, base = 97755331;
    ULL p[N], hl[N], hr[N];
    ULL get(ULL* h, int l, int r) {
        return h[r] - h[l - 1] * p[r - l + 1];
    }
    string shortestPalindrome(string s) {
        int n = s.size();
        p[0] = 1;
        for(int i = 1, j = n - 1; i <= n; i ++, j --) {
            p[i] = p[i - 1] * base;
            hl[i] = hl[i - 1] * base + s[i - 1];
            hr[i] = hr[i - 1] * base + s[j];
        }
        for(int i = n; i; i --) {
            if(get(hl, 1, i) == get(hr, n + 1 - i, n)) {
                auto pre = s.substr(i);
                reverse(pre.begin(), pre.end());
                return pre + s;
            }
        }
        return {};
    }
};

复杂度分析

  • 时间复杂度O(N)O(N)
  • 空间复杂度O(N)O(N)

T2: 336. 回文对

思路

最优解法为字典树+马拉车,O(NM)O(NM)复杂度。
对于原字符串数组中每个每个字符串左右各哈希一遍,随后暴力两重循环判断拼接是否为回文串即可。

Code

using ut = unsigned __int128;
const ut base = 97755331;
const int N = 1e5 + 5;
ut p[N] {1}, hl[N], hr[N];
auto _ = []{
    for(int i = 1; i < N; i ++) p[i] = p[i - 1] * base;
    return 0;
}();
class Solution {
public:
    vector<vector<int>> palindromePairs(vector<string>& s) {
        int n = s.size();
        for(int i = 0; i < n; i ++) {
            int m = s[i].size();
            ut u = 0;
            for(int j = 0; j < m; j ++) u = u * base + s[i][j];
            hl[i] = u;
            u = 0;
            for(int j = m - 1; j >= 0; j --) u = u * base + s[i][j];
            hr[i] = u;
        }
        vector<vector<int>> res;
        for(int i = 0; i < n; i ++) {
            for(int j = 0; j < n; j ++) {
                if(i == j) continue;
                if(hl[i] * p[s[j].size()] + hl[j] == hr[j] * p[s[i].size()] + hr[i])
                    res.push_back({i, j});
            }
        }
        return res;
    }
};

复杂度分析

  • 时间复杂度O(N2)O(N^2)
  • 空间复杂度O(N)O(N)

T3: 1316. 不同的循环子字符串

思路

从前往后做一遍字符串哈希,随后暴力枚举所有可能的偶数长度子串即可

Code

class Solution {
public:
    using T = unsigned long long;
    static const int N = 2e3 + 5, base = 97755331;
    T p[N] {1}, h[N];
    T get(int l, int r) {
        return h[r] - h[l - 1] * p[r - l + 1];
    }
    int distinctEchoSubstrings(string text) {
        int n = text.size();
        for(int i = 1; i <= n; i ++) {
            p[i] = p[i - 1] * base;
            h[i] = h[i - 1] * base + text[i - 1];
        }
        unordered_set<T> st;
        for(int i = 1; i <= n; i ++) {
            for(int len = 2; i + len - 1 <= n; len += 2) {
                int j = i + len -1;
                if(get(i, i + len / 2 - 1) == get(i + len / 2, j)) st.emplace(get(i, j));
            }
        }
        return st.size();
    }
};

复杂度分析

  • 时间复杂度O(N2)O(N^2)
  • 空间复杂度O(N)O(N)

字符串哈希配合二分


T4: 1044. 最长重复子串

思路

对原字符串做一边字符串哈希,随后二分可能长度,从前到后枚举所有可能长度的子串即可

Code

class Solution {
    using ULL = unsigned long long;
    static const int N = 1e5 + 10, base = 97755331;
    ULL p[N] {1}, h[N];
    ULL get(int l, int r) {
        return h[r] - h[l - 1] * p[r - l + 1];
}
public:
    string longestDupSubstring(string s) {
        int n = s.size();
        for(int i = 1; i <= n; i ++) {
            p[i] = (ULL)p[i - 1] * base;
            h[i] = (ULL)h[i - 1] * base + s[i - 1];
            }
        int l = 0, r = n - 1, pos = 0;
        unordered_set<ULL> cnt;
        while(l<r) {
            int mid=l + r + 1 >> 1;
            if([&]{
                cnt.clear();
                for(int i = 1;i + mid - 1 <= n;i ++){
                    ULL cur=get(i,i + mid - 1);
                    if(cnt.count(cur)) {
                        pos = i - 1;
                        return true;
                    }
                    cnt.insert(cur);
                }
                return false;
            }()) l = mid;
            else r = mid - 1;
        }
        return s.substr(pos, l);
    }
};

复杂度分析

  • 时间复杂度O(NlogN)O(NlogN)
  • 空间复杂度O(N)O(N)

T5: 1923. 最长公共子路径

思路

二分可能的最长长度,暴力判断是否所有路径均包含,注意unsigned __int128类型没有自带hash函数,需要配合通用哈希

Code

using ULL = __uint128_t;
const int N = 100010, H = 133331;
ULL h[N], p[N] {1};

auto _ = [] {
    for(int i = 1; i < N; i ++) p[i] = p[i - 1] * H;
    return 0;
}();
ULL get(int l, int r) {
    return h[r] - h[l - 1] * p[r - l + 1];
}

struct bithash {
    template<class T> size_t operator() (T& t) const {
        return hash<string_view> {} ({(char *) &t, sizeof t});
    }
};

class Solution {
public:
    vector<vector<int>> paths;
    unordered_map<ULL, int, bithash> cnt;
    unordered_set<ULL, bithash> S;

    int longestCommonSubpath(int n, vector<vector<int>>& paths) {
        auto check = [&] (int mid) {
            cnt.clear();
            for (int i = 0; i < paths.size(); i ++ ) {
                auto& str = paths[i];
                int n = str.size();
                S.clear();
                for (int j = 1; j <= n; j ++ ) {
                    h[j] = h[j - 1] * H + str[j - 1];
                }
                for (int j = mid; j <= n; j ++ ) {
                    auto t = get(j - mid + 1, j);
                    if (!S.count(t)) {
                        S.insert(t);
                        cnt[t] ++ ;
                    }
                }
            }
            int res = 0;
            for (auto& [k, v]: cnt) res = max(res, v);
            return res == paths.size();
        };
        n = 0;
        int l = 0, r = N;
        for (auto& p: paths) n += p.size(), r = min(r, (int)p.size());
        
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (check(mid)) l = mid;
            else r = mid - 1;
        }
        return r;
    }
};

复杂度分析

  • 时间复杂度O(NlogN)O(NlogN)
  • 空间复杂度O(N)O(N)

T6: 1960. 两个回文子字符串长度的最大乘积

思路

求出以每个字符为中心的最长回文子串,随后枚举分界点,求最大乘积即可。关键难点在求以每个字符为中心的最长回文子串,标准解法为马拉车,这里可以用字符串哈希水过去。

Code

class Solution {
public:
    using LL = long long;
    using ULL = unsigned long long;
    static const int N = 100010, base = 97755331;
    ULL hl[N], hr[N], p[N];
    int ra[N];
    ULL get(ULL *h, int l, int r) {
        return h[r] - h[l - 1] * p[r - l + 1];
    }
    int f[N], g[N];
    long long maxProduct(string s) {
        int n = s.size();
        s = " " + s;
        p[0] = 1;
        for(int i = 1, j = n; i <= n; i ++, j --){
            hl[i] = hl[i - 1] * base + s[i];
            hr[i] = hr[i - 1] * base + s[j];
            p[i] = p[i - 1] * base;
        }
        for(int i = 1; i <= n; i ++) {
            int l = 0, r = min(i - 1, n - i);
            while(l <  r) {
                int mid = l + r + 1 >> 1;
                if(get(hl, i - mid, i) == get(hr, n + 1 - i - mid, n + 1 - i)) l = mid;
                else r = mid - 1;
            }
            ra[i] = l;
        }
        for(int i = 1, j = 1, max_ra = 0; i <= n; i ++) {
            while(j + ra[j] <= i) {
                max_ra = max(max_ra, ra[j]);
                j ++; 
            }
            max_ra = max(max_ra, i - j);
            f[i] = max_ra;
        }    
        for(int i = n, j = n, max_ra = 0; i; i --) {
            while(j - ra[j] >= i) {
                max_ra = max(max_ra, ra[j]);
                j --;
            }
            max_ra = max(max_ra, j - i);
            g[i] = max_ra;
        }
        LL res = 0;
        for(int i = 1; i < n; i ++) {
            res = max(res, (LL)(f[i] * 2 + 1) * (g[i + 1] * 2 + 1));
        }
        return res;
    }
};

复杂度分析

  • 时间复杂度O(NlogN)O(NlogN)
  • 空间复杂度O(N)O(N)


欢迎讨论指正