动态规划之背包问题
Posted bqyb
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态规划之背包问题相关的知识,希望对你有一定的参考价值。
背包问题是动态规划的一个分支,这里先简单介绍一下动态规划的思想。
动态规划程序设计是对解最优化问题的一种途径、一种方法,而不是一种特殊算法。不像搜索或数值计算那样,具有一个标准的数学表达式和明确清晰的解题方法。动态规划程序设计往往是针对一种最优化问题,由于各种问题的性质不同,确定最优解的条件也互不相同,因而动态规划的设计方法对不同的问题,有各具特色的解题方法,而不存在一种万能的动态规划算法,可以解决各类最优化问题。因此在学习时,除了要对基本概念和方法正确理解外,必需具体问题具体分析处理,以丰富的想象力去建立模型,用创造性的技巧去求解。我们也可以通过对若干有代表性的问题的动态规划算法进行分析、讨论,逐渐学会并掌握这一设计方法。(以上来自百度百科)
简单来说动态规划是一个思想,而不是一个固定的算法模板,我们需要通过这种思想确定状态转移方程(一个好的状态),然后再求解。
动态规划适用于很多情况,其中一些情况被统一划分归类,而背包问题则是一种最为常见的动态规划问题。
背包问题又分为许多种,如:01背包,完全背包等
我们这里介绍最为常见的三种,分别是01背包,完全背包,和多重背包。
1.01背包
这是一个经典的动态规划问题,另外在贪心算法里也有背包问题,至于二者的区别在此就不做介绍了。
题目一般都是有N件物品和一个容量为V的背包。第i件物品的体积是v[i],价值是c[i],将哪些物品装入背包可使价值总和最大?
分析起来很简单,每一种物品都有两种可能,即放入背包或者不放入背包。可以用dp[i][j]表示第i件物品放入容量为j的背包所得的最大价值,则状态转移方程可以推出如下:
dp[i][j]=max{dp[i-1][j-v[i]]+c[i],dp[i-1][j]};
先放下代码:
for (int i = 1;i <= N;i++) //枚举物品 { for (int j = 0;j <= V;j++) //枚举背包容量 { f[i][j] = f[i - 1][j]; if (j >= v[i]) { f[i][j] = Max(f[i - 1][j],f[i - 1][j - v[i]] + c[i]); } }
}
我们可以发现0-1背包的状态转移方程 dp[i][j] = max{dp[i-1][j-w[i]]+v[i],dp[i-1][j]}的特点,当前状态仅依赖前一状态的剩余体积与当前物品体积v[i]的关系。根据这个特点,我们可以将dp降到一维即dp[j] = max{dp[j],dp[j-w[i]]+v[i]}。从这个方程中我们可以发现,有两个dp[j],但是要区分开。等号左边的dp[j]是当前i的状态,右边中括号内的dp[j]是第i-1状态下的值。
所以为了保证状态的正确转移,我们需要先更新等号左边中的dp[j](当前状态的dp[j])。
#include <iostream> using namespace std; #define MAXSIZE 100 int w[MAXSIZE]; int v[MAXSIZE]; int maxv; int n; int dp[MAXSIZE]; int max(int a, int b) { if (a > b) return a; else return b; } int main() { cin >> n >> maxv; for (int i = 1; i <= n; i++) { cin >> w[i] >> v[i]; } for (int i = 0; i <= maxv; i++) dp[i] = 0; for (int i = 1; i <= n; i++) { //只有当j >= w[i],dp[j]才能进行选取最大值,否则dp[j]将不作更新,等于dp[i-1][j]。 for (int j = maxv; j >= w[i]; j--) { dp[j] = max(dp[j], dp[j - w[i]] + v[i]); } } cout << dp[maxv] << endl; return 0; }
对于01背包问题,我们通常使用的是第二种方法,相比之第一种,第二种的空间复杂度无疑是减小了许多。
现在我们来看这个状态转移方程:dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
我们先分析上一个状态也就是dp[j-w[i]],我们将上一个状态转移到现在的状态dp[j]有三种情况:
首先在现在的状态dp[j],在不转移的情况下这也是一种情况;
第二种是dp[j-w[i]],即当前物品不放入上一个状态;
第三种就是dp[j-w[i]]+v[i],即当前物品放入上一个状态;
状态转移的过程则是要求我们在这三者当中取最大值,我们知道v[i]必定是大于等于0的,也就是说dp[j-w[i]]+v[i]必定是大于等于dp[j-w[i]]的,我们可以把两者合并为一种情况,然后是dp[j],为什么dp[j]也算是一种情况,我们回到for循环可以发现第一个物品枚举完之后有一些状态dp[k*w[1]]更新了,它们的值都变成v[i],然后我们在接下来的过程当中dp[j]可能是已经有了值的,通过状态转移方程我们可以知道这个值是当物品总重量达到j时刻物品总价值的最大值。所以我们需要将dp[j]也看作是一种情况。
2.完全背包
对于01背包问题,它的特点是:每一件物品之多只能选择一件,即在背包中该物品数量只有0和1两种情况。
现在扩展一下,有一个容积为V的背包,同时有n种物品,每种物品均有无数多个,并且每种物品的都有自己的体积和价值。求使用该背包最多能够装的物品价值总和。
这就是完全背包问题。
如果按照0-1背包的思路求解该问题,可设当前物品的体积为w,价值为v,考虑到背包中最多存放V/w件该物品,那么该物品的可选数量就为V/w件。依次可以对所有的物品进行拆分,最后对拆分的所有物品做0-1背包即可得到答案。但是,这样拆分会使物品数量大大增加,其时间复杂度为:O(V*∑ni=1(V/wi))。
可见,当V较大时每个物品的体积较小时,其时间复杂度会显著增大。所以将完全背包问题转化为0-1背包问题去解决的方法不可靠。
在0-1背包的解决算法中,其中一段代码是该算法的核心算法,如下:
struct Good{ int w; int v; }goods[101]; int dp[101][1001]; int n,S;//n表示有n个物品,S表示背包的最大容积 for (i = 1; i <= n; i++) { for (j = S; j >= goods[i].w; j--) { dp[j] = max(dp[j], dp[j - goods[i].w] + goods[i].v); } }
在这段代码中,之所将j初始化为S,逆序循环更新状态是为了保证在更新dp[j]时,dp[j-goods[i].w]的状态尚未因为本次更新而发生改变,即等价于由
dp[i-1][j-goods[i].w]转移得到dp[i][j]。保证了更新dp[j]时,dp[j-goods[i].w]是没有放入物品i时的数据dp[i-1][j-goods[i].w]。
在解决完全背包问题时,可以借鉴这个思路。在完全背包中,每个物品可以被无限次选择,那么状态dp[i][j]恰好可以由可能已经放入物品i的状态dp[i][j-goods[i].w]转移而来。可以将上面的代码改写如下:
for (i = 1; i <= n; i++) { for (j = goods[i].w; j <= S; j++) dp[j] = max(dp[j], dp[j - goods[i].w] + goods[i].v); }
这样不需要将物品拆分,但是本质上并没什么区别.
3.多层背包
多重背包问题是0-1背包问题和完全背包问题的综合体,可以描述如下:从n种物品向容积为V的背包装入,其中每种物品的体积为w,价值为v,数量为k,问装入的最大价值总和?
我们知道0-1背包问题是背包问题的基础,所以在解决多重背包问题的时候,要将多重背包向0-1背包上进行转换。在多重背包问题中,每种物品有k个,可以将每种物品看作k种,这样就可以使用0-1背包的算法。但是,这样会增加数据的规模。因为该算法的时间复杂度为O(V*∑ni=1ki),所以要降低每种物品的数量ki。
代码和完全背包的代码基本一样,唯一的不一样就是完全背包的每一种物品数量不限.
#include<bits/stdc++.h> use namespace std; const int maxn=10005; int w[maxn],v[maxn] int dp[maxn]; int main() { int n,maxv;
cin>>n>>maxv; int index=0; for(int i=0;i<n;i++) { int a,b,c; cin>>a>>b>>c; while(a--) { w[index]=b; v[index++]=c; } } dp[0]=1; for(int i=0;i<index;i++) { for(int j=maxv;j>=w[i];j--) { dp[j]=max(dp[j],dp[j-w[i]]+v[i]); } } cout<<dp[maxv]<<endl; return 0; }
对于完全背包,不推荐使用上面这个方法,下面有一种复杂度更小的代码:
#include<bits/stdc++.h> using namespace std; const int maxn=1005; struct node{ int w; int v; }p[maxn]; int dp[maxn]; int main(void) { int n,maxv; cin>>n>>maxv; int index=0; for(int i=0;i<n;i++) { int a,b,c; cin>>a>>b>>c; while(a>0) { if(a&1) { p[index].v=c; p[index++].w=b; } a/=2; b*=2; c*=2; } } dp[0]=0; for(int i=0;i<index;i++) { for(int j=maxv;j>=p[i].w;j--) { dp[j]=max(dp[j],dp[j-p[i].w+p[i].v]); } } cout<<dp[maxv]<<endl; return 0; }
这里在划分k个物品的时候用了小技巧,用二进制将所有的情况都表示了出来减少了情况的种类,复杂度相比上一个代码简单了许多,.尤其是在物品数量多的时候.
以上是关于动态规划之背包问题的主要内容,如果未能解决你的问题,请参考以下文章