关于一些哈希

随缘更新,但考虑到马上要退役,毕业前应该没机会力。

求字符串的最长公共前缀

标准

空间复杂度\((\sum_i |s_i|)\),但根据具体场景通常可以缩小至\(O(n)\)
时间复杂度\(O(\sum_i |s_i|)\)预处理,\(O(\log min(|s_i|,|s_j|))\)求两字符串的最长公共前缀

对于每个字符串,预处理其前缀hash数组。查询时二分找到最小的位置mid,令hash(i,mid)=hash(j,mid)。

使用这个技巧的题目,时间复杂度可能为\(O(n\log n)\)

变形

TJOI2017 DNA为例。

题目描述

加里敦大学的生物研究所,发现了决定人喜不喜欢吃藕的基因序列 \(S\),有这个序列的碱基序列就会表现出喜欢吃藕的性状,但是研究人员发现对碱基序列 \(S\),任意修改其中不超过 \(3\) 个碱基,依然能够表现出吃藕的性状。现在研究人员想知道这个基因在 DNA 链 \(S_0\) 上的位置。所以你需要统计在一个表现出吃藕性状的人的 DNA 序列 \(S_0\) 上,有多少个连续子串可能是该基因,即有多少个 \(S_0\) 的连续子串修改小于等于三个字母能够变成 \(S\)

对于 \(100\%\) 的数据,\(S_0,S\) 的长度不超过 \(10^5\)\(0\lt T\leq 10\)

做法

二分哈希,找到前三个需要修改的字符(即无法匹配之处),判断剩余部分是否可以匹配。

点击查看代码
#include <bits/stdc++.h>
using namespace std;
#define lld unsigned long long

const lld base = 19991;
const int N = 1e5+5, maxn = 1e5;
lld h[N], h2[N], bw[N];
int n, m;
char s[N], s2[N];

int get(int l, int r){
	return h[r]-h[l-1]*bw[r-l+1];
}
int get2(int l, int r){
	return h2[r]-h2[l-1]*bw[r-l+1];
}

void init(){
	scanf("%s%s", s+1, s2+1);
	n = strlen(s+1), m = strlen(s2+1);
	for(int i = 1; i <= n; i++) h[i] = h[i-1]*base+s[i];
	for(int i = 1; i <= m; i++) h2[i] = h2[i-1]*base+s2[i];
}


int check(int L, int R){
	int l = L, r = R;
	while(l < r){
		int mid = (l+r)>>1;
		if(get(L, mid) == get2(m-(R-L), m-(R-mid))) l = mid+1;
		else r = mid;
	} return l;
}

void solve(){
	init(); int ans = 0;
	if(m > n){ puts("0"); return ;}
	for(int i = 1; i+m-1 <= n; i++){
		int l = i, r = i+m-1;
		for(int j = 1; j < 4; j++){
			l = check(l, r)+1;
			if(l > r) break;
		} if(l > r){ ans++; continue ;} 
		if(get(l, r) == get2(m-(r-l), m)){
			ans++; continue ;
		}
	} printf("%d\n", ans);
}

int main(){
	bw[0] = 1;
	for(int i = 1; i <= maxn; i++) bw[i] = bw[i-1]*base;
	int t; scanf("%d", &t);
	while(t--) solve();
	return 0;
}

取出整段哈希中的一个区间

标准

不知道怎么描述,看代码差不多就能理解吧!

#define lld unsigned long long
// 这是个不太好的习惯,最好用ull表示……
const lld base = 233, mod = 1e9+7; // 貌似自然溢出也很好
lld hash[N], bw[N]; // hash有撞函数的可能,实际写代码时建议换一个函数名

lld get(int l, int r){ // 取区间[l,r]的哈希值
    return (hash[r]-hash[l-1]*bw[r-l+1]%mod+mod)%mod;
}

int main(){
    bw[0] = 1, hash[0] = 0;
    for(int i = 1; i <= n; i++){ // 预处理
        hash[i] = (hash[i-1]*base%mod+s[i])%mod;
        bw[i] = bw[i-1]*base%mod;
    }
}

例题

[ARC172C] Election:差不多就是板子的意味,但是比起板子,可以更快地上手哈希,对(我这样的)新手很有帮助。

[JSOI2009] 电子字典:这题有更简洁的写法,可以不用取区间再合并,但推荐写一写,熟悉一些细节的处理。注意常数大小,最好用set而非map

回文串的哈希

标准

回文串的哈希前缀数组和后缀数组相同,这是显然的。

运营

[POI2006] PAL-Palindromes为例。

使两回文串拼接成回文串,则有:

\[s+t = t+s\\ \downarrow\\ hash(s)\times base^{|t|} + hash(t) = hash(t)\times base^{|s|} + hash(s) \]

点击查看代码
#include <bits/stdc++.h>
using namespace std;
#define lld unsigned long long

const int N = 2e6+5, maxn = 2e6;
const lld base = 233, mod = 1e9+7;
lld ans = 0, h[N], bw[N];
int n, len[N];
char s[N];

lld qpow(lld x, lld y){
	lld ret = 1;
	while(y){
		if(y&1) ret = ret*x%mod;
		x = x*x%mod; y >>= 1;
	} return ret;
}

unordered_map<lld, int> cnt;

int main(){
	scanf("%d", &n); bw[0] = 1;
	for(int i = 1; i <= maxn; i++)
		bw[i] = bw[i-1]*base%mod;
	for(int i = 1; i <= n; i++){
		scanf("%d%s", &len[i], s+1);
		lld hsh = 0;
		for(int j = 1; j <= len[i]; j++)
			hsh = (hsh*base+s[j])%mod;
		hsh = hsh*qpow(bw[len[i]]-1, mod-2)%mod;
		h[i] = hsh, cnt[hsh]++;
	} for(int i = 1; i <= n; i++)
		ans += cnt[h[i]];
	printf("%llu\n", ans);
	return 0;
}
posted @ 2024-11-18 21:47  _kilo-meteor  阅读(16)  评论(0编辑  收藏  举报