0-1背包问题常见问题的总结

动态规划问题与实际问题联系紧密,所以动态规划算法有着很广泛的应用,各类比赛题中也必不可少。最近花了不少功夫弄明白了经典的0-1背包问题,做个总结。


0-1背包问题:有n种物品,每种只有一个。第i种物品的体积为Vi,重量为Wi。选一些物品到一个容量为C的背包中,使得背包内物品在总体积不超过C的前提下重量尽量大。1<=n<=100,1<=Vi<=C<=10000,1<=Wi<=10^6。


在刘汝佳的紫书中使用”阶段”来描述的状态,可以辅助我们思考。
首先要确定规划的方向,有两种:
1、逆向规划,则我们说定义的当前阶段的状态为d(i,j)表示当前在i层,把第i,第i+1,第i+2,…,第n个物品装入容量为j的背包中的最大的重量和。那么规划的终点就是d(1,C)(把第1,2,3….,n个物品装入背包容量为C的背包中的最大重量和),规划的起点就是d(n,0)或者d(n,C),也就是说从第n个物品开始,因为我们定义的状态已经决定了这个规划的方向,因为我们在更新写一个阶段的时候必须要有上一个阶段的值,否则没办法更新。
不难写出状态转移方程为f(i,j)=max{f(i+1,j),f(i+1,j-V[i])+W[i]}。
给出这部分的代码:

for(int i=n;i>=1;i--)
    for(int j=0;j<=C;j++)
{       
        d[i][j]=(i==n?0:d[i+1][j]);
//用上一阶段值刷新到当前阶段也就是d(i,j)=d(i+1,j)
//因为要与上一阶段留出V[i]容量装第i个物品后做一个大小的比较,所以必须要刷新
        if(j>=V[i])
        d[i][j]=max(d[i][j],d[i+1][j-V[i]]+W[i]);
}

2、正向规划。与逆向规划是对称的方向,那么此时的状态d(i,j)定义为把前i个物品装到容量为j的背包中的最大重量和。规划的起点是d(1,0)或d(1,C),规划的终点为d(n,C)
状态转移方程为:f(i,j)=max{f(i-1,j),f(i-1,j-V[i])+W[i]}
代码如:

for(int i=1;i<=n;i++){
    cin>>V>>W;
    for(int j=0;j<=C;j++)
        d[i][j]=(i==1?0:d[i-1][j]);//用上一阶段值刷新到当前阶段
        if(j>=V[i])d[i][j]=max(d[i][j],d[i-1][j-V]+W);
}

两种规划方向都能得到最优解,但是区别主要有:
1、正向规划可以边录入V、W边进行计算,节约了空间
2、正向规划在需要打印解的时候不方便,一方面打印解的时候需要将V、W都记录下来,另一方面,因为规划方向是正向的,所以打印解的方向必须从终点开始,因为起点的位置不知道,终点的位置是确定的,就算是这样,得到的结果依然是逆序的,还要再反向打印输出才是正确的结果,并且不能保证打印的结果是字典序最小的。
3、反向规划的好处在于打印结果的时候能够保证字典序最小,所以在要求字典序最小的场合,规划方向要选择好。


下面分别说说两种规划方向怎么打印出解:
1、反向规划
输出1代表取这个物品
代码如下,只需要从终点d(1,W)开始按最优的规划顺序遍历即可

        int s = W;
        for (int i = 1; i <= N; i++)
        {
            if (d[i][s]>d[i+1][s])
            {
                s -= Weight[i];
                printf("%d", 1);
            }
            else printf("%d", 0);
            if (i != N) printf(" ");
        }

2、正向规划
从终点d(n,W)开始按最优的规划顺序遍历即可,但是显然遍历的顺序是逆序的,所以遍历一次得到的结果是逆序的,还要再逆序一遍

        //打印路径
        int s = W;
        for (int i = N; i >= 1; i--)
        {
            if (d[i][j] > d[i - 1][j])
            {
                out[i] = 1;
                s -=Weight[i];
            }
        }
        for (int i=1; i <= N; i++)
        {
            int temp = 0;
            out[i] == 1 ? temp = 1 : temp=0;
            if (i == N)
                printf("%d", temp);
            else printf("%d ", temp);   
        }

还可以使用一维数组(又叫滚动数组)解决这个问题,但是不能打印出最终的最佳方案,所以不使用。
j逆序枚举,d[j]相当于保存的是d[i-1][j]值,d[j-v]相当于保存的是d[i-1][j-V]的值,每一个阶段完成之后上一个阶段保存的值就会被覆盖掉。所以不能打印出方案。

memset(d,0,sizeof(d));
for(int i=1;i<=n;i++)
{
    scanf("%d%d",&V,&W);
    for(int j=C;j>=0;j--)//这里要逆序枚举,防止丢失了d[i-1][j-V]状态的值
     if(j>=V)d[j]=max(d[j],d[j-V]+W);
}

最后给出一道NOJ上的题目作为该问题的训练(吐槽下。。这个OJ卡的不行。。。)
http://acm.njupt.edu.cn/acmhome/problemdetail.do?&method=showdetail&id=1308
背包问题
时间限制(普通/Java) : 1000 MS/ 3000 MS 运行内存限制 : 65536 KByte
比赛描述
试设计一个用回溯法搜索子集空间树的函数。该函数的参数包括结点可行性判定函数和上界函数等必要的函数,并将此函数用于解0-1背包问题。

0-1 背包问题描述如下:给定n 种物品和一个背包。物品i 的重量是 wi ,其价值为 v i,背包的容量为C。应如何选择装入背包的物品,使得装入背包中物品的总价值最大?

在选择装入背包的物品时,对每种物品i只有2 种选择,即装入背包或不装入背包。不能将物品i 装入背包多次,也不能只装入部分的物品i。

0-1 背包问题形式化描述:给定C>0, Wi >0, Vi >0,1≤i≤n,要求n 元0-1向量( x1 ,x2 ,…, xn ),xi ∈{0,1},1≤i≤n,使得 达到最大

输入

第一行有2个正整数n和c。n是物品数,c是背包的容量。接下来的1 行中有n个正整数,表示物品的价值。第3 行中有n个正整数,表示物品的重量。

输出
计算出装入背包物品的最大价值和最优装入方案。

样例输入
5 10
6 3 5 4 6
2 2 6 5 4

样例输出
15
1 1 0 0 1

给出两种规划顺序的AC代码:
1、逆向规划

#include <algorithm>
#include <iostream>
#include <string.h>
#include <stdio.h>
using namespace std;
int Value[1001];
int Weight[1001];
int Memory[1001][1001];
int main()
{
        int  N = 0, W = 0, maxValue = 0;
        memset(Memory, 0, sizeof(Memory));
        scanf("%d%d", &N, &W);
        for (int i = 1; i <= N; i++)
            scanf("%d", &Value[i]);
        for (int i = 1; i <= N; i++)
            scanf("%d", &Weight[i]);
        for (int i = N; i >= 1; i--)
        for (int j = 0; j <= W; j++)
        {
            Memory[i][j] = (i == N ? 0 : Memory[i + 1][j]);
            if (j >= Weight[i])
            {
                if (Memory[i +1][j - Weight[i]] + Value[i] > Memory[i][j])
        Memory[i][j] = Memory[i + 1][j - Weight[i]] + Value[i];

            }
        }
        printf("%d\n", Memory[1][W]);

        int s = W;
        for (int i = 1; i <= N; i++)
        {
            if (Memory[i][s]>Memory[i+1][s])
            {
                s -= Weight[i];
                printf("%d", 1);
            }
            else printf("%d", 0);
            if (i != N) printf(" ");
        }   
        return 0;
}

2、正向规划

#include <algorithm>
#include <iostream>
#include <string.h>
#include <stdio.h>
using namespace std;
int Value[1001];
int Weight[1001];
int Memory[1001][1001];
int out[1001];
int main()
{
    int  N = 0, W = 0, maxValue = 0;
        memset(Memory, 0, sizeof(Memory));
        memset(out, 0, sizeof(out));
        scanf("%d%d", &N, &W);
        for (int i = 1; i <= N; i++)
            scanf("%d", &Value[i]);
        for (int i = 1; i <= N; i++)
            scanf("%d", &Weight[i]);
        for (int i = 1; i<= N; i++)
        for (int j = 0; j <= W; j++)
        {
            Memory[i][j] = (i == 1 ? 0 : Memory[i - 1][j]);
            if (j >= Weight[i])
            {

                if (Memory[i -1][j - Weight[i]] + Value[i] > Memory[i][j])
                {

                    Memory[i][j] = Memory[i -1][j - Weight[i]] + Value[i];
                }

            }
        }
        printf("%d\n", Memory[N][W]);

        //打印路径
        int j = W;
        for (int i = N; i >= 1; i--)
        {
            if (Memory[i][j] > Memory[i - 1][j])
            {
                out[i] = 1;
                j = j - Weight[i];
            }
        }
        for (int i=1; i <= N; i++)
        {
            int temp = 0;
            out[i] == 1 ? temp = 1 : temp=0;
            if (i == N)
                printf("%d", temp);
            else printf("%d ", temp);   
        }

    return 0;
}
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值