1. 程式人生 > 其它 >Codeforces 1063F - String Journey(字尾陣列+線段樹+dp)

Codeforces 1063F - String Journey(字尾陣列+線段樹+dp)

字尾陣列+線段樹+dp,神仙題

Codeforces 題面傳送門 & 洛谷題面傳送門

神仙題,做了我整整 2.5h,寫篇題解紀念下逝去的中午

後排膜拜 1 年前就獨立切掉此題的 ymx,我在 2021 年的第 5270 個小時 A 掉此題,而 ymx 在 2020 年的第 5270 就已經 A 掉了此題 %%%%%%

首先注意到一件事情,就是如果存在一個長度為 \(k\) 的 Journey,那麼必然存在一個長度為 \(k\) 的 Journey,滿足相鄰兩個字串長度的差剛好為 \(1\)(方便起見,在後文中我們及其為 Condition),具體構造就是,如果存在一個字串 \(s_k\) 滿足 \(|s_{k}|-|s_{k+1}|\ge 2\)

,那麼我們就找到 \(s_{k+1}\)\(s_k\) 中出現的位置,將 \(s_{k}\) 除了 \(s_{k+1}\) 之外的其他字元全部去掉,如果 \(s_{k+1}\)\(s_k\) 出現的位置左邊還有字元就令 \(s_k\) 往左延伸一格,否則 \(s_k\) 往右延伸一格,如此進行下去直到不存在這樣的 \(k\) 即可,不難證明這樣得到的還是一個長度為 \(k\) 的 Journey。

仿照 CF700E Cool Slogans 的套路,我們可以設計出一個 \(dp\)\(dp_{i}\) 表示 \(s[i...n]\) 的字尾中,第一個字串的開頭位置為 \(i\),並且長度最長的滿足 Condition

的 Journey 的長度是多少。顯然,根據前面的分析,我們只關心滿足 \(|s_k|-|s_{k+1}|=1\) 的 Journey,因此滿足條件的 Journey 的第一個字串必然是 \(s[i...i+dp_i-1]\)。而且我們還可以發現,如果存在一個滿足 Condition 的 Journey 第一個字串是 \(s[l...r](r-l\ge 1)\),那麼必然存在一個滿足 Condition 的 Journey 第一個字串是 \(s[l...r-1]\),具體證明大概也是掐頭去尾,讀者有興趣自己不妨去證證(?)。因此轉移大概就列舉下一個字串的開頭 \(j<i\),分兩種情況:

  • 下一個字元加在結尾,我們假設下一個字串為 \(s[j...r]\),那麼 \(s[j+1...r]\) 應與上一個字串,也就是以 \(i\) 開頭的某段長度 \(\le dp_i\) 的子串相同,那麼這前面一段顯然最長長不過 \(\min(\text{LCP}(s[i...n],s[j...n]),dp_i)\),用這種情況轉移得到下一個字串的長度也不能超過 \(\min(\text{LCP}(s[i...n],s[j...n]),dp_i)+1\),又因為子串不能相交,所以下一個字串的結束位置必須 \(<i\),即長度不能超過 \(i-j\),因此我們有 \(dp_j\leftarrow\min(\min(\text{LCP}(s[i...n],s[j...n]),dp_i)+1,i-j)\)
  • 下一個字元加在開頭,類似地,後面一段應與上一個字串相同,仿照上面的推理過程我們也可以得到 \(dp_j\leftarrow\min(\min(\text{LCP}(s[i...n],s[j+1...n]),dp_i)+1,i-j)\)

\(\text{LCP}\) 可以後綴陣列預處理,暴力轉移是 \(\mathcal O(n)\) 的,因此這個做法複雜度 \(\mathcal O(n^2)\)

附:\(\mathcal O(n^2)\) \(\text{TLE}\) 的程式碼:

const int MAXN=5e5;
const int LOG_N=19;
int n;char s[MAXN+5];pii x[MAXN+5];
int sa[MAXN+5],rk[MAXN+5],ht[MAXN+5],seq[MAXN+5],buc[MAXN+5];
void getsa(){
	int vmax=122,gr=0;
	for(int i=1;i<=n;i++) buc[s[i]]++;
	for(int i=1;i<=vmax;i++) buc[i]+=buc[i-1];
	for(int i=n;i;i--) sa[buc[s[i]]--]=i;
	for(int i=1;i<=n;i++){
		if(s[sa[i]]!=s[sa[i-1]]) ++gr;
		rk[sa[i]]=gr;
	} vmax=gr;
	for(int k=1;k<=n;k<<=1){
		for(int i=1;i<=n;i++){
			if(i+k<=n) x[i]=mp(rk[i],rk[i+k]);
			else x[i]=mp(rk[i],0);
		} memset(buc,0,sizeof(buc));int num=0;gr=0;
		for(int i=n-k+1;i<=n;i++) seq[++num]=i;
		for(int i=1;i<=n;i++) if(sa[i]>k) seq[++num]=sa[i]-k;
		for(int i=1;i<=n;i++) buc[x[i].fi]++;
		for(int i=1;i<=vmax;i++) buc[i]+=buc[i-1];
		for(int i=n;i;i--) sa[buc[x[seq[i]].fi]--]=seq[i];
		for(int i=1;i<=n;i++){
			if(x[sa[i]]!=x[sa[i-1]]) ++gr;
			rk[sa[i]]=gr;
		} vmax=gr;if(vmax==n) break;
	}
}
void getht(){
	int k=0;
	for(int i=1;i<=n;i++){
		if(rk[i]==1) continue;if(k) --k;int j=sa[rk[i]-1];
		while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k]) ++k;
		ht[rk[i]]=k;
	}
}
int st[MAXN+5][LOG_N+2];
void buildst(){
	for(int i=1;i<=n;i++) st[i][0]=ht[i];
	for(int i=1;i<=LOG_N;i++) for(int j=1;j+(1<<i)-1<=n;j++)
		st[j][i]=min(st[j][i-1],st[j+(1<<i-1)][i-1]);
}
int queryst(int l,int r){
	int k=31-__builtin_clz(r-l+1);
	return min(st[l][k],st[r-(1<<k)+1][k]);
}
int getlcp(int x,int y){
	if(x==y) return n-x+1;
	x=rk[x];y=rk[y];if(x>y) swap(x,y);
	return queryst(x+1,y);
}
int dp[MAXN+5];
int main(){
	scanf("%d%s",&n,s+1);getsa();getht();buildst();int res=0;
	for(int i=1;i<=n;i++) dp[i]=1;
	for(int i=n;i;i--){
		for(int j=1;j<i;j++){
			chkmax(dp[j],min(getlcp(i,j)+1,min(i-j,dp[i]+1)));
			if(j+1!=i) chkmax(dp[j],min(min(getlcp(i,j+1),dp[i])+1,i-j));
		} chkmax(res,dp[i]);
//		printf("%d %d\n",i,dp[i]);
	} printf("%d\n",res);
	return 0;
}

接下來考慮優化,首先看到這樣的限制你可以想到一些奇奇怪怪的方式進行優化,包括但不限於笛卡爾樹、單調棧、樹套樹等,但都沒有用(bushi,ymx 去年笛卡爾樹切掉了這道題)。因此我們不妨換個角度,\(dp\) 的單調性入手解決這個問題,不難發現一個性質,就是 \(dp_{i}\le dp_{i+1}+1\),具體證明大概就如果存在一個滿足 Condition 的 Journey,其第一個字串是 \(s[l...r](r-l\ge 1)\),那麼必然也存在一個滿足 Condition 的 Journey 第一個字串是 \(s[l...r-1]\),這個仿照前面 \(s[l+1...r]\) 的證明來就行了,因此如果 \(dp_i\ge dp_{i+1}+2\),那麼必然存在一個滿足 Condition 的 Journey,第一個字串是 \(s[i...i+dp_{i+1}+1]\),也存在滿足 Condition 且第一個字串是 \(s[i+1...i+dp_{i+1}+1]\) 的 Journey,而該字串長度為 \(dp_{i+1}+1\),與我們 \(dp\) 的定義不符,因此不會出現這樣的情況(當然你如果只看上面 \(dp\) 轉移方程也可以看出這條性質)

注意到這個柿子長得有點像咱們求 \(ht\) 時用到的性質,因此考慮仿照求 \(ht\) 的方法——雙針+合法性判定求解 \(dp\) 陣列。我們考慮先令 \(dp_i=dp_{i+1}+1\),然後不斷將 \(dp_i\) 減一直到存在一個滿足 Condition 的 Journey,其第一個字串是 \(s[i...i+dp_i-1]\),這樣問題可以轉化為一個判定性問題。那麼怎麼判定一個 \(dp_i\) 是否可行呢?顯然我們只需要知道是否有一個 \(j\) 滿足 \(\min(\min(\text{LCP}(s[i...n],s[j...n]),dp_j)+1,j-i)\ge dp_i\)\(\min(\min(\text{LCP}(s[i+1...n],s[j...n]),dp_j)+1,j-i)\ge dp_i\) 即可,顯然兩部分是對稱的,這裡考慮第一部分,由於我們取的是最小值,所以三個括號裡的東西都要 \(\ge dp_i\)\(j-i\ge dp_i\) 意味著 \(j\ge i+dp_i\)\(\text{LCP}(s[i...n],s[j...n])+1\ge dp_i\) 意味著 \(j\) 在字典序陣列上是一段區間,這段區間顯然可以二分+ST 表求出,\(dp_j+1\ge dp_i\) 這個條件無法直接處理,不過既然是判定性問題,我們就求一下滿足前兩個條件的 \(j\)\(dp_j\) 的最大值即可。這個最大值乍一看,兩個條件,需要求 \(\max\) 的主席樹,雖然複雜度依舊是 1log,但空間過大,考慮更簡便的實現,不難發現在我們從右往左 DP 的過程中,\(i+dp_i-1\) 始終是不降的,因此我們肯定只有插入新元素,沒有刪除操作,因此如果我們在 \(i+dp_i-1\)\(i+dp_i\) 之間畫一條 dividing line,那麼這個 dividing line 肯定是不斷左移的,因此我們以字典序陣列上的下標為下標開一棵線段樹,維護 dividing line 右邊的元素,每次 dividing line 往左移一格就將新加入的元素加入線段樹,查詢就直接線上段樹對應區間查詢即可。

時間複雜度嚴格 \(\mathcal O(n\log n)\)

const int MAXN=5e5;
const int LOG_N=19;
int n;char str[MAXN+5];pii x[MAXN+5];
int sa[MAXN+5],rk[MAXN+5],ht[MAXN+5],seq[MAXN+5],buc[MAXN+5];
void getsa(){
	int vmax=122,gr=0;
	for(int i=1;i<=n;i++) buc[str[i]]++;
	for(int i=1;i<=vmax;i++) buc[i]+=buc[i-1];
	for(int i=n;i;i--) sa[buc[str[i]]--]=i;
	for(int i=1;i<=n;i++){
		if(str[sa[i]]!=str[sa[i-1]]) ++gr;
		rk[sa[i]]=gr;
	} vmax=gr;
	for(int k=1;k<=n;k<<=1){
		for(int i=1;i<=n;i++){
			if(i+k<=n) x[i]=mp(rk[i],rk[i+k]);
			else x[i]=mp(rk[i],0);
		} memset(buc,0,sizeof(buc));int num=0;gr=0;
		for(int i=n-k+1;i<=n;i++) seq[++num]=i;
		for(int i=1;i<=n;i++) if(sa[i]>k) seq[++num]=sa[i]-k;
		for(int i=1;i<=n;i++) buc[x[i].fi]++;
		for(int i=1;i<=vmax;i++) buc[i]+=buc[i-1];
		for(int i=n;i;i--) sa[buc[x[seq[i]].fi]--]=seq[i];
		for(int i=1;i<=n;i++){
			if(x[sa[i]]!=x[sa[i-1]]) ++gr;
			rk[sa[i]]=gr;
		} vmax=gr;if(vmax==n) break;
	}
}
void getht(){
	int k=0;
	for(int i=1;i<=n;i++){
		if(rk[i]==1) continue;if(k) --k;int j=sa[rk[i]-1];
		while(i+k<=n&&j+k<=n&&str[i+k]==str[j+k]) ++k;
		ht[rk[i]]=k;
	}
}
int st[MAXN+5][LOG_N+2];
void buildst(){
	for(int i=1;i<=n;i++) st[i][0]=ht[i];
	for(int i=1;i<=LOG_N;i++) for(int j=1;j+(1<<i)-1<=n;j++)
		st[j][i]=min(st[j][i-1],st[j+(1<<i-1)][i-1]);
}
int queryst(int l,int r){
	int k=31-__builtin_clz(r-l+1);
	return min(st[l][k],st[r-(1<<k)+1][k]);
}
int getlcp(int x,int y){
	if(x==y) return n-x+1;
	x=rk[x];y=rk[y];if(x>y) swap(x,y);
	return queryst(x+1,y);
}
int dp[MAXN+5];
struct node{int l,r,mx;} s[MAXN*4+5];
void build(int k,int l,int r){
	s[k].l=l;s[k].r=r;if(l==r) return;int mid=l+r>>1;
	build(k<<1,l,mid);build(k<<1|1,mid+1,r);
}
void modify(int k,int x,int v){
	if(s[k].l==s[k].r) return s[k].mx=v,void();
	int mid=s[k].l+s[k].r>>1;
	if(x<=mid) modify(k<<1,x,v);
	else modify(k<<1|1,x,v);
	s[k].mx=max(s[k<<1].mx,s[k<<1|1].mx);
}
int query(int k,int l,int r){
	if(l<=s[k].l&&s[k].r<=r) return s[k].mx;
	int mid=s[k].l+s[k].r>>1;
	if(r<=mid) return query(k<<1,l,r);
	else if(l>mid) return query(k<<1|1,l,r);
	else return max(query(k<<1,l,mid),query(k<<1|1,mid+1,r));
}
pii get_itvl(int x,int len){
	x=rk[x];int L=1,R=x-1,l=x,r=x;
	while(L<=R){
		int mid=L+R>>1;
		if(queryst(mid+1,x)>=len) l=mid,R=mid-1;
		else L=mid+1;
	} L=x+1,R=n;
	while(L<=R){
		int mid=L+R>>1;
		if(queryst(x+1,mid)>=len) r=mid,L=mid+1;
		else R=mid-1;
	} return mp(l,r);
}
bool check(int x,int v){
	if(v==1) return 1;
	pii p=get_itvl(x,v-1);
//	printf("itvl %d %d\n",p.fi,p.se);
	if(query(1,p.fi,p.se)+1>=v) return 1;
	p=get_itvl(x+1,v-1);
//	printf("itvl %d %d\n",p.fi,p.se);
//	printf("%d\n",query(1,p.fi,p.se));
	if(query(1,p.fi,p.se)+1>=v) return 1;
	return 0;
}
int main(){
	scanf("%d%s",&n,str+1);getsa();getht();buildst();
//	for(int i=1;i<=n;i++) printf("%d%c",sa[i]," \n"[i==n]);
	int res=0;build(1,1,n);
	for(int i=n;i;i--){
//		for(int j=1;j<i;j++){
//			chkmax(dp[j],min(min(getlcp(i,j),dp[i])+1,i-j));
//			if(j+1!=i) chkmax(dp[j],min(min(getlcp(i,j+1),dp[i])+1,i-j));
//		}
		dp[i]=dp[i+1]+1;
		while(!check(i,dp[i])){
			modify(1,rk[i+dp[i]-1],dp[i+dp[i]-1]);
			dp[i]--;
		}
		chkmax(res,dp[i]);
//		printf("%d %d\n",i,dp[i]);
	} printf("%d\n",res);
	return 0;
}