1. 程式人生 > 其它 >Atcoder Regular Contest 058 D - 文字列大好きいろはちゃん / Iroha Loves Strings(單調棧+Z 函式)

Atcoder Regular Contest 058 D - 文字列大好きいろはちゃん / Iroha Loves Strings(單調棧+Z 函式)

單調棧+Z 函式,神仙題,ycxyyds!

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

神仙題。

mol 一發現場(bushi)獨立切掉此題的 ycx %%%%%%%

首先咱們可以想到一個非常 naive 的 DP,\(dp_{i,j}\) 表示在前 \(i\) 個字串拼出的長度為 \(j\) 的字串中,字典序的最小的串是什麼,那麼顯然 \(dp_{i,j}\) 的轉移就在 \(dp_{i-1,j-|s_i|}+s_i\)\(dp_{i-1,j}\) 中比個大小即可,但是由於字串字典序比大小,以及儲存字串均可達到線性複雜度,因此該做法時空複雜度均為 \(nk^2\),一臉過不去的亞子。

不過注意到本題的一個性質,就是本題的 \(dp\)

採用的是字典序大小比較,考慮字典序比較的一個特性,就是對於某兩個字串的兩個字首 \(s,t(|s|<|t|)\),如果 \(s,t\) 之間不存在字首關係,那麼不論再往 \(s,t\) 後面新增什麼字元,這兩個字串的字典序已經定下來了。更進一步的,對於本題而言,對於兩個 \(dp_{i,j}\)\(dp_{i,k}\),如果它們都能轉移到 \(dp_{n,m}\)(即,在後 \(n-i\) 個字串中,存在某個字串的子集使其長度之和為 \(m-j\),也存在某個字串的子集使其長度之和為 \(m-k\),這個可以通過一遍反著的揹包預處理出來,處理完之後對於不能轉移到 \(dp_{n,m}\)
\(dp_{i,j}\) 我們就直接跳過即可),如果 \(dp_{i,j}\)\(dp_{i,k}\) 不存在前後綴關係,那麼前 \(\min(j,k)\) 位字典序較大的字串已經沒有用了,因此我們考慮在求解 \(dp_{i,j}\) 時維護一個單調棧儲存當前有用的位置集合,並且從棧頂到棧底下標依次遞減,那麼根據之前的分析,這些有用的位置的集合的 DP 值必然兩兩之間存在前後綴關係,也就是說,假設棧頂元素為 \(s\),那麼對於棧中的某個元素 \(t\)一定有 \(dp_{i,t}\)\(dp_{i,s}\) 長度為 \(t\) 的字首,因此對於每一個 \(i\),我們並不用記錄所有這樣的 \(dp_{i,j}\)
,我們只用記錄棧頂元素的 \(dp\)\(fs_i\) 即可,這樣對於一個 \(dp_{i,j}\) 有兩種來源:

  • \(dp_{i-1,j-|s_i|}+s_i\),此時我們可以檢驗 \(dp_{i-1,j-|s_i|}\) 是否是有用的決策點,如果是那麼我們就用 \(fs_{i-1}\) 長度為 \(j-|s_i|\) 的字首與 \(s_i\) 拼接得到的字串去更新 \(dp_{i,j}\)
  • \(dp_{i-1,j}\),類似於上面的情況。

那麼怎麼判斷一個字串是否是有用的決策點呢?我們考慮額外維護一個數組 \(st_{i,j}\) 表示 \(dp_{i,j}\) 的狀態,如果 \(st_{i,j}=0\) 表示 \(dp_{i,j}\) 為無用決策點,如果 \(st_{i,j}=2\) 表示 \(dp_{i,j}\)\(dp_{i-1,j-|s_i|}+s_i\) 比從 \(dp_{i-1,j}\) 轉移更優,如果 \(st_{i,j}=1\) 表示 \(dp_{i,j}\)\(dp_{i-1,j}\) 比從 \(dp_{i-1,j-|s_i|}+s_i\) 轉移更優,不難發現根據 \(fs_{i-1}\)\(st_{i,j}\) 可以求出 \(dp_{i,j}\),這樣空間就被我們成功地搞到了 \(\mathcal O(nm)\)

還有一個問題就是怎樣維護這樣一個單調棧,假設我們已經求出了 \(dp_{i,j}\),那麼我們考慮棧頂元素 \(dp_{i,s}\),分三種情況考慮:

  • 如果 \(dp_{i,s}\)\(dp_{i,j}\) 的字首,那麼 \(dp_{i,s}\)\(dp_{i,j}\) 都是有用的決策點,因此此時我們可以直接 break 掉並將 \(j\) 壓入單調棧。
  • 如果 \(dp_{i,j}\)\(s\) 位的字典序小於 \(dp_{i,s}\),那麼 \(dp_{i,s}\) 就沒用了,將 \(st_{i,s}\) 設為 \(0\) 並不斷彈出棧頂元素直至棧為空或不存在該情況為止。
  • 如果 \(dp_{i,j}\)\(s\) 位的字典序大於 \(dp_{i,s}\),那麼 \(dp_{i,j}\) 就沒用了,直接退出並不將 \(j\) 壓入棧中

直接暴力進行字典序比較是 \(\mathcal O(nm^2)\) 的,還是無法通過此題,不過還是根據之前的性質:根據 \(fs_{i-1}\)\(st_{i,j}\) 可以求出 \(dp_{i,j}\)。因此我們考慮能不能直接根據 \(st_{i,s}\)\(fs_{i-1}\),進行一些預處理 \(\mathcal O(1)\) 判斷字典序大小呢?

答案是肯定的。

我們假設要對 \(dp_{i,j}\)\(dp_{i,s}\) 比大小 \((j>s)\),那麼分情況討論:

  • \(st_{i,j}=st_{i,s}=1\),那麼顯然它們都是 \(fs_{i-1}\) 的字首,顯然互為字首關係
  • \(st_{i,j}=1,st_{i,s}=2\),那麼 \(dp_{i,j}\)\(fs_{i-1}\) 長度為 \(j\) 的字首,\(dp_{i,s}\)\(fs_{i-1}\) 長度為 \(s-|s_i|\) 的字首拼上 \(s_i\),因此我們只需檢驗 \(s_i\)\(fs_{i-1}[s-|s_i|+1...j]\) 的大小關係即可。
  • \(st_{i,j}=2,st_{i,s}=1\),如果 \(s\le j-|s_i|\),那麼顯然 \(dp_{i,s}\)\(dp_{i,j}\) 的字首,否則我們需要比較 \(fs_{i-1}[j-|s_i|+1...s]\)\(s_i\) 的大小關係。
  • \(st_{i,j}=2,st_{i,s}=2\),那麼我們要比較 \(fs_{i-1}[s+1...s]\)\(s_i\) 的關係,相等的話進一步比較 \(s_i[s+|s_i|-j,...|s_i|]\)\(s_i\) 的大小關係。

如果我們記 \(t_i=s_i+'\#'+fs_{i-1}\),那麼上述比較操作均可表示為比較一個字首與一個子串的大小關係,可以通過比較第一個不相等的位置的字元判斷,而由於我們想求的是一個字首與一個子串的 LCP 的形式,因此可以考慮 Z 函式,這樣即可實現 \(\mathcal O(1)\) 比較。

總複雜度 \(nm+\sum|s_i|\)

const int MAXN=2000;
const int MAXM=1e4;
const int MAXL=1e6;
int n,m,k,dp[MAXN+5][MAXM+5];
string s[MAXN+5],fs[MAXN+5];
bool can[MAXN+5][MAXM+5];
char t[MAXL*2+5];int z[MAXL*2+5];
void z_func(){
	int l=0,r=0;
	for(int i=2;i<=m;i++){
		z[i]=max(min(r-i+1,z[i-l+1]),0);
		while(i+z[i]<=m&&t[i+z[i]]==t[z[i]+1]) ++z[i];
		if(i+z[i]-1>r) l=i,r=i+z[i]-1;
	}
}
int getcmp(int l,int r,int x){//comparison between a prefix and a substring (1 prefix > substring)
	if(l>r) return (!x)?0:1;
	if(z[l]>=min(r-l+1,x)) return 0;
	return 1-((t[l+z[l]]>t[z[l]+1])<<1);
}
int main(){
	scanf("%d%d",&n,&k);can[n+1][0]=1;
	for(int i=1;i<=n;i++){
		static char str[MAXL+5];scanf("%s",str+1);
		int len=strlen(str+1);
		for(int j=1;j<=len;j++) s[i].pb(str[j]);
	}
	for(int i=n;i;i--) for(int j=0;j<=k;j++)
		can[i][j]=can[i+1][j]|((j<s[i].size())?0:can[i+1][j-s[i].size()]);
	dp[0][0]=1;
	for(int i=1;i<=n;i++){
		m=0;for(int j=0;j<s[i].size();j++) t[++m]=s[i][j];
		t[++m]='#';stack<int> stk;
		for(int j=0;j<fs[i-1].size();j++) t[++m]=fs[i-1][j];t[m+1]='\0';
		z_func();
		for(int j=0;j<=k;j++) if(can[i+1][k-j]){
			if(dp[i-1][j]&&j>=s[i].size()&&dp[i-1][j-s[i].size()])
				dp[i][j]=1+(getcmp(s[i].size()+2+j-s[i].size(),s[i].size()+1+j,s[i].size())<=0);
			else if(dp[i-1][j]) dp[i][j]=1;
			else if(j>=s[i].size()&&dp[i-1][j-s[i].size()]) dp[i][j]=2;
			if(dp[i][j]==1){
				while(!stk.empty()){
					int x=stk.top();
					if(dp[i][x]==1) break;
					else{
						int st=getcmp(s[i].size()+2+x-s[i].size(),s[i].size()+1+j,s[i].size());
						if(st==0) break;if(st==1) stk.pop(),dp[i][x]=0;
						if(!~st){dp[i][j]=0;goto end1;}
					}
				} stk.push(j);
				end1:
					;
			} else if(dp[i][j]==2){
				while(!stk.empty()){
					int x=stk.top();
					if(dp[i][x]==2){
						int st=getcmp(s[i].size()+2+x-s[i].size(),s[i].size()+1+j-s[i].size(),s[i].size());
						if(!st){
							int dif=j-x+1;
							if(z[dif]+dif==s[i].size()+1) st=0;
							else st=1-((t[z[dif]+dif]<t[z[dif]+1])<<1);
						} if(st==1) stk.pop(),dp[i][x]=0;if(!st) break;
						if(!~st){dp[i][j]=0;goto end2;}
					} else {
						if(x<=j-s[i].size()) break;
						else{
							int st=getcmp(s[i].size()+2+j-s[i].size(),s[i].size()+1+x,s[i].size());
							if(st==0) break;if(!~st) stk.pop(),dp[i][x]=0;
							if(st==1){dp[i][j]=0;goto end2;}
						}
					}
				} stk.push(j);
				end2:
					;
			}
		}
		if(!stk.empty()){
			fs[i]=(dp[i][stk.top()]==1)?fs[i-1].substr(0,stk.top()):
			fs[i-1].substr(0,stk.top()-s[i].size())+s[i];
		} else fs[i]=fs[i-1];
	} for(int i=0;i<k;i++) putchar(fs[n][i]);
	return 0;
}
/*
5 6
aba
bab
bba
aab
bab

7 7
aa
aabb
a
bbaa
bba
babab
aa
*/