萬能的進制哈希
題外話:
為什么要學字符串算法?
為了快速比較兩個字符串是否相等,眾所周知垃圾C++在比較兩個字符串的時候效率並不高,所以我們需要設計一種算法更高效地比較字符串
大致用途:
1.判斷兩個字符串是否相等;
2.判斷一個字符串是否曾經出現過;
3.讓某些用戶口吐芬芳的時候網頁可以自動屏蔽掉;
定義:
百度百科:Hash,一般翻譯做散列、雜湊,或音譯為哈希,是把任意長度的輸入(又叫做預映射pre-image)通過散列算法變換成固定長度的輸出,該輸出就是散列值。
人話翻譯:把字符賦予進制和模數,將每一個字符串映射為一個小於模數數字。
具體操作:
我們設置進制(base)為131,模數(mod)為1e9+7,現在我們對一個字符串s進行哈希
char s[10];
cin>>(s+1);
int len=strlen(s+1);
int base=131,mod=1e9+7;
for(int i=1;i<=len;++i)
{
hash[i] = ( ( hash[i-1] * base ) + s[i] ) % mod ;
}
這樣hash[len]里面就是字符串s的哈希值了;
hash還有一個方便的操作就是取子串的hash值
hash[l,r] = ( hash [r] - hash[l-1] * pw[r-l+1] ) %mod //偽代碼 pw[r-l+1]為base的(r-l+1)次方
注意:
哈希沖突:
什么是哈希沖突:比如orz的哈希值是2333,然而sto的哈希值也是2333,這樣就會產生哈希沖突,從而讓哈希算法判斷失誤。
解決方法:
1.模數選取大質數
如果選取合數那么他的剩余系將會有所浪費(不了解剩余系請找一篇數論博客QwQ),如果質數過小將會導致剩余系過小,哈希沖突幾率增大(質數過大爆負數,謹慎設置)
2.雙模數哈希
我們可以通過設置兩個不同的哈希方式,對於一個字符串,當且僅當兩個哈希值都相同時才判定相當。
這種方法相當有效,除非出題人對着你的數據卡你,否則正確率近乎100%(詳情請見BZOJ Hash Killer 3)
實戰應用:
1.hash判斷最長公共前綴
題目概述:給定一個字符串,要求維護兩種操作在字符串中插入一個字符詢問某兩個位置開始的 LCP(最長公共前綴)插入操作次數
插入<=200,字符串總長度<=50000,查詢次數<=20000。
分析:插入<=200,考慮每次插入暴力維護 復雜度200*50000
每次查詢二分LCP的長度,然后hash O(1)判斷是否相等
2.哈希判斷回文串
求一個字符串中包含幾個回文串?
manachar?其實哈希也很好用,而且復雜度只多一個log呢QwQ
對於該串維護正反兩個哈希值,我們稱為正向哈希和反向哈希
每次二分一個回文串長度,用正反哈希O(1)判斷是否相等
對於奇偶回文串可以考慮每個字符中間插入一個新字符,也可以分開處理
3.線段樹維護哈希
給出一個1到n的排列,問是否存在長度大於等於3的等差子序列
分析:其實只要找長度等於3的就好了嘛QwQ
一個01串,從前往后掃描這個序列,將掃描過的數字對應位置變為1
對於每一個數字,如果目前不能構成等差序列,那么他兩側的01串必然是一個回文串,我們可以對01串維護一個哈希值進行比較
由於我們需要動態修改和區間查詢哈希值,所以我們考慮權值線段樹來維護正反哈希。
線段樹維護哈希細節較多,這里我細致地說一下,同時為了代碼清晰可讀,這里利用unsigned long long 自然溢出,略去取模操作
首先一些變量函數
ans1[500010]//正向哈希線段樹節點 ans2[500010]//反向哈希線段樹節點 query1函數:正向哈希查詢 query2函數:反向哈希查詢
線段樹push_up向上維護操作
要將正哈希的左兒子的哈希值乘上進制的右區間長度次方,反哈希的右兒子乘上進制的左區間長度次方
ans1[p] = ans1[ls(p)] * pw[r-mid] + ans1[rs(p)] ; ans2[p] = ans2[rs(p)] * pw[mid-l+1] + ans2[ls(p)] ;//注意ls(p)和rs(p)的區別
本題目不需要push_down下放操作,后面會提到
查詢時分類討論:
完全在左兒子區間:直接返回左兒子值;
完全在右兒子區間:直接返回右兒子值;
二者都在:
正向:左兒子值*右查詢長度+右兒子值;
反向:右兒子值*左查詢長度+左兒子值;
inline int query1(int tl,int tr,int l,int r,int p) { if(tl<=l&&r<=tr) return ans1[p]; if(tr<=mid) return query1(tl,tr,l,mid,ls(p)); else if(mid<tl) return query1(tl,tr,mid+1,r,rs(p)); else { int lx=query1(tl,tr,l,mid,ls(p)); int rx=query1(tl,tr,mid+1,r,rs(p)); return lx*pw[min(tr,r)-mid]+rx; } } inline int query2(int tl,int tr,int l,int r,int p) { if(tl<=l&&r<=tr) return ans2[p]; if(tr<=mid) return query2(tl,tr,l,mid,ls(p)); else if(mid<tl) return query2(tl,tr,mid+1,r,rs(p)); else { int lx=query2(tl,tr,l,mid,ls(p)); int rx=query2(tl,tr,mid+1,r,rs(p)); return rx*pw[mid-max(tl,l)+1]+lx; } }
多測不清空,爆零兩行淚
下面貼完整代碼
#include<bits/stdc++.h>
using namespace std; #define int unsigned long long
#define ls(p) (p<<1)
#define rs(p) (p<<1|1)
#define mid ((l+r)>>1) inline int read() { int x=0,f=1; char ch; for(ch=getchar();(ch<'0'||ch>'9')&&ch!='-';ch=getchar()); if(ch=='-') f=0,ch=getchar(); while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();} return f?x:-x; } int T,base=131; int a[100010],n; int ans1[500010],ans2[500010]; int pw[100010]; bool flag; inline void update(int tl,int tr,int l,int r,int p) { if(tl<=l&&r<=tr) { ans1[p]=ans2[p]=1; return; } if(tl<=mid) update(tl,tr,l,mid,ls(p)); else update(tl,tr,mid+1,r,rs(p)); ans1[p] = ans1[ls(p)] * pw[r-mid] + ans1[rs(p)] ; ans2[p] = ans2[rs(p)] * pw[mid-l+1] + ans2[ls(p)] ; } inline int query1(int tl,int tr,int l,int r,int p) { if(tl<=l&&r<=tr) return ans1[p]; if(tr<=mid) return query1(tl,tr,l,mid,ls(p)); else if(mid<tl) return query1(tl,tr,mid+1,r,rs(p)); else { int lx=query1(tl,tr,l,mid,ls(p)); int rx=query1(tl,tr,mid+1,r,rs(p)); return lx*pw[min(tr,r)-mid]+rx; } } inline int query2(int tl,int tr,int l,int r,int p) { if(tl<=l&&r<=tr) return ans2[p]; if(tr<=mid) return query2(tl,tr,l,mid,ls(p)); else if(mid<tl) return query2(tl,tr,mid+1,r,rs(p)); else { int lx=query2(tl,tr,l,mid,ls(p)); int rx=query2(tl,tr,mid+1,r,rs(p)); return rx*pw[mid-max(tl,l)+1]+lx; } } signed main() { T=read(); for(int i=pw[0]=1;i<=100000;++i) pw[i] = pw[i-1] * base; while(T--) { n=read(); flag=0; memset(ans1,0,sizeof(ans1)); memset(ans2,0,sizeof(ans2)); for(int i=1;i<=n;++i) { a[i]=read(); if(!flag) { int d=min(a[i]-1,n-a[i]); if(d) { if(query1(a[i]-d,a[i],1,n,1)^query2(a[i],a[i]+d,1,n,1)) flag=1; } update(a[i],a[i],1,n,1); } } puts(flag?"Y":"N"); } return 0; }
4.哈希判斷循環節
給定一個數字串,要求維護以下兩個操作:
1.將l到r區間內數字全部改為k
2.詢問l到r區間內是否存在長度為k的循環節
前置神仙結論:判斷一個字符串[ l , r ] 是否有長度為k的循環節,只需判斷 [ l+d , r ] 和 [ l , r-d ] 是否相等。
有了上述結論,這道題就變成了一個哈希值的區間修改和區間查詢問題,再碼一顆線段樹就可以了
在這里放一下上面沒有展示的push_down下放代碼
inline void push_down(int l,int r,int p) { int k=tag[p]; ans[ls(p)] = val[k][mid-l+1]; // val[k][len] 預處理出來
ans[rs(p)] = val[k][r-mid]; //表示字符串內全部都是k的長度為len的 哈希值
tag[ls(p)]=tag[rs(p)]=tag[p]; tag[p]=-1; // 修改可能存在 0 所以tag 要賦成 -1
} for(int i=0;i<10;++i) { for(int j=1;j<=100005;++j) { val[i][j]=val[i][j-1] * base + i; } }