zoukankan      html  css  js  c++  java
  • 理解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

  • 相关阅读:
    30、深入浅出MFC学习笔记,多线程
    29、深入浅出MFC学习笔记,多重文件和视图
    5、程序设计实践读书笔记
    6、C++ Primer 4th 笔记,标准IO库(1)
    SQL Server流程控制 7,Try...Catch 语句
    TSQL:流程控制 4,Case 语句
    SQL Server事务处理(Tansaction)与锁(Lock)
    SQL Server9,流程控制 Execute 语句(*)
    SQL Server流程控制 2,If...Else 语句
    SQL Server流程控制 1,Begin...End 语句
  • 原文地址:https://www.cnblogs.com/zxhyxiao/p/6914683.html
Copyright © 2011-2022 走看看