• 基础动态规划 讲解


    P01: 01背包问题

    1.1 问题
    有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大。

    1.2 基本思路
    在不超过背包容量的情况下,最多能获得多少价值
    子问题状态:f[j]:表示前i件物品放入容量为j的背包得到的最大价值
    状态转移方程:f[j] = max{f[j],f[j - weight[i]] + value[i]}
    初始化:f数组初始状态设置为0

    1.3 代码部分

    #include <bits/stdc++.h>
    using namespace std;
    const int N = 3;//物品个数
    const int V = 5;//背包最大容量
    int weight[N + 1] = {1,2,2,3};//物品重量
    int value[N + 1] = {25,10,20,30};//物品价值
    int f[V + 1];
    int ZeroOnePack()
    {
        memset(f,0,sizeof(f));
        //递推
        for (int i = 0;i <= N;i++) //枚举物品(注意物品的编号)
        {
            for (int j = V;j >= weight[i];j--) //枚举背包容量,防越界,j下限为 weight[i]!!!
            {
                f[j] = max(f[j],f[j - weight[i]] + value[i]);
                cout << j << " " << f[j] << endl;
            }
        }
        return f[V];
    }
    int main()
    {
        cout << ZeroOnePack() << endl;
        return 0;
    }
    

    1.4 初始化细节问题
    我们看到的求最优解的背包问题题目中,事实上有两种不太相同的问法。
    有的题目要求“恰好装满背包”时的最优解,有的题目则并没有要求必须把背包装满。

    如果是第一种问法,要求恰好装满背包,那么在初始化时除了F[0]为0,其它F[1..V ]均设为−∞,这样就可以保证最终得到的F[V ]是一种恰好装满背包的最优解。
    如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将F[0..V ]全部设为0。
    这是为什么呢?可以这样理解:初始化的F数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为0的背包可以在什么也不装且价值为0的情况下被“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,应该被赋值为-∞了。
    如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的价值为0,所以初始时状态的值也就全部为0了。

    P02: 完全背包问题

    2.1 问题
    有N种物品和一个容量为V 的背包,每种物品都有无限件可用。放入第i种 物品的耗费的空间是Ci,得到的价值是Wi。求解:将哪些物品装入背包,可使 这些物品的耗费的空间总和不超过背包容量,且价值总和最大。

    2.2 基本思路
    可以考虑将完全背包转换为01背包问题来解。
    在解决01背包问题的过程中,如果背包的容量倒着从V开始减,那么就意味着,每个物品每次只能选择一次,但是反过来就相当于每个物品只要体积之和不超过指定的V,就可以一直取下去,从而就可以看成每个物品都有无限多件。

    2.3 代码部分

    int CompletePack()
    {
        memset(f,0,sizeof(f));
        //递推
        for (int i = 0;i <= N; ++ i) //枚举物品(注意物品的编号)
        {
            for (int j = weight[i];j <= V; ++ j) //枚举背包容量,防越界,j下限为 weight[i]!!!
            {
                f[j] = max(f[j],f[j - weight[i]] + value[i]);
                cout << j << " " << f[j] << endl;
            }
        }
        return f[V];
    }
    

    2.4 一个简单的优化
    完全背包问题有一个很简单有效的优化,是这样的:若两件物品i、j满足Ci ≤ Cj且Wi ≥ Wj,则将可以将物品j直接去掉,不用考虑。不过一般是用不到的。

    P03: 多重背包问题

    3.1 问题
    有N种物品和一个容量为V 的背包。第i种物品最多有Mi件可用,每件耗费的空间是Ci,价值是Wi。求解将哪些物品装入背包可使这些物品的耗费的空间总和不超过背包容量,且价值总和最大。

    3.2 基本思路
    把第i种物品换成Mi件01背包中的物品,则得到了物品数为ΣMi的01背包问题。
    我们可以通过二进制的拆分方法对其优化。对每i件物品,拆分的策略为:新拆分的物品的重量等于1件,2件,4件,..,(2^(k - 1)),Num[i] - (2^(k - 1))件,其中k 是满足Num[i] - 2^k + 1 > 0 的最大整数。
    注意:
    (1)最后一个物品的件数的求法和前面不同,其直接等于 该物品的最大件数 - 前面已经分配之和。
    (2)分成的这几件物品的系数和为Num[i],表明第i种物品取的件数不能多于Num[i]。

    举例:某物品为13件,则其可以分成四件物品,其系数为1,2,4,6.这里k = 3。
    使用二进制的前提还是使用二进制拆分能保证对于0,,,Num[i]间的每一个整数,均可以用若干个系数的和表示。

    3.3 代码部分

    /*
    01背包,v为降序
    f[v]:表示把前i件物品放入容量为v的背包中获得的最大收益。
    f[v] = max(f[v],f[v - Weight[i]] + Value[i]);
    */
    void ZeroOnePack(int nWeight,int nValue)
    {
        for (int v = V; v >= nWeight; v--)
        {
            f[v] = max(f[v],f[v - nWeight] + nValue);
        }
    }
    /*
    完全背包,v为增序。
    f[v]:表示把前i件物品放入容量为v的背包中获得的最大收益。
    f[v] = max(f[v],f[v - Weight[i]] + Value[i]);
    */
    void CompletePack(int nWeight,int nValue)
    {
        for (int v = nWeight; v <= V; v++)
        {
            f[v] = max(f[v],f[v - nWeight] + nValue);
        }
    }
    
    int MultiplePack()
    {
        int k = 1;
        int nCount = 0;
        for (int i = 1; i <= N; i++)
        {
            if (weight[i] * num[i] >= V)
            {
                //此时满足条件Weight[i] * Num[i] >= V时,
                //完全背包:该类物品相当于是无限供应,直到背包放不下为止。
                CompletePack(weight[i],value[i]);
            }
            else
            {
                k = 1;
                nCount = num[i];
                while(k <= nCount)
                {
                    ZeroOnePack(k * weight[i],k * value[i]);
                    nCount -= k;
                    k *= 2;
                }
                ZeroOnePack(nCount * weight[i],nCount * value[i]);
            }
        }
        return f[V];
    }
    

    以上的代码可直接拿做多重背包的模板(记得初始化),但是重要的还是思想,注意灵活运用。

    3.4 实战部分
    HDOJ 2192 悼念512汶川大地震遇难同胞——珍惜现在,感恩生活

    附加题:HDOJ 1059 Dividing

    P04: 混合三种背包问题
    4.1 问题
    如果将前面1、2、3中的三种背包问题混合起来。也就是说,有的物品只可
    以取一次(01背包),有的物品可以取无限次(完全背包),有的物品可以取
    的次数有一个上限(多重背包)。

    4.2 01背包与完全背包的混合

    考虑到01背包和完全背包中给出的伪代码只有一处不同,故如果只有两类
    物品:一类物品只能取一次,另一类物品可以取无限次,那么只需在对每个
    物品应用转移方程时,根据物品的类别选用顺序或逆序的循环即可,复杂度
    是O(V N)。伪代码如下:

     for i = 1 to N
        if 第i件物品属于01背包
             for v = V to Ci
                F[v] = max(F[v], F[v − Ci] + Wi)
        else if 第i件物品属于完全背包
            for v = Ci to V	
                F[v] = max(F[v], F[v − Ci] + Wi)
    

    4.3 三种背包混合

    for i = 1 to N
        if 第i件物品属于01背包 
            ZeroOnePack(F,Ci ,Wi ) 
        else if 第i件物品属于完全背包 
            CompletePack(F,Ci ,Wi ) 
        else if 第i件物品属于多重背包 
            MultiplePack(F,Ci ,Wi ,Ni )
    

    P05: 二维费用的背包问题

    5.1 问题
    二维费用的背包问题是指:对于每件物品,具有两种不同的费用;选择这件物品必须同时付出这两种代价;对于每种代价都有一个可付出的最大值(背包容量)。问怎样选择物品可以得到最大的价值。设这两种代价分别为代价1和代价2,第i件物品所需的两种代价分别为a[i]和b[i]。两种代价可付出的最大值(两种背包容量)分别为V和U。物品的价值为w[i]。

    5.2 基本思路
    费用加了一维,只需状态也加一维即可。设f[i][v][u]表示前i件物品付出两种代价分别为v和u时可获得的最大价值。状态转移方程就是:
    f[i][v][u]=max{f[i-1][v][u],f[i-1][v-a[i]][u-b[i]]+w[i]}
    如前述方法,可以只使用二维的数组:当每件物品只可以取一次时变量v和u采用逆序的循环,当物品有如完全背包问题时采用顺序的循环。当物品有如多重背包问题时拆分物品。根据题目要求使用相应的背包方法。

    5.3 实战部分
    题目链接:HDU 2159 FATE

    P06: 分组背包问题

    6.1 问题
    有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。这些物品被划分为若干组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

    6.2 基本思路
    这个问题变成了每组物品有若干种策略:是选择本组的某一件,还是一件都不选。也就是说设f[k][v]表示前k组物品花费费用v能取得的最大权值,则有f[k][v] = max{ f[k-1][v], f[k-1][v-c[i]]+w[i] | 物品i属于第k组 }。

    6.3 实战部分

    题目链接:HDOJ 3535 AreYouBusy

    P07: 有依赖背包问题

    7.1 问题
    这种背包问题的物品间存在某种“依赖”的关系。也就是说,i依赖于j,表示若选物品i,则必须选物品j,问选择n件物品,可以得到的最大价值。举个例子,在大学中,要修很多功课,但是这些功课有的都有先行课,比如在学C++ 之前,必须要学C++,这时候C++ 就依赖于C,当C没有学习的时候,就没有办法去学习C++。

    7.2 基本思路
    首先先简化一下问题:先设没有某个物品既依赖于别的物品,又被别的物品所依赖;另外,没有某件物品同时依赖多件物品。
    这时候我们可以将被依赖的物品称为“主件”,依赖主件的物品称为“附件”。
    首先先分析一下物品可以怎么选择:对于每个主件和其对应的附件,主件可以分为选与不选。附件可以选一个,两个……普通的背包是无法解决这样的问题。
    解决问题方法:我们可以先将主件对应的附件做一个01 背包的处理。这时候对于每个主件就可以转换成分组背包的问题,每组中包含(不选主件,选主件不选附件,选主件选一个附件,选主件选两个附件……)。

    一般问题方案:依赖关系以图论中“森林”的形式给出(森林即多叉树的集合),也就是说,主件的附件仍然可以具有自己的附件集合,限制只是每个物品最多只依赖于一个物品(只有一个主件)且不出现循环依赖。
    事实上,这是一种树形DP,其特点是每个父节点都需要对它的各个儿子的属性进行一次DP以求得自己的相关属性。

    7.3 实战部分

    题目链接:HDOJ 3449 Consumer

    题目链接HDOJ 1561 The more, The Better

    P08: 泛化背包问题

    8.1 问题
    考虑这样一种物品,它并没有固定的费用和价值,而是它的价值随着你分配给它的费用而变化。这就是泛化物品的概念。在背包容量为V的背包问题中,泛化物品是一个定义域为0..V中的整数的函数h,当分配给它的费用为v时,能得到的价值就是h(v)。

    8.2 基本思路
    如果面对两个泛化物品h和l,要用给定的费用从这两个泛化物品中得到最大的价值,怎么求呢?事实上,对于一个给定的费用v,只需枚举将这个费用如何分配给两个泛化物品就可以了。同样的,对于0..V的每一个整数v,可以求得费用v分配到h和l中的最大价值f(v)。也即f(v)=max{h(k)+l(v-k)|0<=k<=v}。
    这个运算的时间复杂度取决于背包的容量,是O(V^2)。

    P09:背包问题变化

    9.1 背包的第K优解问题

    问题描述:DD 和好朋友们要去爬山啦!他们一共有 K 个人,每个人都会背一个包。这些包的容量是相同的,都是 V。可以装进背包里的一共有 N 种物品,每种物品都有给定的体积和价值。
    在 DD 看来,合理的背包安排方案是这样的:
    每个人背包里装的物品的总体积恰等于包的容量。
    每个包里的每种物品最多只有一件,但两个不同的包中可以存在相同的物品。
    任意两个人,他们包里的物品清单不能完全相同。
    在满足以上要求的前提下,所有包里的所有物品的总价值最大是多少呢?

    解题思路:用q1和q2记录,保持递增,合并成前k优解。 要使得背包恰好装满,就要赋初值-maxint,而当背包容量为0时,赋值0。这题和典型的01背包求最优解不同,是要求第k大的解,所以,最直观的想法就是在01背包的基础上再增加一维,用来保存前k大小的数,然后在递推时,根据前一个状态的前k大小的数推出下一个阶段的前k个数保存下来。

    代码部分:

    #include<iostream>  
    using namespace std;  
    long f[51][50001];  
    int q1[51];  
    int q2[51];  
    int main()  
    {   
        //freopen("in.txt","r",stdin);  
        int k,v,n;  
        int value,weight;  
        cin>>k>>v>>n;  
        for(int i=0;i<=k;i++)  
            for(int j=0;j<=v;j++)  
                f[i][j]=INT_MIN;  
        f[1][0]=0;  
        for(int i=1;i<=n;i++)   
        {  
            cin>>weight>>value;  
            for(int j=v;j>=weight;j--){  
                for(int w=1;w<=k;w++){  
                q1[w]=f[w][j];  
                q2[w]=f[w][j-weight]+value;  
                }  
                int h1=1,h2=1,h=0;  
                while(h<k){  
                    h++;  
                    if(q1[h1]>q2[h2]) {  
                        f[h][j]=q1[h1];  
                        h1++;  
                    }  
                    else {  
                        f[h][j]=q2[h2];  
                        h2++;  
                    }  
                }  
            }  
        }  
            int ans=0;  
            for(int i=1;i<=k;i++)  
                ans+=f[i][v];  
            cout<<ans<<endl;  
            return 0;  
    }  
    
  • 相关阅读:
    批处理读取INI文件
    重装操作系统的20条原则
    SATA串口硬盘Windows Vista系统驱动安装实录
    中国国家地理高清晰的PDF书籍系列经典珍藏版
    单一职责原则
    理解boost::bind的实参传递方式
    理解C++ dynamic_cast
    C# vs C++之三:静态构造函数
    TDD可以驱动设计吗?
    依赖注入与对象间关系
  • 原文地址:https://www.cnblogs.com/cmmdc/p/7985263.html
Copyright © 2020-2023  润新知