动态规划——背包问题九解(01背包)

01背包问题

问题

有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。 第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。 输出最大价值。

输入格式

第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。 接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i
件物品的体积和价值。

输出格式

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

数据范围

0<N,V≤1000 0<vi,wi≤1000

题解

题解(1)二维空间

(1)定义状态: f [ i ] [ j ] f[i][j] f[i][j] i i i件物品,在容量为 j j j下的最优解(最大价值)

解释:当前状态 i i i依赖于上一个状态 i − 1 i-1 i1,,即在f[0][0]时开始,有N件物品,则需要N次决 策,每一次对第 i 件物品的决策,状态f[i][j]不断由之前的状态更新而来。

(2)状态转移方程:当背包容量足够时可以选择装入第 i 件物品或者不装。
装入: f [ i ] [ j ] = f [ i − 1 ] [ j − w [ i ] + v [ i ] f[i][j] = f[i-1][j - w[i] + v[i] f[i][j]=f[i1][jw[i]+v[i]
不装: f [ i ] [ j ] = f [ i − 1 ] [ j ] f[i][j] = f[i-1][j] f[i][j]=f[i1][j]

因此状态转移方程为:
f [ i ] [ j ] = m a x { f [ i − 1 ] [ j ] , f [ i − 1 ] [ j − w [ i ] + v [ i ] } f[i][j] = max\{f[i-1][j], f[i-1][j - w[i] + v[i]\} f[i][j]=max{f[i1][j],f[i1][jw[i]+v[i]}
(3)边界条件
当背包容量不足时: j < w [ i ] j < w[i] j<w[i],此时下一状态前 i 个物品最优解即为前 i−1 个物品最优解。
当 前 状 态 f [ i ] [ j ] = f [ i − 1 ] [ j ] 当前状态f[i][j] = f[i-1][j] f[i][j]=f[i1][j]
代码:

#include <iostream>
using namespace std;

const int N = 1010;
int V[N], W[N];
int f[N][N];
//int f[N];
int n, w;

int main(void)
{
    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 = 1; j <= w; j++)
        {
            if(W[i] > j)
                f[i][j] = f[i-1][j];
            else if(W[i] <= j)
                f[i][j] = max(f[i][j], f[i-1][j-W[i]] + V[i]);
                
        }
    }
    for(int i = 0; i <= n; i++)
    {
        for(int j = 0; j <= v; j++)
            cout << f[i][j] << " ";
        cout << endl;
    }
    /*for(int j = 0; j <= w; j++)
        cout << f[j] << " ";*/
    cout << endl;
    cout << f[n][w] << endl;
    system("pause");
    return 0;
}

题解(2)状态压缩至一维

将二维状态 f [ i ] [ j ] f[i][j] f[i][j]压缩至一维 f [ j ] f[j] f[j],此时少了维度 i ,之所以可以压缩状态 i 是因为每一次的第 i 个状态只与上一状态 i - 1 有关,与其他无关,因此可以对物品维度进行压缩,直接在 N 件物品上寻找最优值。
(1)定义状态: f [ j ] f[j] f[j]:N 件物品,在背包容量为 j 时的最优解
注意1:对背包容量 j 进行遍历时要从 w (最大容量)开始,然后递次倒序遍历。
注意2之所以要倒序遍历,是因为在二维情况下,状态 f [ i ] [ j ] f[i][j] f[i][j]是由上一轮 i − 1 i - 1 i1的状态得来的, f [ i ] [ j ] f[i][j] f[i][j] f [ i − 1 ] [ j ] f[i - 1][j] f[i1][j]是独立的。而优化到一维后,如果我们还是正序,则有 f [ 较 小 体 积 ] f[较小体积] f[]更新到 f [ 较 大 体 积 ] f[较大体积] f[],则有可能本应该用第 i − 1 i-1 i1轮的状态却用的是第 i 轮的状态。

(2)状态转移方程
f [ j ] = m a x { f [ j ] , f [ j − w [ i ] ] + v [ i ] } f[j] = max\{f[j], f[j - w[i]] + v[i]\} f[j]=max{f[j],f[jw[i]]+v[i]}
代码:

#include <iostream>
using namespace std;

const int N = 1010;
int V[N], W[N];
//int f[N][N];
int f[N];
int n, w;

int main(void)
{
    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 >= W[i]; j--)
        {
            if(W[i] > j)
                //f[i][j] = f[i-1][j];
                f[j] = f[j];
            else if(W[i] <= j)
                //f[i][j] = max(f[i][j], f[i-1][j-W[i]] + V[i]);
                f[j] = max(f[j], f[j - W[i]] + V[i]);
                
        }
    }
    /*for(int i = 0; i <= n; i++)
    {
        for(int j = 0; j <= v; j++)
            cout << f[i][j] << " ";
        cout << endl;
    }*/
    for(int j = 0; j <= w; j++)
        cout << f[j] << " ";
    cout << endl;
    cout << f[w] << endl;
    system("pause");
    return 0;
}

求解在恰好装满背包的情况下具体装入方案

为求解恰好装满背包时取得的最大价值,这里需要引入有效状态无效状态,我们最开始时初始化转移矩阵全为0,这个初始化的方法意思是:对任意的一个状态,把当前的价值初始化为0,代表背包为空时所包含的物体价值为0 。即认为不管背包当前容量,背包是空的那么价值就是0,,没装满背包的状态都是有效的状态,我们称之为有效状态。
当要求恰好装满时,我们就不可以这么做了,因为我们认为背包没恰好装满的话,当前背包的状态无效,即无效状态,只有恰好装满时候才是有效状态。
我们不妨定义当背包的状态为无效状态时,f[i]的值是负无穷,这样我们就可以从状态转移矩阵中区分出有效状态和无效状态了。
先不关心如何计算f,有了这个定义,对于恰好装满的01背包问题,我们只需要判断f[V]中的值是否是负无穷,就知道背包能否装满了。当f[V]中的值不是负无穷,我们接下来需要设计状态转移方程,使f[V]就是背包恰好装满时候的最大价值。

这里参考了另一篇博客,那里有推到有效状态和无效状态的证明
https://blog.csdn.net/Iseno_V/article/details/100697105

这里只介绍如何解决问题
也就是说在初始化转移矩阵上下文章,即f[0] = 0,else f[i] = -INF,这里的意思是说智能从f[0]开始算有效状态,其他均为无效状态。恰好把问题转化成背包容量为0时,装的物体体积为0是有效状态。

for(int i = 0; i < N; i++)
        f[i] = -100000000;
    f[0] = 0;
    for(int i = 1; i <= n; i++)
    {
        for(int j = w; j >= W[i]; j--)
        {
            //f[i][j] = max(f[i][j], f[i-1][j-W[i]] + V[i]);
            if(f[j] < f[j - W[i]] + V[i])
            {
                path[i][j] = 1;
                 f[j] = f[j - W[i]] + V[i];
            }
            if(f[j] < 0)
                f[j] = -100000000;
        }
    }
下面介绍如何求解被选物品

(1)定义
需要定义一个路径矩阵path[N][N],表示有n(物品数)行m(背包重量)列,在进行状态转移时,若 f [ j ] < f [ j − W [ i ] ] + V [ i ] f[j] < f[j - W[i]] + V[i] f[j]<f[jW[i]]+V[i]则在路径矩阵中赋值 1 ,其他为 0 ,这样求解出物品矩阵了

(2)输出
对背包物品个数 n 进行倒序循环,若path[i][w]为 1 ,则输出 W[i],并把当前背包容量减去W[i],即w - W[i],代码如下

int p = w;
    for(int i = n; i >= 0; i--)//路径输出 
	{
		if(w < 0) break;
		if(path[i][p])
		{
			cout << W[i] << ' ';
			p -= W[i];
		}
    }

完整代码

#include <iostream>
using namespace std;

const int N = 1010;
int V[N], W[N];
//int f[N][N];
int f[N];
int n, w;
int path[N][N];

int main(void)
{
    cin >> n >> w;
    for(int i = 1; i <= n; i++)
    {
        cin >> W[i] >> V[i];
    }
    for(int i = 0; i < N; i++)
        f[i] = -100000000;
    f[0] = 0;
    for(int i = 1; i <= n; i++)
    {
        for(int j = w; j >= W[i]; j--)
        {
            //f[i][j] = max(f[i][j], f[i-1][j-W[i]] + V[i]);
            if(f[j] < f[j - W[i]] + V[i])
            {
                path[i][j] = 1;
                 f[j] = f[j - W[i]] + V[i];
            }
            if(f[j] < 0)
                f[j] = -100000000;
        }
    }
    /*for(int i = 0; i <= n; i++)
    {
        for(int j = 0; j <= v; j++)
            cout << f[i][j] << " ";
        cout << endl;
    }*/
    // for(int j = 0; j <= w; j++)
    //     cout << f[j] << " ";
    // cout << endl;
    // cout << f[w] << endl;
    int p = w;
    for(int i = n; i >= 0; i--)//路径输出 
	{
		if(w < 0) break;
		if(path[i][p])
		{
			cout << W[i] << ' ';
			p -= W[i];
		}
    }
    cout << endl;
    if (f[w] > 0)
	{
		cout << f[w] << endl;//背包恰好装满了,输出结果
	}
	else
	{
		cout << "error" << endl;//背包不能恰好装满
	}
    system("pause");
    return 0;
}
  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值