DP动态规划学后思考与总结(C++)

本篇主要分享一下最近学习动态规划算法思想的一些收获、总结,以及反思。(参考《算法笔记》)

先来写写收获吧,我学到了什么。

首先,DP(Dynamic Programming)动态规划和分治思想的相似点和区别:

相似点:能使用动态规划分治思想的问题都能分解为求解子问题。

区别:

1.动态规划解决的问题必须具有重复求解子问题的步骤,使用动态规划思想就能将这些重复要求的子问题解存至一个一维数组或者二维数组中去(具体看问题),从而避免后面反复求解,直接使用存好的答案,从而降低时间复杂度。而分治问题可以解决不具有重复求解子问题步骤的问题,例如前序遍历二叉树,只需要遍历根节点、左子树、右子树即可,其中把二叉树按照根节点、左子树、右子树分治处理,但显然,根节点的访问和左子树的遍历工作以及右子树的遍历工作都是不重复的。(注意:递归是分治思想的一种实现手段)

2.动态规划一定是求解最优化问题,而分治不一定是求解最优化问题。

其次,动态规划贪心思想的相似点和区别:

相似点:能使用动态规划和贪心思想的问题都能分解为求解子问题。

区别:贪心思想基于一种最优化策略,需要自己用归纳法证明该策略求得的解一定是最优解,由局部最优可以推至全局最优。而动态规划求得的解一定是最优解,无需证明。我认为《算法笔记》上的比喻十分贴切,贪心思想有点像“壮士断腕”,选择了就不后悔,不走回头路,一条路走到底。而动态规划则要看哪个笑到最后,暂时的领先算不了什么,如果当前的不是最优还会回头选择。

然后,总结一下:

动态规划的目的就是去除重复的计算步骤从而降低时间复杂度。

动态规划的基本思路是存储后面所要用到的子问题解,以便要用的时候直接使用。

动态规划思考的主要步骤是:

1.假设已知问题某个参数下的问题的答案;(这就需要我们设计一种状态)

2.推导下一个参数的问题答案。(这就是状态转移方程的由来)

动态规划的代码书写步骤主要是:

1.定义dp数组;

2.初始化dp边界;

3.确定问题求解顺序;

4.书写dp状态转移方程;

下面我们通过思考两类经典的动态规划问题来更深入地理解动态规划的本质:

1.01背包问题

2.完全背包问题

————————————————

1.01背包问题

题目:有𝑛件物品,每件物品的重量为𝑤𝑖,价值为𝑐𝑖。现在需要选出若干件物品放入一个容量为𝑉的背包中(每件物品至多选一次),使得在选入背包的物品重量之和不超过容量𝑉的前提下,让背包中物品的价值之和最大,求最大价值与对应的最优方案数

输入:

第一行两个整数𝑛​、𝑉​(1≤𝑛≤100,1≤𝑉≤103​),分别表示物品数量、背包容量;

第二行为用空格隔开的𝑛​个整数𝑤𝑖​(1≤𝑤𝑖≤100​),表示物品重量;

第三行为用空格隔开的𝑛​个整数𝑐𝑖​(1≤𝑐𝑖≤100​),表示物品价值。

输出:

输出两个整数,分别表示最大价值与最优方案数,中间用空格隔开。由于结果可能很大,因此将结果对10007取模后输出。

思考:首先,我们来分析一下这道问题,如果采取暴力算法,那么方案选择将有2^n种(指数级复杂度),计算每种方案的价值又需要n次计算,复杂度将达到O(2^n * n),n较大时,大约在n为30时一般的oj系统就已经无法承受了(每秒10^8左右)。那么如何降低它的时间复杂度呢,我们思考一下,暴力算法中是否包含大量的重复计算?是的,每次选择或者不选择第i种物品时,前i种的方案都会重复计算一次。那么我们再思考,问题是否可以分解为子问题呢?根据我们的生活经验,我们肯定会将把重量轻且价值高的物品先放入背包,因为它必定是要放进入的,再考虑后面放什么,而后面选择放物品时背包已经继承了前面已经放的物品的重量和价值,说明后面的物品选择和前面有关。

我们现在假设已知将前i种物品恰好放入重量为j的背包的最大价值为dp[i][j];

现在背包重量不变,后面还有可供选择的物品即物品选择可变。那么第i+1种物品如何放入背包呢?

只有两种策略,放或者不放。

1.如果选择将第i+1种物品放入背包,那么此时就要将背包腾出w[i+1]的重量出来放第i+1种物品,同时,背包的价值变为了dp[i][j-w[i+1] + c[i+1];

2.如果选择不放入第i+1种物品,那么此时背包的价值还是放入前i种物品的最大价值dp[i-1][j];

那么我们要是将前i+1种物品恰好放入重量为j的背包的最大价值,就要取上面应用两种策略时背包的价值更大的那一种策略。

从而可以设计状态:

dp[i][j]代表将前i种物品恰好放入重量为j的背包的最大价值;

确定边界dp[1][w[i]]=c[1];(当w[i]<=maxWeight时,其他默认为0);

确定求解顺序,从物品前1种到物品前n种,背包价值为maxWeight到背包价值为1;

可以得到状态转移方程:

dp[i][j] = max(dp[i-1][j-w[i]] + c[i], dp[i-1][j]);

再来分析最优方案数,如何得到最优方案的数量呢?我们不妨重新设计一种动态规划算法单独求解最优方案数。类似地考虑:

现在假设已知将前i种物品恰好放入重量为j的背包得到最大价值maxvalue(== dp[i][j])的方案数即最优方案数为cnt[i][j];

那么对第i+1种物品做决策时最优方案数是如何改变的呢?

仍然,只有两种策略,将第i+1种物品放入背包,或者不放入背包:

1.如果第i+1种物品不放入背包,那么最优方案数不变仍为cnt[i][j];

2.如果第i+1种物品放入背包,那么需要腾出重量为w[i]且价值为c[i]的物品出来以放入第i+1种物品从而保持仍为最优方案,即最优方案数应该为cnt[i][j-w[i]];

那么前i+1种物品恰好放入重量为j的背包的最优方案数为 cnt[i][j] + cnt[i][j-w[i]]。

从而可以设计状态:

cnt[i][j]代表将前i种物品恰好放入重量为j的背包的所得价值为dp[i][j]的方案数;

确定边界cnt[0][0]=1;

确定求解顺序,从物品前1种到物品前n种,背包价值为maxWeight到背包价值为1;

可以得到状态转移方程:

if(w[i] <= maxWeight && c[i] + dp[i-1][j-w[i]] == dp[i-1][j]){

        cnt[i][j] = cnt[i-1][j] + cnt[i-1][j-w[i]];

} else{

        cnt[i][j] = cnt[i-1][j];

}

综上,可以写出代码:

#include <cstdio>
#include <algorithm>
using namespace std;
const int MAXN = 101;
const int MAXV = 1001;
int dp[MAXN][MAXV] = {0};
int cnt[MAXN][MAXV];
int n,maxWeight;
int w[MAXN];
int c[MAXN];
int main(){
    scanf("%d%d",&n,&maxWeight);
    for(int i=1; i<=n; i++){
        scanf("%d",&w[i]);
    }
    for(int i=1; i<=n; i++){
        scanf("%d",&c[i]);
    }
    //边界
    if(w[1] <= maxWeight) dp[1][w[1]] = c[1];
    for(int i=0; i<=maxWeight; i++){
    }
    for(int i=1; i<=n; i++){
        for (int v = 0; v <= maxWeight; v++) {
            if (v >= w[i]) { 
                dp[i][v] = max(dp[i - 1][v], dp[i - 1][v - w[i]] + c[i]); //求最大价值的状态转移方程
                if (dp[i][v] == dp[i - 1][v]) {
                    cnt[i][v] = cnt[i - 1][v]; //当不选择第i中物品是最优方案时,最优方案数继承前i-1种物品的最优方案数
                }
                if (dp[i][v] == dp[i - 1][v - w[i]] + c[i]) { //当选择第i种物品也是最优方案时,在继承不选择第i种物品的最优方案数的基础上加上选择第i种物品的最优方案数
                    cnt[i][v] = (cnt[i][v] + cnt[i - 1][v - w[i]]) % 10007;
                }
            } else { //如果第i种物品重量大于指定重量v,则必定不选第i种物品,直接继承前i-1种物品的最大价值和最优方案数得到前i种物品的最大价值和最优方案数
                dp[i][v] = dp[i - 1][v];
                cnt[i][v] = cnt[i - 1][v];
            }
        }
    }
    int maxvalue=-1;
    int weight;
    for(int j=maxWeight; j>0; j--){
        if(maxvalue < dp[n][j]){
            maxvalue = dp[n][j];
            weight = j;
        }
    }
    printf("%d %d",maxvalue,cnt[n][weight]);
    return 0;
}

————————————————

2.完全背包问题

题目:有𝑛​种物品,每种物品的重量为𝑤𝑖​,价值为𝑐𝑖​。现在需要选出若干件物品放入一个容量为𝑉​的背包中(每种物品可以选任意次),使得在选入背包的物品重量之和不超过容量𝑉​​的前提下,让背包中物品的价值之和最大,求最大价值。

输入:

第一行两个整数𝑛​、𝑉​(1≤𝑛≤100,1≤𝑉≤103​),分别表示物品数量、背包容量;

第二行为用空格隔开的𝑛​个整数𝑤𝑖​(1≤𝑤𝑖≤100​),表示物品重量;

第三行为用空格隔开的𝑛​个整数𝑐𝑖​(1≤𝑐𝑖≤100​),表示物品价值。

输出:

输出一个整数,表示最大价值。

分析:

假设已知前i-1种物品恰好放入重量为j的背包的最大价值为dp[i-1][j];

则前i种物品恰好放入重量为j的背包的最大价值为多少呢?

1.如果不能将物品i放入背包,则此时背包价值仍为dp[i-1][j];

2.如果能将物品i放入背包,则此时背包价值为dp[i-1][j-w[i]];

但是物品i有无限个,那么怎么描述继续考虑添加物品i呢?

既然物品个数不限,不好设为变量,那么将物品种类设为定量,考虑将背包限重看作变量j,考虑j在w[i]~maxWeight范围;限重从w[i]开始,那么这就可以涵盖不能将物品i放入背包时就继承dp[i-1][j]的情况,而背包限重w[i] >= w[i]开始一直到maxWeight,更新前i种物品的最大价值为max(dp[i-1][j],dp[i-1][j-w[i]]),思考当j增大到2倍w[i]时,其实这时就实现了继续添加物品i,同理在不大于背包限重的情况下j增到任意倍w[i],就实现了计算添加任意个物品i的最大价值;

同时,我们发现,状态转移方程中:

dp[i][j] = max(dp[i-1][j],dp[i-1][j-w[i]]);

每到后一种物品i,只需要用到前一种物品i-1的信息,而不需要i-2,i-3...等更前面的信息,因此,我们可以考虑将二维数组降为一维数组,舍去前一维的物品信息,将动态规划数组设为dp[j];(这就是滚动数组)

由此,我们可以书写代码:

#include <cstdio>
#include <algorithm>
using namespace std;
const int MAXN = 101;
const int MAXV = 1001;
int dp[MAXN]= {0};
int n,maxWeight;
int w[MAXN];
int c[MAXN];
int main(){
    scanf("%d%d",&n,&maxWeight);
    for(int i=1; i<=n; i++){
        scanf("%d",&w[i]);
    }
    for(int i=1; i<=n; i++){
        scanf("%d",&c[i]);
    }
    for(int i=1; i<=n; i++){
        for(int j=w[i]; j<= maxWeight; j++){ //注意该循环实际上就包含了w[i] <= maxWeight才执行的条件,并保证j一直小于等于maxWeight
            dp[j] = max(dp[j],dp[j-w[i]] + c[i]);
        }
    }
    int maxval=-1;
    for(int i=1; i<=maxWeight; i++){
        if(maxval < dp[i]){
            maxval = dp[i];
        }
    }
    printf("%d",maxval);
    return 0;
}

OK,看到这里,你可能会说,看得迷糊,OK,那就对了,即使我缝缝补补改好了代码,我现在也是看得迷糊,这还只是动态规划的开始,最经典的几道简单题之一,可想而知,动态规划的算法思想有多么精秒,基本上代码都比较简短,并且实现时间复杂度较低,但稍微一个字母或者一个大于等于号写得不准确,就可能出错,还是得多思考,多训练,多看看不同类型的动态规划问题,积累动态规划的思路,和书写代码的经验。

对于DP问题,我只能说,一个字,秒,两个字,精妙,三个字,太精妙,六个字,精妙得不得了。啊,再见,同志们,感觉我的脑子需要休息一下了~-~

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值