1. 程式人生 > 資訊 >55 划算節狂促:羅蒙 Polo 衫 69 元 2 折清倉(67 款任選)

55 划算節狂促:羅蒙 Polo 衫 69 元 2 折清倉(67 款任選)

1. Idea

用來處理一些有關回文串的問題,可以用優秀的 \(O(n)\) 時間求出原字串串中任意一位為對稱中心的奇數長度最長迴文子串。

也正是因為只能求出奇數長度的最長迴文子串,所以我們可以在每兩位字元之間插入一個相同間隔符,這樣以間隔符為對稱中心的最長迴文子串在原串中長度就是偶數的了。還可以在原串的最前面額外新增一個其它間隔符,這樣就可以避免左右擴充套件求迴文子串時還要特判當前是否超出邊界。

先想想怎麼暴力求出以第 \(i\) 位為對稱中心的最長迴文子串:從 \(i\) 同時向左右擴充套件,若相同,則將回文子串長度加 \(1\) 後繼續擴充套件,否則停止。時間複雜度 \(O(n^2)\)

。於是考慮優化這個暴力。

考慮從左到右處理原串中的每一位,設當前處理到 \(s_i\),記以前 \(i-1\) 位字元為對稱中心的最長迴文子串右端點的最大值為 \(r\),取到這個最大值的對稱中心為 \(d\),以第 \(i\) 位為對稱中心的最長迴文半徑為 \(p_i\)

處理 \(s_i\) 時,若 \(r<i\),則直接從 \(s_i\) 暴力向左右擴充套件;若 \(r>i\),則說明 \(s_i\) 是右端點為 \(r\) 的以 \(d\) 為迴文中心的最長迴文子串的一部分。由於最長迴文子串迴文(廢話),所以以 \(d\) 為對稱中心的,\(d\) 左側距離 \(d\) 這個字元 \(i-d+1\)

個字元的答案是可以被 \(i\) 參考繼承的,由中點座標公式得,這個字元為 \(s_{2d-i}\)。為什麼說是參考呢?因為若以 \(s_{2d-i}\) 為對稱中心的最長迴文子串超出了以 \(d\) 為中心的最長迴文子串的範圍,那麼超出的部分與 \(d\) 的右側是不形成對稱關係的。故可以將 \(p_i\) 設為 \(p_{2d-i}\)\(r-i+1\) 的最小值,然後再繼續向左右暴力擴充套件。

擴充套件完畢後,若此時右端點大於 \(r\),則更新 \(r\)\(d\) 即可。

觀察上述過程我們可以發現,Manacher 演算法對於求最長迴文子串的優化在於,它在求後面的子串時也用到了前面已經求出的最長迴文子串輔助更新。
由於每次暴力左右擴充套件時,\(r\)

必然向右更新,所以均攤時間複雜度為 \(O(n)\)

2. Example

2.1 【模板】manacher 演算法

模板題,沒什麼好說的。

將每一位的最長迴文子串長度取最大值即可,注意剔除間隔符的長度。

核心程式碼超短。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define N 22000005
int n,p[N],ans;
char in[N],s[N];
ll read(){
	ll wh=0,fh=1;
	char c=getchar();
	while (c>'9'||c<'0'){
		if (c=='-') fh=-1;
		c=getchar();
	}
	while (c>='0'&&c<='9'){
		wh=(wh<<3)+(wh<<1)+(c^48);
		c=getchar();
	}
	return wh*fh;
} 

int main(){
	scanf("%s",in+1);
	s[0]='!',s[1]='|',n=strlen(in+1);
	for (int i=1;i<=n;i++) s[i<<1]=in[i],s[i<<1|1]='|';
	for (int i=1,r=0,d=0;i<=n*2;i++){
		p[i]=(r<i)?1:min(r-i+1,p[d*2-i]);
		while (s[i-p[i]]==s[i+p[i]]) ++p[i];
		if (i+p[i]-1>r) r=i+p[i]-1,d=i;
		ans=max(ans,p[i]-1);
	}
	printf("%d\n",ans);
	return 0;
}

2.2 P4555 [國家集訓隊]最長雙迴文串

這種原問題需要求一個,它卻讓加倍求兩個的問題,就可以考慮對於每一個位置從前到後求一個,再從後到左求一個,然後將兩個答案拼起來求最大值即可。(如求序列最大雙段子段和等)。

本題也是一樣,我們可以考慮分別求出以每一個位置為開頭和結尾的最長迴文子串長度,然後對於前後兩個位置將答案拼起來取最大值。

那麼怎麼求呢?我們拿求以每一個位置為結尾的最長迴文子串長度為例。

這個東西其實好求。考慮以 \(i+p_i-1\) 更新 \(r\) 時,以 \([r+1,i+p_i-1]\) 為結尾的最長迴文子串的對稱中心其實都為 \(i\)(仔細想想),故更新時順便列舉這個區間的位置更新答案即可。

求以每一個位置為開頭的最長迴文子串長度同理,倒著求即可。

時間複雜度 \(O(n)\)

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define N 200005
int n,a[N],b[N],m,p[N];
char s[N],in[N];

ll read(){
	ll wh=0,fh=1;
	char c=getchar();
	while (c>'9'||c<'0'){
		if (c=='-') fh=-1;
		c=getchar();
	}
	while (c>='0'&&c<='9'){
		wh=(wh<<3)+(wh<<1)+(c^48);
		c=getchar();
	}
	return wh*fh;
} 

int main(){
	scanf("%s",in+1);
	n=strlen(in+1);
	s[0]='!',s[++m]='|';
	for (int i=1;i<=n;i++) s[++m]=in[i],s[++m]='|';
	for (int i=1,r=0,d=0;i<=m;i++){
		p[i]=(r<i)?1:min(r-i+1,p[d*2-i]);
		while (s[i-p[i]]==s[i+p[i]]) ++p[i];
		if (i+p[i]-1>r){
			d=i;
			for (r=r+1;r<=i+p[i]-1;r++)
				if (s[r]!='|') a[r]=r-i+1;
			--r;
		}
	}
	for (int i=m,r=m+1,d=m+1;i>=1;i--){
		p[i]=(r>i)?1:min(i-r+1,p[d*2-i]);
		while (s[i-p[i]]==s[i+p[i]]) ++p[i];
		if (i-p[i]+1<r){
			d=i;
			for (r=r-1;r>=i-p[i]+1;r--)
				if (s[r]!='|') b[r]=i-r+1;
			++r;
		}
	}
	int ans=0;
	for (int i=1;i<=m-2;i++)
		ans=max(ans,a[i]+b[i+2]);
	printf("%d\n",ans);
	return 0;
}

2.3. P1659 [國家集訓隊]拉拉隊排練

當我們找到以每一個位置為對稱中心的最長迴文子串時,設該子串迴文半徑為 \(r\),則迴文半徑為 \([1,r-1]\) 的子串顯然也是迴文的。

所以我們可以開一個桶 \(s\),記 \(s_i\) 表示迴文半徑為 \(i\) 的迴文子串的個數,初始時跑一遍 Manacher ,將以每一個位置為對稱中心的最長迴文子串放入桶中,然後從大到小掃描整個桶,若 \(s_i\) 有值,則將這些子串的答案統計到答案中,又由於這些子串的存在肯定意味著與他們個數相同的半徑為 \(i-1\) 的迴文子串也對應存在,故將這些個數再加入 \(s_{i-1}\) 中。直到當前統計了 \(k\) 個子串為止。

統計答案時若選擇了 \(y\) 個長度為 \(x\) 的迴文子串,則將答案乘上 \(x^y\),用快速冪處理即可。

注意由於題中限定了迴文子串長度必須為奇數,所以就不需要加入分隔符了。

那麼為什麼這個做法可以在不找出所有迴文子串的情況下還能找出前 \(k\) 長的迴文子串呢?其實就是利用了部分迴文子串之間的有序性,即對於一個對稱中心 \(i\),若存在迴文半徑為 \(l\) 的迴文子串,則一定存在長度為 \(l-1\) 的迴文子串。所以對於每一個對稱中心,其實就相當於已經開了一個隱式的堆。這個思想在 蚯蚓 那道題中可以深刻體現。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define N 2000005
const ll mod=19930726;
ll n,k,m,p[N],ans[N],anss=1,maxn;
char s[N];
ll read(){
	ll wh=0,fh=1;
	char c=getchar();
	while (c>'9'||c<'0'){
		if (c=='-') fh=-1;
		c=getchar();
	}
	while (c>='0'&&c<='9'){
		wh=(wh<<3)+(wh<<1)+(c^48);
		c=getchar();
	}
	return wh*fh;
} 
ll ksm(ll x,ll y){
	ll ans=1,now=x;
	while (y){
		if (y&1) ans=ans*now%mod;
		now=now*now%mod;
		y>>=1;
	}
	return ans;
}
int main(){
	n=read(),k=read();
	scanf("%s",s+1);
	s[0]='!';
	for (ll i=1,r=0,d=0;i<=n;i++){
		p[i]=(r<i)?1:min(p[d*2-i],r-i+1);
		while (s[i+p[i]]==s[i-p[i]]) ++p[i];
		if (i+p[i]-1>r) r=i+p[i]-1,d=i;
		++ans[p[i]];
		maxn=max(maxn,p[i]);
	}
	for (ll i=maxn;i>=1;i--){
		if (k>ans[i]) anss=(anss*ksm(i*2-1,ans[i]))%mod,ans[i-1]+=ans[i],k-=ans[i];
		else{
			anss=(anss*ksm(i*2-1,k))%mod;
			break;
		}
	}
	printf("%lld\n",anss);
	return 0;
}

2.4 P5446 [THUPC2018]綠綠和串串

考慮對於一個字串,不難想到若存在某一個迴文子串 \([l,r]\),對稱中心為 \(d\),其中 \(r\) 為字串最右端,那麼將 \([1,d]\) 按照題目要求翻折過後,前 \(r\) 項一定為原字串。

那麼再來想,如果已知 \([1,d]\) 翻折過後是原字串,那麼如果存在一個迴文子串 \([l',r']\),對稱中心為 \(d'\),其中 \(r'\)\(d\),那麼如果將 \([1,d']\) 按照題目要求翻轉,前 \(r\) 項一定也是原字串……嗎?有點問題,因為這樣翻折我們只能保證前 \(r'\) 項和原串相同,而區間 \((r',r]\) 可能會因為和 \([1,l')\) 不對稱而導致翻折後“串不對版”。由此還需要加一個限制條件,就是我們找到的二次迴文子串,也就是右端點不為 \(r\) 的迴文子串,左端點必須為 \(1\),這樣才能保證翻折之後 \([1,l')\) 為空。

所以先跑一遍 Manacher,然後從右到左判斷每個對稱中心的迴文子串是否滿足條件,滿足條件後將子串左端點標記以便於前面的子串進行判斷。記錄答案後從左到右輸出即可。

時間複雜度 \(O(\sum |S|)\)

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define N 1000005
int n,t,p[N],jl[N],jtot,ans[N],atot;
bool vis[N];
char s[N];
ll read(){
	ll wh=0,fh=1;
	char c=getchar();
	while (c>'9'||c<'0'){
		if (c=='-') fh=-1;
		c=getchar();
	}
	while (c>='0'&&c<='9'){
		wh=(wh<<3)+(wh<<1)+(c^48);
		c=getchar();
	}
	return wh*fh;
} 

int main(){
	t=read();
	while (t--){
		scanf("%s",s+1);
		n=strlen(s+1);
		s[0]='!';
		vis[n]=1,atot=jtot=0;
		for (int i=1,r=0,d=0;i<=n;i++){
			p[i]=(r<i)?1:min(r-i+1,p[d*2-i]);
			while (s[i-p[i]]==s[i+p[i]]) ++p[i];
			if (i+p[i]-1>r) r=i+p[i]-1,d=i;
		}
		for (int i=n;i>=1;i--){
			if (vis[i+p[i]-1]&&(i==p[i]||i+p[i]-1==n)) ans[++atot]=i,vis[i]=1,jl[++jtot]=i;
		}
		for (int i=atot;i>=1;i--) printf("%d ",ans[i]);
		puts("");
		for (int i=1;i<=jtot;i++) vis[jl[i]]=0;
	}
	return 0;
}