1. 程式人生 > 實用技巧 >【筆記】Hash

【筆記】Hash

雜言

話說字串 \(Hash\) 是真的香,不知為何,我一直很喜歡這種概率性的東西,尤其是這種正確率不穩定的方法,因為當你感覺過不了的時候突然 \(AC\) 的時候那種激動的心情是別的題目不能具有的。

好了,不多說了,開正題。

Hash

\(Hash\) 表又稱散列表,一般由 \(Hash\) 函式與了連結串列結構共同實現。

建立一個大小等於值域的陣列進行統計和對映是最簡單的 \(Hash\) 思想。——藍書

好像我也描述不大清,看百度百科吧......

\(Hash\),一般翻譯做雜湊、雜湊,或音譯為雜湊,是把任意長度的輸入(又叫做預對映\(pre-image\))通過雜湊演算法變換成固定長度的輸出,該輸出就是雜湊值。這種轉換是一種壓縮對映,也就是,雜湊值的空間通常遠小於輸入的空間,不同的輸入可能會雜湊成相同的輸出,所以不可能從雜湊值來確定唯一的輸入值。簡單的說就是一種將任意長度的訊息壓縮到某一固定長度的訊息摘要的函式。——百度百科。

字串Hash

終於開正題了。

所謂字串 \(Hash\) 就是把一個字串對映成為一個數字,在我理解下就是為了便於查詢和查詢而產生的一種演算法。

對於一個字串,取一固定值 \(P\) ,可以把字串看成 \(P\) 進位制數,並分配一個大於零的數值,代表每種字元,一般來說,我們分配的數值都遠小於 \(P\) 。例如,對於小寫字母構成的字串,可以令 $a = 1,b = 2,\cdots,z = 26 $。取一固定值 \(M\) ,求出該 \(P\) 進位制數對 \(M\) 的餘數,作為該字串的 \(Hash\)。——藍書

對於進位制數的選擇

進位制數一定要取一個質數!!!,當你選擇一個合數的時候,意味著你和零分差不了多遠了。

因為 \(Hash\) 是一種不完全演算法,會有衝突的事件產生,例如 \(orzc\)\(orzhjw\) 肯定是不同的,但是他們的 \(Hash\) 值都是 \(233\) ,然後計算機就會認為它們倆一樣,出現了雜湊衝突。

而合數就會加大這個概率。

推薦進位制數 : \(27,131,31,10007,2017\)

給張表格

計算雜湊值

1.【單雜湊】

long long Hash()
{
	int len = strlen(s);
	long long tot=0;
	for(int i = 0;i < len; i ++){
		tot =( (tot * p + (long long) s[i]) % mod + mod) % mod;
	}
	return tot;
}

2.【雙雜湊】

long long Hash1()
{
	int len = strlen(s);
	long long tot=0;
	for(int i = 0;i < len; i ++){
		tot =( (tot * p + (long long) s[i]) % mod1 + mod1) % mod1;
	}
	return tot;
}

long long Hash2()
{
	int len = strlen(s);
	long long tot=0;
	for(int i = 0;i < len; i ++){
		tot =( (tot * p + (long long) s[i]) % mod2 + mod2) % mod2;
	}
	return tot;
}

也就是把單雜湊寫兩遍,兩者都一樣的時候才算一樣,提高了正確性。

3.【自然溢位雜湊】

unsigned long long()
{
	int len = strlen(s);
	unsigned long long tot=0;
	for(int i = 0;i < len; i ++){
		tot =(tot * p + (unsigned long long) s[i]);
	}
	return tot;
}

說實話這種是我做題時寫的最多的,一是因為 $unsigned\ long \ long $ 自動取模,省去了取模的時間,二是它好寫(by 重度懶癌患者)。

例題

以 $luogu\ 3370 $ 為例。

#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<vector>
#define ull unsigned long long
#define M 1000010
using namespace std;
const int mod1 = 19260817;
const int mod2 = 19660813;
const int p=131;
/*================================================*/

ull n,ans=0;
string s;
struct node{
	ull x,y;
}s1[M];

/*================================================*/

ull Hash1(string s)
{
	int len=s.size();
	ull tot=0;
	for(int i=0;i<len;i++){
		tot=((tot*p+(ull)s[i])%mod1+mod1)%mod1;
	}
	return tot;
}
ull Hash2(string s)
{
	int len=s.size();
	ull tot=0;
	for(int i=0;i<len;i++){
		tot=((tot*p+(ull)s[i])%mod2+mod2)%mod2;
	}
	return tot;
}
bool cmp(node a,node b)
{
	return a.x<b.x;
}
/*=================================================*/

signed main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++) {
		cin>>s;
		s1[i].x=Hash1(s);
		s1[i].y=Hash2(s);
	}
	sort(s1+1,s1+n+1,cmp);
	for(int i=1;i<=n;i++){
		if(s1[i].x!=s1[i-1].x||s1[i].y!=s1[i-1].y) ans++;
	}
	cout<<ans;
	return 0;
}

這就是一個雙雜湊的模板題目。

計運算元串的雜湊值

字首雜湊值和字尾雜湊值的求法

如果我們已知字串 \(S\)\(Hash\) 值是 \(H(S)\) ,字串 \(S + T\)\(Hash\) 值是 \(H(S + T)\) ,那麼字串 \(T\)\(Hash\) 值就是

\[H(T) = (H(S + T) - H(S) * p^{length(T)})\ mod\ Mod \]

根據這個性質就可以進行運算。

字首 \(Hash\)
$$Hash[k] = Hash[i] - Hash[i - k] * power[k]$$

字尾 \(Hash\)
$$Hash[k] = Hash[i - k + 1] - Hash[i + 1] * power[k]$$

之後查詢子串的時候就可以 \(O(1)\) 查詢。

void prepare()//前後綴Hash值和進位制。 
{
    po[0] = 1;
    for(int i = 1;i <= n;i ++) {//進位制
        po[i] = po[i - 1] * p;
    }
    for(int i = 1;i <= n;i ++) {
        Hash_z[i] = Hash_z[i - 1] * p + s[i];//字首Hash值
    }
    for(int i = n;i >= 1;i --) {
        Hash_f[i] = Hash_f[i + 1] * p + s[i];//字尾Hash值
    }
    return;
}

例題

link loj 10036

題意請自己看題面。

這個題目就是一個查詢子串的例目,先預處理出子串的雜湊值,在判斷字首的雜湊值是否等於字尾,得出答案。

#include<cstdio>
#include<iostream>
#include<vector>
#define ull unsigned long long
#define ll long long
#define M 1000010
using namespace std;
const int mod1 = 19260817;
const int mod2 = 19660813;
const int p = 31;
/*================================================*/

char s[M >> 1];
ll po[M];
ll Hash[M];

/*================================================*/
void prepare(int x)
{
	po[0] = 1;
	for(int i = 1;i <= N; i++) {
		po[i] = po[i - 1] * p;
	}
	for(int i = 0;i < x; i++) {
		Hash[i] = Hash[i - 1] * p + (ll)s[i];
	}
	return;
}
/*=================================================*/

signed main()
{
	while(cin >> s) {
		int len = strlen(s);
		prepare(len);//計算出進位制,計算Hash值 
		for(int i = 1;i <= len;i ++) {
			int l = Hash[i - 1];
			int r = Hash[len - 1] - Hash[len - i - 1] * po[i];
			if(l == r) {
				printf("%d ",i);
			}
		}
		printf("\n");
	}
	return 0;
}

還有這道 link luogu 3498

這道題目字首雜湊值和字尾雜湊值都用到了,也就是最多能分成多少個子串,如果,大於當前最大值就換掉,最後求得的結果就是最終答案。

#include<cstdio>
#include<cmath>
#include<iostream>
#include<cstring>
#include<queue>
#include<algorithm>
#include<map>
#include<vector>
#include<set>
#define ull unsigned long long
#define ll long long
#define M 1000010
#define N 1010
#define INF 0x3f3f3f3f
using namespace std;
const int p = 10007;
/*================================================*/

int n;
int s[M];
ull Hash_z[M], Hash_f[M], po[M];;//正Hash值 ,反Hash值,進位制
int cnt;//計算有幾個最優解
int ans_cnt;//看可分為幾段不同的值
vector<ull> qp;//儲存每次分割的答案
int maxn = -1e5;//計算最終的可分的最大的段數
vector<int> ans;//儲存每個最優解
int m; //剪枝用


/*================================================*/

inline int read()
{
	int s = 0, f = 0;char ch = getchar();
	while (!isdigit(ch)) f |= ch == '-', ch = getchar();
	while (isdigit(ch)) s = s * 10 + (ch ^ 48), ch = getchar();
	return f ? -s : s;
}

void prepare()//前後綴Hash 值和進位制位 
{
	po[0] = 1;
	for(int i = 1;i <= n;i ++) {
		po[i] = po[i - 1] * p;
	}
	for(int i = 1;i <= n;i ++) {
		Hash_z[i] = Hash_z[i - 1] * p + s[i];
	}
	for(int i = n;i >= 1;i --) {
		Hash_f[i] = Hash_f[i + 1] * p + s[i];
	}
	return;
}

bool check(int x)//判斷函式,判斷是否有這個子串
{
	for(int i = 0;i <qp.size(); i ++) {
		if(qp[i] == x) return false;
	}
	return true;
}

void solve(int k)
{	
	ans_cnt = 0;
	for(int i = k;i <= n;i += k) {
		int sum_z = Hash_z[i] - Hash_z[i - k] * po[k];
		//這個子串字首Hash值
		int sum_f = Hash_f[i - k + 1] - Hash_f[i + 1] * po[k];//字尾
		if(check(sum_z)) {//判斷
			qp.push_back(sum_z);
			ans_cnt++;//累加可分的段數
			//if(check(sum_f)) //剪枝 1 ,效果如上
			qp.push_back(sum_f);
		}
	}
	if(maxn == ans_cnt) {//當已知的最優解和新求出的解相同時
		ans.push_back(k);//記錄k 值 
		cnt++;//最優解個數++
	} else if(maxn < ans_cnt) {//又新求出更優的解
		ans.clear();//清空原來的答案序列
		ans.push_back(k);//新增解
		maxn = ans_cnt;
		cnt = 1; 
	}
	if(ans_cnt != 0) {
		m = min(m,n / ans_cnt);//剪枝 2 效果如上
	}
	qp.clear();//多次分割記得清空
}
/*=================================================*/

signed main()
{
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	n = read();
	for(int i = 1;i <= n;i ++) {
		s[i] = read();
	}
	prepare();
	m = n;
	for(int i = 1;i <= m;i ++) solve(i);
	printf("%d %d\n",maxn,cnt);
	for(int i = 0;i <ans.size();i ++) printf("%d ",ans[i]);//輸出
	return 0;
}

這個題看不太懂的話去看 link

總結

說一下我寫字串雜湊出錯的地方吧。

  • 取進位制數的時候取小了,使得程式衝突性太高,本來滿分的程式被卡到了 \(72\) 分。

  • 求子串的雜湊值時寫了個 \(power[1] = 0\) ,天知道我咋想的。

  • 取模數時取了個合數。

  • 列舉子串的雜湊值時,左右區間取錯,(常有的事)。

結束了$ \cdots $。