哈希,kmp,trie树和AC自动机

1.哈希

字符串哈希实际上就是把一个字符串转化为一个数字

比如 \(abc=1*2^2+2*2^1+3*2^0\) (也就是把 a 映射为1,b 映射为2,c 映射为3)

然后就没啥了。。。

关于自然溢出:

但在有时候,会出现两个字符串不相同但是整数相同的情况,此时我们把“进制”取为131/1331/13331...,模数一般取为 \(2^{64}-1\) ,因为 \(unsigned\) \(long\) \(long\) 可以自然溢出,所以就不用取模了。

核心代码:

1.准备工作

p[0]=1;	
for(int i=1;i<maxn;i++)
{
	p[i]=p[i-1]*P;
}
for(int i=1;i<=n;i++)
{
	h[i]=h[i-1]*P+(ull)s[i]-'a';
}

2.求l,r区间的哈希值

ull gethash(int l,int r)
{
	return h[r]-h[l-1]*p[r-l+1];
}

注意,以上字符串是从1开始的。

2.KMP

显然,我还没有学懂

upd on:2024/7/2

这个算法主要解决的是字符串中关键字搜索。

暴力:从左到右一个一个匹配,时间复杂度为 \(O(nm)\) ,不够优秀。

KMP算法:利用已经部分匹配的有效信息,只修改模式串的指针,让模式串尽量移动到有效位置。

由于我没有图并且自己也不想画,下面的请自行脑补,见谅。

会发现在主串(i 指针)和模式串(j 指针)不匹配的情况下 \(j\) 指针要移动到 最长相同前后缀前缀 的下一个位置(假设这一位为 \(k\) )。因为前后缀都相同了,所以就不用匹配了,直接从下一个位置开始,利用了已经匹配的有效信息。

数学公式表达:\(p[0\) ~ \(k-1]==p[j-k\) ~ \(j-1]\)(p 为模式串)

定义\(next\) 数组表示当 \(i,j\) 指针失配时 \(j\) 要跳转的位置。

\(p[k]==p[j]\) 的时候,\(next[j+1]=next[j]+1\)

//以下是处理next数组
void getnext(string s,int nxt[]) 
{
	int n=s.size();
	int j=0;
	nxt[0]=0;
	for(int i=1;i<n;i++)
	{
		while(j>0&&s[i]!=s[j])
		{
			j=nxt[j-1];
		}
		if(s[i]==s[j]) j++;
		nxt[i]=j;
	}
} 
//以下是匹配
void kmp(string s,string t)
{
	int n=s.size(), m=t.size();
	for(int i=0,j=0;i<n;i++)
	{
		while(j>0&&s[i]!=t[j]) j=nxt[j-1];
		if(s[i]==t[j]) j++;
		if(j==m)
		{
			j=nxt[j-1];
			//i就是t串末尾 
		}
	}
}

3.trie树

差不多就是把一些字符串转化为一棵树

是一种用于快速查询某个字符串/字符前缀是否存在的数据结构。

插入/生成

//idx代表当前字符的编号,根节点为0 
//son数组一维下标是父节点的idx,二维下标是这个父节点的直接子节点的str[i]-'a'的值
//cnt数组表示以该idx结尾的字符串的个数,例如:有几个'abc'的字符串 
void insert(char str[])
{
	int p=0;
	for(int i=0;str[i];i++)
	{
		int u=str[i]-'a';
		if(!son[p][u]) son[p][u]=++idx;
		p=son[p][u];
	}
	cnt[p]++;
}

查询

int query(char str[])//查询字符串出现的次数 
{
	int p=0;
	for(int i=0;str[i];i++)
	{
		int u=str[i]-'a';
		if(!son[p][u]) return 0;
		p=son[p][u];	
	}
	return cnt[p];
} 

例如:\(sea,she,sell\) 我们可以得到下面的\(Trie\)
点我

关于trie树,第2,3两道题,推荐这篇题解 》》》( ⊙ o ⊙ )啊!

4.AC自动机

由于前面学的知识都忘了,一整个复习+回顾代码,发现很难肝,顺便修整了博客(


黄色的边就是fail指针。

对于代码中类似路径压缩的那一部分:son[u][i]=son[fail[u]][i];

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+5, M=1e6+5;
int n, son[N][26], cnt[N], idx, T;
char s[100], s1[M];
int fail[N];//失配指针 
void insert(char str[])
{
	int p=0;
	for(int i=0;str[i];i++)
	{
		int u=str[i]-'a';
		if(!son[p][u]) son[p][u]=++idx;
		p=son[p][u];
	}
	cnt[p]++;
}
void build()//建AC自动机
{
	queue<int> q;
	for(int i=0;i<26;i++)
	{
		if(son[0][i]) q.push(son[0][i]);
	}
	while(q.size())
	{
		int u=q.front();//用于BFS遍历字典树 
		q.pop();
		for(int i=0;i<26;i++)
		{
			int v=son[u][i];
			if(v) //存在 
			{
				fail[v]=son[fail[u]][i];//失配指针就是父节点的失配指针的同样的儿子 
				q.push(v);
			}
			else son[u][i]=son[fail[u]][i];//一种特殊处理,很像并查集里的路径压缩 
		}
	}
}
int query(char str[])
{
	int ans=0, u=0;
	for(int i=0;str[i];i++)
	{
		u=son[u][str[i]-'a'];
		for(int j=u;j&&cnt[j]!=-1;j=fail[j])
		{
			ans+=cnt[j], cnt[j]=-1;//防止重复计算 
		}
	}
	return ans;
} 
int main()
{
	cin>>T;
	while(T--)
	{
		memset(fail, 0, sizeof(fail));
		memset(son, 0, sizeof(son));
		memset(cnt, 0, sizeof(cnt));
		cin>>n;
		for(int i=0;i<n;i++)
		{
			cin>>s;
			insert(s);
		}
		cin>>s1;
		build();
		cout<<query(s1)<<endl;
	}
	return 0;
} 

AC自动机上dp大多为刷表法

练习题

1. [HNOI2006] 最短母串问题

link:https://www.luogu.com.cn/problem/P2322
虽说是AC自动机+dp,但这个题bfs也是可以过的。观察到n的范围很小,考虑状压来记录,id[i] 表示trie树上以i结尾的字符串的 \(2^{编号}\)(i为时间戳),build好后跑bfs,维护一个st表示经过的字符串,并记下答案,若 st=(1<<n)-1 ,输出答案即可。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e7+5;
int n, son[1000][30], idx, T, id[1000], st, tot, vis[1000][(1<<13)+5], c[N], ans[N], fa[N];
char s[100000];
int fail[100000];
void insert(char str[],int pos)
{
	int p=0;
	for(int i=0;str[i];i++)
	{
		int u=str[i]-'A';
		if(!son[p][u]) son[p][u]=++idx;
		p=son[p][u];
	}
	id[p]|=(1<<pos);
}
void build()
{
	queue<int> q;
	for(int i=0;i<26;i++)
	{
		if(son[0][i]) q.push(son[0][i]);
	}
	while(q.size())
	{
		int u=q.front();
		q.pop();
		for(int i=0;i<26;i++)
		{
			int v=son[u][i];
			if(v)
			{
				fail[v]=son[fail[u]][i];
				id[v]|=id[son[fail[u]][i]];
				q.push(v);
			}
			else son[u][i]=son[fail[u]][i];
		}
	}
}
void bfs()
{
	queue<pair<int,int> > q;
	q.push(make_pair(0, 0));
	vis[0][0]=0;
	int num=0, cnt=0;
	while(q.size())
	{
		int u=q.front().first, st=q.front().second;
		q.pop();
		if(st==((1<<n)-1))
		{
			int cnt1=0;
			while(num)
			{
				c[++cnt1]=ans[num];
				num=fa[num];
			}
			for(int i=cnt1;i>=1;i--) cout<<(char)(c[i]+'A');
			return ;
		}
		for(int i=0;i<26;i++)
		{
			int v=son[u][i];
			if(!v) continue;
			if(!vis[v][st|id[v]])
			{
				vis[v][st|id[v]]=1;
				q.push(make_pair(v, st|id[v]));
				fa[++cnt]=num;
				ans[cnt]=i;
			}
		}
		num++;
	}
}
int main()
{
	cin>>n;
	for(int i=0;i<n;i++)
	{
		cin>>s;
		insert(s, i);
	}
	build();
	bfs();
	return 0;
} 

2. 文本生成器

link:https://www.luogu.com.cn/problem/P4052

真正意义上的第一道AC自动机上跑dp。
正难则反,想到了。即 \(26^m\) 减去不合法的数量。
这种dp设定啥的一般比较套路,比如此题 \(dp[i][j]\) 表示当前在 j,填的串长为 i 的不合法数量,如果 \(son[u][i]\) 合法,\(dp[i+1][son[u][i]]+=dp[i][u]\) ,合法指的就是跳 fail 没有模式串。

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=65*26, mod=1e4+7;
int n, m, cnt[10005], dp[105][10005], son[10005][30], idx, ans=0, ban[10005];
char s[10005];
int fail[100000];
void insert(char str[])
{
	int p=0;
	for(int i=0;str[i];i++)
	{
		int u=str[i]-'A';
		if(!son[p][u]) son[p][u]=++idx;
		p=son[p][u];
	}
	ban[p]=1;
}
void build()
{
	queue<int> q;
	for(int i=0;i<26;i++)
	{
		if(son[0][i]) q.push(son[0][i]);
	}
	while(q.size())
	{
		int u=q.front();
		q.pop();
		for(int i=0;i<26;i++)
		{
			int v=son[u][i];
			if(v)
			{
				fail[v]=son[fail[u]][i];
				ban[v]|=ban[fail[v]];
				q.push(v);
			}
			else son[u][i]=son[fail[u]][i];
		}
	}
}
int qpow(int a,int b)
{
	int res=1;
	while(b)
	{
		if(b&1) res=res*a%mod;
		a=a*a%mod;
		b>>=1;
	}
	return res;
}
signed main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		cin>>s;
		insert(s);
	}
	dp[0][0]=1;
	build();
	for(int i=0;i<m;i++)
	{
		for(int j=0;j<=idx;j++)
		{
			for(int k=0;k<26;k++)
			{
				if(ban[son[j][k]]==0) 
				{
					dp[i+1][son[j][k]]=(dp[i+1][son[j][k]]+dp[i][j])%mod;
				}
			}
		}
	}
	ans=qpow(26, m)%mod;
	for(int i=0;i<=idx;i++) ans=((ans-dp[m][i])%mod+mod)%mod;
	cout<<ans%mod;
	return 0;
}

3. [BZOJ 2905]背单词

link:https://www.gxyzoj.com/d/hzoj/p/P433

一个A如果是B的子串,那么必能从一个B的子串跳fail跳到A。所以对于第i个串的答案,就是i以前所有i的子串的答案的最大值+w[i](注意要和0取max)(代码实现的这一部分把串的字母顺序建了一条链)。然后把这个值加到fail树上i串末尾以及他的子树。最后统计即可。

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define lid id<<1
#define rid id<<1|1
using namespace std;
const int maxn=5e5+5;
int n, T, w[maxn], pos[maxn], st[maxn], ed[maxn], idx, tot, fa[maxn], son[maxn][28];
string s[maxn];
int head[maxn], edgenum, fail[maxn];
struct edge{
    int nxt;
    int to;
}edge[maxn<<1];
void add(int from,int to)
{
    edge[++edgenum].nxt=head[from];
    edge[edgenum].to=to;
    head[from]=edgenum;
}
void insert(string str,int id)
{
	int p=0;
	for(int i=0;str[i];i++)
	{
		int u=str[i]-'a';
		if(!son[p][u]) son[p][u]=++tot, fa[tot]=p;
		p=son[p][u];
	}
	pos[id]=p;
}
void build_ac()
{
	queue<int> q;
	while(q.size()) q.pop();
	for(int i=0;i<26;i++)
	{
		if(son[0][i]) q.push(son[0][i]);
	}
	while(q.size())
	{
		int u=q.front(); 
		q.pop();
		for(int i=0;i<26;i++)
		{
			int v=son[u][i];
			if(v) 
			{
				fail[v]=son[fail[u]][i]; 
				q.push(v);
			}
			else son[u][i]=son[fail[u]][i];
		}
	}
}
struct seg_tree{
	int l, r, max, lazy;
}t[maxn<<2];
void pushup(int id) 
{
	t[id].max=max(t[lid].max, t[rid].max);
}
void build(int id, int l, int r) 
{
	t[id].l=l, t[id].r=r;
	if(l==r) 
	{
		t[id].max = 0;
		return ;
	}
	int mid=(l+r)>>1;
	build(lid, l, mid);
	build(rid, mid+1, r);
	pushup(id);
}
void pushdown(int id) 
{
	if(t[id].lazy&&t[id].l!=t[id].r) 
	{
		t[lid].max=max(t[lid].max, t[id].lazy);
		t[rid].max=max(t[rid].max, t[id].lazy);
		t[lid].lazy=max(t[lid].lazy, t[id].lazy);
		t[rid].lazy=max(t[rid].lazy, t[id].lazy);
		t[id].lazy=0;
	}
}
void modify(int id, int l, int r, int val) 
{
	pushdown(id);
	if(t[id].l==l&&t[id].r==r) 
	{
		t[id].max=max(t[id].max, val);
		t[id].lazy=max(t[id].lazy, val);
		return ;
	}
	int mid=(t[id].l+t[id].r)>>1;
	if(r<=mid) modify(lid, l, r, val);
	else if(l>mid) modify(rid, l, r, val);
	else modify(lid, l, mid, val), modify(rid, mid+1, r, val);
	pushup(id);
}

int query(int id, int x) 
{
	pushdown(id);
	if(t[id].l==t[id].r&&t[id].l==x) 
	{
		return t[id].max;
	}
	int mid=(t[id].l+t[id].r)>>1;
	if(x<=mid) return query(lid, x);
	else return query(rid, x);
}
void dfs(int u,int fa)
{	
	if(u)
		st[u]=++idx;
	for(int i=head[u];i;i=edge[i].nxt)
	{
		int v=edge[i].to;
		if(v==fa) continue;
		dfs(v, u);
	}
	ed[u]=idx;
}
void clear()
{
	tot=idx=edgenum=0;
	memset(son, 0, sizeof(son));
	memset(ed, 0, sizeof ed);
	memset(st, 0, sizeof st);
	memset(fa, 0, sizeof fa);
	memset(head, 0, sizeof head);
	memset(pos, 0, sizeof pos);
	memset(w, 0, sizeof w);
	memset(edge, 0, sizeof edge);
	memset(fail, 0, sizeof fail);
	memset(t, 0, sizeof t);
}
signed main()
{
	//freopen("1.txt","r",stdin);
	//freopen("2.txt","w",stdout);
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin>>T;
	while(T--)
	{
		cin>>n;
		clear();
		for(int i=1;i<=n;i++)
		{
			cin>>s[i]>>w[i];	
			insert(s[i], i);
		}
		build_ac();
		for(int i=1;i<=tot;i++)//建fail树 
		{
			add(fail[i], i);
		}
		dfs(0, -1);
		build(1, 1, idx);
		int ans=0;
		
		for(int i=1;i<=n;i++)
		{
			int x=pos[i];
			int res=0; 
			while(x)
			{
				res=max(res, query(1, st[x]));
				x=fa[x];//所有前面的串的字母 
			}
			modify(1, st[pos[i]], ed[pos[i]], max(0ll, max(res, res+w[i]))); 
			ans=max({ans, max(0ll, max(res, res+w[i]))});
		}
		cout<<ans<<"\n";
		
	}
	return 0;
} 

4. [JSOI2009] 密码

link:https://www.luogu.com.cn/problem/P4045

自己应该是能做出来的,但想复杂了,果然AC自动机的dp都很套路,设 \(dp[i][j][k]\) 表示到字符串第i位,AC自动机上j节点,模式串集合位k的方案数,注意用刷表法。转移很简单,和T2一样。
考虑输出,显然方案数≤42的直接用记忆化搜索判断可行性再输出即可,具体需要两个搜索:

点击查看代码
int dfs(int l,int u,int st)//记搜判断可行性 
{
	if(l==len)
	{
		vis[l][u][st]=1;
		g[l][u][st]=(st==tot);
		return g[l][u][st];	
	}
	if(vis[l][u][st]) return g[l][u][st];
	vis[l][u][st]=1;
	int res=0;
	for(int i=0;i<26;i++)
	{
		res|=dfs(l+1, son[u][i], st|id[son[u][i]]);
	}
	return g[l][u][st]=res;
}
void print(int l,int u,int st)
{
	if(!g[l][u][st]) return ;
	if(l==len)
	{
		for(int i=1;i<=l;i++) cout<<(char)(a[i]+'a');
		cout<<endl;
		return ; 
	}
	for(int i=0;i<26;i++)
	{
		a[l+1]=i;
		print(l+1, son[u][i], st|id[son[u][i]]);
	}
}

5. [BJWC2011] 禁忌

link:https://www.luogu.com.cn/problem/P4569

AC自动机dp+矩阵快速幂。

拼尽全力无法战胜!!!记得回来看看我

总结

这个题单刷完了,总体来说(只AC自动机+dp这部分)dp状态很好想,转移也很套路,包括标记通过fail转移什么的(ban[u]|=ban[fail[u]];)没有太大问题,但是,我的矩阵快速幂学得很不好!我已经三次尝试去自己理解,但无法战胜,于是我决定暂时跳过这方面的题,我还会回来的。


即使所有人都放弃你,仍有几十亿细胞为你而活。

posted @ 2024-09-02 20:18  zhouyiran2011  阅读(38)  评论(0)    收藏  举报