ACAM 题乱做

本篇笔记尚待修订

之前做了不少 ACAM,不过没怎么整理起来,还是有点可惜的。

打 * 的是推荐一做的题目。

I. *CF1437G Death DBMS

我的题解

II. *CF1202E You Are Given Some Strings...

我的题解

III. *CF1400F x-prime Substrings

题意简述:一个字符串为 x-prime 当且仅当它每一位数字之和为 x 且其所有子串的每一位数字之和不为 x 的真约数(即 x 的不为 x 的约数绕)。求给出字符串 s 至少要删掉多少字符才能使其不包含 x-prime 的子字符串。

在洛谷博客查看

hot tea.

一个并不显然的条件是对于所有 xx-prime 字符串的总长度不超过 6000。可能的原因是字符串中不能含有 1(除了 x=1)。那么暴力 dfs 就可以找到所有字符串,对其建立一个 ACAM,然后在上面 DP 即可。设 fi,p 表示 s[1:i] 至少删掉多少字符才能在 ACAM 上跑到状态 p。记 nxt=sonp,si+1,若 nxt 在 fail 树上与根节点的链之间没有终止节点(这是基本操作),那么可以更新 fi+1,nxtmin(fi+1,nxt,fi,p)。同时别忘记更新 fi+1,jmin(fi+1,j,fi,j+1),表示删掉 si+1

Lx 为所有 x-prime 字符串的长度之和,Σ 为字符集,则时间复杂度为 O(nLx|Σ|),空间可以通过滚动数组优化(不过没有必要),可以通过。

/*
	Powered by C++11.
	Author : Alex_Wei.
*/

#include <bits/stdc++.h>
using namespace std;

//#pragma GCC optimize(3)
//#define int long long

#define pb emplace_back
#define mem(x,v) memset(x,v,sizeof(x))

const int S=4e4+5;
const int N=1e3+5;

int n,x,cnt,ans=S,son[S][10],f[S],ed[S],g[N][S];
string s;
void ins(string s){
	int p=0;
	for(char it:s){
		if(!son[p][it-'0'])son[p][it-'0']=++cnt;
		p=son[p][it-'0'];
	} ed[p]=1;
} void build(){
	queue <int> q;
	for(int i=0;i<10;i++)if(son[0][i])q.push(son[0][i]);
	while(!q.empty()){
		int t=q.front(); q.pop();
		for(int i=0;i<10;i++)
			if(son[t][i])q.push(son[t][i]),f[son[t][i]]=son[f[t]][i];
			else son[t][i]=son[f[t]][i];
		ed[t]|=ed[f[t]];
	} 
} bool check(string s){
	for(int i=0;i<s.size();i++)
		for(int j=i;j<s.size();j++){
			int cnt=0;
			for(int k=i;k<=j;k++)cnt+=s[k]-'0';
			if(cnt<x&&x%cnt==0)return 0;
		} return 1;
} void dfs(int num,string s=""){
	if(num==x){
		if(check(s))ins(s);
		return;
	} for(int i=1;i<10;i++)
		if(num+i<=x)
			dfs(num+i,s+(char)(i+'0'));
}

int main(){
	cin>>s>>x,dfs(0),build();
	mem(g,0x3f),g[0][0]=0;
	for(int i=0;i<s.size();i++)
		for(int j=0;j<=cnt;j++){
			int p=son[j][s[i]-'0'];
			if(!ed[p])g[i+1][p]=min(g[i+1][p],g[i][j]);
			g[i+1][j]=min(g[i+1][j],g[i][j]+1);
		}
	for(int i=0;i<=cnt;i++)ans=min(ans,g[s.size()][i]);
	cout<<ans<<endl;
	return 0;
}

IV. *CF1207G Indie Album

题意简述:有 n 种操作,给出整数,整数和字符 op,j(op=2),c。若 op=1si=c;否则 si=sj+cm 次询问给出 i,t,求 tsi 中的出现次数。

在洛谷博客查看

以前打过这场比赛,要是我当时会 ACAM 多好啊。

注意到如果我们对操作串 s 建出 ACAM 需要动态修改 fail 树的结构,不太可行。那么换个思路,考虑对所有询问串 t 建出 ACAM。那么这样就是在 ACAM 上跑 si,求出有多少个跑到的节点在 fail 树上以 t 的终止节点的子树中。这个可以对 fail 树进行一遍 dfs,用每个节点的 dfs 序和 size 维护。这样就是单点修改,区间查询,用树状数组即可。

可是 si 的总长度可能会很大。不难发现每个 si 形成了一个依赖关系,建出树,我们只需要再对这个 “操作树” 进行 dfs,先计算贡献(位置 sonp,ci 加上 1),再更新并下传跑到的位置 p=sonp,ci,最后撤销贡献即可。

时间复杂度 O((n+m)log|t|)

/*
	Powered by C++11.
	Author : Alex_Wei.
*/

#include <bits/stdc++.h>
using namespace std;

//#pragma GCC optimize(3)
//#define int long long

#define pb emplace_back

const int N=4e5+5;

int n,m,ans[N];
int cnt,dn,son[N][26],ed[N],fa[N],sz[N],dfn[N];
vector <int> e[N],f[N],ft[N];
char ad[N];
void ins(int id,string s){
	int p=0;
	for(char it:s){
		if(!son[p][it-'a'])son[p][it-'a']=++cnt;
		p=son[p][it-'a'];
	} ed[id]=p;
} void build(){
	queue <int> q;
	for(int i=0;i<26;i++)if(son[0][i])q.push(son[0][i]);
	while(!q.empty()){
		int t=q.front(); q.pop();
		for(int i=0;i<26;i++)
			if(son[t][i])q.push(son[t][i]),fa[son[t][i]]=son[fa[t]][i];
			else son[t][i]=son[fa[t]][i];
		ft[fa[t]].pb(t);
	}
} void dfs(int id){
	dfn[id]=++dn,sz[id]=1;
	for(int it:ft[id])dfs(it),sz[id]+=sz[it];
}

int c[N];
void add(int x,int v){while(x<=dn)c[x]+=v,x+=x&-x;}
int query(int x){int ans=0; while(x)ans+=c[x],x-=x&-x; return ans;}
int query(int l,int r){return query(r)-query(l-1);}
void cal(int id,int p){
	if(id)p=son[p][ad[id]-'a'],add(dfn[p],1);
	for(int it:e[id])ans[it]=query(dfn[ed[it]],dfn[ed[it]]+sz[ed[it]]-1);
	for(int it:f[id])cal(it,p);
	add(dfn[p],-1);
}

int main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		int tp,p=0; cin>>tp;
		if(tp==2)cin>>p;
		f[p].pb(i),cin>>ad[i];
	} cin>>m;
	string q;
	for(int i=1,id;i<=m;i++)
		cin>>id>>q,e[id].pb(i),ins(i,q);
	build(),dfs(0),cal(0,0);
	for(int i=1;i<=m;i++)printf("%d\n",ans[i]);
	return 0;
}

V. *P4569 [BJWC2011]禁忌

我的题解

VI. *CF1483F Exam

题意简述:给出字典 si,求有多少对 (i,j) 满足 ijsjsubseq(si) 且不存在 k (ki,kj) 使得 sjsubseq(sk)sksubseq(si)

hot tea!赛时看 F 的时候只剩 40min 了,估摸着写不出来就没写。事实上,这是一个巨大的错误。

对于这种字符串匹配的题目优先考虑 ACAM & SAM,不过这里 SAM 似乎不太好做(因为要广义 SAM,实际上也是可以的),故选用 ACAM。

考虑枚举每一个串 si 作为最长串,那么对于其它的所有串 sk (ik)sisk 符合题意当且仅当 sksi 中的出现次数等于 sksi 中不被别的串所包含的出现次数。考虑怎么求后者:倒序枚举 si 的每一个位置 j 作为与别的串 sk 匹配的结束位置。找到最长sk 使得 sk=si[j|sk|+1:j],如果 [j+1,|si|] 中所有位置与别的串的成功匹配的左端点的最小值 pre 大于 j|sk|+1,那么这就是 sk 的一次不被别的串所包含的出现。维护 pre 直接用 j|sk|+1 更新即可。

最长的 sk 也就是 si[1:j] 在 ACAM 上的状态在 fail 树上最近的结束位置所代表的字符串,在建 ACAM 的时候一并求出即可。别忘了特判一下 si[1:|si|],这时就是用该状态的父亲计算上述过程。

为什么要倒序枚举 j:这样后考虑的字符串对一开始考虑的字符串没有影响,因为结束位置在 si 较前的字符串不可能包含结束位置在 si 较后的字符串。而如果正序枚举,那么一开始认为没有被覆盖的字符串很有可能在后面被覆盖了。即r1<r2,则 [l1,r1] 是永远不会覆盖 [l2,r2] 的,而 [l2,r2] 很有可能覆盖 [l1,r1]。这样需要撤销贡献,很麻烦。

还有这个求出现次数是 ACAM 基操了,dfs 序 + 树状数组维护一下即可。

时间复杂度 O(nlogn)。这份代码在 CF 上暂时是最短代码(2021.3.23)。

#include <bits/stdc++.h>
using namespace std;

const int N=1e6+5;
const int S=26;

int node,son[N][S],fa[N],ed[N],edp[N];
int n,ans,dnum,dfn[N],sz[N];
string s[N];
vector <int> e[N];
void ins(string s,int id){
	int p=0;
	for(char it:s){
		if(!son[p][it-'a'])son[p][it-'a']=++node;
		p=son[p][it-'a'];
	} ed[p]=id,edp[id]=p;
} void build(){
	queue <int> q;
	for(int i=0;i<26;i++)if(son[0][i])q.push(son[0][i]);
	while(!q.empty()){
		int t=q.front(); q.pop();
		for(int i=0;i<26;i++)
			if(son[t][i])q.push(son[t][i]),fa[son[t][i]]=son[fa[t]][i];
			else son[t][i]=son[fa[t]][i];
		ed[t]=ed[t]?ed[t]:ed[fa[t]];
		e[fa[t]].push_back(t);
	}
} void dfs(int id){
	dfn[id]=++dnum,sz[id]=1;
	for(int it:e[id])dfs(it),sz[id]+=sz[it];
}

int c[N],buc[N];
void add(int x,int v){while(x<=dnum)c[x]+=v,x+=x&-x;}
int query(int x){int ans=0; while(x)ans+=c[x],x-=x&-x; return ans;}

int main(){
	cin>>n;
	for(int i=1;i<=n;i++)cin>>s[i],ins(s[i],i);
	build(),dfs(0);
	for(int i=1;i<=n;i++){
		vector <int> pa,cnt;
		int p=0,pre=1e9;
		for(char it:s[i]){
			pa.push_back(p=son[p][it-'a']);
			add(dfn[p],1);
		} for(int j=pa.size()-1;~j;j--){
			int id=j==pa.size()-1?ed[fa[pa[j]]]:ed[pa[j]];
			if(!id)continue;
			int l=j-s[id].size();
			if(l<pre)pre=l,buc[id]++,cnt.push_back(id);
		} for(int it:cnt){
			if(!buc[it])continue;
			int p=edp[it],ap=query(dfn[p]+sz[p]-1)-query(dfn[p]-1);
			if(ap==buc[it])ans++; buc[it]=0;
		} for(int p:pa)add(dfn[p],-1);
	} cout<<ans<<endl;
	return 0;
}

VII. CF163E e-Government

好久没写 ACAM,都快忘掉了。

显然,对于这类字符串匹配问题,我们最好的选择是 SAM ACAM。当然这题应该也可以用广义 SAM 来做,就是把所有询问的字符串和原来的字符串全部拿过来搞一个广义 SAM,修改就类似 ACAM 用 fail 树的 dfs 序 + BIT 维护一下即可。

一不小心直接讲完了。

首先对字符串集合 S 建出 ACAM TS。考虑用查询的字符串 tTS 上面跳。根据 ACAM 的实际意义,假设当前通过字符 ti 跳到了节点 p,那么在 fail 树上从 p 到根节点这一整条路径上的所有节点都表示以 ti 结尾且与 t1i 的后缀匹配的 S 的所有前缀的全新的一次出现。对于 S 的每个字符串记录它在 Ts 的末节点,这样就是单点修改 + 链和,可以用树链剖分维护。

但是,因为链的顶端是根节点,所以有一个经典的单点修改 + 链和 转 子树修改 + 单点查询的经典套路:对于每次单点修改,将其影响扩大至该点的整个子树,那么每次链和查询只需要求链底这一点的值即可。显然,后者可以 dfs 序 + BIT 轻松维护。时间复杂度 O(mlogm),其中 m 是字符集大小。

两个注意点:

  • 多次重复添加算一次,删除也是。
  • BIT 循环上界不是 n 而是 ACAM 节点个数。
#include <bits/stdc++.h>
using namespace std;

const int N=1e6+5;

int n,k,buc[N];
int node,ed[N],son[N][26],fa[N];
vector <int> e[N];
void ins(string s,int id){
	int p=0;
	for(char it:s){
		if(!son[p][it-'a'])son[p][it-'a']=++node;
		p=son[p][it-'a'];
	} ed[id]=p;
}
void build(){
	queue <int> q;
	for(int i=0;i<26;i++)if(son[0][i])q.push(son[0][i]);
	while(!q.empty()){
		int t=q.front(); q.pop();
		for(int i=0;i<26;i++)
			if(son[t][i])fa[son[t][i]]=son[fa[t]][i],q.push(son[t][i]);
			else son[t][i]=son[fa[t]][i];
		e[fa[t]].push_back(t);
	}
}

int dnum,dfn[N],sz[N],c[N];
void add(int x,int v){while(x<=node)c[x]+=v,x+=x&-x;}
int query(int x){int s=0; while(x)s+=c[x],x-=x&-x; return s;}
void dfs(int id){
	dfn[id]=dnum++,sz[id]=1;
	for(int it:e[id])dfs(it),sz[id]+=sz[it]; 
}

int main(){
	cin>>n>>k;
	for(int i=1;i<=k;i++){
		string s; cin>>s,ins(s,i);
	}
	build(),dfs(0);
	for(int i=1;i<=k;i++){
		int id=ed[i];
		add(dfn[id],1);
		add(dfn[id]+sz[id],-1);
		buc[i]=1;
	}
	for(int i=1;i<=n;i++){
		char c; cin>>c;
		if(c=='?'){
			string s; cin>>s;
			long long p=0,ans=0;
			for(char it:s){
				p=son[p][it-'a'];
				ans+=query(dfn[p]);
			}
			cout<<ans<<endl;
		}
		else if(c=='-'){
			int id; cin>>id;
			if(!buc[id])continue;
			buc[id]=0;
			id=ed[id];
			add(dfn[id],-1);
			add(dfn[id]+sz[id],1);
		}
		else if(c=='+'){
			int id; cin>>id;
			if(buc[id])continue;
			buc[id]=1;
			id=ed[id];
			add(dfn[id],1);
			add(dfn[id]+sz[id],-1);
		}
	}
	return 0;
}
posted @   qAlex_Weiq  阅读(951)  评论(0编辑  收藏  举报
编辑推荐:
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示