PTA L3-020 至多刪三個字符 (序列dp/序列自動機)
給定一個全部由小寫英文字母組成的字符串,允許你至多刪掉其中 3 個字符,結果可能有多少種不同的字符串?
輸入格式:
輸入在一行中給出全部由小寫英文字母組成的、長度在區間 [4, 1] 內的字符串。
輸出格式:
在一行中輸出至多刪掉其中 3 個字符後不同字符串的個數。
輸入樣例:
ababcc
輸出樣例:
25
提示:
刪掉 0 個字符得到 "ababcc"。
刪掉 1 個字符得到 "babcc", "aabcc", "abbcc", "abacc" 和 "ababc"。
刪掉 2 個字符得到 "abcc", "bbcc", "bacc", "babc", "aacc", "aabc", "abbc", "abac" 和 "abab"。
刪掉 3 個字符得到 "abc", "bcc", "acc", "bbc", "bac", "bab", "aac", "aab", "abb" 和 "aba"。
解法:
前置技能:求一個序列中所有的不同子序列個數。
eg:FZU - 2129
設dp[i]為序列a的前i個元素所組成的不同子序列個數,則有狀態轉移方程:$dp[i]=\left\{\begin{matrix}\begin{aligned}&2dp[i-1]+1,pre[a[i]]=-1\\&2dp[i-1]-dp[pre[a[i]]-1],pre[a[i]]\neq -1\end{aligned}\end{matrix}\right.$
其中pre[a[i]]表示a[i]前面第一個和a[i]相同的元素的下標。
解釋:第i個元素a[i]有兩種選擇:選或不選。
若不選a[i],則dp[i]繼承dp[i-1]的全部子序列,因此有dp[i]+=dp[i-1]。
若選a[i],則dp[i]在dp[i-1]的全部子序列的尾部填加了個元素a[i],因此仍有dp[i]+=dp[i-1]。但這樣會有很多重復的序列,因此要去重,即去掉前面和a[i]相同的元素之前的序列(因為它們加上a[i]形成的序列已經被算過了),因此有dp[i]-=dp[pre[a[i]]-1]。特別地,如果a[i]前面沒有與a[i]相同的元素,那麽沒有重復的序列,並且a[i]自己單獨形成一個新序列,此時dp[i]++。
1 #include<cstdio> 2 #include<cstring> 3 using namespace std; 4 typedef long long ll; 5 typedef double db; 6 const int N=1e6+10,mod=1e9+7; 7 int a[N],n,dp[N],pre[N]; 8 int main() { 9 while(scanf("%d",&n)==1) { 10 memset(pre,-1,sizeof pre); 11 for(int i=1; i<=n; ++i)scanf("%d",&a[i]); 12 dp[0]=0; 13 for(int i=1; i<=n; ++i) { 14 dp[i]=(ll)dp[i-1]*2%mod; 15 if(~pre[a[i]])dp[i]=((ll)dp[i]-dp[pre[a[i]]-1])%mod; 16 else dp[i]=(dp[i]+1)%mod; 17 pre[a[i]]=i; 18 } 19 printf("%d\n",(dp[n]+mod)%mod); 20 } 21 return 0; 22 }
回到正題,此題是上題的升級版,等價於求一個長度為n的序列中長度為n,n-1,n-2,n-3的不同子序列個數之和。
基本思路是一致的,只需要在上述代碼的基礎上稍作改動即可。
設dp[i][j]為前i個元素刪了j個元素所形成的子序列個數,則有$dp[i]=\left\{\begin{matrix}\begin{aligned}&dp[i-1][j-1]+dp[i-1][j],pre[a[i]]=-1,j\neq i-1\\&dp[i-1][j-1]+dp[i-1][j]+1,pre[a[i]]=-1,j=i-1\\&dp[i-1][j-1]+dp[i-1][j]-dp[pre[a[i]]-1][j-(i-pre[a[i]])],pre[a[i]]\neq -1\end{aligned}\end{matrix}\right.$
推導過程類似,註意j的變化即可。
1 #include<cstdio> 2 #include<cstring> 3 using namespace std; 4 typedef long long ll; 5 typedef double db; 6 const int N=1e6+10; 7 char a[N]; 8 int n,pre[300]; 9 ll dp[N][4]; 10 int main() { 11 memset(pre,-1,sizeof pre); 12 scanf("%s",a+1),n=strlen(a+1); 13 for(int i=1; i<=n; ++i) { 14 for(int j=0; j<=3; ++j) { 15 if(j>0)dp[i][j]+=dp[i-1][j-1]; 16 dp[i][j]+=dp[i-1][j]; 17 if(~pre[a[i]]&&j>=i-pre[a[i]])dp[i][j]-=dp[pre[a[i]]-1][j-(i-pre[a[i]])]; 18 else if(i==j+1)dp[i][j]++; 19 } 20 pre[a[i]]=i; 21 } 22 printf("%lld\n",dp[n][0]+dp[n][1]+dp[n][2]+dp[n][3]); 23 return 0; 24 }
還有另一種解法是利用序列自動機,很簡單,設go[i][j]為第i個元素後第一個元素j出現的位置,先用類似dp的方式建立自動機,則問題轉化成了一個DAG上的dp問題。
但是由於序列自動機空間消耗較大,直接dfs可能會爆內存,比如這樣:
1 #include<bits/stdc++.h> 2 using namespace std; 3 typedef unsigned long long ll; 4 const int N=1e6+10; 5 string s; 6 int n; 7 set<string> st; 8 string subs(string& s,int l,int r) {return l>r?"":s.substr(l,r-l+1);} 9 10 int main() { 11 cin>>s,n=s.length(); 12 for(int i=0; i<n; ++i) 13 for(int j=i; j<n; ++j) 14 for(int k=j; k<n; ++k) 15 st.insert(subs(s,0,i-1)+subs(s,i+1,j-1)+subs(s,j+1,k-1)+subs(s,k+1,n-1)); 16 printf("%d\n",st.size()+1); 17 return 0; 18 }
解決方法是自底而上,一遍dp一遍更新go數組,成功AC:
1 #include<bits/stdc++.h> 2 using namespace std; 3 typedef long long ll; 4 typedef double db; 5 const int N=1e6+10,M=26; 6 char s[N]; 7 int n,go[M]; 8 ll dp[N][4]; 9 int main() { 10 scanf("%s",s),n=strlen(s); 11 dp[n][0]=dp[n][1]=dp[n][2]=dp[n][3]=1; 12 for(int i=n-1; i>=0; --i) { 13 go[s[i]-‘a‘]=i+1; 14 for(int j=0; j<=3; ++j) { 15 dp[i][j]=(j+(n-i)<=3); 16 for(int k=0; k<M; ++k)if(go[k]&&j+go[k]-i-1<=3)dp[i][j]+=dp[go[k]][j+go[k]-i-1]; 17 } 18 } 19 printf("%lld\n",dp[0][0]); 20 return 0; 21 }
雖然序列自動機的功能比較強大,但時間和空間的消耗都與元素集合的大小有關,因此當元素集合過大的時候,可能就並不吃香了~~
PTA L3-020 至多刪三個字符 (序列dp/序列自動機)