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

来源:互联网 发布:excel表重复数据筛选 编辑:程序博客网 时间:2024/06/07 14:37

动态规划问题与实际问题联系紧密,所以动态规划算法有着很广泛的应用,各类比赛题中也必不可少。最近花了不少功夫弄明白了经典的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;}
原创粉丝点击