1. 程式人生 > 實用技巧 >題解-CF163E e-Government

題解-CF163E e-Government

題面

CF163E e-Government

\(n\) 個字串 \(s_i\)\(q\) 個詢問,剛開始字串都服役。每次操作將集合中的一個字串設為退役或服役,或查詢與文字串 \(S_i\) 的匹配的服役字串總次數。

資料範圍:\(1\le n,q\le 10^5\)\(1\le \sum|s_i|,\sum|S_i|\le 10^6\)


蒟蒻語

這是個AC自動機的套路題,但是畢竟套路巧妙而且不得不學,所以蒟蒻寫一篇題解。


蒟蒻解

當這題的字串不退役時,這就是AC自動機的模板。

回憶一下蒟蒻們是怎麼做的:先建一棵Trie樹,在有字串終止節點的位置 \(tag=1\)。然後考慮到包含一個字串必然包含一個字串的字尾,建立 \(fail\)

鏈成為AC自動機,\(fail\) 鏈連線節點成為 \(parent\) 樹,重算一個節點的 \(tag\) 為它在 \(parent\) 樹上到根節點的路徑上的節點的 \(tag\) 之和。每次匹配的時候,在AC自動機上跑一遍文字串,累計一下 \(tag\) 即可。

讓一個字串退役,就相當於將該字串在Trie樹上的終止節點 \(p\)\(tag=1\) 變成 \(tag=0\)。建AC自動機重算 \(tag\) 的時候,每個在 \(parent\) 樹上到根節點的路徑上包含 \(p\) 的節點的 \(tag\) 都會減 \(1\)容易發現 \(tag\) 減了 \(1\) 的節點,正好就是 \(parent\)

樹上 \(p\) 的子樹。

這時候就可以做了,巨佬可以寫個樹鏈剖分或LinkCutTree。但是考慮到這題只需要操作子樹,不需要操作鏈,所以可以不寫輕重鏈剖分,求每個節點的 \(dfs\) 序及其子樹的 \(dfs\) 序區間即可。區間修改、單點查詢可以用差分加樹狀陣列。

當然這題有很多細節,而且程式碼很長,估計能寫寫調調好久……看蒟蒻程式碼吧。


程式碼

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

//Start
typedef long long ll;
typedef double db;
#define mp(a,b) make_pair(a,b)
#define x first
#define y second
#define be(a) a.begin()
#define en(a) a.end()
#define sz(a) int((a).size())
#define pb(a) push_back(a)
const int inf=0x3f3f3f3f;
const ll INF=0x3f3f3f3f3f3f3f3f;

//Data
const int N=1e5,M=1e6+1;
int n,at[N];
bool vis[N];

//FenwickTree
int c[M+2];
void add(int x,int v){
	for(int i=x+1;i<M+2;i+=i&-i) c[i]+=v;
}
int sum(int x){
	int res=0;
	for(int i=x+1;i>=1;i-=i&-i) res+=c[i];
	return res;
}

//ACAM
int cnt=1,ch[M][26];
void insert(int x,string&s){
	int p=0;
	for(int i=0;i<sz(s);i++){
		int c=s[i]-'a';
		if(!~ch[p][c]) ch[p][c]=cnt++;
		p=ch[p][c];
	}
	at[x]=p; //記錄第x個字串的終止節點,方便查詢dfs序
}
int fa[M],ind,ld[M],rd[M];//[ld,rd)是自動機節點的子樹dfs序區間,ld正好是該節點的dfs序
vector<int> e[M];
void Dfs(int p){
	ld[p]=ind++;
	for(int v:e[p]) Dfs(v);
	rd[p]=ind;
}
void build(){
	queue<int> q;
	for(int c=0;c<26;c++)
		if(~ch[0][c]){
			fa[ch[0][c]]=0;
			e[0].pb(ch[0][c]); //加邊建parent樹
			// cout<<0<<"->"<<ch[0][c]<<'\n';
			q.push(ch[0][c]); 
		} else ch[0][c]=0;
	while(sz(q)){
		int p=q.front(); q.pop();
		for(int c=0;c<26;c++)
			if(~ch[p][c]){
				fa[ch[p][c]]=ch[fa[p]][c];
				e[fa[ch[p][c]]].pb(ch[p][c]); //加邊建parent樹
				// cout<<fa[ch[p][c]]<<"->"<<ch[p][c]<<'\n';
				q.push(ch[p][c]);
			} else ch[p][c]=ch[fa[p]][c];
	}
	Dfs(0);
}

//Main
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	int T; cin>>T>>n;
	for(int p=0;p<M;p++){
		fa[p]=-1;
		for(int c=0;c<26;c++) ch[p][c]=-1;
	}
	for(int i=0;i<n;i++){
		string s; cin>>s;
		insert(i,s);
	}
	build();
	for(int i=0;i<n;i++) //剛開始字串都服役
		vis[i]=1,add(ld[at[i]],1),add(rd[at[i]],-1);
	while(T--){
		char c;	cin>>c;
		if(c=='+'){
			int i; cin>>i,--i;
			if(vis[i]) continue;
			vis[i]=1,add(ld[at[i]],1),add(rd[at[i]],-1);
		} else if(c=='-'){
			int i; cin>>i,--i;
			if(!vis[i]) continue;
			vis[i]=0,add(ld[at[i]],-1),add(rd[at[i]],1);
		} else if(c=='?'){
			string s; cin>>s;
			int res=0,p=0;
			for(int i=0;i<sz(s);i++){
				int c=s[i]-'a';
				p=ch[p][c],res+=sum(ld[p]);
			}
			cout<<res<<'\n';
		}
	}
	return 0;
}

祝大家學習愉快!