背包问题 动态规划 滚动数组实现

来源:互联网 发布:交互式视频制作软件 编辑:程序博客网 时间:2024/06/05 18:21

参考博客
http://blog.csdn.net/liusuangeng/article/details/38374405
http://blog.sina.com.cn/s/blog_8cf6e8d90100zldn.html

比起别的讲解背包问题的博客,这一篇更加注重在于理解如何用滚动数组实现动态规划。

背包问题特征
1.存在类似一个集合的求解所有子集的特性。关于一个集合的所有子集,会直接考虑每一个集合元素存在或不存在于子集中,最后对于一个由n个元素构成的集合,其子集数目是2的n次方个。也就是1/0问题。
2.在某些约束下,取舍以达到某种目的,一般为优化,也有一些是求解组合。


基础的背包问题

有N件物品和一个容量为V的背包。第i件物品的重量是w[i],价值是v[i]。求解将哪些物品装入背包可使这些物品的重量总和不超过背包容量,且价值总和最大。每种物品仅有一件,可以选择放或不放。百度百科

每一种物品可以选择放进背包或者不放进背包,那就一件一件物品来考量。
定义 dp[i][j] 表示遍历到第 i 个物品的时候,放入物品 i 或者不放入物品 i ,背包容量为 j 时的最大价值。两种情况:

(1)放进物品 i : 那其价值就等于 dp[i-1][j - w[i]] + value[i]。第一项是指背包里有物品 i 时,改变了原来用前 i - 1个物品填充背包的内容,其原来背包的价值已经发生改变,变成了用前面遍历过的 i - 1个物品来填充容量 j - w[i] 的背包的最大价值。第二项是本身物品 i 带来的价值。
(2)不放进物品 i :那就是仅仅使用前 i - 1 个物品来填充容量为 j 的背包的最大价值。

要计算dp[i][j] 则是在上面两种情况里面选择能够得到最大价值的情况。

转移方程为 dp[i][j] = max{ dp[i-1][j - w[i]] + value[i], dp[i-1][j] }.

二维数组实现

int volumn, numItem;int weights[numItem + 1], value[numItem + 1];//TODO:初始化背包总容量volumn, 物体种数numItem,物体重量数组weights,物体价值数组valueint dp[numItem + 1][volumn + 1];//TODO: 初始化dp全为0for(int i = 1; i<= numItem; i++){    //对于背包容量 j 小于物体容量weights[i]的情况不需要考虑    for(int j = 1; j <= volumn; j++){        if(j < weights[i]){            dp[i][j] = dp[i-1][j];        }else{            dp[i][j] = max(dp[i-1][j], dp[i-1][j-weights[i]] + value[i]);        }    }}return dp[numItem][volumn];//背包最大价值

使用滚动数组实现

重新观察转移方程 dp[i][j] = max{ dp[i-1][j - w[i]] + value[i], dp[i-1][j] }
发现 dp[i] 的状态仅仅和 dp[i-1]有关,所以仅仅只需要保存 i-1 时刻dp的状态。
考虑用一个一位数组 dp, dp[j] 表示背包容量 j 时的最大价值。
在考虑是否要放进物体 i 的之前,此时 dp[j] 数组保存的状态还是用前 i - 1 个物体放进容量为 j 时候的最大价值。所以可以直接用原来的dp[j] 来代替原来的 dp[i-1][j]。
对于状态方程的一项 dp[i-1][j - w[i]] + value[i],可以明确 j - w[i] < j。因为考虑物体 i 时需要更新的 dp[j] (即dp[i][j])需要通过dp[i-1][j - w[i]]来计算。为了保证使用的dp[j - w[i]]是仅考虑完第 i - 1个物体时候的值,所以dp[j - w[i]]的值更新要发生在dp[j]之后。又因为 j - w[i] < j,所以 dp[j] 需要逆序更新。如果是顺序更新,那么容量为 j - w[i] 和容量为 j 两种情况下,很有可能会放进同一个物体两次只要该物体的容量比 j - w[i]还小,所以不符合这个背包问题的约束。

int volumn, numItem;int weights[numItem + 1], value[numItem + 1];//TODO:初始化背包总容量volumn, 物体种数numItem,物体重量数组weights,物体价值数组valueint dp[volumn + 1];//TODO: 初始化dp全为0for(int i = 1; i<= numItem; i++){    //对于背包容量 j 小于物体容量weights[i]的情况不需要考虑    for(int j = volumn; j >= weights[i]; j++){        dp[j] = max(dp[j], dp[j-weights[i]] + value[i]);        }    }}return dp[numItem][volumn];//背包最大价值

在之前的二维数组实现里面,有一个if的判断,

if(j < weights[i]){    dp[i][j] = dp[i-1][j];}else{    dp[i][j] = max(dp[i-1][j], dp[i-1][j-weights[i]] + value[i]);}

滚动数组实现中,由于在考虑物体 i 时,dp[j]不更新则保留了考虑物体 i - 1 时候的状态,所以这里的判断可以转化成滚动数组实现里面的 j 从 volumn递减到weight[i]为止即可。


完全背包问题

有N件物品和一个容量为V的背包。第i件物品的重量是w[i],价值是v[i]。求解将哪些物品装入背包可使这些物品的重量总和不超过背包容量,且价值总和最大。每种物品有无穷多件,可以选择放或不放。

转移方程为 f[i][j]=max{ f[i-1][j-k*w[i]]+k*value[i]|0<=k*w[i]<=j }.

顺序枚举背包容量时,由于 j - w[i] < j , 则会先计算 j - w[i]。在背包容量为 j - w[i] 时,一旦装入了物品 i,由于求f[v]需要使用f[i - 1][ j - w[i]],而若求f[v]时也可以装入物品 i 的话,那么在背包容量为 j 时就装入两次物品 i 。又若 j - w[i]是由之前的状态推出,它们也成功装入物品 i 的话,那么容量为 j 的背包就装入了多次物品 i 了。
注意,此时,在计算 f[j] 时,已经把物品 i 能装入的全装入容量为 j 的背包了,此时装入物品 i 的次数为最大。

顺序枚举容量是完全背包问题最简捷的解决方案。

int volumn, numItem;int weights[numItem + 1], value[numItem + 1];//TODO:初始化背包总容量volumn, 物体种数numItem,物体重量数组weights,物体价值数组valueint dp[volumn + 1];//TODO: 初始化dp全为0for(int i = 1; i<= numItem; i++){    //对于背包容量 j 小于物体容量weights[i]的情况不需要考虑    for(int j = weights[i]; j <= volumn; j++){        dp[j] = max(dp[j], dp[j-weights[i]] + value[i]);        }    }}return dp[numItem][volumn];//背包最大价值

多重背包问题

有N种物品和一个容量为V的背包。第i种物品最多有n[i]件可用,每件重量是w[i],价值是value[i]。求解将哪些物品装入背包可使这些物品的重量总和不超过背包容量,且价值总和最大。

转移方程 f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i]|0<=k<=n[i]}

一个直接的方法是加多一重循环去循环k在遍历物品和遍历背包容量中间。复杂度是O(V*Σn[i])。

另外一个是将n[i]件物品 分解成多个不同重量的新物体,重量倍数分别为一件物品的1,2,4,…,2^(k-1),n[i]-2^k+1(且k是满足n[i]-2^k+1>0的最大整数)倍,然后再求解。复杂度会降到O(V*Σlog n[i])。二进制优化了解

代码如下

int k, t;k = 1;t = n[i];while(t > k){    for(j=W; j>=c[i]*k; --j)    {        f[j] = max(f[j], f[j-c[i]*k] + w[i]*k);    }    t -= k;    k *= 2;}for(j=W; j>=c[i]*t; --j){    f[j] = max(f[j], f[j-c[i]*t] + w[i]*t);}

到这里,几个比较基础的背包问题已经讲完了。动态规划的转移方程如果能写出来,基本上就能解决问题了。不过,这就是最难的地方了。

0 0
原创粉丝点击