Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情。
LeetCode字符串哈希合集
概述
字符串哈希是将原字符串看成是其长度位数的特定进制数,再通过取模映射到一定范围内的离散数值,字符串哈希中不考虑哈希碰撞的问题,因此进制和模数的选择就非常关键。生日悖论告诉我们,如果随机选择23个人,则其中至少有两人生日相同的概率大于50%,对于一般情况,如果我们在N中可重复的选择K个数,那么在K * K= O(N)的前提下,我们可以选择的K个数中存在重复的概率非常高,因此,对于O(N)的数据量,我们的模数选择需要远大于, 目前常用好写的方式是用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 {};
}
};
复杂度分析
- 时间复杂度
- 空间复杂度
T2: 336. 回文对
思路
最优解法为字典树+马拉车,复杂度。
对于原字符串数组中每个每个字符串左右各哈希一遍,随后暴力两重循环判断拼接是否为回文串即可。
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;
}
};
复杂度分析
- 时间复杂度
- 空间复杂度
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();
}
};
复杂度分析
- 时间复杂度
- 空间复杂度
字符串哈希配合二分
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);
}
};
复杂度分析
- 时间复杂度
- 空间复杂度
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;
}
};
复杂度分析
- 时间复杂度
- 空间复杂度
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;
}
};
复杂度分析
- 时间复杂度
- 空间复杂度
欢迎讨论指正