之前做了不少 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.
一個並不顯然的條件是對於所有 \(x\),x-prime 字符串的總長度不超過 \(6000\)。可能的原因是字符串中不能含有 \(\texttt{1}\)(除了 \(x=1\))。那么暴力 dfs 就可以找到所有字符串,對其建立一個 ACAM,然后在上面 DP 即可。設 \(f_{i,p}\) 表示 \(s[1:i]\) 至少刪掉多少字符才能在 ACAM 上跑到狀態 \(p\)。記 \(nxt=son_{p,s_{i+1}}\),若 \(nxt\) 在 fail 樹上與根節點的鏈之間沒有終止節點(這是基本操作),那么可以更新 \(f_{i+1,nxt}\gets \min(f_{i+1,nxt},f_{i,p})\)。同時別忘記更新 \(f_{i+1,j}\gets \min(f_{i+1,j},f_{i,j}+1)\),表示刪掉 \(s_{i+1}\)。
記 \(L_x\) 為所有 x-prime 字符串的長度之和,\(\Sigma\) 為字符集,則時間復雜度為 \(\mathcal{O}(nL_x|\Sigma|)\),空間可以通過滾動數組優化(不過沒有必要),可以通過。
/*
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=1\) 則 \(s_i=c\);否則 \(s_i=s_j+c\)。\(m\) 次詢問給出 \(i,t\),求 \(t\) 在 \(s_i\) 中的出現次數。
以前打過這場比賽,要是我當時會 ACAM 多好啊。
注意到如果我們對操作串 \(s\) 建出 ACAM 需要動態修改 fail 樹的結構,不太可行。那么換個思路,考慮對所有詢問串 \(t\) 建出 ACAM。那么這樣就是在 ACAM 上跑 \(s_i\),求出有多少個跑到的節點在 fail 樹上以 \(t\) 的終止節點的子樹中。這個可以對 fail 樹進行一遍 dfs,用每個節點的 dfs 序和 size 維護。這樣就是單點修改,區間查詢,用樹狀數組即可。
可是 \(s_i\) 的總長度可能會很大。不難發現每個 \(s_i\) 形成了一個依賴關系,建出樹,我們只需要再對這個 “操作樹” 進行 dfs,先計算貢獻(位置 \(son_{p,c_i}\) 加上 \(1\)),再更新並下傳跑到的位置 \(p=son_{p,c_i}\),最后撤銷貢獻即可。
時間復雜度 \(\mathcal{O}((n+m)\log \sum|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
題意簡述:給出字典 \(s_i\),求有多少對 \((i,j)\) 滿足 \(i\neq j\),\(s_j\in \mathrm{subseq}(s_i)\) 且不存在 \(k\ (k\neq i,k\neq j)\) 使得 \(s_j\in \mathrm{subseq}(s_k)\) 且 \(s_k\in \mathrm{subseq}(s_i)\)。
hot tea!賽時看 F 的時候只剩 40min 了,估摸着寫不出來就沒寫。事實上,這是一個巨大的錯誤。
對於這種字符串匹配的題目優先考慮 ACAM & SAM,不過這里 SAM 似乎不太好做(因為要廣義 SAM,實際上也是可以的),故選用 ACAM。
考慮枚舉每一個串 \(s_i\) 作為最長串,那么對於其它的所有串 \(s_k\ (i\neq k)\),\(s_i\) 與 \(s_k\) 符合題意當且僅當 \(s_k\) 在 \(s_i\) 中的出現次數等於 \(s_k\) 在 \(s_i\) 中不被別的串所包含的出現次數。考慮怎么求后者:倒序枚舉 \(s_i\) 的每一個位置 \(j\) 作為與別的串 \(s_k\) 匹配的結束位置。找到最長的 \(s_k\) 使得 \(s_k=s_i[j-|s_k|+1:j]\),如果 \([j+1,|s_i|]\) 中所有位置與別的串的成功匹配的左端點的最小值 \(pre\) 大於 \(j-|s_k|+1\),那么這就是 \(s_k\) 的一次不被別的串所包含的出現。維護 \(pre\) 直接用 \(j-|s_k|+1\) 更新即可。
最長的 \(s_k\) 也就是 \(s_i[1:j]\) 在 ACAM 上的狀態在 fail 樹上最近的結束位置所代表的字符串,在建 ACAM 的時候一並求出即可。別忘了特判一下 \(s_i[1:|s_i|]\),這時就是用該狀態的父親計算上述過程。
為什么要倒序枚舉 \(j\):這樣后考慮的字符串對一開始考慮的字符串沒有影響,因為結束位置在 \(s_i\) 較前的字符串不可能包含結束位置在 \(s_i\) 較后的字符串。而如果正序枚舉,那么一開始認為沒有被覆蓋的字符串很有可能在后面被覆蓋了。即若 \(r_1<r_2\),則 \([l_1,r_1]\) 是永遠不會覆蓋 \([l_2,r_2]\) 的,而 \([l_2,r_2]\) 很有可能覆蓋 \([l_1,r_1]\)。這樣需要撤銷貢獻,很麻煩。
還有這個求出現次數是 ACAM 基操了,dfs 序 + 樹狀數組維護一下即可。
時間復雜度 \(\mathcal{O}(n\log n)\)。這份代碼在 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 \(T_S\)。考慮用查詢的字符串 \(t\) 在 \(T_S\) 上面跳。根據 ACAM 的實際意義,假設當前通過字符 \(t_i\) 跳到了節點 \(p\),那么在 fail 樹上從 \(p\) 到根節點這一整條路徑上的所有節點都表示以 \(t_i\) 結尾且與 \(t_{1\sim i}\) 的后綴匹配的 \(S\) 的所有前綴的全新的一次出現。對於 \(S\) 的每個字符串記錄它在 \(T_s\) 的末節點,這樣就是單點修改 + 鏈和,可以用樹鏈剖分維護。
但是,因為鏈的頂端是根節點,所以有一個經典的單點修改 + 鏈和 轉 子樹修改 + 單點查詢的經典套路:對於每次單點修改,將其影響擴大至該點的整個子樹,那么每次鏈和查詢只需要求鏈底這一點的值即可。顯然,后者可以 dfs 序 + BIT 輕松維護。時間復雜度 \(\mathcal{O}(m\log m)\),其中 \(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;
}