編程訓練[C語言]——避免不該有的罰時?試試背誦幾個常見的動態規劃程序

昨天晚上回顧了以前在onenote上記的動態規劃筆記,發現很多程序都有相似之處,且最近兩天寫的動態規劃程序都沒有一遍AC。所以將這兩天寫的動態規劃程序總結至此,以便背誦、默寫用(這種題被罰時實在太虧)。

背誦的時候要特別注意dp數組的功能和其遞推公式

一、hdu1284 錢幣兌換問題

先來道最簡單的背誦。

【題目描述】
一個國家只有1,2,3分錢,輸入非負整數n(不超過10000),輸出兌換金額n一共有多少種換法,多組輸入輸出。

【示例程序】
(沒有一遍AC的原因寫在了註釋裏)

#include<stdio.h>

int dp[2][10005];   //dp[i][j]代表用0~i硬幣兌換金額j共有多少種換法,
					//遞推式是dp[i][j]=dp[i][j-a[i]]+dp[i-1][j],
					//意思是不用第i種面值湊齊j加上用第i種面值的情況下湊齊j

int main(){
    int n;
    int a[3]={1,2,3};

	//初始化dp數組
    for(int j=0;j<10000;j++){
        dp[0][j]=1;		//只用第0種面值(1分錢)進行兌換,則無論換多少都只有一種方式
    }
    for(int i=0;i<3;i++){
        dp[i][0]=1;		//兌換金額爲0,則只有一種兌換方式:所有面值都是0張
    }
    for(int i=1;i<3;i++){	//從第0~1種面值開始循環至用第0~2種面值
        for(int j=1;j<=10000;j++){	//從兌換1分錢開始循環至兌換10000分錢
            int x,y;
            y=dp[i-1][j];	//y存儲不用第i種面值的兌換種數
            //x存儲使用至少1張第i種面值的兌換種數
            if(j-a[i]<0)x=0;    //如果要兌換的金額數小於0,則兌換方式是0種
            else x=dp[i][j-a[i]];
            dp[i][j]=x+y;	//用第0~i種面值兌換j分錢的種數
        }
    }
    while(scanf("%d",&n)==1){
        printf("%d\n",dp[2][n]);    //不慎寫成dp[3][n],導致無論n是多少,輸出都爲0.
    }
    return 0;
}

二、0-1揹包問題

【題目描述】
第一行輸入n代表共有n(不超過100)種物品,第二行依次輸入這些物品的重量(不超過100),第三行依次輸入這些物品的價值(不超過100),第四行輸入揹包能承受的總重(10000),輸出揹包能裝的物品的最大總價值。

比如輸入:

4
3 1 2 3 
4 2 3 2
5

輸出

7

【示例代碼】

#include<stdio.h>

#define MAX_N 100
#define MAX_W 10000

int dp[MAX_N+1][MAX_W+1];   //dp[i][j]代表從0~i-1號這前i個物品中選擇的最大總價值

int main(){
    int n;
    int w[MAX_N];   //物品重量
    int v[MAX_N];   //物品價值
    int total_w;    //總重量
    
    while(scanf("%d",&n)==1){
        for(int i=0;i<n;i++){
            scanf("%d",w+i);
        }
        for(int i=0;i<n;i++){
            scanf("%d",v+i);
        }
        scanf("%d",&total_w);
        
        //step1:初始化dp數組
        for(int j=0;j<=total_w;j++){   //從0~-1號物品中選任何重量上限的物品,總價值都是0
            dp[0][j]=0;
        }
        
        //step2:完善dp數組
        
        //【錯誤一】不慎將for循環寫成這樣,造成了Thread 1: EXC_BAD_ACCESS (code=1, address=0x141257284)的錯誤,檢查發現數組w中有一位數據發生了溢出(數值是一個非常小的負數)
//        for(int i=1;i<MAX_N;i++){
//            for(int j=0;j<MAX_W;j++){
        //【錯誤二】將for循環寫成如下這樣,會導致最終需要輸出的dp[n][total_w]未被賦值
//        for(int i=1;i<n;i++){
//            for(int j=1;j<total_w;j++){
        for(int i=1;i<=n;i++){
            for(int j=0;j<=total_w;j++){
                int x,y;
                x=dp[i-1][j];   //x存儲從0~i-2號物品中選擇的總價值(即不選第i-1號物品)
                
                //y存儲選擇一個第i-1號物品的前提下的最大總價值
                if(j>=w[i-1]){
                    y=dp[i-1][j-w[i-1]]+v[i-1];
                }
                else{   //重量上限不足以放下第i-1號物品
                    y=0;
                }
//                dp[i][j]=x+y;   //【錯誤三,最致命】不慎寫成這句話
                dp[i][j]=(x>y)?x:y;
            }
        }
        
        //step3:利用dp數組回答問題
        printf("%d\n",dp[n][total_w]);
    }
    return 0;
}

【總結】
可以看出來,這種類型的動態規劃的核心是初始化並完善dp數組,大致順序就是:
0、察覺到這是動態規劃題,確定大致算法流程;
1、確認dp[i][j]含義和遞推式;
2、初始化dp數組;
3、完善dp數組。

然後就是利用dp數組中的元素回答問題。

這道題犯的錯集中在dp數組的完善部分,說明我需要注意數組下標變化、注意遞推式的正確使用,以及最終的的是:保持對dp數組功能的認知

不能一遍AC的根源

在被這兩道題瘋狂罰時之後,我發現我的錯誤都不是算法問題,而是集中在數組下標沒把握好上,屬於細節問題。於是博主決定不輕視任何一道題,任何題都要在紙上寫出算法思路、數據結構,規定好數據範圍、數組下標這類細節,然後再進行編碼。抱着這樣的想法,我做了一道0-1揹包升級版——完全揹包問題,這一次,終於一遍就AC了:

三、完全揹包問題

【題目描述】
依舊是輸入物品種數n,每種物品的重量,每個物品的價值,揹包的承重上限,輸出揹包能裝的物品的最大總價值。和0-1揹包問題不同的是,每種物品能選無限多件。

【示例代碼】

#include<stdio.h>

#define MAX_N 100
#define MAX_W 10000

int main(){
    int w[MAX_N];
    int v[MAX_N];
    int max_w;
    int n;
    int dp[MAX_N+1][MAX_W+1];   //注意行數和列數,因爲要多用一行所以加一
    
    while(scanf("%d",&n)==1){
        for(int i=0;i<n;i++){
            scanf("%d",w+i);
        }
        for(int i=0;i<n;i++){
            scanf("%d",v+i);
        }
        scanf("%d",&max_w);
        
        //初始化dp數組
        for(int j=0;j<=max_w;j++){
            dp[0][j]=0;
        }
        
        //遞推式完善dp數組
        for(int i=1;i<=n;i++){
            for(int j=0;j<=max_w;j++){
                int x,y;
                x=dp[i-1][j];
                if(j<w[i-1]){
                    y=0;
                }
                else{
                    y=dp[i][j-w[i-1]]+v[i-1];
                }
                dp[i][j]=(x>y)?x:y;
            }
        }
        
        //根據dp回答問題
        printf("%d\n",dp[n][max_w]);
    }
    
    return 0;
}

展示以下我草稿紙上定義的dp數組的功能和遞推關係的推導:
dp[i][j]代表從前i類(0~i-1號)物品挑選出總重不超過j的最大價值。

dp數組的初始化:顯然dp[0][…]應當都爲0。

遞推關係推導:
dp[i][j]=max{j&gt;kw[i1]dp[i1][j],dp[i1][jw[i1]]+v[i1],dp[i1][j2w[i1]]+2v[i1],...,dp[i1][jkw[i1]]+kv[i1]} \begin{aligned} dp[i][j]=&amp;max\{j&gt;k*w[i-1]|dp[i-1][j],dp[i-1][j-w[i-1]]+v[i-1],\\ &amp;dp[i-1][j-2*w[i-1]]+2*v[i-1],...,dp[i-1][j-k*w[i-1]]+k*v[i-1]\} \end{aligned}
這個表達式可以簡化,大括號中除了第一項,其餘項的最大值就是dp[i][jw[i1]]+v[i1]dp[i][j-w[i-1]]+v[i-1]
所以,遞推關係可以簡化成如下:
dp[i][j]=max{dp[i1][j],dp[i][jw[i1]]+v[i1]},(j&lt;w[i1],)dp[i][j]=max\{dp[i-1][j],dp[i][j-w[i-1]]+v[i-1]\},(如果j&lt;w[i-1],則去掉大括號中第二項)

總之遇到動態規劃的題,遵循以下步驟可以大大降低錯誤率

1、在紙上書寫大致流程、數據(存儲)結構;
2、規定dp數組的含義;
3、初始化dp數組;
4、確定遞推式完善dp數組;
5、根據確定的dp數組回答問題。

⚠️注意不要輕視任何題目,以及有條件的話背誦一些經典的動態規劃代碼,比如本文寫的幾個。

以上所提放在其他類型的算法題上,也是適用的。

四、最長公共子序列問題

和揹包問題思路不同的動態規劃題。

【問題描述】分別輸入字符串s和t的長度,再輸入s和t兩個字符串,輸出s和t的最長公共子序列

比如輸入:

4 4
abcd
becd

由於兩個字符串的公共部分是bcd,有三個字符,則輸出:

3

由於在上一題已經嚐到了先在紙上分析的甜頭,所以這一題先進行分析:
1、規定dp數組:dp[i][j]代表s[1]~s[i]和t[1]~t[j]的公共子序列,注意我不用s[0]和t[0],所以定義存儲串s和串t的數組的長度應當額外加一;
2、初始化dp數組:dp[0][…]和dp[…][0]肯定都爲0;
3、確定遞推關係:

如果s[i+1]==t[i+1]s[i+1]==t[i+1],則
dp[i+1][j+1]=max{dp[i][j]+1,dp[i+1][j],dp[i][j+1]}dp[i+1][j+1]=max\{dp[i][j]+1,dp[i+1][j],dp[i][j+1]\}
反之
dp[i+1][j+1]=max{dp[i][j+1],dp[i+1][j]}dp[i+1][j+1]=max\{dp[i][j+1],dp[i+1][j]\}
4、程序Output:dp[n][m],n和m分別爲用戶輸入的s和t的長度。

【示例代碼】
我又一次因爲紙上打草稿而避免了罰時

#include<stdio.h>

#define MAX_N 1000
#define MAX_M 1000

int main(){
    int n,m;
    char s[MAX_N+1],t[MAX_M+1];     //從下標1開始使用,所以額外加一
    int dp[MAX_N+1][MAX_M+1];
    
    while(scanf("%d %d",&n,&m)==2){
        getchar();  //吸收回車
        for(int i=1;i<=n;i++){
            s[i]=getchar();
        }
        getchar(); //吸收回車
        for(int i=1;i<=m;i++){
            t[i]=getchar();
        }
        getchar();  //吸收回車
        
        //初始化dp數組
        for(int j=0;j<=m;j++){
            dp[0][j]=0;
        }
        for(int i=0;i<=n;i++){
            dp[i][0]=0;
        }
        
        //根據遞推式完善dp數組
        for(int i=0;i<n;i++){
            for(int j=0;j<m;j++){
                if(s[i+1]!=t[j+1]){
                    dp[i+1][j+1]=(dp[i][j+1]>dp[i+1][j])?dp[i][j+1]:dp[i+1][j];
                }
                else{
                    int temp=(dp[i][j]+1>dp[i+1][j])?dp[i][j]+1:dp[i+1][j];
                    dp[i+1][j+1]=(temp>dp[i][j+1])?temp:dp[i][j+1];
                }
            }
        }
        
        //根據dp數組回答問題
        printf("%d\n",dp[n][m]);
    }
    
    return 0;
}

小結

把以上幾道題背會,足以掌握動態規劃的基本方法,也足以舉一反三地應對簡單一些的賽事和考試。對於高級賽事,仍需要多練習,感悟爲主。

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