前言
此文主要介紹hash的各種亂搞方法,hash入門請參照我之前這篇文章
不好意思hash真的可以為所欲為
在開頭先放一下題表(其實就是我題解中的hash題目qwq)
查詢子串hash值
必備的入門操作,因為OI中用到的hash一般都是進制哈希,因為它有一些極其方便的性質,比如說,是具有和前綴和差不多的性質的。
假設一個字符串的前綴hash值記為\(h[i]\),我們hash時使用的進制數為\(base\),那么顯然\(h[i]=h[i-1]*base+s[i]\)
記\(p[i]\)表示\(base\)的\(i\)次方,那么我們可以通過這種方式\(O(1)\)得到一個子串的hash值(設這個子串為s[l]...s[r])
typedef unsigned long long ull;
ull get_hash(int l, int r) {
return h[r] - h[l - 1] * p[r - l + 1];
}
可是為什么呢?
我們知道,進行進制哈希的過程本質上就是把原先得到的哈希值在\(base\)進制上強行左移一位,然后放進去當前的這個字符。
現在的目的是,取出\(l\)到\(r\)這段子串的hash值,也就是說,\(h[l-1]\)這一段是沒有用的,我們把在\(h[r]\)這一位上,\(h[l-1]\)這堆字符串的hash值做的左移運算全部還原給\(h[l-1]\),就可以知道\(h[l-1]\)在\(h[r]\)中的hash值,那么減去即可。(簡單的容斥思想)
這是基本操作,現在來看一個這個的拓展問題。
題意
現在有一個字符串\(s\),每次詢問它的一個子串刪除其中一個字符后的hash值(刪除的字符時給定的)
要求必須\(O(1)\)回答詢問
Sol
刪除操作?那不能像上面那樣子簡單粗暴的來搞了,但是其實本質上是一樣的。
假設我們現在詢問的區間為\([l,r]\),刪除的字符為\(x\)(指位置,不是字符)
類比上面的做法,我們可以先\(O(1)\)得到區間\([l,x-1]\)和區間\([x+1,r]\)的hash值,那么現在要做的事情就是把這兩段拼起來了,由於我們使用的是進制hash,所以其實很簡單,強行將前面的區間強行左移\(r-x\)位(這么看可能會好理解一點:\(r-(x+1)+1\))就好。
代碼實現也很簡單
typedef unsigned long long ull;
ull get_hash(int l, int r) {
return h[r] - h[l - 1] * p[r - l + 1];
}
ull get_s(int l, int r, int x) {
return get_hash(l, x - 1) * p[r - x] + get_hash(x + 1, r);
}
這題的原題是LOJ#2823. 「BalticOI 2014 Day 1」三個朋友 ,需要分類討論一下,不過知道上面這個也就不難了
用hash求最長回文子串/回文子串數
最長回文子串!我知道!馬拉車!可以\(O(n)\)!
可是如果你馬拉車寫掛了呢?或者像我一樣不會馬拉車
這時候就得靠hash來水分了
我們知道,回文子串是具有單調性的
如果字符串s[l...r]為回文子串,那么s[x...y](l<x,y<r)也一定是回文子串
單調性!我們是不是可以二分?
我們暫時只討論長度為奇數的回文子串。(事實上,長度為偶數的回文子串與奇數的只是處理上的一些細節不同,僅此而已)
考慮枚舉回文子串的中點,並二分回文子串的長度(不過一般來說,二分回文子串的長度的1/2可能會更好寫一點),那么我們使用上文提到的\(O(1)\)查詢子串hash值的方法,就可以\(O(1)\)判斷二分得到的這個子串是不是回文子串了。
對於長度為偶數的回文子串,枚舉中點左邊/右邊的字符即可
效率是\(O(nlogn)\)的,復雜度較馬拉車算法比較遜色,不過如果馬拉車算法打掛或者是時間復雜度允許的情況下,hash也是一個不錯的選擇。
然后還有一種方法,適合像我這種下標總是搞錯的,可以直接處理出正串和反串的hash值,然后每次根據二分出來的長度計算整個字符串的起止,判斷正串和反串的hash值是否相等即可。(這樣就不用研究惡心的下標了...研究下標還得分奇偶討論...)
字符串的很多特性是具有單調性的,二分求解是一個常見的思路,配合哈希進行判斷操作一般可以做到在\(O(nlogn)\)效率內完成問題
例題:SP7586 NUMOFPAL - Number of Palindromes
練習:LOJ#2452. 「POI2010」反對稱 Antisymmetry
例題代碼
#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
#define N 10100
#define base 13131
char s[N];
ull h1[N], p[N], h2[N], ans = 0;
int n;
ull gh1(int l, int r) { return h1[r] - h1[l - 1] * p[r - l + 1]; }
ull gh2(int l, int r) { return h2[l] - h2[r + 1] * p[r - l + 1]; }
ull query1(int x) { //奇
int l = 1, r = min(x, n - x);
while(l <= r) {
int mid = (l + r) >> 1;
if(gh1(x - mid, x + mid) == gh2(x - mid, x + mid)) l = mid + 1;
else r = mid - 1;
}
return r;
}
ull query2(int x) { //偶
int l = 1, r = min(x, n - x);
while(l <= r) {
int mid = (l + r) >> 1;
if(gh1(x - mid + 1, x + mid) == gh2(x - mid + 1, x + mid)) l = mid + 1;
else r = mid - 1;
}
return r;
}
int main() {
scanf("%s", s + 1); p[0] = 1;
n = strlen(s + 1);
for(int i = 1; i <= n; ++i) {
h1[i] = h1[i - 1] * base + s[i];
p[i] = p[i - 1] * base;
}
for(int i = n; i; i--) h2[i] = h2[i + 1] * base + s[i];
for(int i = 1; i < n; ++i) {
ans += query1(i) + query2(i);
}
printf("%llu\n", ans + n);
}
用hash代替kmp算法
關於kmp算法,可以看pks大佬的blog,講的真的很好!
但是我們這里不講kmp算法,我們利用hash來代替kmp算法求解單模式串匹配問題。
但是kmp算法的next數組真的很妙!可以解決很多神奇的東西,強烈推薦去學學!
好了,步入正題。
單模式串匹配問題是什么?
給出兩個字符串\(s1\)和\(s2\),其中\(s2\)為\(s1\)的子串,求\(s2\)在\(s1\)中出現多少次/出現的位置。
如果有認真看過該篇文章的第一子目的話,應該不難想到這題的hash做法。
具體做法是預處理出來兩個串的hash值,因為求的是\(s2\)在\(s1\)中出現的次數,所以我們要匹配的長度被壓縮到了\(s2\)的長度,所以我們只需要枚舉\(s2\)在\(s1\)中的起點,看看后面一段長度為\(len\)的區間的hash值和\(s2\)的hash值一不一樣就好。
時間復雜度是\(O(n+m)\)的!和kmp算法一樣!
例題:LOJ #103. 子串查找 (本來想放洛谷的結果要輸出next數組就沒辦法了23333)
例題代碼
#include <bits/stdc++.h>
using namespace std;
#define N 1000010
#define ull unsigned long long
#define base 233
ull h[N], p[N], ha;
char s1[N], s2[N];
int main() {
scanf("%s%s", s1 + 1, s2 + 1);
int n = strlen(s1 + 1), m = strlen(s2 + 1);
for(int i = 1; i <= m; ++i) ha = ha * base + (ull)s2[i];
p[0] = 1;
for(int i = 1; i <= n; ++i) {
h[i] = h[i - 1] * base + (ull)s1[i];
p[i] = p[i - 1] * base;
}
int l = 1, r = m, ans = 0;
while(r <= n) {
if(h[r] - h[l - 1] * p[m] == ha) ++ans;
++l, ++r;
}
printf("%d\n", ans);
}
用hash代替其他一些字符串算法
因為博主並沒有寫過,所以並不打算深入講(沒寫過不熟悉啊...)
這一子目會分析一下hash還能代替哪些算法以及使用hash算法代替的復雜度是多少
manacher算法
求最長回文串/回文串個數manacher算法是可以做到\(O(n)\)的
使用hash+二分可以做到\(O(nlogn)\),並且實現簡單
kmp算法
進行單模式串匹配可以使用hash進行
復雜度\(O(n+m)\),kmp算法復雜度也是\(O(n+m)\)。但是kmp的next數組可以做到一些hash做不到的事情。
上面兩個是前面兩子目分析過的。
AC自動機
多模式串匹配:求文本串中各個模式串出現了多少次。
設文本串的長度為\(n\),模式串的總長度為\(len\),模式串的個數為\(m\)
hash出文本串中每個子串,並存入一個map中,復雜度是\(O(n^2logn)\)的(用map主要是便於查詢)。然后hash出每個模式串,復雜度是\(O(len)\)的。
對每個模式串,查詢對應的map中文本串的子串的個數即可。復雜度\(O(mlogn)\)
總復雜度是\(O(n^2logn+len+mlogn)\)
這個\(log\)可以去掉的(自行寫個哈希表)。
所以並沒有什么用...還是用AC自動機實在。
用AC自動機可以做到\(O(n+len)\)
后綴數組
求后綴數組中的SA數組。(如果不知道請自行百度)(給定的串為S)
最暴力的做法是直接對每個后綴進行排序,並逐字符匹配,這樣會達到\(O(n^2logn)\)
那么有沒有不這么無腦的做法?
有!有個hash+二分的神仙做法可以做到\(O(nlognlogn)\)
我們處理出整個串S的hash值。
在排序中對兩個子串進行排序的過程中,采用二分找相同的前綴(比較用hash,可以\(O(1)\)),那么設我們最后二分到的值為r,則直接比較\(s[x+r+1]\)和\(s[y+r+1]\)的大小即可(設子串1的起點為\(x\),子串2的起點為\(y\))。這樣每次比較的復雜度就是\(O(logn)\)了。
加上排序,總的復雜度為\(O(nlognlogn)\)
並且其實還能求出height數組的,但是我自己對height數組的理解也不大行,所以這里就不討論這個。
而后綴數組的復雜度是\(O(nlogn)\)(使用倍增法)
后綴數組這部分主要參考自李煜東的《算法競賽進階指南》。
使用hash的幾個要注意的地方
在復雜度允許的情況下,盡量采用多hash(不過一般雙hash就夠)
比賽時能不用自然溢出就不要(平時刷題如果用自然溢出被卡可以及時換掉,但是比賽時如果用自然溢出,OI賽制就GG了)
模數用大質數這個不用說了
並且進制數不要選太簡單的,比如\(233\)和\(13131\)這樣的,盡量大一點,比如\(13131\)和\(233333\)。太小容易被卡。
以及要合理應對各種卡hash方法的最好方法就是自己去卡一遍hash,詳情請參考BZOJ hash killer系列。