理解01背包

最近刚刚学习背包问题,挤时间先来总结一下自己理解的01背包问题。
  背包问题属于组合优化问题,我翻阅过好多运筹学书籍发现好多书上都有它的身影,解决它的方法也不少:动态规划法、回溯法、分支界定法、贪心法。作为初学者水平有限,只能浅谈一些关于动态规划算法的认识。
  首先看一下01背包问题的定义(定义形式多种多样,但都大同小异):
  有一背包最大容量为W。有 N 种物品,其价值和体积分为Vi,Wi,每种物品只有一件。从所有物品中选择若干件放入背包,最大能得到的价值是多少?这就 是 01 背包问题,0 和 1 指的某件物品选还是不选 。
首先从理解上采用dfs比较合适,现在先略,主要先谈动态规划想法。
定义f[i][j]表示把前i个物品装入容量为j的背包时获得的最大价值,则可得如下状态转移方程:
    f[i][j]=max{f[i-1][j],f[i-1][j-w[i]]+v[i]}
边界为:i=0时为0,j<0时为负无穷,最终答案为f[n][W].
对此方程我的理解是,既然用动态规划来解,那么这个问题的状态必然只跟它前一个或者或一个相邻的状态有关,此处按f[i][j]的定义,可以发现把前i个物品放入容量为j的背包这个问题只和它的前一个状态:把前i-1个物品放入背包。如果不放第i件物品,那么问题就转化成把前i-1个物品放入容量为j的背包,如果放第i件物品,那么就需要把前i-1件物品放入容量为W-wi的背包。这里是用物品i来表示阶段i,以前理解的不深刻,只知道f[i][j]由前面f[i-1][j]和f[i-1][j-w[i]]+v[i]得来,现在想想,这种正向推的方法是因为在计算时f[i-1][j]和f[i-1][j-w[i]]已经先计算过了,所以才可以推到f[i][j]。
举个例子加深理解:(分享个在线模拟01背包的网址:http://karaffeltut.com/NEWKaraffeltutCom/Knapsack/knapsack.html :D)
比如N=,W=6 四个物品为(vi,wi) (2,3) (6,2) (12,3) (3,4):

代码如下:

  

//代码1.

#include<iostream>

#include<cstdio>

#include<algorithm>

#include<cstring>

using namespace std;

const int maxn=105;

int v[maxn],w[maxn];

int f[maxn][maxn];

int N,W;

int main()

{

    while(scanf("%d%d",&N,&W)==2)

    {

     memset(f,0,sizeof(f));

        for (int i=1;i<=N;i++)  scanf("%d",&v[i]);

        for (int i=1;i<=N;i++)  scanf("%d",&w[i]);

        for (int i=1;i<=N;i++)

         for (int j=0;j<=W;j++){

            f[i][j]=(i==1?0:f[i-1][j]);

            if (j>=w[i]) f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+v[i]);

        }

        printf("%d ",f[N][W]);

    }

    return 0;

}


 01背包一般有两种问法,一是恰好使背包装满,一是没有要求必须装满背包。两种问法的矛盾就体现在是否需要“恰好”装满背包。这个矛盾的解决方法就在初始化处理上:

  恰好装满时除第零列(背包容量为零)值为零其他初始化为无穷小,而未要求装满则全值为零就行(如上程序)。

其实我以前没有想到解决这个矛盾竟然只在初始化上!感觉好神奇。按照《背包九讲》的说法:

  初始化的f数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为0的背包可能被价值为0的nothing“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,它们的值就都应该是-∞了。如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的价值为0,所以初始时状态的值也就全部为0了。

我只能写个程序看看输出来理解(数据按上面的):

恰好装满时:

  

参考代码:

#include<iostream>
#include<algorithm>
#include<cstdio>
using namespace std;
const int minmum=-1000;
int f[5][7]={{0}};
int w[5]={0,3,2,3,4};
int v[5]={0,2,6,12,3};
int N=4,V=6;

int main()
{
    for (int i=0;i<=N;i++){
        for (int j=0;j<=V;j++)
            f[i][j]=minmum;
    }
    for (int i=0;i<=N;i++)
        f[i][0]=0;
    cout<<endl;
    printf("  0        ");
        for (int i = 1; i <= V; i++)
            printf("%-9d", i);
        cout << endl;
    for (int i=0;i<=N;i++){
        for (int j=0;j<=V;j++){
            if (j==0) printf("%d:",i);
            printf("%-9d",f[i][j]);
        }
        cout<<endl;
    }
    cout<<endl;
    cout<<endl;

    for (int i=1;i<=N;i++){
        for (int j=1;j<=V;j++){
            f[i][j]=f[i-1][j];
            if (j>=w[i])
                f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+v[i]);
        }
    }
    cout<<"ans:=  "<<f[N][V]<<endl;
    printf("  0        ");
        for (int i = 1; i <= V; i++)
            printf("%-9d", i);
        cout << endl;
    for (int i=0;i<=N;i++){
        for (int j=0;j<=V;j++){
            if (j==0) printf("%d:",i);
            printf("%-9d",f[i][j]);
        }
        cout<<endl;
    }
    return 0;
}

不要求恰好装满时:

  

参考代码:

  

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring> using namespace std; const int minmum = -1000; int f[5][7] = { { 0 } }; int w[5] = { 0,3,2,3,4 }; int v[5] = { 0,2,6,12,3 }; int N = 4, V = 6; int main() { memset(f, 0, sizeof(f)); cout << endl; printf(" 0 "); for (int i = 1; i <= V; i++) printf("%-9d", i); cout << endl; for (int i = 0; i <= N; i++) { for (int j = 0; j <= V; j++) { if (j == 0) printf("%d:", i); printf("%-9d", f[i][j]); } cout << endl; } cout << endl; cout << endl; for (int i = 0; i <= N; i++) f[i][0] = 0; for (int i = 1; i <= N; i++) { for (int j = 1; j <= V; j++) { f[i][j] = f[i - 1][j]; if (j >= w[i]) f[i][j] = max(f[i - 1][j], f[i - 1][j - w[i]] + v[i]); } } cout << "ans:= " << f[N][V] << endl; printf(" 0 "); for (int i = 1; i <= V; i++) printf("%-9d", i); cout << endl; for (int i = 0; i <= N; i++) { for (int j = 0; j <= V; j++) { if (j == 0) printf("%d:", i); printf("%-9d", f[i][j]); } cout << endl; } return 0; }

由于背包的每个状态只跟他相邻的状态有关,所以可以想到能否用一维数组代替二维数组进行进一步优化。

答案是肯定的,用“滚动数组”,所谓“滚动数组”就是为了优化空间的,由于每个状态只跟它相邻的上一个状态有关,也就是说当前状态是由上一个状态推过来的,如果我只想要最后的结果,是没有必要保留每个阶段的状态的,自然我可以用一个一维数组每次状态更新时直接覆盖上一个状态。并且在二维时我们是先计算f[i-1][j-w[i]]+v[i],再计算f[i-1][j]的,所以若还是直接从后往前推的话会先覆盖f[i-1][j-w[i]]+v[i]的位置,那么计算f[i-1][j]时用到的就是刚才更新的结果而不是原来的结果,这样会造成一个背包被多次利用,所以要采用从前往后推的方法来计算。

参考代码:

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
using namespace std;
const int minmum=-1000;
int f[10];
int w[5]={0,3,2,3,4};
int v[5]={0,2,6,12,3};
int N=4,V=6;

int main()
{
    memset(f,0,sizeof(f));
    for (int i=1;i<=N;i++){
        for (int j=V;j>=w[i];j--)
            f[j]=max(f[j],f[j-w[i]]+v[i]);
    }
    cout<<f[V]<<endl;
    return 0;
}

参考博客:http://blog.csdn.net/insistgogo/article/details/8579597

       http://blog.csdn.net/pi9nc/article/details/8142876

原文地址:https://www.cnblogs.com/zxhyxiao/p/6914683.html