hash進階:使用字符串hash亂搞的姿勢


前言

此文主要介紹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)

練習:UVA10298 Power Strings

例題代碼

#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系列。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM