完全背包问题(动态规划)

完全背包问题

题目:

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

 

一、验证动态规划求解的可行性

这个问题可以不可以像01背包问题一样使用动态规划来求解呢?来证明一下即可。

首先,先用反证法证明最优化原理:

假设完全背包的解为F(n1,n2,...,nN)(n1,n2 分别代表第1、第2件物品的选取数量),完全背包的子问题为,将前i种物品放入容量为t的背包并取得最大价值,其对应的解为:F(n1,n2,...,ni),假设该解不是子问题的最优解,即存在另一组解F(m1,m2,...,mi),使得F(m1,m2,...,mi) > F(n1,n2,...,ni),那么F(m1,m2,...,mi,...,nN) 必然大于 F(n1,n2,...,nN),因此 F(n1,n2,...,nN) 不是原问题的最优解,与原假设不符,所以F(n1,n2,...,ni)必然是子问题的最优解。

再来看看无后效性:

对于子问题的任意解,都不会影响后续子问题的解,也就是说,前i种物品如何选择,只要最终的剩余背包空间不变,就不会影响后面物品的选择。即满足无后效性。

因此,完全背包问题也可以使用动态规划来解决。

二、基本思路

 

这个问题非常类似于 01背包问题,所不同的是每种物品都有无限件。也就是从每种物品的角度考虑,与它相关的策略已并非取或不取两种,而是有取 0 件、取 1 件、取 2 件 ......等很多种。如果仍然按照解 01背包时的思路,令 f[i][v] 表示前 i 种物品恰放入一个容量为 v 的背包的最大权值。仍然可以按照每种物品不同的策略写出状态转移方程:

状态转移方程:

这跟 01背包问题一样有  O(VN)  个状态需要求解,但求解每个状态的时间已经不是常数了,求解状态  f[i][v]  的时间是  ,总的复杂度可以认为是  ,是比较大的。

基本代码如下:


#include <iostream>
#include <algorithm>
#define N 1002
using namespace std;

int f[N][N];
int w[N];
int v[N];

int main() {
    int n,W; cin >> n >> W;
    for(int i=1;i<=n;i++) {
        cin >> w[i] >> v[i];
    }
    for ( int i = 1; i <= n; i++ ) {
        for ( int j = 0; j <= W; j ++) {
            for (int k = 0; k*w <= j; k++) {
                f[i][j] = max(f[i-1][j],f[i-1][j-w[i]*k] + v[i]*k);
            }
        }
    }
    cout << f[n][W] <<endl;
    return 0;
}

状态转移方程:同 01 背包问题一样,完全背包问题空间复杂度可以优化到Θ(N)1。

空间复杂度优化后代码如下:


#include <iostream>
#include <algorithm>
#define N 1002
using namespace std;

int f[N];
int w[N];
int v[N];

int main() {
    int n,W; cin >> n >> W;
    for(int i=1;i<=n;i++) {
        cin >> w[i] >> v[i];
    }
    for ( int i = 1; i <= n; i++ ) {
        for ( int j = W; j >= 0; j --) {
            for (int k = 0; k*w <= j; k++) {
                f[j] = max(f[j],f[j-w[i]*k] + v[i]*k);
            }
        }
    }
    cout << f[W] <<endl;
    return 0;
}
 
 

二、一个简单有效的优化

完全背包问题有一个很简单有效的优化,是这样的:若两件物品 i 、j 满足  且   ,则将物品 j 去掉,不用考虑。这个优化的正确性显然:任何情况下都可以将价值小费用高的 j 换成物美价廉的 i ,得到至少不会更差的方案。对于随机生成的数据,这个方法往往会大大减少物品的件数,从而加快速度。这个优化可以简单的 Θ(N2) 实现,一般都可以承受。

然而这个并不能改善最坏情况的复杂度,因为有可能特别设计的数据可以一件物品也去不掉。另外,针对背包问题而言,比较不错的一种方法是:首先将费用大于 V 的物品去掉,然后使用类似计数排序的做法,计算出费用相同的物品中价值最高的哪个,可以 Θ(V + N) 地完成这个优化。

三、转化为01背包问题求解 

既然 01背包问题是最基本的背包问题,那么我们可以考虑把完全背包问题转化为 01背包问题来解。最简单的想法是,考虑到第 i 种物品最多选  V/c[i]  件,于是可以把第 i 种物品转化为  V/c[i]  件费用及价值均不变的物品,然后求解这个 01背包问题。

这样完全没有改进基本思路的复杂度,但毕竟给了我们将完全背包问题转化为 01背包问题的思路:将一种物品拆成多件物品。

更高效的转化方法是:把第 i 种物品拆成费用为  、价值为   的若干件物品,其中 k 满足    。这是二进制的思想,因为不管最优策略选几件第 i 种物品,总可以表示成若干个  件物品的和。这样把每种物品拆成    件物品,是一个很大的改进。

 
 

四、O(VN)的算法

这个算法使用一维数组,先看伪代码:

for i=1..N
    for v=0..V
        f[v]=max{f[v],f[v-cost]+weight}
 

你会发现,这个伪代码与 01背包问题的伪代码只有v的循环次序不同而已。

为什么这样一改就可行呢?首先想想为什么01背包问题中要按照v=V..0的逆序来循环。

这是因为要保证第i次循环中的状态f[i][v]是由状态f[i-1][v-c[i]]递推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第i件物品”这件策略时,依据的是一个绝无已经选入第i件物品的子结果f[i-1][v-c[i]]。

而现在完全背包的特点恰是每种物品可选无限件,所以在考虑“加选一件第i种物品”这种策略时,却正需要一个可能已选入第i种物品的子结果f[i][v-c[i]],所以就可以并且必须采用v=0..V的顺序循环。这就是这个简单的程序为何成立的道理。

值得一提的是,上面的伪代码中两层for循环的次序可以颠倒。这个结论有可能会带来算法时间常数上的优化。

这个算法也可以以另外的思路得出。例如,将基本思路中求解f[i][v-c[i]]的状态转移方程显式地写出来,代入原方程中,会发现该方程可以等价地变形成这种形式:

f[i][v]=max{f[i-1][v],f[i][v-c[i]]+w[i]}

这个方程表示的意思:

“ 将前 i 件物品放入容量为 v 的背包中 ” 这个子问题,若只考虑第 i 件物品的策略(放多少个),那么就可以转化为一个只牵扯前 i 件物品的问题。

如果不放第 i 件物品,那么问题就转化为 “ 前 i-1 件物品放入容器为 v 的背包中 ” ,价值为 f[i-1][v] ;

如果放第 i 件物品,那么问题就转化为 “ 前 i 件物品(这里不是前 i-1 的原因是,这里的物品都有无限件,可能此前已经放了第 i 件物品)放入剩下的容量为 v-c[i] 的背包中 ”,背包中此时能获得的最大价值就是 f[i][v-c[i]] 价值再加上通过放入第 i 件物品获得的价值 w[i]。

代码:


#include <iostream>
#include <algorithm>
#define N 1002
using namespace std;

int f[N];
int w[N];
int v[N];

int main() {
    int n,W; cin >> n >> W;
    for(int i=1;i<=n;i++) {
        cin >> w[i] >> v[i];
    }
    for ( int i = 1; i <= n; i++ ) {
        for ( int j = w[i]; j <= W; j ++) {
            f[j] = max(f[j],f[j-w[i]] + v[i]);
        }
    }
    cout << f[W] <<endl;
    return 0;
}

最后抽象出处理一件完全背包类物品的过程伪代码:

procedure CompletePack(cost,weight)
    for v=cost..V
        f[v]=max{f[v],f[v-c[i]]+w[i]}
 
发布了118 篇原创文章 · 获赞 63 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/qq_42185999/article/details/104499556
今日推荐