這里是 SAM 做題筆記。
本來是在一篇隨筆里面,然后 Latex 太多加載不過來就分成了兩篇。
標 * 的是推薦一做的題目。
trick 是我總結的技巧。
I. P3804 【模板】后綴自動機 (SAM)
題意簡述:求一個字符串 \(s\) 的所有子串長度乘上其出現次數的最大值。
代碼還沒寫過,到時候來補一下。
update:嘗試只看自己的博客寫出代碼,然而失敗了 >.<
update:好家伙,第二次跳 \(p\) 的時候(即把 \((p_i,q)\) 變為 \((p_i,q')\) 的時候)忘記跳了(即 \(p\gets \mathrm{link}(p)\)),並且連邊的 vector 開小了(應該開 \(2\times 10^6\))。
- 額外信息:設在構造 SAM 時,每個前綴所表示的狀態(也就是每次的 \(cur\))為終點節點。這樣我們可以得到 \(n\) 個終點節點。
結論 4:在 \(\mathrm{link}\) 樹上,每個節點的 \(\mathrm{endpos}\) 集合等於其子樹內所有終點節點對應的終點的集合。感性理解,證明略。(之前的結論可以看 SAM 感性瞎扯)
- 將狀態 \(p\) 所表示的 \(\mathrm{endpos}\) 集合大小記為 \(ed_p\)。
對於一個狀態 \(p\),我們怎么求它所代表的子串在 \(s\) 中的出現次數呢?其實很簡單,根據定義,我們只需求出該狀態 \(\mathrm{endpos}\) 集合的大小即可。根據結論 4,即在 \(\mathrm{link}\) 樹上 \(p\) 的子樹所包含的終點節點個數。這樣,我們可以在構造 SAM 時順便記錄一下每個點是否是終點節點。構造完成后我們建出 \(\mathrm{link}\) 樹,並通過一次 dfs 求出每個點的 \(\mathrm{endpos}\) 集合大小。那么答案為
代碼奉上,手捏的 SAM 版本(doge):
SAM version 1.0
const int N=1e6+5;
struct node{
int nxt[26],len,link,ed;
}sam[N<<1];
int cur,cnt;
void init(){
sam[0].link=-1;
}
void ins(char s){
int las=cur,p=las,it=s-'a'; sam[cur=++cnt].ed=1; // cur 是終點節點
sam[cur].len=sam[las].len+1; // init
while(~p&&!sam[p].nxt[it])sam[p].nxt[it]=cur,p=sam[p].link; // jump link
if(p==-1)return; // case 1
int q=sam[p].nxt[it];
if(sam[p].len+1==sam[q].len){ // case 2
sam[cur].link=q;
return;
} int cl=sam[cur].link=++cnt; // case 3 : clone
sam[cl].len=sam[p].len+1;
sam[cl].link=sam[q].link;
for(int i=0;i<26;i++)sam[cl].nxt[i]=sam[q].nxt[i];
sam[q].link=cl;
while(~p&&sam[p].nxt[it]==q)sam[p].nxt[it]=cl,p=sam[p].link;
}
vector <int> e[N<<1];
ll ans;
void dfs(int id){
for(int it:e[id])dfs(it),sam[id].ed+=sam[it].ed;
if(sam[id].ed>1)ans=max(ans,1ll*sam[id].len*sam[id].ed);
}
char s[N];
int n;
int main(){
scanf("%s",s+1),n=strlen(s+1),init();
for(int i=1;i<=n;i++)ins(s[i]);
for(int i=1;i<=cnt;i++)e[sam[i].link].pb(i);
dfs(0),cout<<ans<<endl;
return 0;
}
當然,用結構體來表示一個節點的信息有時太過麻煩,所以當需要儲存的信息不多時,我們可以直接用數組儲存。下面是簡化過的版本。
SAM version 1.1
const int N=2e6+5;
const int S=26;
int cur,cnt;
int son[N][S],fa[N],len[N],ed[N];
void ins(char s){
int las=cur,p=cur,it=s-'a';
ed[cur=++cnt]=1,len[cur]=len[las]+1;
while(~p&&!son[p][it])son[p][it]=cur,p=fa[p];
if(p==-1)return;
int q=son[p][it];
if(len[p]+1==len[q]){
fa[cur]=q;
return;
} int c=fa[cur]=++cnt;
len[c]=len[p]+1,fa[c]=fa[q],fa[q]=c;
for(int i=0;i<26;i++)son[c][i]=son[q][i];
while(~p&&son[p][it]==q)son[p][it]=c,p=fa[p];
}
void build(char *s){
int n=strlen(s+1); fa[0]=-1;
for(int i=1;i<=n;i++)ins(s[i]);
}
vector <int> e[N<<1];
ll ans;
void dfs(int id){
for(int it:e[id])dfs(it),ed[id]+=ed[it];
if(ed[id]>1)ans=max(ans,1ll*len[id]*ed[id]);
}
char s[N];
int n;
int main(){
scanf("%s",s+1),build(s);
for(int i=1;i<=cnt;i++)e[fa[i]].pb(i);
dfs(0),cout<<ans<<endl;
return 0;
}
這難道不更好看么?
II. P3975 [TJOI2015]弦論
給出 \(s,t,k\),求 \(s\) 字典序第 \(k\) 小子串,不存在輸出 \(\texttt{-1}\)。\(t=0\) 表示不同位置的相同子串算一個,\(t=1\) 表示不同位置的相同子串算多個。
算是一道經典題了。
根據結論 2,可知 \(s\) 不同子串的個數等於從 \(T\) 出發的不同路徑的條數,且每一條路徑對應一個子串。設 \(d_p\) 表示從狀態 \(i\) 開始的路徑數量(包括長度為 \(0\) 的數量),可以通過拓撲排序 + DP 計算,即
如果 \(t=0\),那么我們要找的就是 SAM 中從 \(T\) 開始的字典序第 \(k\) 小的路徑,這可以通過貪心輕松實現。如果 \(t=1\),那么將上述轉移式中的 \(1\) 修改為 \(ed_p\) 即可。代碼如下:
Luogu P3975 代碼
/*
Powered by C++11.
Author : Alex_Wei.
*/
#include <bits/stdc++.h>
using namespace std;
const int N=1e6+5;
const int S=26;
// Suffix_Automaton
int cur,cnt;
int son[N][S],f[N],len[N],ed[N];
int deg[N],val[N];
vector <int> le[N],se[N];
void ins(char s){
int las=cur,p=cur,it=s-'a';
ed[cur=++cnt]=1,len[cur]=len[las]+1;
while(p&&!son[p][it])son[p][it]=cur,p=f[p];
if(p==0){
f[cur]=1;
return;
} int q=son[p][it];
if(len[p]+1==len[q]){
f[cur]=q;
return;
} int c=++cnt;
f[c]=f[q],f[q]=f[cur]=c,len[c]=len[p]+1;
for(int i=0;i<26;i++)son[c][i]=son[q][i];
while(p&&son[p][it]==q)son[p][it]=c,p=f[p];
} void build(char *s){
int n=strlen(s+1); cnt=cur=1;
for(int i=1;i<=n;i++)ins(s[i]);
for(int i=1;i<=cnt;i++){
le[f[i]].emplace_back(i);
for(int j=0;j<26;j++)if(son[i][j])
se[son[i][j]].emplace_back(i),deg[i]++;
}
}
void dfs(int id){
for(int it:le[id])dfs(it),ed[id]+=ed[it];
}
char s[N],ans[N];
int t,k;
void find1(int p,int l){
for(int i=0;i<26;i++){
if(!son[p][i])continue;
if(k>val[son[p][i]])k-=val[son[p][i]];
else if(k>(t?ed[son[p][i]]:1)){
k-=(t?ed[son[p][i]]:1),ans[l]=i+'a';
find1(son[p][i],l+1);
return;
} else{
ans[l]=i+'a',cout<<ans<<endl;
return;
}
}
}
int main(){
scanf("%s",s+1),build(s);
cin>>t>>k; dfs(1);
queue <int> q;
for(int i=1;i<=cnt;i++)if(!deg[i])q.push(i);
while(!q.empty()){
int tt=q.front(); q.pop();
val[tt]+=(t?ed[tt]:1);
for(int it:se[tt]){
val[it]+=val[tt];
if(!--deg[it])q.push(it);
}
}
if(val[1]<k)puts("-1");
else find1(1,0);
return 0;
}
/*
aabcd
1 15
*/
然后你會發現它竟然 TLE 了!太離譜了!!!!111
經過不斷地調試之后我發現 vector 連邊耗時竟然這么大(大概 800ms,就離譜),可是不用 vector 連邊就要寫非常麻煩的鏈式前向星,巨麻煩無比,否則沒法求出來 \(ed\) 和 \(d\),怎么辦?逛了一圈題解區,有一個特別 nb 的技巧:
trick 1:將編號按照 \(\mathrm{len}(i)\) 降序排序,得到的就是 SAM DAG 反圖的拓撲序,這樣直接循環更新 \(ed\) 和 \(d\) 就可以了。可是這樣會破壞 SAM \(\mathcal{O}(n)\) 的優秀時間復雜度
(其實常數巨大,還沒 SA 跑得快),那么直接雞基排就好了。
SAM version 2.0
/*
Powered by C++11.
Author : Alex_Wei.
*/
#include <bits/stdc++.h>
using namespace std;
const int N=1e6+5;
const int S=26;
// Suffix_Automaton
int cur,cnt;
int son[N][S],f[N],len[N],ed[N];
int val[N],id[N],buc[N];
void ins(char s){
int las=cur,p=cur,it=s-'a';
ed[cur=++cnt]=1,len[cur]=len[las]+1;
while(p&&!son[p][it])son[p][it]=cur,p=f[p];
if(p==0){
f[cur]=1;
return;
} int q=son[p][it];
if(len[p]+1==len[q]){
f[cur]=q;
return;
} int c=++cnt;
f[c]=f[q],f[q]=f[cur]=c,len[c]=len[p]+1;
for(int i=0;i<26;i++)son[c][i]=son[q][i];
while(p&&son[p][it]==q)son[p][it]=c,p=f[p];
} void build(char *s){
int n=strlen(s+1); cnt=cur=1;
for(int i=1;i<=n;i++)ins(s[i]);
for(int i=1;i<=cnt;i++)buc[len[i]]++;
for(int i=1;i<=cnt;i++)buc[i]+=buc[i-1];
for(int i=cnt;i;i--)id[buc[len[i]]--]=i;
for(int i=cnt;i;i--)ed[f[id[i]]]+=ed[id[i]];
}
char s[N],ans[N];
int t,k;
void find(int p,int l){
for(int i=0;i<26;i++){
if(!son[p][i])continue;
if(k>val[son[p][i]])k-=val[son[p][i]];
else if(k>(t?ed[son[p][i]]:1)){
k-=(t?ed[son[p][i]]:1),ans[l]=i+'a';
find(son[p][i],l+1);
return;
} else{
ans[l]=i+'a',cout<<ans<<endl;
return;
}
}
}
int main(){
scanf("%s",s+1),build(s);
cin>>t>>k;
for(int i=cnt;i;i--){
val[id[i]]=(t?ed[id[i]]:1);
for(int j=0;j<26;j++)val[id[i]]+=val[son[id[i]][j]];
}
if(val[1]-ed[1]<k)puts("-1");
else find(1,0);
return 0;
}
III. P3763 [TJOI2017]DNA
題意簡述:求 \(S\) 有多少個長度為 \(|S_0|\) 的子串滿足與 \(S_0\) 至多有 \(3\) 個對應位置上的字符不等。(字符集為 \(\texttt{\{A,C,G,T\}}\))
用 SAM 口胡一波。因為我是用 SA 寫的(怎么混進了一個奇怪的東西)。
SAM 的話,一個想法是直接暴力 dfs,向四個方向都搜索一遍,如果轉移方向字符與當前匹配的位置上的字符不同就計數器自增 \(1\) ,匹配完成就答案加上 \(ed_p\)(\(p\) 是匹配到的狀態)即可。不過不會分析時間復雜度。
SA 的話直接用 \(height\) 數組的區間 RMQ 加速匹配,這樣匹配就是 \(\mathcal{O}(n)\) 的。SA 又好想又好寫,何樂而不為呢?
Luogu P3763 代碼
/*
Powered by C++11.
Author : Alex_Wei.
*/
#include <bits/stdc++.h>
using namespace std;
#define mem(x,v) memset(x,v,sizeof(x))
const int N=2e5+5;
const int K=18;
// Suffix_Array
int n,sa[N],rk[N<<1],ht[N],ind[N];
int buc[N],px[N],id[N],ork[N<<1],mi[N][K];
char s[N];
void clear(){
mem(sa,0),mem(rk,0),mem(ind,0),mem(buc,0),mem(mi,0);
}
bool cmp(int a,int b,int w){
return ork[a]==ork[b]&&ork[a+w]==ork[b+w];
}
void build(){
int m=1<<7,p=0;
for(int i=1;i<=n;i++)buc[rk[i]=s[i]]++;
for(int i=1;i<=m;i++)buc[i]+=buc[i-1];
for(int i=n;i;i--)sa[buc[rk[i]]--]=i;
for(int w=1;w<n;w<<=1,m=p,p=0){
for(int i=n;i>n-w;i--)id[++p]=i;
for(int i=1;i<=n;i++)if(sa[i]>w)id[++p]=sa[i]-w;
for(int i=0;i<=m;i++)buc[i]=0;
for(int i=1;i<=n;i++)buc[px[i]=rk[id[i]]]++;
for(int i=1;i<=m;i++)buc[i]+=buc[i-1];
for(int i=n;i;i--)sa[buc[px[i]]--]=id[i];
memcpy(ork,rk,sizeof(rk)),p=0;
for(int i=1;i<=n;i++)rk[sa[i]]=cmp(sa[i],sa[i-1],w)?p:++p;
if(p==n)break;
}
for(int i=1,k=0;i<=n;i++){
if(k)k--;
while(s[i+k]==s[sa[rk[i]-1]+k])k++;
ht[rk[i]]=k;
}
for(int j=0;j<K;j++)
for(int i=1;i+(j?1<<j-1:0)<=n;i++)
mi[i][j]=(j==0?ht[i]:min(mi[i][j-1],mi[i+(1<<j-1)][j-1]));
}
int gmin(int l,int r){
if(l>r)swap(l,r);
int d=log2(r-l);
return min(mi[l+1][d],mi[r-(1<<d)+1][d]);
}
int t,ans;
int main(){
cin>>t;
while(t--){
clear(),ans=0,scanf("%s",s+1);
int l=strlen(s+1);
for(int i=1;i<=l;i++)ind[i]=1;
scanf("%s",s+l+2),s[l+1]=127,n=strlen(s+1);
for(int i=l+2;i<=n;i++)ind[i]=2;
build();
for(int i=1;i<=n;i++){
if(ind[sa[i]]==1&&sa[i]+(n-l-1)-1<=l){
int p=l+2,id=sa[i];
for(int k=0;k<4;k++){
int d=gmin(rk[id],rk[p]);
id+=d+(k<3),p+=d+(k<3);
if(p>=n+1)break;
} ans+=p>=n+1;
}
}
cout<<ans<<endl;
}
return 0;
}
IV. P4070 [SDOI2016]生成魔咒
求 \(s\) 的每一個前綴的本質不同子串個數。
這題一看就很 SAM。然而我就是要用 SA 做!!!!1111!!!然后成功 WA 掉。
一開始的想法是直接正着做,然后維護一下所有相鄰后綴的 \(height\)(統計有多少 \(lcp\) 是因為后面被截掉而沒有計算完的,每次向右移動就要減去這些 \(lcp\) 的數量,直到其中的 \(lcp\) 到了相應的位置)。然而,\(s[1:i]\) 后綴排序后所有后綴排名的相對位置,在 \(s[1:n]\) 中可能會改變。一個例子是 \(s=[1,1,2,1,2]\),那么 \(s[1:4]\) 時 \(s_4\) 排在 \(s_1\) 前面,而 \(s[1:5]\) 時 \(s_4\) 排在 \(s_1\) 的后面。這樣就悲催了。看了題解后發現了新大陸一個小技巧:
trick 2:將 \(s\) 翻轉后,從后往前添加后綴。這樣可以避免在末尾添加字符時導致所有后綴原有的順序改變,而翻轉不會影響到一個字符串的本質不同子串個數。
這樣就做完了。又是用 SA 水 SAM 題目的一天。
Luogu P4070 代碼
/*
Powered by C++11.
Author : Alex_Wei.
*/
#include <bits/stdc++.h>
using namespace std;
#define se second
#define ll long long
const int N=1e5+5;
const int K=17;
// Suffix_Array
int n,sa[N],rk[N<<1],ht[N],s[N];
map <int,int> buc;
int px[N],id[N],ork[N<<1],mi[N][K];
bool cmp(int a,int b,int w){
return ork[a]==ork[b]&&ork[a+w]==ork[b+w];
}
void build(){
int p=0;
for(int i=1;i<=n;i++)buc[rk[i]=s[i]]++;
for(auto it=++buc.begin(),pre=buc.begin();it!=buc.end();it++,pre++)(*it).se+=(*pre).se;
for(int i=n;i;i--)sa[buc[rk[i]]--]=i;
for(int w=1;w<n;w<<=1,p=0){
for(int i=n;i>n-w;i--)id[++p]=i;
for(int i=1;i<=n;i++)if(sa[i]>w)id[++p]=sa[i]-w;
buc.clear(); for(int i=1;i<=n;i++)buc[px[i]=rk[id[i]]]++;
for(auto it=++buc.begin(),pre=buc.begin();it!=buc.end();it++,pre++)(*it).se+=(*pre).se;
for(int i=n;i;i--)sa[buc[px[i]]--]=id[i];
memcpy(ork,rk,sizeof(rk)),p=0;
for(int i=1;i<=n;i++)rk[sa[i]]=cmp(sa[i],sa[i-1],w)?p:++p;
if(p==n)break;
}
for(int i=1,k=0;i<=n;i++){
if(k)k--;
while(s[i+k]==s[sa[rk[i]-1]+k])k++;
ht[rk[i]]=k;
}
for(int j=0;j<K;j++)
for(int i=1;i+(j?1<<j-1:0)<=n;i++)
mi[i][j]=(j==0?ht[i]:min(mi[i][j-1],mi[i+(1<<j-1)][j-1]));
}
int lcp(int l,int r){
int d=log2(r-l);
return min(mi[l+1][d],mi[r-(1<<d)+1][d]);
}
ll cur;
set <int> st;
int main(){
cin>>n;
for(int i=1;i<=n;i++)cin>>s[i];
reverse(s+1,s+n+1);
build(),st.insert(rk[n]);
cout<<1<<endl;
for(int i=n-1;i;i--){
st.insert(rk[i]);
auto it=st.lower_bound(rk[i]);
if(it!=st.begin()&&it!=--st.end()){
auto pre=--it,suf=++++it;
cur+=-lcp(*pre,*suf)+lcp(*pre,rk[i])+lcp(rk[i],*suf);
} else if(it!=st.begin()){
auto pre=--it;
cur+=lcp(*pre,rk[i]);
} else{
auto suf=++it;
cur+=lcp(rk[i],*suf);
} cout<<1ll*(n-i+1)*(n-i+2)/2-cur<<endl;
}
return 0;
}
SAM 的話直接 \(ans\gets ans+\mathrm{len}(cur)-\mathrm{len}(\mathrm{link}(cur))\) 就好了。
*V. CF1037H Security
題意簡述:給出 \(s,q\),\(q\) 次詢問每次給出 \(l,r,t\),求字典序最小的 \(s[l:r]\) 的子串 \(s'\) 使得 \(s'>t\)。
神仙題,又讓我學會了一個神奇的操作(其實是我菜沒見過套路)。
就是對於這種區間子串的題目,我們直接在 SAM 上貪心的時候,不知道當前的選擇是否可行(即選一個字符后判斷可不可能當前選取的整個字符串落在區間 \([l,r]\) 里面),那么可以……
trick 3:用線段樹合並維護 \(\mathrm{endpos}\) 集合。
如果有了 \(\mathrm{endpos}\) 集合,直接貪心選取就好了。注意要貪到第 \(|T|+1\) 位(因為可能當前 \(s'=t\),那么再選一個字符就好了)。
為了寫這道題目甚至去學了一下線段樹合並。
CF1037H 代碼
/*
Powered by C++11.
Author : Alex_Wei.
*/
#include <bits/stdc++.h>
using namespace std;
//#pragma GCC optimize(3)
//using int = long long
//using i128 = __int128;
using uint = unsigned int;
using ll = long long;
using ull = unsigned long long;
using db = double;
using ld = long double;
using pii = pair <int,int>;
using pll = pair <ll,ll>;
using pdd = pair <double,double>;
using vint = vector <int>;
using vpii = vector <pii>;
#define fi first
#define se second
#define pb emplace_back
#define mpi make_pair
#define all(x) x.begin(),x.end()
#define sor(x) sort(all(x))
#define rev(x) reverse(all(x))
#define mem(x,v) memset(x,v,sizeof(x))
#define mcpy(x,y) memcpy(x,y,sizeof(y))
#define Time 1.0*clock()/CLOCKS_PER_SEC
pii operator + (pii a,pii b){return {a.fi+b.fi,a.se+b.se};}
pll operator + (pll a,pll b){return {a.fi+b.fi,a.se+b.se};}
const int N=2e5+5;
const int S=26;
// Suffix_Automaton
int las,tot;
int son[N][S],fa[N],len[N];
int ed[N],id[N],buc[N];
void insS(char s){
int cur=++tot,p=las,it=s-'a';
len[cur]=len[las]+1,las=cur,ed[cur]=1;
while(p&&!son[p][it])son[p][it]=cur,p=fa[p];
if(!p)return fa[cur]=1,void();
int q=son[p][it];
if(len[p]+1==len[q])return fa[cur]=q,void();
int c=++tot;
fa[c]=fa[q],fa[q]=c,fa[cur]=c,len[c]=len[p]+1;
for(int i=0;i<26;i++)son[c][i]=son[q][i];
while(p&&son[p][it]==q)son[p][it]=c,p=fa[p];
} void build(char *s){
int n=strlen(s+1); las=tot=1;
for(int i=1;i<=n;i++)insS(s[i]),ed[las]=i;
for(int i=1;i<=tot;i++)buc[len[i]]++;
for(int i=1;i<=tot;i++)buc[i]+=buc[i-1];
for(int i=tot;i;i--)id[buc[len[i]]--]=i;
}
// Chairman_Tree
int node,rt[N],ls[N<<5],rs[N<<5];
void insC(int l,int r,int p,int ori,int &x){
x=++node;
if(l==r)return;
int m=l+r>>1;
if(p<=m)insC(l,m,p,ls[ori],ls[x]),rs[x]=rs[ori];
else insC(m+1,r,p,rs[ori],rs[x]),ls[x]=ls[ori];
} int merge(int l,int r,int x,int y){
if(!x||!y)return x|y;
if(x==y)return x;
int m=l+r>>1,z=++node;
ls[z]=merge(l,m,ls[x],ls[y]),rs[z]=merge(m+1,r,rs[x],rs[y]);
return z;
} bool query(int l,int r,int ql,int qr,int x){
if(!x||ql>qr)return 0;
if(ql<=l&&r<=qr)return 1;
int m=l+r>>1; bool ans=0;
if(ql<=m)ans|=query(l,m,ql,qr,ls[x]);
if(m<qr)ans|=query(m+1,r,ql,qr,rs[x]);
return ans;
}
char s[N],t[N],ans[N];
int q,n,l,r,tag;
bool dfs(int i,int p){
if(i==n+2)return 0;
int it=(i>n?0:t[i]-'a'),q=son[p][it];
for(int j=it;j<26;j++){
q=son[p][j];
if(q&&query(1,tot,l+i-1,r,rt[q])){
if(j==it&&i<=n&&!dfs(i+1,q))continue;
ans[i]=j+'a';
return 1;
}
} return 0;
}
int main(){
scanf("%s",s+1),build(s);
for(int i=1;i<=tot;i++)if(ed[i])insC(1,tot,ed[i],0,rt[i]);
for(int i=tot;i;i--)rt[fa[id[i]]]=merge(1,tot,rt[fa[id[i]]],rt[id[i]]);
cin>>q;
while(q--){
cin>>l>>r,tag=1;
scanf("%s",t+1),n=strlen(t+1);
if(dfs(1,1))cout<<(ans+tag)<<endl;
else puts("-1");
for(int i=1;ans[i];i++)ans[i]=0;
}
return 0;
}
*VI. P4770 [NOI2018] 你的名字
題意簡述:給出 \(s,q\),\(q\) 次詢問 \(l,r,t\),求 \(t\) 有多少個本質不同子串沒有在 \(s[l:r]\) 中出現過。
一寫寫一天,最后還是看了題解。
記 \(pre_i\) 為與 \(s[l:r]\) 匹配的所有 \(t[1:i]\) 后綴的最長的長度,直接在 \(s\) 的 SAM 上面跳即可。設當前位置為 \(p\),匹配長度為 \(L\),區間為 \(l,r\),那么直接查詢是否存在一個位置 \(x\) 使得 \(x\in[l+L-1,r]\) 且 \(x\in\mathrm{endpos}(p)\) 即可(保證當前狀態當前長度的字符串在 \(s[l:r]\) 中出現過),如果存在直接跳,不存在就將匹配長度減小 \(1\)(注意不是直接跳 \(\mathrm{link}\)!可能狀態 \(p\) 時當前長度不滿足,但是長度減小就滿足了),如果長度減小到 \(\mathrm{len(link}(p))\) 再向上跳。根據上面一題的套路用線段樹合並維護 \(\mathrm{endpos}\) 即可。
然后對 \(t\) 建 SAM,那么答案即為 \(\sum \max(0,\mathrm{len}(p)-\max(\mathrm{len(link}(p)),pre_{\mathrm{minr}(p)}))\)。其中 \(\mathrm{minr}(p)\) 表示 \(p\) 的 \(\mathrm{endpos}\) 集合中最小的位置。
稍微解釋一下:該位置只能表示長度為 \((\mathrm{len(link}(p),\mathrm{len}(p)]\) 的子串,而如果長度不大於 \(pre_{\mathrm{minr}(p)}\) 就能被 \(s[l,r]\) 匹配,不符合題意。當然,如果不是 \(\mathrm{minr}\) 也可以,因為如果存在 \(pos,pos'\in \mathrm{endpos}(p)\) 使得 \(pre_{pos}\neq pre_{pos'}\),那么 \(pre_{pos'}\) 顯然不小於 \(\mathrm{len}(p)\),因此可以推出 \(pre_{pos}\geq \mathrm{len}(p)\),對答案沒有貢獻,只不過 \(\mathrm{minr}\) 好維護一點。
\(\mathrm{minr}\) 可以在建出 SAM 的時候一並維護。
Luogu P4770 代碼
/*
Powered by C++11.
Author : Alex_Wei.
*/
#include <bits/stdc++.h>
using namespace std;
//#pragma GCC optimize(3)
//using int = long long
//using i128 = __int128;
using uint = unsigned int;
using ll = long long;
using ull = unsigned long long;
using db = double;
using ld = long double;
using pii = pair <int,int>;
using pll = pair <ll,ll>;
using pdd = pair <double,double>;
using vint = vector <int>;
using vpii = vector <pii>;
#define fi first
#define se second
#define pb emplace_back
#define mpi make_pair
#define all(x) x.begin(),x.end()
#define sor(x) sort(all(x))
#define rev(x) reverse(all(x))
#define mem(x,v) memset(x,v,sizeof(x))
#define mcpy(x,y) memcpy(x,y,sizeof(y))
#define Time 1.0*clock()/CLOCKS_PER_SEC
pii operator + (pii a,pii b){return {a.fi+b.fi,a.se+b.se};}
pll operator + (pll a,pll b){return {a.fi+b.fi,a.se+b.se};}
const int N=1e6+5;
const int S=26;
const int K=N*50;
struct SegTreeFusion{
int node,rt[N],ls[K],rs[K];
void ins(int l,int r,int p,int ori,int &x){
x=++node;
if(l==r)return void();
int m=l+r>>1;
if(p<=m)ins(l,m,p,ls[ori],ls[x]),rs[x]=rs[ori];
else ins(m+1,r,p,rs[ori],rs[x]),ls[x]=ls[ori];
} int merge(int l,int r,int x,int y){
if(!x||!y)return x|y;
if(l==r)return x;
int m=l+r>>1,z=++node;
ls[z]=merge(l,m,ls[x],ls[y]);
rs[z]=merge(m+1,r,rs[x],rs[y]);
return z;
} bool query(int l,int r,int ql,int qr,int x){
if(!x||ql>qr)return 0;
if(ql<=l&&r<=qr)return 1;
int m=l+r>>1,ans=0;
if(ql<=m)ans|=query(l,m,ql,qr,ls[x]);
if(m<qr)ans|=query(m+1,r,ql,qr,rs[x]);
return ans;
}
}st;
int n,q;
struct SAM{
int cnt,las;
int son[N][S],fa[N],len[N];
int buc[N],id[N],minr[N];
void clear(){
mem(son[1],0),cnt=las=1;
} void ins(char s,bool seg){
int p=las,cur=++cnt,it=s-'a'; mem(son[las=cur],0);
minr[cur]=len[cur]=len[p]+1;
if(seg)st.ins(1,n,len[cur],0,st.rt[cur]);
while(p&&!son[p][it])son[p][it]=cur,p=fa[p];
if(!p)return fa[cur]=1,void();
int q=son[p][it];
if(len[p]+1==len[q])return fa[cur]=q,void();
int c=++cnt;
fa[c]=fa[q],fa[q]=fa[cur]=c,len[c]=len[p]+1,minr[c]=minr[q];
for(int i=0;i<26;i++)son[c][i]=son[q][i];
while(p&&son[p][it]==q)son[p][it]=c,p=fa[p];
} void build(char *s,int ln,bool seg){
clear();
for(int i=1;i<=ln;i++)ins(s[i],seg);
for(int i=0;i<=ln;i++)buc[i]=0;
for(int i=1;i<=cnt;i++)buc[len[i]]++;
for(int i=1;i<=ln;i++)buc[i]+=buc[i-1];
for(int i=cnt;i;i--)id[buc[len[i]]--]=i;
if(seg)for(int i=cnt;i>1;i--)
st.rt[fa[id[i]]]=st.merge(1,n,st.rt[fa[id[i]]],st.rt[id[i]]);
} void trans(int &p,int &ln,int l,int r,int c){
while(1){
if(son[p][c]&&st.query(1,n,l+ln,r,st.rt[son[p][c]]))
return ln++,p=son[p][c],void();
if(!ln)return;
if(--ln==len[fa[p]])p=fa[p];
}
} ll cal(int p[]){
ll ans=0;
for(int i=2;i<=cnt;i++)ans+=max(0,len[i]-max(len[fa[i]],p[minr[i]]));
return ans;
}
}sams,samt;
int p[N];
char s[N],t[N];
int main(){
scanf("%s",s+1),n=strlen(s+1);
sams.build(s,n,1),cin>>q;
for(int i=1,l,r;i<=q;i++){
scanf("%s",t+1),cin>>l>>r;
int len=strlen(t+1),pos=1;
samt.build(t,len,0);
for(int i=1;i<=len;i++)sams.trans(pos,p[i]=p[i-1],l,r,t[i]-'a');
cout<<samt.cal(p)<<endl;
}
return 0;
}
*VII. CF666E Forensic Examination
題意簡述:給出字符串 \(s\) 與 \(t_{1,2,\cdots,m}\),\(q\) 次詢問,求出 \(t_{[l,r]}\) 中出現 \(s[pl:pr]\) 次數最多的字符串編號最小值與次數。
碼題十分鍾,debug de 一年。
首先有這樣一個技巧:
trick 4:找到 \(s[l:r]\) 在一個 SAM 中的狀態,可以記錄 \(s[1:r]\) 在 SAM 中匹配的的狀態,然后在 \(link\) 樹上倍增。需要特判 \(s[1:r]\) 在 SAM 中匹配長度小於 \(r-l+1\) 的情況,這時 \(s[l:r]\) 在 SAM 里面是沒有的(如果 \(s\) 也在 SAM 中就不需要了,因為一定存在這個狀態)。
將所有 \(t_i\) 建出一個廣義 SAM,然而我不會廣義 SAM,那么每次添加一個新字符串時,將 \(las\) 設為 \(1\) 即可。
多串 SAM 如果直接 \(las=1\) 不能判重!會掛掉!!
除此以外,假設跳到了表示 \(s[pl:pr]\) 的狀態 \(p\),那我們還需找到一個最小的 \(i\in[l,r]\) 使得 \(p\) 及 \(p\) 的子樹中 \(t_i\) 的結束狀態的個數最大,顯然要線段樹合並維護一個狀態的 \(endpos\) 集合中出現在每個 \(t_i\) 中的位置個數,然后直接用線段樹維護區間最大值和區間最大值的編號最小值即可。
注意點:如果多串 SAM 直接將 \(las\) 設為 \(1\) 並且不判重(即直接 \(cur=las+1\) 而不判斷是否 \(las\) 已經有當前字符的轉移)(只能不判重!!否則會破壞原有 SAM 的結構!),那么如果兩個字符串的開頭字符相同,可能會導致一個節點成了空節點(即沒有入邊,不包含任何字符串),從而使 \(len(link(i))=len(i)\)。這時就不能用桶排求拓撲序了,必須用 dfs。
這玩意調了 1.5h,刻骨銘心。
當然如果直接把 \(s\) 也塞進 SAM 也可以,不過會慢一些。
時間復雜度 \(\mathcal{O}((|s|+\sum|t_i|+q)\log \sum|t_i|)\)。
CF666E 代碼
/*
Powered by C++11.
Author : Alex_Wei.
*/
#include <bits/stdc++.h>
using namespace std;
//#pragma GCC optimize(3)
//using int = long long
//using i128 = __int128;
using uint = unsigned int;
using ll = long long;
using ull = unsigned long long;
using db = double;
using ld = long double;
using pii = pair <int,int>;
using pll = pair <ll,ll>;
using pdd = pair <double,double>;
using vint = vector <int>;
using vpii = vector <pii>;
#define fi first
#define se second
#define pb emplace_back
#define mpi make_pair
#define all(x) x.begin(),x.end()
#define sor(x) sort(all(x))
#define rev(x) reverse(all(x))
#define mem(x,v) memset(x,v,sizeof(x))
#define mcpy(x,y) memcpy(x,y,sizeof(y))
#define Time 1.0*clock()/CLOCKS_PER_SEC
pii operator + (pii a,pii b){return {a.fi+b.fi,a.se+b.se};}
pll operator + (pll a,pll b){return {a.fi+b.fi,a.se+b.se};}
namespace IO{
char buf[1<<23],*p1=buf,*p2=buf,obuf[1<<24],*O=obuf;
#ifdef __WIN32
#define gc getchar()
#else
#define gc (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<22,stdin),p1==p2)?EOF:*p1++)
#endif
#define pc(x) (*O++=x)
#define flush() fwrite(obuf,O-obuf,1,stdout)
inline ll read(){
ll x=0; bool sign=0; char s=gc;
while(!isdigit(s))sign|=s=='-',s=gc;
while(isdigit(s))x=(x<<1)+(x<<3)+(s-'0'),s=gc;
return sign?-x:x;
}
inline void print(ll x){
if(x<0)pc('-'),print(-x);
else{
if(x>9)print(x/10);
pc(x%10+'0');
}
}
} using namespace IO;
const int N=2e6+5;
const int M=5e4+5;
const int S=26;
int node,rt[N],ls[M<<6],rs[M<<6];
pii val[M<<6];
pii merge(pii x,pii y){
int z=max(x.fi,y.fi);
if(x.se>y.se)swap(x,y);
return {z,x.fi==z?x.se:y.se};
} void ins(int l,int r,int p,int &x){
if(!x)x=++node;
if(l==r)return val[x].fi++,val[x].se=p,void();
int m=l+r>>1;
if(p<=m)ins(l,m,p,ls[x]);
else ins(m+1,r,p,rs[x]);
val[x]=merge(val[ls[x]],val[rs[x]]);
} int merge(int l,int r,int x,int y){
if(!x||!y)return x|y;
int z=++node,m=l+r>>1;
if(l==r){
val[z].fi=val[x].fi+val[y].fi;
val[z].se=min(val[x].se,val[y].se);
return z;
} ls[z]=merge(l,m,ls[x],ls[y]),rs[z]=merge(m+1,r,rs[x],rs[y]);
return val[z]=merge(val[ls[z]],val[rs[z]]),z;
} pii query(int l,int r,int ql,int qr,int x){
if(!x)return {0,0};
if(ql<=l&&r<=qr)return val[x];
int m=l+r>>1; pii ans={0,0};
if(ql<=m)ans=query(l,m,ql,qr,ls[x]);
if(m<qr)ans=merge(ans,query(m+1,r,ql,qr,rs[x]));
return ans;
}
int n,m,cnt,las;
int son[N][S],fa[N],len[N];
int buc[N],id[N],f[N][S],ed[N],mxl[N];
vector <int> e[N];
void ins(char s,int id){
int p=las,it=s-'a',cur=++cnt;
len[cur]=len[las]+1,las=cur,ins(1,m,id,rt[cur]);
while(p&&!son[p][it])son[p][it]=cur,p=fa[p];
if(!p)return fa[cur]=1,void();
int q=son[p][it];
if(len[p]+1==len[q])return fa[cur]=q,void();
int c=++cnt;
fa[c]=fa[q],fa[q]=fa[cur]=c,len[c]=len[p]+1;
for(int i=0;i<26;i++)son[c][i]=son[q][i];
while(p&&son[p][it]==q)son[p][it]=c,p=fa[p];
} void build(char *s,int id){
int n=strlen(s+1); las=1;
if(id==1)cnt=1;
for(int i=1;i<=n;i++)ins(s[i],id);
} void dfs(int id){
for(int it:e[id])dfs(it),rt[id]=merge(1,m,rt[id],rt[it]);
}
int p,q,pl,pr,l,r;
char s[N],t[N];
int main(){
scanf("%s",s+1),cin>>m;
n=strlen(s+1);
for(int i=1;i<=m;i++)scanf("%s",t+1),build(t,i);
for(int i=1,p=1,l=0;i<=n;i++){
while(p&&!son[p][s[i]-'a'])p=fa[p],l=len[p];
if(!p)p=1,l=0; else p=son[p][s[i]-'a'],l++;
ed[i]=p,mxl[i]=l;
}
for(int j=0;1<<j<=cnt;j++)for(int i=1;i<=cnt;i++)f[i][j]=j?f[f[i][j-1]][j-1]:fa[i];
for(int i=2;i<=cnt;i++)e[fa[i]].pb(i); dfs(1);
cin>>q; while(q--){
l=read(),r=read(),pl=read(),pr=read(),p=ed[pr];
if(mxl[pr]<pr-pl+1){
cout<<l<<" 0\n";
continue;
} for(int i=log2(cnt);~i;i--)if(pr-len[f[p][i]]+1<=pl)p=f[p][i];
pii ans=query(1,m,l,r,rt[p]);
cout<<max(l,ans.se)<<" "<<ans.fi<<"\n";
}
return 0;
}
VIII. P4022 [CTSC2012]熟悉的文章
題意簡述:給出字典 \(T_{1,2,\cdots,m}\),多次詢問一個字符串 \(s\) 的 \(L_0\),其中 \(L_0\) 表示:將 \(s\) 分為若干子串,使得所有長度不小於 \(l\) 且在字典 \(T\) 中出現過的子串長度之和不小於 \(0.9|s|\) 的 \(l\) 的最大值。
首先這個 \(L_0\) 顯然具有可二分性,那我們將題目轉化為給出 \(l\) 求滿足條件的長度最大值。設 \(f_i\) 表示 \(s[1:i]\) 能匹配的最大值,那么顯然有 \(f_i=\max(f_{i-1},\max_{j=i-pre_i}^{i-l} f_j+1)\),其中 \(pre_i\) 是 \(s[1:i]\) 在字典 \(T\) 中的最大匹配長度。可以發現決策點單調不減(因為每向右移動一位,\(pre\) 最多增加一位,所以 \(i-pre_i\) 單調不減),那么單調隊列就好了。
求 \(pre_i\) 直接廣義 SAM 即可。注意如果在插入新字符串時直接 \(las=1\),是不能判斷當前狀態是否已有轉移並直接跳過去(而不是新建一個狀態)的,因為這樣會破壞原有的 SAM 的結構。
時間復雜度 \(\mathcal{O}(\sum |T_i|+\sum |s|\log \sum |s|)\)。
Luogu P4022 代碼
/*
Powered by C++11.
Author : Alex_Wei.
*/
#include <bits/stdc++.h>
using namespace std;
#define mcpy(x,y) memcpy(x,y,sizeof(y))
const int N=2.2e6+5;
// Suffix_Automaton
int n,m;
int cnt,las;
int fa[N],len[N],son[N][2];
void ins(int it){
int p=las,cur=++cnt;
len[cur]=len[las]+1,las=cur;
while(p&&!son[p][it])son[p][it]=cur,p=fa[p];
if(!p)return fa[cur]=1,void();
int q=son[p][it];
if(len[p]+1==len[q])return fa[cur]=q,void();
int cl=++cnt;
fa[cl]=fa[q],fa[q]=fa[cur]=cl,len[cl]=len[p]+1;
son[cl][0]=son[q][0],son[cl][1]=son[q][1];
while(p&&son[p][it]==q)son[p][it]=cl,p=fa[p];
} void build(char *s){
int n=strlen(s+1); las=1;
for(int i=1;i<=n;i++)ins(s[i]-'0');
}
int f[N],d[N],hd,tl;
char s[N];
int check(int x){
int n=strlen(s+1),p=1,l=0,ans=0; hd=1,tl=0;
for(int i=1;i<=n;i++){
int it=s[i]-'0';
while(p&&!son[p][it])p=fa[p],l=len[p];
if(!p)p=1,l=0;
else p=son[p][it],l++;
if(i>=x){
while(hd<=tl&&f[d[tl]]+(i-x-d[tl])<=f[i-x])tl--;
d[++tl]=i-x;
} while(hd<=tl&&d[hd]+l<i)hd++;
if(hd<=tl)f[i]=max(f[i-1],f[d[hd]]+(i-d[hd]));
else f[i]=f[i-1];
ans=max(ans,f[i]);
} return ans;
}
int main(){
cin>>n>>m,cnt=1;
for(int i=1;i<=m;i++)scanf("%s",s+1),build(s);
for(int i=1;i<=n;i++){
scanf("%s",s+1);
int n=strlen(s+1),l=0,r=n;
while(l<r){
int m=(l+r>>1)+1;
if(check(m)>=n*0.9)l=m;
else r=m-1;
} cout<<l<<"\n";
}
return 0;
}
IX. CF616F Expensive Strings
題意簡述:給出 \(t_{1,2,\cdots,n}\) 和 \(c_{1,2,\cdots,n}\),求 \(\max f(s)=\sum_i^n c_i\times p_{s,i} \times |s|\) 的最大值,其中 \(s\) 為任意字符串,\(p_{s,i}\) 為 \(s\) 在 \(t_i\) 中的出現次數。
廣義 SAM 板子題。
考慮 SAM 上每個狀態所表示的意義:出現位置相同的字符串集合。也就是說,對於 SAM 上的一個狀態 \(t\),它所表示的所有字符串 \(s\) 的 \(\sum_{i=1}^n c_i\times p_{s,i}\) 是相同的,所以它對答案的可能貢獻就是 \(\sum_{i=1}^n c_i\times p_{s,i}\times len(t)\)。\(\sum_{i=1}^n c_i\times p_{s,i}\) 可以直接在 \(link\) 樹上樹形 DP 求出。我一開始還以為要線段樹合並,做題做傻了。
一些注意點:如果你寫的是 \(las=1\) 版本的偽廣義 SAM,如果不判重,可能會建空節點 \(p\),此時 \(len(link(p))=len(p)\)。所以特判一下這種情況就行了,否則會 WA on 16,並且 “expected 0,found 500”。
同時,答案的初始值應賦為 \(0\) 而不是 \(-\infty\),因為只要讓 \(s\) 不在任何一個 \(t_i\) 中出現過就可以 \(f(s)=0\)。
一開始直接拿 P4022 熟悉的文章 的廣義 SAM 寫的,那個題目是 01 串,所以復制兒子只復制了 0 和 1(這題就是 a 和 b),然后過了 43 個測試點。
CF616F 代碼
/*
Powered by C++11.
Author : Alex_Wei.
*/
#include <bits/stdc++.h>
using namespace std;
//#pragma GCC optimize(3)
//using int = long long
//using i128 = __int128;
using uint = unsigned int;
using ll = long long;
using ull = unsigned long long;
using db = double;
using ld = long double;
using pii = pair <int,int>;
using pll = pair <ll,ll>;
using pdd = pair <double,double>;
using vint = vector <int>;
using vpii = vector <pii>;
#define fi first
#define se second
#define pb emplace_back
#define mpi make_pair
#define all(x) x.begin(),x.end()
#define sor(x) sort(all(x))
#define rev(x) reverse(all(x))
#define mem(x,v) memset(x,v,sizeof(x))
#define mcpy(x,y) memcpy(x,y,sizeof(y))
#define Time 1.0*clock()/CLOCKS_PER_SEC
const int N=1e6+5;
// Suffix_Automaton
int cnt,las;
int fa[N],len[N],son[N][26];
ll val[N];
vector <int> e[N];
void ins(int it,int v){
int p=las,cur=++cnt;
len[cur]=len[las]+1,las=cur,val[cur]=v;
while(p&&!son[p][it])son[p][it]=cur,p=fa[p];
if(!p)return fa[cur]=1,void();
int q=son[p][it];
if(len[p]+1==len[q])return fa[cur]=q,void();
int cl=++cnt;
fa[cl]=fa[q],fa[q]=fa[cur]=cl,len[cl]=len[p]+1;
mcpy(son[cl],son[q]);
while(p&&son[p][it]==q)son[p][it]=cl,p=fa[p];
} void build(string s,int v){
las=1;
for(int i=0;i<s.size();i++)ins(s[i]-'a',v);
} void dfs(int id){
for(int it:e[id])dfs(it),val[id]+=val[it];
}
int n;
ll ans;
string s[N];
int main(){
cin>>n,cnt=1;
for(int i=1;i<=n;i++)cin>>s[i];
for(int i=1,c;i<=n;i++)cin>>c,build(s[i],c);
for(int i=1;i<=cnt;i++)e[fa[i]].pb(i);
dfs(1); for(int i=1;i<=cnt;i++)if(len[fa[i]]!=len[i])ans=max(ans,len[i]*val[i]);
cout<<ans<<endl;
return 0;
}
X. P4094 [HEOI2016/TJOI2016]字符串
題意簡述:給出字符串 \(s\),多次詢問 \(a,b,c,d\) 求 \(s[a:b]\) 的所有子串與 \(s[c:d]\) 的最長公共前綴的最大值。
這個 SAM 套路見多了的話還是挺簡單的吧。
首先,SAM 不太方便處理前綴,所以將整個串翻轉(詢問不要忘記翻轉),這樣就轉化為了最長公共后綴。接下來求 \(s[1:d]\) 所代表的狀態,設為 \(p\),直接在建 SAM 時預處理即可。
直接不管 \(c\) 的限制,問題轉化為求出 \(s[a:b]\) 所有子串與 \(s[1:d]\) 的最長公共后綴長度,並與 \(d-c+1\) 取 \(\min\)。
根據 SAM 的性質,\(link\) 樹上所有 \(p\) 的祖先都表示 \(s[1:d]\) 的一個或多個后綴。我們可以找到一個狀態 \(q\) 滿足 \(q\) 是 \(p\) 的祖先且 \(\left(\max_{x\in endpos(q),x\leq b}x\right)-a+1\leq len(q)\)(也就是該狀態所表示的字符串在 \(b\) 或 \(b\) 之前出現的最靠右的結束位置,至於為什么要最靠右顯而易見(右邊的出現位置肯定優於左邊的出現位置,因為有左端點 \(a\) 的限制),讀者可自行理解),且 \(len(q)\) 的值最小,那么最長公共后綴肯定在 \(q\) 或 \(link(q)\) 所表示的子串中。
-
先說說為什么要 \(len(q)\) 最小:假設存在 \(q'\) 滿足上述條件,但 \(len(q')>len(q)\),即 \(q\) 是 \(q'\) 的祖先(同時 \(q'\) 是 \(p\) 的祖先)。記 \(\max_{x\in endpos(q),x\leq b}x\) 為 \(maxp(q,b)\),那么根據 \(endpos\) 和 \(link\) 的性質,即 \(endpos(q')\subsetneq endpos(q)\),因此,\(maxp(q',b)\leq maxp(q,b)\),即 \(q'\) 點所表示字符串在 \(b\) 或 \(b\) 之前出現的最大結束位置,一定不大於 \(q\) 點所表示的字符串在 \(b\) 或 \(b\) 之前出現的最大結束位置。因此 \(maxp(q',b)-a+1\leq maxp(q,b)-a+1\)。又因為 \(len(q)\ (len(q'))\geq maxp(q,b)\ (maxp(q',b)) -a+1\),即 \(q\) 和 \(q'\) 所表示的的最長字符串超出了 \(a\) 的限制,所以我們是用 \(maxp\) 值 \(-a+1\) 求出在 \(a\) 的限制下該狀態對答案的貢獻。故 \(q\) 一定比 \(q'\) 更優。
-
再說說為什么要算上 \(link(q)\):
一 目 了 然,不 言 而 喻。
-
同時,因為 \(link(q)\) 的貢獻已經是 \(len(q)\) 了,如果再往上跳 \(maxp\) 遞增,貢獻也一定是該點的 \(len\) 值,這是遞減的,所以不需要再往上考慮。
說完了思路,接下來講講怎么實現:用線段樹合並維護 \(endpos\) 集合可以輕松在 \(\log\) 時間內求出 \(maxp\)。同時,因為滿足條件的 \(q\) 滿足二分條件,所以求 \(q\) 直接用 \(p\) 在 \(link\) 樹上倍增即可。那么最后答案即為 \(\min(\max(maxp(q,b)-a+1,len(link(q))),d-c+1)\)。(不需要特判答案為 \(0\) 的情況,因為此時 \(maxp(q,b)-a+1\) 不小於 \(0\),而 \(len(link(q))\) 顯然為 \(0\))
時間復雜度 \(\mathcal{O}(q\log^2 n)\)。
Luogu P4094 代碼
/*
Powered by C++11.
Author : Alex_Wei.
*/
#include <bits/stdc++.h>
using namespace std;
//#pragma GCC optimize(3)
//using int = long long
//using i128 = __int128;
using uint = unsigned int;
using ll = long long;
using ull = unsigned long long;
using db = double;
using ld = long double;
using pii = pair <int,int>;
using pll = pair <ll,ll>;
using pdd = pair <double,double>;
using vint = vector <int>;
using vpii = vector <pii>;
#define fi first
#define se second
#define pb emplace_back
#define mpi make_pair
#define all(x) x.begin(),x.end()
#define sor(x) sort(all(x))
#define rev(x) reverse(all(x))
#define mem(x,v) memset(x,v,sizeof(x))
#define mcpy(x,y) memcpy(x,y,sizeof(y))
const int N=2e5+5;
const int S=26;
int node,rt[N],ls[N<<5],rs[N<<5],val[N<<5];
void push(int x){
val[x]=max(val[ls[x]],val[rs[x]]);
} void ins(int l,int r,int p,int &x){
x=++node;
if(l==r)return val[x]=p,void();
int m=l+r>>1;
if(p<=m)ins(l,m,p,ls[x]);
else ins(m+1,r,p,rs[x]);
push(x);
} int merge(int l,int r,int x,int y){
if(!x||!y)return x|y;
int z=++node,m=l+r>>1;
if(l==r)return val[z]=max(val[x],val[y]),z;
ls[z]=merge(l,m,ls[x],ls[y]),rs[z]=merge(m+1,r,rs[x],rs[y]);
return push(z),z;
} int query(int l,int r,int ql,int qr,int x){
if(!x)return 0;
if(ql<=l&&r<=qr)return val[x];
int m=l+r>>1,ans=0;
if(ql<=m)ans=query(l,m,ql,qr,ls[x]);
if(m<qr)ans=max(ans,query(m+1,r,ql,qr,rs[x]));
return ans;
}
// Suffix_Automaton
int a,b,c,d;
int n,m,K,cnt,las;
int fa[N],len[N],son[N][S];
int buc[N],id[N],f[N][S],ed[N];
vector <int> e[N];
void ins(int it){
int p=las,cur=++cnt;
len[cur]=len[las]+1,las=cur;
ins(1,n,len[cur],rt[cur]),ed[len[cur]]=cur;
while(p&&!son[p][it])son[p][it]=cur,p=fa[p];
if(!p)return fa[cur]=1,void();
int q=son[p][it];
if(len[p]+1==len[q])return fa[cur]=q,void();
int cl=++cnt;
fa[cl]=fa[q],fa[q]=fa[cur]=cl,len[cl]=len[p]+1;
mcpy(son[cl],son[q]);
while(p&&son[p][it]==q)son[p][it]=cl,p=fa[p];
} void build(char *s){
las=cnt=1,K=log2(n);
for(int i=1;i<=n;i++)ins(s[i]-'a');
for(int i=1;i<=cnt;i++)buc[len[i]]++;
for(int i=1;i<=n;i++)buc[i]+=buc[i-1];
for(int i=cnt;i;i--)id[buc[len[i]]--]=i;
for(int i=cnt;i>1;i--)rt[fa[id[i]]]=merge(1,n,rt[fa[id[i]]],rt[id[i]]);
for(int j=0;j<=K;j++)for(int i=1;i<=cnt;i++)f[i][j]=j?f[f[i][j-1]][j-1]:fa[i];
} int qpos(int pos){
return query(1,n,1,b,rt[pos]);
}
char s[N];
int main(){
cin>>n>>m,scanf("%s",s+1);
reverse(s+1,s+n+1),build(s);
while(m--){
cin>>a>>b>>c>>d;
a=n-a+1,b=n-b+1,c=n-c+1,d=n-d+1,swap(a,b),swap(c,d);
int p=ed[d];
for(int i=K;~i;i--)if(f[p][i]){
int pp=f[p][i],pos=qpos(pp);
if(len[pp]>=pos-a+1)p=pp;
} int pos=qpos(p);
cout<<min(d-c+1,max(pos-a+1,len[f[p][0]]))<<endl;
}
return 0;
}
*XI. P5284 [十二省聯考2019]字符串問題
題意自己看吧,懶得簡述了。
這題目一看就很 SAM,而且 SAM 套路做多了就是一眼題。
首先看到 “\(B\) 類串為 \(t_{i+1}\) 的前綴” 直接建出反串的 SAM,因此以一個 \(B\) 類串 \(B_i\) 為后綴的所有 \(S\) 的子串為在 SAM 上 \(B_i\) 所表示狀態 \(p\) 在 fail 樹上的子樹。因此一個 \(A\) 類串 \(A_i\) 可以和若干個 fail 樹的子樹接在一起。那么直接向能接在一起的所有相鄰的子串連邊,然后如果出現環則無解。可是這樣連邊是 \(\mathcal{O}(n^2)\) 的。
trick 5:使用 SAM 的 fail 樹優化建圖。
既然是一個點向所有子樹連邊,那么直接在原 fail 樹的基礎上將該點與子樹的根節點連起來即可。這樣就可做到 \(\mathcal{O}(n)\) 規模。
一個注意點:注意到一個狀態可能對應多個子串,那么將該狀態的所有 \(A,B\) 類串按長度從小到大為第一關鍵字,是否是 \(B\) 類串為第二關鍵字排序,然后按順序拆點即可(所在狀態相同的長度相同 \(A,B\) 類串相等,\(A\) 串可以作 \(B\) 串的子串,所以 \(B\) 串要是 \(A\) 串的祖先)
然而我沒有想到在 fail 樹上建圖,是直接用線段樹優化建圖,線段樹優化 DAG 上 DP,於是 2h 碼了細節巨大多的 5k。看到題解后才學會這樣的技巧。好題!
能看懂代碼算我輸。
Luogu P5284 代碼
/*
Powered by C++11.
Author : Alex_Wei.
*/
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using vint = vector <int>;
#define pb emplace_back
#define all(x) x.begin(),x.end()
#define rev(x) reverse(all(x))
#define mem(x,v) memset(x,v,sizeof(x))
#define mcpy(x,y) memcpy(x,y,sizeof(y))
const int N=4e5+5;
const int S=26;
const int inf=1e9+7;
// Segtree_Min
int deg[N],val[N<<2],laz[N<<2];
void up(int x){
val[x]=min(val[x<<1],val[x<<1|1]);
} void down(int x){
if(laz[x]){
val[x<<1]-=laz[x],val[x<<1|1]-=laz[x];
laz[x<<1]+=laz[x],laz[x<<1|1]+=laz[x];
} laz[x]=0;
} void build(int l,int r,int x){
laz[x]=val[x]=0;
if(l==r)return val[x]=deg[l],void();
int m=l+r>>1;
build(l,m,x<<1),build(m+1,r,x<<1|1),up(x);
} void modify(int l,int r,int ql,int qr,int x){
if(ql<=l&&r<=qr)return val[x]--,laz[x]++,void();
int m=l+r>>1; down(x);
if(ql<=m)modify(l,m,ql,qr,x<<1);
if(m<qr)modify(m+1,r,ql,qr,x<<1|1); up(x);
} int query(int l,int r,int x){
if(l==r)return val[x]=inf,l;
int m=l+r>>1,ans; down(x);
if(!val[x<<1])ans=query(l,m,x<<1);
else ans=query(m+1,r,x<<1|1);
return up(x),ans;
}
// SegTree_Max
ll ini[N<<2],val2[N<<2],laz2[N<<2];
void cmax(ll &x,ll y){
x=max(x,y);
} void up2(int x){
val2[x]=max(val2[x<<1],val2[x<<1|1]);
} void down2(int x){
if(laz[x]!=-1){
cmax(laz2[x<<1],laz2[x]),cmax(laz2[x<<1|1],laz2[x]);
cmax(val2[x<<1],laz2[x]),cmax(val2[x<<1|1],laz2[x]);
} laz2[x]=-1;
} void build2(int l,int r,int x){
val2[x]=laz2[x]=-1;
if(l==r)return val2[x]=ini[l],void();
int m=l+r>>1;
build2(l,m,x<<1),build2(m+1,r,x<<1|1),up2(x);
} void modify2(int l,int r,int ql,int qr,int x,ll v){
if(ql<=l&&r<=qr)return cmax(val2[x],v),cmax(laz2[x],v),void();
int m=l+r>>1; down2(x);
if(ql<=m)modify2(l,m,ql,qr,x<<1,v);
if(m<qr)modify2(m+1,r,ql,qr,x<<1|1,v); up2(x);
} ll query2(int l,int r,int p,int x){
if(l==r)return val2[x];
int m=l+r>>1; down2(x);
if(p<=m)return query2(l,m,p,x<<1);
return query2(m+1,r,p,x<<1|1);
}
// Suffix_Automaton
int n,K,cnt,las;
int fa[N],len[N],ed[N],son[N][S],ff[N][S];
vint FAIL[N];
void ins(char s){
int p=las,cur=++cnt,it=s-'a';
len[cur]=len[las]+1,ed[len[cur]]=cur,las=cur;
while(p&&!son[p][it])son[p][it]=cur,p=fa[p];
if(!p)return fa[cur]=1,void();
int q=son[p][it];
if(len[p]+1==len[q])return fa[cur]=q,void();
int cl=++cnt;
fa[cl]=fa[q],fa[q]=fa[cur]=cl,len[cl]=len[p]+1;
mcpy(son[cl],son[q]);
while(son[p][it]==q)son[p][it]=cl,p=fa[p];
} void build(char *s){
for(int i=1;i<=n;i++)ins(s[i]);
for(int i=2;i<=cnt;i++)FAIL[fa[i]].pb(i),ff[i][0]=fa[i];
K=log2(cnt);
for(int i=1;i<=K;i++)for(int j=1;j<=cnt;j++)ff[j][i]=ff[ff[j][i-1]][i-1];
} int getpos(int l,int r){
int p=ed[r];
for(int i=K;~i;i--)if(r-len[ff[p][i]]+1<=l)p=ff[p][i];
return p;
}
char s[N];
int na,nb,tot,m;
int dnum,lens[N],tmp[N],rev[N],id[N],sz[N];
vint DAG[N],tag[N];
bool cmp(int a,int b){
return lens[a]!=lens[b]?lens[a]<lens[b]:a>b;
} int dfs(int d){
int z=tag[d].size(),l=dnum+1,r=dnum+z;
sort(all(tag[d]),cmp);
for(int it:tag[d])id[it]=++dnum;
for(int it:FAIL[d])z+=dfs(it);
for(int i=l;i<=r;i++)sz[i]=z-(i-l);
return z;
}
void clear(){
for(int i=1;i<=cnt;i++)mem(son[i],0),mem(ff[i],0),ed[i]=len[i]=fa[i]=0;
for(int i=1;i<=cnt;i++)FAIL[i].clear(),tag[i].clear();
for(int i=1;i<=tot+1;i++)lens[i]=id[i]=sz[i]=deg[i]=0;
for(int i=1;i<=na;i++)DAG[i].clear();
las=cnt=1,dnum=na=nb=tot=0;
} void init(){
scanf("%s%d",s+1,&na),n=strlen(s+1);
reverse(s+1,s+n+1),build(s);
for(int i=1;i<=na;i++){
int l,r; scanf("%d%d",&l,&r);
l=n-l+1,r=n-r+1,swap(l,r),lens[i]=r-l+1;
tag[getpos(l,r)].pb(i);
} scanf("%d",&nb),tot=na+nb;
for(int i=1;i<=nb;i++){
int l,r; scanf("%d%d",&l,&r);
l=n-l+1,r=n-r+1,swap(l,r),lens[i+na]=r-l+1;
tag[getpos(l,r)].pb(i+na);
} scanf("%d",&m);
for(int i=1;i<=m;i++){
int x,y; scanf("%d%d",&x,&y);
DAG[x].pb(y+na);
} dfs(1);
for(int i=1;i<=tot;i++)tmp[id[i]]=lens[i];
for(int i=1;i<=tot;i++)lens[i]=tmp[i],rev[id[i]]=i;
}
queue <int> q;
bool update(){
if(val[1])return 0;
int p=query(1,tot,1);
return ini[p]=0,q.push(p),1;
} bool calc_deg(){
for(int i=1;i<=na;i++)
for(int it:DAG[i]){
int l=id[it],r=l+sz[l]-1;
if(l<=id[i]&&id[i]<=r)return 1;
deg[l]++,deg[r+1]--;
}
for(int i=1;i<=tot;i++)deg[i]+=deg[i-1];
for(int i=na+1;i<=tot;i++)deg[id[i]]=inf;
return build(1,tot,1),0;
} ll topo(){
for(int i=1;i<=tot;i++)ini[i]=-1;
while(update()); build2(1,tot,1);
ll ans=0;
while(!q.empty()){
ll t=q.front(),v=query2(1,tot,t,1)+lens[t]; q.pop();
cmax(ans,v);
for(int it:DAG[rev[t]]){
int l=id[it],r=l+sz[l]-1;
modify(1,tot,l,r,1),modify2(1,tot,l,r,1,v);
while(update());
}
} return val[1]<1e6?-1:ans;
}
void solve(){
clear(),init();
if(calc_deg())return puts("-1"),void();
cout<<topo()<<endl;
} int main(){
int t; cin>>t;
while(t--)solve();
return 0;
}
XII. CF235C Cyclical Quest
題意簡述:給出 \(s\),多次詢問給出字符串 \(t\) 所有循環同構串去重后在 \(s\) 中出現次數之和。
如果沒有循環同構那么就是 ACAM/SA/SAM 板子題。關於循環同構的一個常見套路就是將 \(t\) 復制一份在后面。那么我們如法炮制,用 \(2t\) 在 SAM 上跑匹配。如果當前長度大於 \(|t|\),那么就不斷將匹配長度 \(d\) 減一,同時判斷當前狀態是否能表示長度為 \(d\) 的字符串(即是否有 \(len(link(p))<d\leq len(p)\)),如果沒有就要向上跳。
注意到題目需要去重,同時兩個長度為 \(|t|\) 的 \(s\) 的不同子串一定被不同的狀態表示,所以計算一個位置貢獻后打上標記,后面再遇到這個位置就不算貢獻了,每次查詢后撤銷標記即可(可以用 vector 記錄打上標記的位置)。
時間復雜度為 \(\mathcal{O}(|s||\Sigma|+\sum|t|)\),其中 \(\Sigma\) 為字符集。
CF235C 代碼
/*
Powered by C++11.
Author : Alex_Wei.
*/
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define pb emplace_back
const int N=2e6+5;
const int S=26;
int las,cnt;
int son[N][S],len[N],fa[N],ed[N];
int buc[N],id[N],vis[N];
void ins(char s){
int p=las,cur=++cnt,it=s-'a';
len[cur]=len[p]+1,ed[cur]++,las=cur;
while(p&&!son[p][it])son[p][it]=cur,p=fa[p];
if(!p)return fa[cur]=1,void();
int q=son[p][it];
if(len[p]+1==len[q])return fa[cur]=q,void();
int cl=++cnt;
fa[cl]=fa[q],fa[q]=fa[cur]=cl,len[cl]=len[p]+1;
memcpy(son[cl],son[q],sizeof(son[q]));
while(son[p][it]==q)son[p][it]=cl,p=fa[p];
} void build(char *s){
int n=strlen(s+1); las=cnt=1;
for(int i=1;i<=n;i++)ins(s[i]);
for(int i=1;i<=cnt;i++)buc[len[i]]++;
for(int i=1;i<=cnt;i++)buc[i]+=buc[i-1];
for(int i=cnt;i;i--)id[buc[len[i]]--]=i;
for(int i=cnt;i;i--)ed[fa[id[i]]]+=ed[id[i]];
}
int n;
char s[N];
int main(){
scanf("%s%d",s+1,&n),build(s);
for(int i=1;i<=n;i++){
scanf("%s",s+1);
ll p=1,l=strlen(s+1),d=0,ans=0;
vector <int> del;
for(int i=1;i<l*2;i++){
int it=s[i>l?i-l:i]-'a';
while(p&&!son[p][it])p=fa[p],d=len[p];
if(p){
p=son[p][it],d++;
while(d>l)if((--d)<=len[fa[p]])p=fa[p];
if(d>=l&&!vis[p])ans+=ed[p],vis[p]=1,del.pb(p);
} else p=1;
} cout<<ans<<endl;
for(int it:del)vis[it]=0;
}
return 0;
}
XIII. CF1073G Yet Another LCP Problem
見 CF1073G 題解。
怎么混進來一道 SA。
XIV. CF802I Fake News (hard)
題意簡述:給出 \(s\),求所有 \(s\) 的子串 \(p\) 在 \(s\) 中的出現次數平方和,重復的子串只算一次。
這是什么板子題?
對 \(s\) 建出 SAM 可以自動去重,考慮每個狀態 \(p\),它所表示的字串個數為 \(len(p)-len(link(p))\),出現次數為 \(p\) 在 \(link\) 樹上的子樹所包含的終止節點個數(終止節點是 \(s\) 所有前綴在 SAM 上表示的狀態),記為 \(ed_p\)。那么答案為 \(\sum_{i=1}^{cnt} ed^2_p\times (len(p)-len(link(p)))\)。
時間復雜度線性。
CF802I 代碼
/*
Powered by C++11.
Author : Alex_Wei.
*/
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define mem(x,v) memset(x,v,sizeof(x))
const int N=2e5+5;
const int S=26;
int cnt,las,son[N][S],ed[N],fa[N],len[N],buc[N],id[N];
void clear(){
mem(son,0),mem(ed,0),mem(fa,0),mem(len,0),mem(buc,0);
cnt=las=1;
} void ins(char s){
int p=las,cur=++cnt,it=s-'a';
len[cur]=len[p]+1,las=cur,ed[cur]=1;
while(!son[p][it])son[p][it]=cur,p=fa[p];
if(!p)return fa[cur]=1,void();
int q=son[p][it];
if(len[p]+1==len[q])return fa[cur]=q,void();
int cl=++cnt;
fa[cl]=fa[q],fa[q]=fa[cur]=cl,len[cl]=len[p]+1;
memcpy(son[cl],son[q],sizeof(son[q]));
while(son[p][it]==q)son[p][it]=cl,p=fa[p];
} ll build(char *s){
int n=strlen(s+1); clear();
for(int i=1;i<=n;i++)ins(s[i]);
for(int i=1;i<=cnt;i++)buc[len[i]]++;
for(int i=1;i<=n;i++)buc[i]+=buc[i-1];
for(int i=cnt;i;i--)id[buc[len[i]]--]=i;
for(int i=cnt;i;i--)ed[fa[id[i]]]+=ed[id[i]];
ll ans=0;
for(int i=1;i<=cnt;i++)ans+=1ll*ed[i]*ed[i]*(len[i]-len[fa[i]]);
return ans;
}
int n;
char s[N];
int main(){
cin>>n;
for(int i=1;i<=n;i++)scanf("%s",s+1),cout<<build(s)<<endl;
return 0;
}
XV. CF123D String
題意簡述:給出 \(s\),求所有 \(s\) 的子串 \(p\) 在 \(s\) 中的出現位置的所有子串個數,字符串的重復子串只算一次。
這是什么板子題?
對 \(s\) 建出 SAM 可以自動去重,考慮每個狀態 \(p\),它所表示的字串個數為 \(len(p)-len(link(p))\),出現次數為 \(p\) 在 \(link\) 樹上的子樹所包含的終止節點個數(終止節點是 \(s\) 所有前綴在 SAM 上表示的狀態),記為 \(ed_p\)。那么答案為 \(\sum_{i=1}^{cnt} \frac{ed_p(ed_p+1)}{2}\times (len(p)-len(link(p)))\)。
時間復雜度線性。
CF123D 代碼
/*
Powered by C++11.
Author : Alex_Wei.
*/
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define mem(x,v) memset(x,v,sizeof(x))
const int N=2e5+5;
const int S=26;
int cnt,las,son[N][S],ed[N],fa[N],len[N],buc[N],id[N];
void clear(){
mem(son,0),mem(ed,0),mem(fa,0),mem(len,0),mem(buc,0);
cnt=las=1;
} void ins(char s){
int p=las,cur=++cnt,it=s-'a';
len[cur]=len[p]+1,las=cur,ed[cur]=1;
while(!son[p][it])son[p][it]=cur,p=fa[p];
if(!p)return fa[cur]=1,void();
int q=son[p][it];
if(len[p]+1==len[q])return fa[cur]=q,void();
int cl=++cnt;
fa[cl]=fa[q],fa[q]=fa[cur]=cl,len[cl]=len[p]+1;
memcpy(son[cl],son[q],sizeof(son[q]));
while(son[p][it]==q)son[p][it]=cl,p=fa[p];
} ll build(char *s){
int n=strlen(s+1); clear();
for(int i=1;i<=n;i++)ins(s[i]);
for(int i=1;i<=cnt;i++)buc[len[i]]++;
for(int i=1;i<=n;i++)buc[i]+=buc[i-1];
for(int i=cnt;i;i--)id[buc[len[i]]--]=i;
for(int i=cnt;i;i--)ed[fa[id[i]]]+=ed[id[i]];
ll ans=0;
for(int i=1;i<=cnt;i++)ans+=1ll*ed[i]*(ed[i]+1)/2*(len[i]-len[fa[i]]);
return ans;
}
int n;
char s[N];
int main(){
scanf("%s",s+1),cout<<build(s)<<endl;
return 0;
}
*XVI. P4384 [八省聯考2018]制胡竄
題意簡述:給出字符串 \(s\),多次詢問給出 \(l,r\),求有多少對 \((i,j)\ (1\leq i<j\leq n,i+1<j)\) 使得 \(s_{1,i},s_{i+1,j-1},s_{j,n}\) 中至少出現一次 \(s_{l,r}\)。
套路題大賞 & 阿巴細節題(五一勞動節當然要寫碼農題)。
約定:記 \(len=r-l+1\),\(t=s_{l,r}\),\(l_{1,2,\cdots,c},r_{1,2,\cdots,c}\) 為 \(t\) 在 \(s\) 中所有出現位置(\(l\) 開頭,\(r\) 結尾,有 \(l_i+len-1=r_i\))。
轉化題目所求數對 \((i,j)\):不難看出其等價於在 \(i,j-1\) 處切兩刀所得到的三個字符串中至少出現一次 \(t\)。正難則反,將答案寫成 \(\binom{n-1}{2}-ans\),其中 \(ans\) 表示切兩刀切不出 \(t\) 的方案數。
-
當有三個及三個以上互不相交的 \(t\) 時:顯然 \(ans=0\)。
-
當最左邊的 \(t\) 與最右邊的 \(t\) 相交(\(r_1+len>r_c\))時:
- 若第一刀切在 \(l_1\) 左邊,那么第二刀必須切在相交部分(\([l_c,r_1]\))中間,方案數為 \((l_1-1)(r_1-p_c)\)。
- 若第一刀切在 \(l_i\) 與 \(l_{i+1}\ (i<c)\) 間,那么第二刀必須切在 \(l_c\) 與 \(r_{i+1}\) 間,方案數為 \((l_{i+1}-l_i)(r_{i+1}-l_c)\)。
- 若第一刀切在相交部分中間,第二刀可以切在其右邊的任意一個位置,方案數為 \((n-r_1)+(n-r_1+1)+\cdots+(n-l_c-1)=\frac{(2n-r_1-l_c-1)(r_1-l_c)}{2}\)。
- 若第一刀切在相交部分右邊,則 \(s_{1,i}\) 必然包含 \(t\),舍去。
比較麻煩的是 part 2,因為枚舉每一個位置時間復雜度必然會爆炸。根據兩個字符串出現的開頭結尾的相對位置不變進行變形:
\[\begin{aligned}&\sum_{1\leq i<c}(l_{i+1}-l_i)(r_{i+1}-l_c)\\=&\sum_{1\leq i<c}(r_{i+1}-r_i)(r_{i+1}-l_c)\\=&\sum_{1\leq i<c}r^2_{i+1}-r_ir_{i+1}-(r_{i+1}-r_i)l_c\\=&-(r_c-r_1)l_c+\sum_{1\leq i<c}r^2_{i+1}-r_ir_{i+1}\end{aligned} \]因此我們只需在線段樹上維護 \(\sum r^2_i\) 和 \(\sum r_ir_{i+1}\) 即可。
-
當左邊的 \(t\) 與最右邊的 \(t\) 不相交時:設 \(m\) 為使 \(r_i+len\leq r_c\) 的最大的 \(i\)。
- 若第一刀切在 \(l_m\) 左邊,那么其右邊有兩個不相交的 \(t\),但只能切割其中一個,舍去。
- 若第一刀切在 \(l_m\) 與 \(r_1\) 間,發現不方便統計,繼續分類:設 \(lim\) 為使 \(l_i\leq r_1\) 的最大的 \(i\)。
- 若第一刀切在 \(l_i\) 與 \(l_{i+1}\ (m\leq i<lim)\) 間:類似上文推一推即可,方案數為 \(-(r_{lim}-r_m)l_c+\sum_{m\leq i<lim}r^2_{i+1}-r_ir_{i+1}\)。
- 若第一刀切在 \(l_{lim}\) 與 \(r_1\) 間,第二刀必須切在 \(l_c\) 與 \(r_{lim+1}\) 間(因為必須切掉第 \(lim+1\) 個 \(t\)),方案數為 \((r_1-l_{lim})(r_{lim+1}-l_2)\)。
- 若第一刀切在 \(r_1\) 右邊,不符合題意,舍去。
理論分析完畢,接下來是實現:首先對 \(s\) 建出 SAM;根據 trick 4 用線段樹合並維護 endpos 集合,以及區間 \(\min,\max,r^2_i,r_ir_{i+1}\);同時根據 trick 3 可以倍增跳到 \(t\) 所表示的區間。總時間復雜度 \(\mathcal{O}((n+q)\log n)\)。
碼完一遍過,可喜可賀。
P4384 代碼
/*
Author : Alex_Wei
Problem : P4384 [八省聯考2018]制胡竄
Powered by C++11
2021.4.26 20:22
*/
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N=2e5+5;
const int inf=1e9+7;
const int K=17;
const int S=10;
ll n,node;
char s[N];
int rt[N],ls[N<<6],rs[N<<6];
ll mi[N<<6],mx[N<<6],val[N<<6],sq[N<<6];
void push(int x){
mi[x]=min(mi[ls[x]],mi[rs[x]]);
mx[x]=max(mx[ls[x]],mx[rs[x]]);
val[x]=val[ls[x]]+val[rs[x]]+mx[ls[x]]*(mi[rs[x]]<inf?mi[rs[x]]:0);
sq[x]=sq[ls[x]]+sq[rs[x]];
} void modify(int l,int r,int p,int &x){
x=++node;
if(l==r)return mi[x]=mx[x]=l,sq[x]=1ll*l*l,void();
int m=l+r>>1;
if(p<=m)modify(l,m,p,ls[x]);
else modify(m+1,r,p,rs[x]);
push(x);
} int merge(int l,int r,int x,int y){
if(!x||!y)return x|y;
int z=++node,m=l+r>>1;
if(l==r)return mi[z]=mx[z]=l,sq[x]=1ll*l*l,z;
ls[z]=merge(l,m,ls[x],ls[y]),rs[z]=merge(m+1,r,rs[x],rs[y]);
return push(z),z;
}
struct data{
ll mi,mx,val,sq;
data operator + (data x){
return {min(mi,x.mi),max(mx,x.mx),val+x.val+mx*(x.mi<inf?x.mi:0),sq+x.sq};
}
};
data query(int l,int r,int ql,int qr,int x){
if(ql<=l&&r<=qr)return {mi[x],mx[x],val[x],sq[x]};
int m=l+r>>1;
if(ql<=m&&m<qr)return query(l,m,ql,qr,ls[x])+query(m+1,r,ql,qr,rs[x]);
if(ql<=m)return query(l,m,ql,qr,ls[x]);
return query(m+1,r,ql,qr,rs[x]);
}
int cnt,las;
int son[N][S],fa[N],len[N],ed[N];
int id[N],buc[N],anc[N][K];
void ins(char s){
int it=s-'0',cur=++cnt,p=las;
len[las=cur]=len[p]+1,ed[len[cur]]=cur;
modify(1,n,len[cur],rt[cur]);
while(!son[p][it])son[p][it]=cur,p=fa[p];
if(!p)return fa[cur]=1,void();
int q=son[p][it];
if(len[p]+1==len[q])return fa[cur]=q,void();
int cl=++cnt;
fa[cl]=fa[q],fa[q]=fa[cur]=cl,len[cl]=len[p]+1;
memcpy(son[cl],son[q],sizeof(son[q]));
while(p&&son[p][it]==q)son[p][it]=cl,p=fa[p];
} void build(char *s){
las=cnt=1;
for(int i=0;i<n;i++)ins(s[i]);
for(int i=0;i<K;i++)
for(int j=1;j<=cnt;j++)
if(i)anc[j][i]=anc[anc[j][i-1]][i-1];
else anc[j][i]=fa[j];
for(int i=1;i<=cnt;i++)buc[len[i]]++;
for(int i=1;i<=n;i++)buc[i]+=buc[i-1];
for(int i=cnt;i;i--)id[buc[len[i]]--]=i;
for(int i=cnt;i;i--)rt[fa[id[i]]]=merge(1,n,rt[fa[id[i]]],rt[id[i]]);
}
ll sum(ll a,ll b){return (a+b)*(b-a+1)/2;}
int q,l,r;
int main(){
memset(mi,0x3f,sizeof(mi));
scanf("%d%d%s",&n,&q,s),build(s);
while(q--){
scanf("%d%d",&l,&r);
int p=ed[r],ln=r-l+1;
for(int i=K-1;~i;i--)if(len[anc[p][i]]>=ln)p=anc[p][i];
data dt=query(1,n,1,n,rt[p]);
ll lp=dt.mi,l1=lp-ln+1,rp=dt.mx,l2=rp-ln+1;
ll ans=1ll*(n-1)*(n-2)/2;
if(lp>=l2){
ll cover=lp-l2+1;
ans-=(l1-1)*(cover-1);
ans-=(dt.sq-lp*lp)-dt.val-(rp-lp)*l2;
ans-=sum(n-lp,n-l2-1);
printf("%lld\n",ans);
continue;
}
data dm=query(1,n,1,rp-ln,rt[p]);
ll mp=dm.mx,lm=mp-ln+1;
if(lp+ln<=mp){
printf("%lld\n",ans);
continue;
}
data dr=query(1,n,mp,lp+ln-1,rt[p]);
ans-=(dr.sq-mp*mp)-dr.val-(dr.mx-mp)*l2;
ans-=(lp-(dr.mx-ln+1))*(query(1,n,dr.mx+1,n,rt[p]).mi-l2);
printf("%lld\n",ans);
}
return 0;
}
*XVII. (SA)P6095 [JSOI2015]串分割
顯然的貪心是讓最大位數最小,即 \(len=\lceil\frac{n}{k}\rceil\)。
同時答案滿足可二分性,那么我們破環成鏈,枚舉 \(len\) 個斷點並判斷是否可行。具體來說,假設當前匹配到 \(i\),若 \(s_{i,i+len-1}\) 不大於二分的答案,那么就匹配 \(len\) 位,否則匹配 \(len-1\) 位。若總匹配位數不小於 \(n\) 則可行。
正確性證明:若可匹配 \(len\) 位時匹配 \(len-1\) 位,則下一次最多匹配 \(len\) 位,這與首先匹配 \(len\) 位的下一次匹配的最壞情況(即匹配 \(len-1\) 為)相同(\((len-1)+len=len+(len-1)\))。得證。
P6095
#include <bits/stdc++.h>
using namespace std;
const int N=4e5+5;
char s[N];
int n,k,len;
int sa[N],rk[N<<1],ork[N<<1];
int buc[N],id[N],px[N];
bool cmp(int a,int b,int w){
return ork[a]==ork[b]&&ork[a+w]==ork[b+w];
}
void build(int n){
int m=128;
for(int i=1;i<=n;i++)buc[rk[i]=s[i]]++;
for(int i=1;i<=m;i++)buc[i]+=buc[i-1];
for(int i=n;i;i--)sa[buc[rk[i]]--]=i;
for(int w=1,p=0;w<=n;w<<=1,m=p,p=0){
for(int i=n;i>n-w;i--)id[++p]=i;
for(int i=1;i<=n;i++)if(sa[i]>w)id[++p]=sa[i]-w;
for(int i=0;i<=m;i++)buc[i]=0;
for(int i=1;i<=n;i++)buc[px[i]=rk[id[i]]]++;
for(int i=1;i<=m;i++)buc[i]+=buc[i-1];
for(int i=n;i;i--)sa[buc[px[i]]--]=id[i];
for(int i=1;i<=n;i++)ork[i]=rk[i]; p=0;
for(int i=1;i<=n;i++)rk[sa[i]]=cmp(sa[i],sa[i-1],w)?p:++p;
if(p==n)break;
}
}
bool check(int d){
for(int i=1;i<=len;i++){
int pos=i;
for(int j=1;j<=k;j++){
pos+=len-(rk[pos]>d);
if(pos>=i+n)return 1;
}
} return 0;
}
int main(){
scanf("%d%d%s",&n,&k,s+1);
for(int i=1;i<=n;i++)s[i+n]=s[i];
len=(n-1)/k+1,build(n<<1);
int l=1,r=n*2;
while(l<r){
int m=l+r>>1;
if(check(m))r=m;
else l=m+1;
} for(int i=sa[l];i<sa[l]+len;i++)cout<<s[i];
return 0;
}