zoukankan      html  css  js  c++  java
  • [背包九讲1]

    P1.01基础背包问题
    对于N个宝石,每个宝石的价值为vi,重量花费为wi。背包的总载重量为W,则试问对于一个背包这么放宝石才能使其装的宝石总价值最大。
    具体思路:考虑状态,利用i表示第i个宝石,j表示当前背包的已用空间,d[i][j]就可以表示当前状况下背包内宝石的最大价值。则要求的问题可以转化为d[N][W]的求取。
    然后构建状态转移方程:

    d[i][j]=max{d[i−1][j],d[i−1][j−w[i]]+v[i]}d[i][j]=max{d[i−1][j],d[i−1][j−w[i]]+v[i]}
    值得注意的是这里max内只有两项是因为一个宝石只有选和不选两种情况,所以DP状态转移方程的思想就是:

    当前状态 = 取最优{前一状态1,前一状态2…前一状态n}

    从上可以看出状态的选取很重要,并且由于是通过将最终问题不断分为前一种状态下的最优解(即最优子结构),所以要采用自底向上的方式计算,即先计算最小问题单元,并记录,然后计算后一级问题时就可以以直接调用节约时间。所以时自底向上。也就是对解空间树从计算叶子问题开始逐步推向主问题。
    仔细考虑这一过程也可以发现,在逐步上推的过程中,母问题的决策方案是不会对之前的子问题造成影响的,因此这就是无后效性的表现。(叶子问题的决策->子问题1的决策->…->子问题n的决策->母问题的决策)
    伪代码为:

    d[0][0...W] = 0;
    for i = 1->N
    for j = 0->W
    if(w[i] > j)
    d[i][j] =d[i-1][j]
    else
    d[i][j] = max{d[i-1][j],d[i-1][j-w[i]]+v[i]}

    图中青色代表d[2][5],可以看出他由绿色的两个各种中的数值得出。即当前状态由前一状态的两个解中的最优解得出。接下来黄色的三个圈也一样。因此DP的路线就是从左上角不断计算得到右下角的最终问题的解。要注意的是DP有个赋予初值的步骤。

    问题优化

    刚好放满的求解方法

    由上图看出,如果我想求解刚好放满背包时候的背包的最大价值,可以采用的方法就是初值的赋予技巧,即只对d[0][0] = 0,d[0][1…W]都赋予负无穷,这样就可以筛选掉不满的情况。

    空间的优化

    从这张图里面还可以看出,每个宝石i的选择,其实只需要上一次的i-1时候的选择情况。因此其实可以用一个一维数组而不是二维数组表示。 
    先看伪代码

    d[0...W] = 0;
    for i = 1->N
    for j = W->0 //这里是唯一的不同
    if(w[i] > j)
    d[j] =d[j]
    else
    d[j] = max{d[j],d[j-w[i]]+v[i]}

    伪代码只是对第二个for循环的顺序做了改变。即改为了从W到0,然后就把数组换成了一维数组。 

    结合上图我们可以看下发生了什么。那个一维数组内存取的东西如图红色部分。是两个状态的结合。可以看出,当我要求d[2][6]时,我需要的d[1][6]和d[1][1],转换到一维数组时,就是d[6]和d[1],然后算出来新的d[6]对原数组进行覆盖。这样就完成了空间的压缩

    进一步优化

    在压缩到一维数组后,其实我们还可以继续优化,即对二层循环的上下限做手脚。 
    伪代码如下:

    d[0...W] = 0;
    for i = 1->N
    for j = W->w[i] //下限不同了
    /*
    if(w[i] > j)
    d[j] =d[j]
    else
    */
    d[j] = max{d[j],d[j-w[i]]+v[i]}

    为何是w[i]呢?考虑下之前的情况和图,如果j

    再进一步

    上一节是从左边考虑遍历的节省界限。即去掉从0开始到w[i]的部分。
    这一节是从右边考虑遍历的节省界限。
    考虑对于结果第n个宝石,其考虑的就是d[W]项,那其前一项,其实只需要提供从W到W-w[n]项即可。如下图蓝色项。因此,对于第i个宝石时,我们指控考虑的d[j]项的范围为:W到max{w[i],W−sum(w[i],w[i+1],...,w[n])}W到max{w[i],W−sum(w[i],w[i+1],...,w[n])}

    d[0...W] = 0; for i = 1->N for j = W->max{w[i],W-sum(w[i->n])} d[j] = max{d[j],d[j-w[i]]+v[i]}

    public void zeroOnePack(w[i], v[i]){
    for j = W->max{w[i],W-sum(w[i->n])}
    d[j] = max{d[j],d[j-w[i]]+v[i]}
    }

    P2.完全背包问题

    假若每种宝石的数量不设上限,则问题转变为完全背包问题。 
    该问题的求解有两种方法,一个是基础方法,一个抽象方法。

    基础方法

    其实说是无限,还是有限的,即一种宝石数量肯定小于背包容量V/w[i]
    考虑子状态,最直接的就是

    d[i][j]=max{d[i−1][j−k∗w[i]]+k∗v[i]|0≤k≤W/w[i]}d[i][j]=max{d[i−1][j−k∗w[i]]+k∗v[i]|0≤k≤W/w[i]}
    即当前状态 = 取最优{前一状态1,前一状态2…前一状态n}

    改进方法是,将一种宝石ni个看成ni个统一规格的宝石,则总共有sum(n1, n2, … , nn)个宝石,大循环为
    for i = 1 -> sum(n1, n2, … , nn)
    这个方法只是换了个角度看问题,并没有降低代价。不过从这个角度考虑,其实我们可以考虑对同一种宝石,我们拿的个数可以等价为一颗大宝石,比如,取2颗宝石i则相当于取了一颗中2*w[i],价值2*v[i]的大宝石。
    现在的情况就是如何构建各种大宝石来保证取的时候不会重复,这时候可以考虑计算机的二进制存储方式(只要有2的0,1,…,n次方的数,就可以组合出任意1到2的n次方之间的数)。构建的大宝石价值应该为1颗i,2颗i,4颗i。。。2的k次方颗i。这样,我只要对这些构造的大宝石进行是否装入判断,就可以判断出最优解是装几颗宝石i,其中k要满足w[i]∗2k≤Ww[i]∗2k≤W。
    伪代码

    for i = 1->N

    k = 1

    while(k <= W/w[i])

    zeroOnePack(k*w[i],k*v[i])

    k = k*2

    其实上式是有冗余的,即,k不用取那么大,只要保证1,2,4,…,k的宝石加起来总的个数是小于W/w[i]的最大值就可以,而不用非要第k个是小于W/w[i]的最大值。因此构建新的k的边界应该是sum(1,2,4,…,k)<=W/w[i]时的最大值。并且为了保证总和为W/w[i],还要再补上一颗(W/w[i]-sum(1,2,4,…,k))*w[i]的大宝石。
    伪码如下:

    for i = 1->N
    k = 1
    amount = W/w[i]
    while(k <= amount)
    zeroOnePack(k*w[i],k*v[i])
    amount = amount - k
    k = k*2
    zeroOnePack(k*w[i],k*v[i])

    抽象方法—更快

    还是这张图,可以看出,当初为了保证是从d[i-1][j],d[i-1][j-w[i]]推出d[i][j],我们采用从W到w[i]的方式,其目的就是保证不会产生从d[i][0]推出[i][j]的情况,现在,既然是一个宝石i要有无限个,那么明显可以采用从d[i][0]推出[i][j]的情况,所以给出的for循环是从0->W.
    伪代码如下:

    d[0...W] = 0;
    for i = 1->N
    for j = 0->W
    if(w[i] > j)
    d[j] =d[j]
    else
    d[j] = max{d[j],d[j-w[i]]+v[i]}
    我们可以定义该过程为一个新方法:

    public void completePack(w[i], v[i]){
    for j = 0->W
    if(w[i] > j)
    d[j] =d[j]
    else
    d[j] = max{d[j],d[j-w[i]]+v[i]}
    }

    P3.多重背包

    多重背包在这里指的是当宝石i的个数给定为ni时的背包问题。这里可以考虑,当ni = 1时,就是01背包问题,当ni大于W/w[i]时,就是完全背包问题。当ni在两者之间时,可以转换成完全背包问题里面的基础解法。因此可以构造如下伪代码:

    public void multiplePack(w[i], v[i], amount)
    if(amount > W/w[i])
    completePack(w[i], v[i])
    else{
    k = 1
    amount = W/w[i]
    while(k <= amount)
    zeroOnePack(k*w[i],k*v[i])
    amount = amount - k
    k = k*2
    zeroOnePack(k*w[i],k*v[i])
    }

    P9.背包问法变化

    构造最优解

    构造最优解的方法,我学到的有两种。 
    第一种是采用01基础背包方案时,得到了d[i][j],若令i = N;j = W;则可以根据求出的二维数组进行逆推求出每个最优选择。伪代码如下:

    public getOptSolution(){
    i = N
    j = W
    while(d[i][j] > d[0][j])
    if d[i][j] == d[i-1][j]
    没有选择第i个宝石
    i = i - 1
    else
    选择了第i个宝石
    i = i - 1
    j = j - w[i]
    }
    第二中是采用了一维数组去进行计算的时候,特别是在求取多重背包或者完全背包问题时的构造最优解方法,关键就是建立辅助的数组进行帮忙记录,不过由于在多重背包问题时,每种宝石的具体展开大宝石数量事先计算比较麻烦,因此可以采用动态数组ArrayList来帮忙。 
    伪代码:

    //--------------------构造记录用动态数组(在大循环中)------------------
    d[0->W] = 0
    动态数组recorder
    for i = 1->N
    multiplePack(w[i], v[i], amount)

    //--------------------改进01背包方法,增加记录数组(在小循环中)----------------

    public void zeroOnePack(w[i], v[i]){
    boolean[] rec
    for j = W->w[i]
    rec[j] = isPut(d[j],d[j-w[i]]+v[i]) //构造子方法判断,判断依据同max
    d[j] = max{d[j],d[j-w[i]]+v[i]}
    recorder.add(rec)

    //---------------------构造最优解-------------------------
    同 getOptSolution()方法

    补充
    其实只要理解多重背包的基础方法,是可以不用构造记录数组也可以完成最优解的构造的。不过会比较麻烦。在此再提及一下多重背包的思想:
    假设原来有N种宝石1,2,..,N.每种有n[i]个
    则现在多重背包的基础方法的思想是转换成M种宝石,分别为
    (n[1],2*n[1],4*n[1],…,k[1]*n[1]),(n[2],2*n[2],4*n[2],…,k[2]*n[2]),…………….,(n[N],2*n[N],4*n[N],…,k[N]*n[N])即可。

  • 相关阅读:
    Bzoj 1878: [SDOI2009]HH的项链 莫队
    BZOJ 2140: 稳定婚姻 Tarjan Map
    Bzoj 2190 : [SDOI2008]仪仗队 数论 特判
    bzoj 16801740 : [Usaco2005 Mar]Yogurt factory 贪心 双倍经验
    BZOJ 5387: [Lydsy1806月赛]质数拆分
    BZOJ 1379: [Baltic2001]Postman 水题
    Bzoj : 1823: [JSOI2010]满汉全席
    4952: [Wf2017]Need for Speed 二分
    BZOJ 2301: [HAOI2011]Problem b 2045: 双亲数 同BZOJ 1101 Zap 莫比乌斯反演 3倍经验
    BZOJ 1030: [JSOI2007]文本生成器 AC自动机
  • 原文地址:https://www.cnblogs.com/-Wind-/p/10175586.html
Copyright © 2011-2022 走看看