DP專題 5 | 顏色的長度 - UVA1625(線性DP)

【題意】 

輸入兩個長度分別爲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++、機器學習等。

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章