字符串专题

rt,会罗列一些近期做的串串题

一些题单:

清烛 - 字符串

Hoks -『提高进阶篇』ACAM

Polaris_Australis - 后缀数组,后缀自动机,回文自动机题单

hs_black - 【字符串】后缀系列

一些博文:

alex-wei - 常见字符串算法 好像是老文章了?不过里面有修订版索引。

Part 1 ACAM

I.基础题

P3966 [TJOI2013] 单词

ACAM

ACAM 练手题,先考虑给这个文本串两两之间用一个没出现过的符号隔开就可以搞出论文,然后建出字典树,然后用论文去跑,那么如果当前节点在 \(x\),那么所有 \(fail_x\) 那些单词都可以作为这个的后缀,所以先全打标记,最后从后往前释放即可。

#include<bits/stdc++.h>
using namespace std;
#define ll long long
namespace IO
{
	template<typename T>
	void read(T &_x){_x=0;int _f=1;char ch=getchar();while(!isdigit(ch)) _f=(ch=='-'?-1:_f),ch=getchar();while(isdigit(ch)) _x=_x*10+(ch^48),ch=getchar();_x*=_f;}
	template<typename T,typename... Args>
	void read(T &_x,Args&...others){Read(_x);Read(others...);}
	const int BUF=20000000;char buf[BUF],to,stk[32];int plen;
	#define pc(x) buf[plen++]=x
	#define flush(); fwrite(buf,1,plen,stdout),plen=0;
	template<typename T>inline void print(T x){if(!x){pc(48);return;}if(x<0) x=-x,pc('-');for(;x;x/=10) stk[++to]=48+x%10;while(to) pc(stk[to--]);}
}
using namespace IO;
const int N = 2e6+10,M = 210;
int n,tr[N][27],c[M],fail[N],b[N],cnt = 1,o,cnt1,st[N],cnt2,x,y,z;
string s,t;
ll sum[N];
queue<int>p;
inline int insert(int x)
{
	for(int j = 1;j <= cnt1;j++) 
	{
		if(!tr[x][b[j]]) tr[x][b[j]] = ++cnt;
		x = tr[x][b[j]];
	}
	return x;
}
inline void fail_build()
{
	for(int i = 0;i < 26;i++) 
		if(tr[0][i]) p.push(tr[0][i]);
	while(!p.empty())
	{
		x = p.front(),p.pop(); st[++cnt2] = x;
		for(int i = 0;i < 26;i++)
			if(tr[x][i]) p.push(tr[x][i]),fail[tr[x][i]] = tr[fail[x]][i];
			else tr[x][i] = tr[fail[x]][i];
	}
}
inline void query(int x){ for(int j = 1;j <= cnt1;j++) x = tr[x][b[j]],sum[x]++; }//注意到要么有,要么是fail,直接取tr 
signed main()
{
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	read(n);
	for(int i = 1;i <= n;i++) 
	{
		cin >> s,cnt1 = s.size(),t += s,s = ' '+s; t += 'A';
		for(int j = 1;j <= cnt1;j++) b[j] = s[j]-'a';
		c[i] = insert(0);
	} fail_build();
	cnt1 = t.size(),t = ' '+t;
	for(int j = 1;j <= cnt1;j++) b[j] = t[j]-'a';
	query(0);
	for(int i = cnt2;i >= 1;i--) sum[fail[st[i]]] += sum[st[i]];
	for(int i = 1;i <= n;i++) print(sum[c[i]]),pc('\n');
	flush(); 
	return 0;
}

P4052 [JSOI2007] 文本生成器

dp+ACAM

ACAM+dp,首先我们建出字典树。至少有一个单词出现转化为全都不在文章里出现,等会容斥就好了,然后考虑每次取下一个字符会发生什么。

在求 \(fail\) 的时候,需要让 \(S_x |= S_{fail_x}\),这样我们每次放字符只需要保留最长的后缀即可看是否里面出现了单词。

显然就是找一个最长的后缀去匹配,这样有效的字符串会尽可能多。

然后每次转移,不转移 \(S_i = 1\) 的,\(S_i = 1\) 的不转移即可,复杂度 \(m*len*26\)\(len\) 是字典树大小。

code

#include<bits/stdc++.h>
using namespace std;
namespace IO
{
	template<typename T>
	void read(T &_x){_x=0;int _f=1;char ch=getchar();while(!isdigit(ch)) _f=(ch=='-'?-1:_f),ch=getchar();while(isdigit(ch)) _x=_x*10+(ch^48),ch=getchar();_x*=_f;}
	template<typename T,typename... Args>
	void read(T &_x,Args&...others){Read(_x);Read(others...);}
	const int BUF=20000000;char buf[BUF],to,stk[32];int plen;
	#define pc(x) buf[plen++]=x
	#define flush(); fwrite(buf,1,plen,stdout),plen=0;
	template<typename T>inline void print(T x){if(!x){pc(48);return;}if(x<0) x=-x,pc('-');for(;x;x/=10) stk[++to]=48+x%10;while(to) pc(stk[to--]);}
}
using namespace IO;
const int N = 6010,M = 110,mod = 1e4+7;
int n,m,len,tr[N][28],fail[N],S[N],cnt,root,x,y,ans,f[M][N];
queue<int>p;
string s;
inline void insert(int x)
{
	for(int i = 1;i <= len;i++)
	{
		y = s[i]-'A';
		if(!tr[x][y]) tr[x][y] = ++cnt;
		x = tr[x][y];
	} S[x] = 1;
}
inline void fail_build()//root默认为0,这样没有就跳到0,数组默认就是0,方便一些 
{
	//fail_i:i失配之后能跳到的最长的后缀 
	for(int i = 0;i < 26;i++)
		if(tr[0][i]) p.push(tr[0][i]);
	while(!p.empty())
	{
		x = p.front(); p.pop();
		for(int i = 0;i < 26;i++)
			if(!tr[x][i]) tr[x][i] = tr[fail[x]][i];
			else fail[tr[x][i]] = tr[fail[x]][i],p.push(tr[x][i]),S[tr[x][i]] |= S[tr[fail[x]][i]];//如果fail不行,它肯定也不行
	}
}
inline void Mod(int &x,int y){ x += y; if(x >= mod) x -= mod; }
signed main()
{
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	read(n),read(m); ans = 1;
	for(int i = 1;i <= m;i++) ans = ans*26%mod;
	for(int i = 1;i <= n;i++) cin >> s,len = s.size(),s = ' '+s,insert(root);
	fail_build(); f[0][0] = 1;
	for(int i = 0;i < m;i++)
		for(int j = 0;j <= cnt;j++)
			if(!S[j])
			{
				for(int z = 0;z < 26;z++)
					Mod(f[i+1][tr[j][z]],f[i][j]);
			}//
	for(int j = 0;j <= cnt;j++) if(!S[j]) Mod(ans,mod-f[m][j]);
	print(ans); flush();
	return 0;
}

P2444 [POI 2000] 病毒

dp+ACAM

本质其实就是有一个环,然后无论走多少步都不会有后缀等于那些病毒段。

本质上就是在字典树上走出环就好了,先套路的学习上一题,然后有些点就不能走,然后看有没有环还不简单,直接跑 dfs 即可,不会的话我简单说一下,对于 \(x\) 这个点,如果跑完了还没有结束,说明 \(x\) 一定不在环里,将 \(x\) 永久标记,否则就跑。

所以复杂度是 \(n+m\) 及点加边的。

code

P2414 [NOI2011] 阿狸的打字机

数据结构,ACAM,dfs(深度优先搜索)

很有趣的一道题。

考虑题目的给出其实就是给了一颗字典树,直接建出来并给每个点对应的字典树下标。

然后考虑询问暴力怎么做,其实就是对于根到 \(y\) 的每个点看后缀有没有 \(s_x\)

那么就是根到 \(y\) 上一直跳 \(fail\) 能到 \(x\) 的点。

那么建出 \(fail\) 树,本质上在求什么:
\(fail\) 树里面是 \(x\) 的儿子的点并且在 \(trie\) 树上是 \(y\) 的祖先的点。
\(fail\) 树跑一下给一个dfn_i
\(dfn_x \le dfn_i \le dfn_x+siz_x-1\)
然后,\(Dfn_i \le Dfn_y \le Dfn_i+Siz_i-1\)
直接建可持久化线段树,就可以在线解决,还可以考虑离线,然后dfs跑acam,然后动态加删,然后处理询问,直接区间查询即可。

离线代码:code

P3041 [USACO12JAN] Video Game G

诶你们文本编辑器怎么还有变体(doge)

\(f_{i,j}\) 表示长度为 \(i\) 到字典树 \(j\) 号点最大匹配,然后每一次选下一个点,然后统计一下新串的多出来的后缀与 \(n\) 个相同的个数,处理参考文本编辑器,然后就没了。

code

P3311 [SDOI2014] 数数

字符串,动态规划 DP,数位 DP,AC 自动机

套路的设 \(f_{i,j,0/1,0/1}\) 表示匹配 \(i\) 位到位置 \(j\),是否前 \(i\) 位(从高往低看的最高位)都与 \(n\) 相等,是否前面全是 \(0\)(即还没开始正式选),然后在 ACAM 上跑即可,具体转移见代码,有注释。

code

#include<bits/stdc++.h>
using namespace std;
namespace IO
{
	template<typename T>
	void read(T &_x){_x=0;int _f=1;char ch=getchar();while(!isdigit(ch)) _f=(ch=='-'?-1:_f),ch=getchar();while(isdigit(ch)) _x=_x*10+(ch^48),ch=getchar();_x*=_f;}
	template<typename T,typename... Args>
	void read(T &_x,Args&...others){Read(_x);Read(others...);}
	const int BUF=20000000;char buf[BUF],to,stk[32];int plen;
	#define pc(x) buf[plen++]=x
	#define flush(); fwrite(buf,1,plen,stdout),plen=0;
	template<typename T>inline void print(T x){if(!x){pc(48);return;}if(x<0) x=-x,pc('-');for(;x;x/=10) stk[++to]=48+x%10;while(to) pc(stk[to--]);}
}
using namespace IO;
const int N = 110,M = 1510,mod = 1e9+7;
int m,tr[M][10],cnt,fail[M],S[M],f[M][M][2][2],len,tot,x,y,ans;
string s,t;
inline void insert(int x)
{
	for(int i = 1;i <= len;i++)
	{
		y = s[i]-'0';
		if(!tr[x][y]) tr[x][y] = ++cnt;
		x = tr[x][y];
	} S[x] = 1;
}
queue<int>p;
inline void fail_build()
{
	for(int i = 0;i < 10;i++)
		if(tr[0][i]) p.push(tr[0][i]);
	while(!p.empty())
	{
		x = p.front(); p.pop();
		for(int i = 0;i < 10;i++)
			if(tr[x][i]) fail[tr[x][i]] = tr[fail[x]][i],S[tr[x][i]] |= S[tr[fail[x]][i]],p.push(tr[x][i]);
			else tr[x][i] = tr[fail[x]][i];
	}
}
inline void Mod(int &x,int y){ x += y; if(x >= mod) x -= mod; }
inline void solve()
{
	f[0][0][1][1] = 1;//初值 
	for(int i = 0;i < tot;i++)
		for(int j = 0;j <= cnt;j++)
			if(!S[j])
			{
				Mod(f[i+1][tr[j][0]][0][0],f[i][j][0][0]);
				Mod(f[i+1][j][0][1],f[i][j][0][1]);//前导0不匹配!! 
				for(int z = 1;z <= 9;z++) 
				{
					Mod(f[i+1][tr[j][z]][0][0],f[i][j][0][0]);
					Mod(f[i+1][tr[j][z]][0][0],f[i][j][0][1]);
				} y = t[i+1]-'0';
				for(int z = 1;z < y;z++)//转过去,全变成无限制,然后有值 
				{
					Mod(f[i+1][tr[j][z]][0][0],f[i][j][1][0]);
					Mod(f[i+1][tr[j][z]][0][0],f[i][j][1][1]); 
				}
				if(y != 0) 
				{
					Mod(f[i+1][tr[j][0]][0][0],f[i][j][1][0]);
					Mod(f[i+1][j][0][1],f[i][j][1][1]);//不匹配,但是变成无限制 
					Mod(f[i+1][tr[j][y]][1][0],f[i][j][1][1]);
				}
				else Mod(f[i+1][j][1][1],f[i][j][1][1]);//仍然有限制(其实删了应该也没事,因为n无前导0,选0变无限制,不选0也不会有这个) 
				Mod(f[i+1][tr[j][y]][1][0],f[i][j][1][0]);
			}
}
signed main()
{
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	cin >> t; read(m); tot = t.size(); t = ' '+t;
	for(int i = 1;i <= m;i++)
	{
		cin >> s; len = s.size(); s = ' '+s;
		insert(0);
	} fail_build();
	solve();
	for(int j = 0;j <= cnt;j++)
		if(!S[j]) 
		{
			Mod(ans,f[tot][j][0][0]);
			Mod(ans,f[tot][j][1][0]);
		}
	print(ans); flush();
	return 0;
}
/*
n这个范围,这个题...肯定是数位dp啦
不能出现一些串,感觉像文本编辑器
考虑acam上跑数位dp?
f_{i,j,0/1,0/1}表示走了j位,到位置i,是否前j位都和n一样,是否前j位都是0
求1~n的,所以也不需要单独看0 
*/

Part 2.manachar

UVA11475 Extend to Palindrome

无脑 manachar,直接跑出来,找到最前面一个点使得这一个回文是后缀的,然后这一段就可以少输出一次,就是最短的那个了。

code

#include<bits/stdc++.h>
using namespace std;
const int N = 2e5+10;
int n,f[N],mx,r,o; //两倍空间. 
string s,s1;
int main()
{
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	while(cin >> s)
	{//求最长后缀是回文的长度 
		n = s.size(); s = ' '+s; s1 = "$"; mx = r = 0;
		for(int i = 1;i <= n;i++) s1 += ".",s1 += s[i]; s1 += ".",s1 += "*";
		n = s1.size()-1; 
		for(int i = 1;i <= n;i++) f[i] = 0;
		for(int i = 1;i <= n;i++)
		{
			if(i <= mx) f[i] = min(f[2*r-i],mx-i+1);//r-(i-r),注意到如果f[2*r-i]<mx-i+1,根据对称说明一定拓展不了
			while(s1[i-f[i]] == s1[i+f[i]]) f[i]++;
			if(i+f[i]-1 > mx) mx = i+f[i]-1,r = i;
			//否则每一次拓展都会变大
		} o = 0;
		for(int i = 1;i < n;i++)
		{
			if(i+f[i]-1 == n-1) break; //最后一位没用,到了底这一段可以少输一次
			else o = i;//,cout<<s1[i]<<" "; 
		} o++;
		for(int i = 1;i < n;i++)
			if(s1[i] != '.') cout<<s1[i];
		for(int i = o-f[o];i >= 1;i--)
			if(s1[i] != '.') cout<<s1[i];
		cout<<'\n';
	}
	return 0;
}
/*
amanaplanacanal
      lanacanalpanama
*/
posted @ 2025-10-25 10:39  kkxacj  阅读(3)  评论(0)    收藏  举报