【題意】
輸入兩個長度分別爲n和m的顏色序列(n,m<=5000),要求按一定規則合併成一個序列,規則是每次可以把一個序列開頭的顏色放到新序列的尾部。例如對於序列GBBY和YRRGB,它們可以合成很多中結果,其中包含這樣兩種結果,GBYBRYRGB和YRRGGBBYB,對於每個顏色c來說,其跨度L(c)等於新序列中顏色c出現的最大位置和最小位置之差,比如對於上面的兩種結果,每個顏色的L(c)和相應的總和如下表所示
你的任務是找到一種最合理的合併方式,使得新序列的L(c)總和最小
【思路】
紫書276頁的一道例題,一開始連狀態轉移怎麼轉都想不出來,看了紫書的講解也也很暈,最後是看了好多別人的題解之後才弄明白的。首先dp的狀態就是書上所講,但我的求解順序和書上是反過來的,dp(i,j)表示的是從第一個序列頭部取走i個元素,第二個序列頭部取走j個元素的狀態下當前的花費值,所以最後的答案就是dp(n,m). 不難想到dp(i,j)的狀態一定是從dp(i-1,j)和dp(i,j-1)的狀態轉移而來的,所以狀態轉移方程就一定是這樣一個類似於LCS問題的式子
dp(i,j) = min {dp(i-1,j), dp(i,j-1)} + x
那這個x是個什麼,按照書上所說,每次狀態轉移的時候,都要把所有的“已經出現但還沒結束”的顏色的L(c)值加1,所以在dp(i-1,j)向dp(i,j)的轉移過程中,這個x就應該是第一個字符串的前i-1個字符和第二個字符串的前j個字符中所有“已經出現但還沒結束”的字符個數,同理在dp(i,j-1)向dp(i,j)的轉移過程中,這個x就應該是第一個字符串的前i個字符和第二個字符串的前j-1個字符中所有“已經出現但還沒結束”的字符個數。
結合下面的表格,比如兩個原始序列爲題目描述中的GBBY和YRRGB,現在的狀態是dp(1,3)也就是從第一個字符串中取出G,第二個字符串中取出YRR,假設新序列是YRRG,現在要向着dp(1,4)做狀態轉移,也就是要再從第二個字符串中把G取出來,這時字母Y的頭上要加1,G的頭上要加1,R不用管因爲R已經全部結束了,兩個字符串中已經沒有R了。所以現在的狀態轉移x的值爲2,也就是dp(1,3)對應的新串中“已經出現但還沒結束”的字符個數。
狀態 新串 第一個串 第二個串
那這個x的值到底怎麼求呢,這就需要依賴於dp前的預處理了,用兩個數組f[2][26]和g[2][26]分別記錄每個字母在每個字符串中第一次出現的位置,最後一次出現的位置,有了這樣兩個數組,在dp過程中,另開一個c數組,c[i][j]記錄當前(i,j)狀態下的新串中“已經出現但還沒結束”的字符個數,那麼最終的狀態轉移方程就是dp(i,j) = min {dp(i-1,j)+c[i-1][j],dp(i,j-1)+c[i][j-1]} 在dp過程進行的時候藉助於f和g不斷更新c數組即可,注意數組c的結果也是遞推計算得到的。
#include<bits/stdc++.h>
using namespace std;
const int inf=2e9;
const int maxn=5050;
char s[2][maxn];
int len[2];
int f[2][26],g[2][26];//記錄每個字母在每個字符串中的第一次和最後一次出現位置
int dp[maxn][maxn],c[maxn][maxn];//c[i][j]記錄當前狀態下新串中出現過但還沒結束的字符的個數
void init(){//預處理
//初始化
for(int k=0;k<2;++k){
for(int ch=0;ch<26;++ch){
f[k][ch]=inf;
g[k][ch]=-1;
}
}
for(int k=0;k<2;++k){//處理每個字符串
for(int ch=0;ch<26;++ch){//處理每個字母
for(int i=1;i<=len[k];++i){//記錄第一次出現的位置
if(ch+'A'==s[k][i]){
f[k][ch]=i;
break;
}
}
for(int i=len[k];i>=1;--i){//記錄最後一次出現的位置
if(ch+'A'==s[k][i]){
g[k][ch]=i;
break;
}
}
}
}
}
void solve(){
dp[0][0]=0;
c[0][0]=0;
for(int i=0;i<=len[0];++i){
for(int j=0;j<=len[1];++j){
if(0==i && 0==j) continue;
//計算當前狀態dp[i][j],一定由dp[i-1][j]和dp[i][j-1]轉移而來
dp[i][j]=inf;
if(i>0) {//由dp[i-1][j]轉移而來,取出的是s[0][i]
dp[i][j]=min(dp[i][j],dp[i-1][j]+c[i-1][j]);
c[i][j]=c[i-1][j];
int ch=s[0][i]-'A';
if(i==f[0][ch] && j<f[1][ch]) ++c[i][j];//判斷s[0][i]是不是在新串中第一次出現
if(i==g[0][ch] && j>=g[1][ch]) --c[i][j];//判斷s[0][i]是不是在新串中最後一次出現
}
if(j>0) {//由dp[i][j-1]轉移而來,取出的是s[1][j]
dp[i][j]=min(dp[i][j],dp[i][j-1]+c[i][j-1]);
c[i][j]=c[i][j-1];
int ch=s[1][j]-'A';
if(j==f[1][ch] && i<f[0][ch]) ++c[i][j];
if(j==g[1][ch] && i>=g[0][ch]) --c[i][j];
}
}
}
printf("%d\n",dp[len[0]][len[1]]);
}
int main(){
int t;
scanf("%d",&t);
while(t--){
for(int k=0;k<2;++k) {
scanf("%s",1+s[k]);//字符串的下標從1開始便於處理
len[k]=strlen(1+s[k]);
}
init();
solve();
}
return 0;
}
拿滾動數組做優化,還可以進一步優化空間複雜度
#include<bits/stdc++.h>
using namespace std;
const int inf=2e9;
const int maxn=5050;
char s[2][maxn];
int len[2];
int f[2][26],g[2][26];//記錄每個字母在每個字符串中的第一次和最後一次出現位置
int dp[2][maxn],c[2][maxn];
void init(){//預處理
//初始化
for(int k=0;k<2;++k){
for(int ch=0;ch<26;++ch){
f[k][ch]=inf;
g[k][ch]=-1;
}
}
for(int k=0;k<2;++k){//處理每個字符串
for(int ch=0;ch<26;++ch){//處理每個字母
for(int i=1;i<=len[k];++i){//記錄第一次出現的位置
if(ch+'A'==s[k][i]){
f[k][ch]=i;
break;
}
}
for(int i=len[k];i>=1;--i){//記錄最後一次出現的位置
if(ch+'A'==s[k][i]){
g[k][ch]=i;
break;
}
}
}
}
}
void solve(){
dp[0][0]=0;
c[0][0]=0;
for(int i=0;i<=len[0];++i){
for(int j=0;j<=len[1];++j){
if(0==i && 0==j) continue;
//計算當前狀態dp[i][j],存到滾動數組中的dp[i%2][j]的位置
int now=i%2, pre=1-now;
dp[now][j]=inf;
if(i>0) {
dp[now][j]=min(dp[now][j],dp[pre][j]+c[pre][j]);
c[now][j]=c[pre][j];
int ch=s[0][i]-'A';
if(i==f[0][ch] && j<f[1][ch]) ++c[now][j];
if(i==g[0][ch] && j>=g[1][ch]) --c[now][j];
}
if(j>0) {
dp[now][j]=min(dp[now][j],dp[now][j-1]+c[now][j-1]);
c[now][j]=c[now][j-1];
int ch=s[1][j]-'A';
if(j==f[1][ch] && i<f[0][ch]) ++c[now][j];
if(j==g[1][ch] && i>=g[0][ch]) --c[now][j];
}
}
}
printf("%d\n",dp[len[0]%2][len[1]]);
}
int main(){
int t;
scanf("%d",&t);
while(t--){
for(int k=0;k<2;++k) {
scanf("%s",1+s[k]);//字符串的下標從1開始便於處理
len[k]=strlen(1+s[k]);
}
init();
solve();
}
return 0;
}
個人公衆號(acm-clan):ACM算法日常
專注於基礎算法的研究工作,深入解析ACM算法題,五分鐘閱讀,輕鬆理解每一行源代碼。內容涉及算法、C/C++、機器學習等。