【学习笔记】哈希
【学习笔记】哈希
Hash 的核心思想在于,将输入映射到一个值域较小、可以方便比较的范围。
主要用来判重!
如何辨别哈希题?大概就通过一句话:当需要用
字符串哈希
感觉这一块 OI_wiki 讲得比较清楚。
通常我们采用的是多项式 Hash 的方法,对于一个长度为
例如,对于字符串
其中
可选取 M = 1145141, 1e7+19, 1e9+9; b = 131, 233, 16943.
当然也可以选择 unsigned long long
自然溢出,但是被卡的概率会大大增加。
询问子串哈希:
令:
求:
解:
注意取模相减时可能出现负数。
贴一个 [NOI2016] 优秀的拆分 的 95pts 暴力代码,回顾一下双哈希:(虽然单哈希也能 95pts)
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 30005;
ll base[N], hsh[N], Base[N], Hsh[N];
const int p1=10000019, p2=1000000009;
const int bs1=233, bs2=16943;
char s[N];
int L[N], R[N];
pair<ll, ll> hshnum(int l, int r){
return {(hsh[r]-hsh[l-1]*base[r-l+1]%p1+p1)%p1, (Hsh[r]-Hsh[l-1]*Base[r-l+1]%p2+p2)%p2};
}
int main(){
base[0] = Base[0] = 1;
for(int i=1; i<N; i++){
base[i] = base[i-1]*bs1%p1;
Base[i] = Base[i-1]*bs2%p2;
}
int T; scanf("%d", &T);
while(T--){
scanf("%s", s+1);
int n = strlen(s+1);
for(int i=1; i<=n; i++){
hsh[i] = (hsh[i-1]*bs1%p1+s[i])%p1;
Hsh[i] = (Hsh[i-1]*bs2%p2+s[i])%p2;
}
for(int i=1; i<=n; i++){
L[i] = R[i] = 0;
for(int j=i-1; j>=1; j-=2){
int mid = (i+j)/2;
if(hshnum(j, mid) == hshnum(mid+1, i)) L[i]++;
}
for(int j=i+2; j<=n; j++){
int mid = (i+j+1)/2;
if(hshnum(i+1, mid) == hshnum(mid+1, j)) R[i]++;
}
}
ll ans = 0;
for(int i=1; i<=n; i++)
ans += L[i]*R[i];
printf("%lld\n", ans);
}
return 0;
}
允许 次失配的字符串匹配:
“模板”题:P3763 [TJOI2017] DNA
大差不差,主要是限定了
枚举源串的子串,尝试比对出前三个不同的地方,最后一段如果能匹配上那么这个子串就是一个答案,反之则不是。如果还没找到三处不同就已经匹配完整个目标串了,那么这个子串也是一个答案。
哈希的匹配具有单调性。具体来讲,如果两个字符串的前
考虑二分,二分出前三个不同的地方,如果匹配完了就结束二分并累加答案,否则判断剩余部分是否匹配即可。
总的时间复杂度为
另外此题 KMP 不可做。
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 1e5+5;
const int bs = 233, p = 1e9+9;
ll base[N], hsha[N], hshb[N];
int hshnum(ll hs[], int l, int r){
return (hs[r]-hs[l-1]*base[r-l+1]%p+p)%p;
}
int lcp(int x, int y, int rt){
int lt=0, ans;
while(lt<=rt){
int mid = (lt+rt)>>1;
if(hshnum(hsha, x, x+mid-1)==hshnum(hshb, y, y+mid-1)){
ans = mid;
lt = mid+1;
} else{
rt = mid-1;
}
}
return ans;
}
bool check(int l, int ed){
int st = 1, r = l+ed-1;
// st 是目标串的头,l 是当前子串的头
for(int i=1; i<4; i++){ // 允许失配至多 3 次
int t = lcp(l, st, ed-st+1); // 二分找当前字串与目标串的最长的相同长度
// 跳过失配的字符
l += t+1;
st += t+1;
if(st > ed) return true;
}
return hshnum(hsha, l, r) == hshnum(hshb, st, ed);
}
int main(){
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int T; cin>>T;
base[0] = 1;
for(int i=1; i<N; i++) base[i] = (base[i-1]*bs)%p;
while(T--){
string a, b; cin>>a>>b;
int n = a.length(), m = b.length();
if(n < m){cout<<"0\n"; continue;}
a = ' '+a, b = ' '+b;
for(int i=1; i<=n; i++)
hsha[i] = (hsha[i-1]*bs%p + a[i])%p;
for(int i=1; i<=m; i++)
hshb[i] = (hshb[i-1]*bs%p + b[i])%p;
int ans = 0;
for(int i=1; i+m-1<=n; i++){
if(check(i, m)) ans++;
}
cout<<ans<<"\n";
}
return 0;
}
集合哈希
集合与序列的不同点在于,集合是无序的,也就是说,序列要求每个位置一一相等,而集合只需要对应的元素出现次数相等即可。
此时的哈希函数一般可以表示成如下形式:
特殊地,常用的哈希函数有两种:
-
随机权值哈希,即我们给每个元素
赋一个随机数 ,然后 。 -
将原集合映射成一个
序列,即若该元素 出现在集合中,第 位置为 ,否则为 ,然后对得到的 序列进行字符串哈希即可;如果原集合是可重集,那么对应的,原来 的每个位置上保存每个元素的出现次数即可,也就是哈希函数 。
CF1175F The Number of Subpermutations
题目大意:给定数组
因为异或满足结合律与交换律,与顺序无关。所以可以考虑异或哈希。
为了避免重复,先将原数组的值随机映射到一个对应的数值。
我们考虑枚举所有
枚举所有
#include <bits/stdc++.h>
using namespace std;
#define ull unsigned long long
#define ll long long
const int N = 3e5+5;
ull hsh[N]; // 将 a 数组中的值随机映射,保证第 i 个数只能由前 i 个 a[i] 异或得到,而不能由其它数字异或得到
ull pre[N]; // 前缀异或和
ull num[N]; // 1 ~ n 的前缀异或和
int n, a[N];
int calc(int pos){
int res = 0, mx = 1;
for(int i=pos+1; i<=n&&a[i]!=1; i++){
mx = max(a[i], mx);
if(i>=mx && i-pos+1<=mx && num[mx]==(pre[i]^pre[i-mx]))
res++;
}
return res;
}
int main(){
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
random_device seed;
mt19937_64 rnd(seed());
cin>>n;
generate(hsh+1, hsh+1+n, rnd);
for(int i=1; i<=n; i++){
cin>>a[i];
num[i] = num[i-1]^hsh[i];
pre[i] = pre[i-1]^hsh[a[i]];
}
int ans = 0;
for(int i=1; i<=n; i++){
if(a[i] == 1) ans += calc(i)+1; // 单独一个1的贡献
}
reverse(a+1, a+1+n);
for(int i=1; i<=n; i++)
pre[i] = pre[i-1]^hsh[a[i]];
for(int i=1; i<=n; i++){
if(a[i] == 1) ans += calc(i);
}
cout<<ans;
return 0;
}
树哈希
本质上还是集合哈希。
树哈希是哈希中的一个种类,这里先给出哈希函数(以下均省略取模):
其中 xorshift 是一种基于位操作的伪随机数生成算法。
树同构是树哈希与换根 dp 的结合。
设
再设
可得:
可以理解为原来的
P5043 【模板】树同构
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int MOD = 1e9 + 7;
const int RP = 1145141919;
int n, m;
bool flag;
vector<int> g[55];
int sz[55], hsh[55], f[55], rt;
set<int> S[52];
int xor_shift(int x){
x ^= RP;
x ^= x<<13;
x ^= x>>7;
x ^= x<<17;
return x;
}
void getHash(int u, int fa){
hsh[u] = 1;
for(int v : g[u]){
if(v == fa) continue;
getHash(v, u);
hsh[u] += xor_shift(hsh[v]);
}
}
void dfs(int u, int fa){
if(fa == 0)
f[u] = hsh[u];
else
f[u] = xor_shift(f[fa]-xor_shift(hsh[u])) + hsh[u];
for(int v : g[u]){
if(v == fa) continue;
dfs(v, u);
}
}
signed main(){
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin>>m;
for(int i=1; i<=m; i++){
cin>>n;
rt = 0;
for(int i=1; i<=n; i++)
g[i].clear();
for(int j=1; j<=n; j++){
int x; cin>>x;
if(x){
g[x].push_back(j);
g[j].push_back(x);
}
else rt = j;
}
getHash(rt, 0);
dfs(rt, 0);
int ans = i;
for(int j=1; j<=n; j++)
S[i].insert(f[j]);
for(int j=1; j<=n; j++){
for(int k=1; k<m; k++){
if(S[k].count(f[j]))
ans = min(ans, k);
}
}
cout<<ans<<"\n";
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本