昨天晚上回顧了以前在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。
遞推關係推導:
這個表達式可以簡化,大括號中除了第一項,其餘項的最大值就是,
所以,遞推關係可以簡化成如下:
總之遇到動態規劃的題,遵循以下步驟可以大大降低錯誤率
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、確定遞推關係:
如果,則
反之
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;
}
小結
把以上幾道題背會,足以掌握動態規劃的基本方法,也足以舉一反三地應對簡單一些的賽事和考試。對於高級賽事,仍需要多練習,感悟爲主。