【算法->动态规划】01背包问题 PAT1068 C++实现

来源:《算法笔记》

01背包问题是一个多阶段动态规划问题。所谓多阶段动态规划问题,是指它可以描述成若干个有序的阶段,且每个阶段的状态只和上一个阶段的状态有关。

在这里插入图片描述

问题描述

有n件物品,每件物品的重量为w[i],价值为c[i]。现有一个容量为V的背包,问如何选取物品放入背包,使得背包内物品的总价值最大。其中每种物品都只有1件。

样例:

5 8		//n==5, V==8
3 5 1 2 2 //w[i]
4 5 2 1 3 //c[i]

如果暴力枚举,复杂度为O(2n);儿动态规划可以将复杂度将为O(nV)。

如何解决

令dp[i][v]表示前i件物品(1≤i≤n, 0≤v≤V),恰好装入容量为v的背包中所能获得的最大价值。怎么求解dp[i][v]?

  1. 不放第i件物品,那么问题转化为前i-1件物品恰好装入容量为v的背包中所能获得的最大价值,即 d p [ i − 1 ] [ v ] dp[i-1][v] dp[i1][v]
  2. 放第i件物品,那么问题转化为前i-1件物品恰好装入容量为v-w[i]的背包中所能获得的最大价值,即 d p [ i − 1 ] [ v − w [ i ] ] + c [ i ] dp[i-1][v-w[i]] + c[i] dp[i1][vw[i]]+c[i]

由于只有这两种策略,且要求获得最大价值,因此
d p [ i ] [ v ] = m a x { d p [ i − 1 ] [ v ] , d p [ i − 1 ] [ v − w [ i ] ] + c [ i ] } ( 1 ≤ i ≤ n , w [ i ] ≤ v ≤ V ) dp[i][v] = max\{dp[i-1][v], dp[i-1][v-w[i]] + c[i]\}\\ (1≤i≤n, w[i]≤v≤V) dp[i][v]=max{dp[i1][v],dp[i1][vw[i]]+c[i]}(1in,w[i]vV)
上面这个是状态转移方程。注意到 d p [ i ] [ v ] dp[i][v] dp[i][v]只与之前的状态 d p [ i − 1 ] [ ] dp[i-1][] dp[i1][]有关,所以可以枚举i从1到n,v从0到V,通过边界 d p [ 0 ] [ v ] = 0 ( 0 ≤ v ≤ V ) dp[0][v] = 0(0≤v≤V) dp[0][v]=0(0vV)(即前0件物品(表示没有物品)放入任何容量v的背包中都只能获得价值0)就可以把整个dp的数组递推出来。而由于 d p [ i ] [ v ] dp[i][v] dp[i][v]表示的是恰好为v的情况,所以需要枚举 d p [ n ] [ v ] ( 0 ≤ v ≤ V ) dp[n][v](0≤v≤V) dp[n][v](0vV),取其最大值才是最后的结果。

for(int i=1; i<=n; i++){
	for(int v=w[i]; v<= V; v++){
        dp[i][v] = max(dp[i-1][v], dp[i-1][v-w[i]]+c[i]);	
    }
}

可以知道,时间复杂度和空间复杂度都是O(nV),接下来还可以优化空间复杂度。(要注意这个演化的过程

如图所示,状态转移方程中计算 d p [ i ] [ v ] dp[i][v] dp[i][v]总是只需要 d p [ i − 1 ] [ v ] dp[i-1][v] dp[i1][v]左侧部分的数据(正上方或左上方),且当计算 d p [ i + 1 ] [ ] dp[i+1][] dp[i+1][]的部分时, d p [ i − 1 ] dp[i-1] dp[i1]的数据又完全用不到了(只需要 d p [ i ] [ ] dp[i][] dp[i][]),因此不妨直接开一维数组dp[v](即省去第一维),但是需要枚举方向改变为i从1到n,v从V到0(逆序),状态转移方程改为
d p [ v ] = m a x { d p [ v ] , d p [ v − w [ i ] ] + c [ i ] } ( 1 ≤ i ≤ n , w [ i ] ≤ v ≤ V ) dp[v] = max\{dp[v], dp[v-w[i]] + c[i]\}\\ (1≤i≤n, w[i]≤v≤V) dp[v]=max{dp[v],dp[vw[i]]+c[i]}(1in,w[i]vV)
在这里插入图片描述

这样修改对应到图中可以这样理解:v的枚举顺序变为从右往左, d p [ i ] [ v ] dp[i][v] dp[i][v]右边的部分为刚计算过的需要保存给下一行使用的数据,而 d p [ i ] [ v ] dp[i][v] dp[i][v]左上角的阴影部分为当前需要使用的部分。将这两者结合一下,即把 d p [ i ] [ v ] dp[i][v] dp[i][v]左上角和右边的部分放在一个数组里,每计算出一个 d p [ i ] [ v ] dp[i][v] dp[i][v],就相当于把 d p [ i − 1 ] [ v ] dp[i-1][v] dp[i1][v]抹消,因为在后面的运算中 d p [ i − 1 ] [ v ] dp[i-1][v] dp[i1][v]再也用不到了。这种技巧称为滚动数组

代码如下:

for(int i=0; i<=n; i++){
    for(int v=V; v>=w[i]; v--){
        dp[v] = max(dp[v], dp[v-w[i]]+c[i]);
    }
}

这样01背包问题就可以用一维数组表示来解决了,空间复杂度为O(V)。

完整代码

#include <cstdio>
#include <algorithm>
using namespace std;

const int maxn = 100; //物品最大件数
const int maxv = 1000; //V的上限

int w[maxn], c[maxn], dp[maxv];

int main(){
    int n, V;
    scanf("%d%d",&n, &V);
    for(int i=0;i<n;i++){
        scanf("%d",&w[i]);
    }
    for(int i=0;i<n;i++){
        scanf("%d",&c[i]);
    }
    
    //边界
    for(int v=0; v<=V; v++){
        dp[v] = 0;
    }
    for(int i=1; i<=n; i++){
        for(int v=V; v>=w[i]; v--){
            //状态转移方程
            dp[v] = max(dp[v], dp[v-w[i]] + c[i]);
        }
    }
    
    //寻找dp[0...V]中最大的即为答案
    int max = 0;
    for(int v=0; v<=V; v++){
        if(dp[v] > max){
            max = dp[v];
        }
    }
    
    printf("%d\n",max);
    
    return 0;
}

/*
输入:
5 8
3 5 1 2 2
4 5 2 1 3

输出:
10
*/

动态规划是如何避免重复计算的问题在01背包问题中非常明显。在一开始暴力枚举每件物品放或者不放入背包是,其实忽略了一个特写:第i件物品放或者不放而产生的最大值是完全可以由前面i-1件物品的最大值来决定的,而暴力做法无视了这一点。

另外,01背包中的每个物品都可以看作一个阶段,这个阶段中的状态有 d p [ i ] [ 0 ] dp[i][0] dp[i][0]~ d p [ i ] [ V ] dp[i][V] dp[i][V],它们均由上一个阶段的状态得到。事实上,能够划分阶段的问题来说,都可以尝试把阶段作为状态的一维,这可以使我们更方便地得到满足无后效性的状态。从而也可以得到这么一个技巧,如果当前设计的状态不满足无后效性,那么不妨把状态进行升维,即增加意味或者若干维来表示相应的信息,这样可能就能满足无后效性了

一个例题

这里再给出一个例题,PAT1068,实际上我是从这道题出发,才开始学习了前面《算法笔记》书上01背包问题的基础内容,再参考了https://blog.csdn.net/a617976080/article/details/99694845这篇博客,里面的c[i][v]的使用精髓是书上没有的,可以参考学习。动态规划的题目很难,需要多学习多训练才能慢慢领悟。这里我附上带注释的代码,供大家参考。

#include <cstdio>
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

const int MAX_N = 10010;
const int MAX_M = 110;
//将其看做价值和质量等同的0-1背包问题,将硬币从大到小排序后,依次考虑是否放入第i个硬币

//w[i]表示金币面值(同时是重量w[i]和价值c[i]),dp[v]表示空间为v时最大的价值
int w[MAX_N],dp[MAX_M];
//c[i][v]表示达到价值v时是否放入第i个硬币
int c[MAX_N][MAX_M];

//N为硬币个数,M为应付金额
int N, M;

bool cmp1(int a, int b){
    return a>b;
}

int main(){
    cin>>N>>M;
    //下标从1开始
    for(int i=1; i<=N; i++) cin>>w[i];

    //从大到小排序w[1]~w[N]。为了后面逆向输出(从小到的大的)路径
    sort(w+1, w+N+1, cmp1);

    //边界
    for(int v=0; v<=M; v++){
        dp[v] = 0;
    }

    for(int i=1; i<=N; i++){
        for(int v=M; v>=w[i]; v--){
            //状态转移方程
            //等价于dp[v] = max(dp[v], dp[v-w[i]]+c[i]); 在这道题中w[i]=c[i]
            //一定得是>=,这表示如果有机会选后面的(小的)(也就是当前的i),一定选后面
            if(dp[v-w[i]] + w[i] >= dp[v]){
                dp[v] = dp[v-w[i]] + w[i];
                //表示在当前的i物品下的当前v下这个物品被选中了
                c[i][v] = true;
            }else{
                c[i][v] = false;
            }
        }
    }

    vector<int> res;
    //遍历完成后如果dp[M]!=M即没有一个序列能达到M,否则逆向查找出路径(题目要求输出字典序小的)
    if(dp[M] != M){
        printf("No Solution");
    }else{
        //res.push( c[k从N->1][v -= w[k]] )
        int i = N, v = M;
        while(i >= 1){
            if(c[i][v] == true){
                res.push_back(w[i]);
                v-= w[i];
            }
            i--;
        }

        for(int i=0; i<res.size(); i++){
            printf("%d",res[i]);
            if(i != res.size()-1) printf(" ");
        }
    }

    return 0;
}

/*
8 9
5 9 8 7 2 3 4 1

1 3 5


4 8
7 2 4 3

No Solution
*/

参考文献

  1. 《算法笔记》
  2. https://blog.csdn.net/a617976080/article/details/99694845
  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值