编程训练——避免不该有的罚时?试试背诵几个常见的动态规划程序

github博客同步发布

昨天晚上回顾了以前在onenote上记的动态规划笔记,发现很多程序都有相似之处,且最近两天写的动态规划程序都没有一遍AC。所以将这两天写的动态规划程序总结至此,以便背诵、默写用(这种题被罚时实在太亏)。

背诵的时候要特别注意dp数组的功能和其递推公式

阅读指南

六道题分别是钱币兑换、0-1背包、完全背包、最长公共子序列、最长上升子序列、划分数

前两题较简单,从第三题开始有公式推导,二和三题末尾有技巧总结。


一、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。

递推关系推导:
d p [ i ] [ j ] = m a x { j > k ∗ w [ i − 1 ] ∣ d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − w [ i − 1 ] ] + v [ i − 1 ] , d p [ i − 1 ] [ j − 2 ∗ w [ i − 1 ] ] + 2 ∗ v [ i − 1 ] , . . . , d p [ i − 1 ] [ j − k ∗ w [ i − 1 ] ] + k ∗ v [ i − 1 ] } \begin{aligned} dp[i][j]=&max\{j>k*w[i-1]|dp[i-1][j],dp[i-1][j-w[i-1]]+v[i-1],\\ &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][j]=max{j>kw[i1]dp[i1][j],dp[i1][jw[i1]]+v[i1],dp[i1][j2w[i1]]+2v[i1],...,dp[i1][jkw[i1]]+kv[i1]}
这个表达式可以简化,大括号中除了第一项,其余项的最大值就是 d p [ i ] [ j − w [ i − 1 ] ] + v [ i − 1 ] dp[i][j-w[i-1]]+v[i-1] dp[i][jw[i1]]+v[i1]
所以,递推关系可以简化成如下:
d p [ i ] [ j ] = m a x { d p [ i − 1 ] [ j ] , d p [ i ] [ j − w [ i − 1 ] ] + v [ i − 1 ] } , ( 如 果 j < w [ i − 1 ] , 则 去 掉 大 括 号 中 第 二 项 ) dp[i][j]=max\{dp[i-1][j],dp[i][j-w[i-1]]+v[i-1]\},(如果j<w[i-1],则去掉大括号中第二项) dp[i][j]=max{dp[i1][j],dp[i][jw[i1]]+v[i1]},(j<w[i1],)

总之遇到动态规划的题,遵循以下步骤可以大大降低错误率

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] s[i+1]==t[i+1],则
d p [ i + 1 ] [ j + 1 ] = m a x { d p [ i ] [ j ] + 1 , d p [ i + 1 ] [ j ] , d p [ 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][j+1]}
反之
d p [ i + 1 ] [ j + 1 ] = m a x { d p [ i ] [ j + 1 ] , d p [ i + 1 ] [ j ] } 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;
}


五、最长上升子序列

【题目描述】
在这里插入图片描述
图片来自《挑战程序设计竞赛(第2版)》

这道题和上道题题目很像,但意思完全不同。依旧先分析实现方法:
1、dp[i+1]代表以a[i]结尾的最长上升子序列的长度;
2、初始化dp[0]和dp[1]为0;
3、递推公式是:
d p [ i ] = m a x { 1 , d p [ j ] + 1 ∣ j < i 且 a [ j ] < a [ i ] } dp[i]=max\{1,dp[j]+1|j<i且a[j]<a[i]\} dp[i]=max{1,dp[j]+1j<ia[j]<a[i]}

【示例代码】

#include<stdio.h>

#define MAX_N 1000

int main(){
    int n;
    int a[MAX_N];
    int dp[MAX_N+1];
    int current;
    while(scanf("%d",&n)==1){
        for(int i=0;i<n;i++){
            scanf("%d",a+i);
        }
        
        //初始化dp数组
        dp[0]=0;
        dp[1]=1;
        
        //完善dp数组
        for(int i=2;i<=n;i++){
            dp[i]=dp[i-1];
            for(int j=i-2;j>=0;j--){
                if(a[i-1]>a[j]){
                    current=dp[j+1]+1;
                }
                else{
                    continue;
                }
                dp[i]=(dp[i]>current)?dp[i]:current;
            }
        }
        
        //利用dp数组回答问题
        printf("%d\n",dp[n]);
    }
    
    return 0;
}


六、有关计数问题的dp——划分数

【题目描述】
在这里插入图片描述
将题目中“模M的余数”去掉,我们直接输出划分方法总数。

图片来自《挑战程序设计竞赛(第2版)》。

实现方法分析:
1、dp[i][j]代表j划分为不超过i组的种数;
2、初始化dp[0][非零]为0,因为没有非零数能被划分为不超过0组,dp[…][0]为1,0被划分为任意多组方式都为一种;
3、j划分成不超过i组,可以等价为将j划分为i组,然后每一组的值可以为0。这样的话就假设j划分成的i个数都为非零和至少一个零两种情况,前者等于dp[i][j-i],后者等于dp[i-1][j],所以递推式如下:
d p [ i ] [ j ] = d p [ i ] [ j − i ] + d p [ i − 1 ] [ j ] dp[i][j]=dp[i][j-i]+dp[i-1][j] dp[i][j]=dp[i][ji]+dp[i1][j]
( i > = 1 , j > = 1 ) (i>=1,j>=1) (i>=1,j>=1)

⚠️注意不要将dp[i][j-i]写成dp[i][j-1]

【示例代码】

#include<stdio.h>

#define MAX_M 1000
#define MAX_N 1000

int main(){
    int n,m;
    int dp[MAX_M][MAX_N];
    
    while(scanf("%d %d",&n,&m)==2){
        //初始化dp数组
        for(int i=0;i<=m;i++){
            dp[i][0]=1;     //将0划分成任意多组,方式都只有一种
        }
        for(int j=1;j<=n;j++){
            dp[0][j]=0;     //任何正数都无法划分成0组
        }
        
        //利用递推公式完善dp数组
        for(int i=1;i<=m;i++){
            for(int j=1;j<=n;j++){
//                dp[i][j]=dp[i-1][j]+dp[i][j-1];   //错误写法
                dp[i][j]=dp[i-1][j]+dp[i][j-i];
            }
        }
        
        //利用dp回答问题
        printf("%d\n",dp[m][n]);
    }
    
    return 0;
}


小结

把以上几道题背会,足以掌握动态规划的基本方法,也足以举一反三地应对简单一些的赛事和考试。对于高级赛事,仍需要多练习,感悟为主。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值