【原理】
字符串Hash函數把一個任意長度的字符串映射成一個非負整數,並且其衝突概率幾乎爲 。
- 取一固定值 ,把字符串看作 進制數,並分配一個大於 的數值,代表每種字符。一般來說,我們分配的數值都遠小於P。例如,對於小寫字母構成的字符串,可以令 。
- 取一固定值 ,求出該 進制數對 的餘數,作爲該字符串的Hash值。
一般來說, 我們取 或 ,此時Hash值產生衝突的概率極低,只要Hash值相同,我們就可以認爲原字符串是相等的。
通常我們取 ,即直接使用 unsigned long long 類型存儲這個Hash值,在計算時不處理算術溢出問題,產生溢出時相當於自動對 取模,這樣可以避免低效的取模運算。
int 溢出相當於 mod
unsigned int 溢出相當於 mod
unsigned long long 溢出相當於 mod
除了在極特殊構造的數據上,上述Hash算法很難產生衝突,一般情況下上述Hash算法完全可以出現在題目的標準解答中。我們還可以多取一些恰當的 和 的值(例如大質數),多進行幾組Hash運算,當結果都相同時才認爲原字符串相等,就更加難以構造出使這個Hash產生錯誤的數據。
【應用】
對字符串的各種操作,都可以直接對 進制數進行算術運算反映到Hash值上。
如果我們已知字符串 的Hash值爲 ,那麼在 後添加一個字符 構成的新字符串 的Hash值就是 。其中乘 就相當於 進制下的左移運算,是我們爲 選定的代表數值。
如果我們已知字符串 的Hash值爲 ,字符串 的Hash值爲 ,那麼字符串 的Hash值 。這就相當於通過 進制下在 後邊補 的方式把 左移到與 的左端對齊,然後二者相減就得到了 。
例如,
則: 表示爲 進制數:
表示爲 進制數:
在 進制下左移 位:
二者相減就是 表示爲 進制數:
根據上面兩種操作,我們可以通過 的時間預處理字符串所有前綴Hash值,並在 的時間內查詢它的任意子串的Hash值。
【模板】
typedef unsigned long long ull;
const int maxn = 1000010, base = 131;
char str[maxn];
ull h[maxn], p[maxn];
//O(1)查詢[L,R]子串的哈希值
ull get(int L, int R)
{
return h[R] - h[L-1] * p[R-L+1];
}
int main()
{
scanf("%s", str + 1);
int n = strlen(str + 1);
p[0] = 1;
for(int i = 1; i <= n; ++i)
{
h[i] = h[i-1] * base + str[i] - 'a' + 1;
p[i] = p[i-1] * base;
}
int m;
scanf("%d", &m);
while(m--) //m次查詢
{
int L, R;
scanf("%d%d", &L, &R);
printf("%llu\n", get(L,R));
}
return 0;
}