SAM 做題筆記(各種技巧,持續更新,SA)


SAM 感性瞎扯

這里是 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}\) 集合大小。那么答案為

\[\max_i \mathrm{len}(i)\times |\mathrm{endpos(longest}(i))| \]

代碼奉上,手捏的 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 計算,即

\[d_p=1+\sum_{(p,q)\in\mathrm{SAM}}d_q \]

如果 \(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;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM