背包九讲的小理解

动态规划的入门算法, 简单形象又好理解 :
大牛的博客
只提醒一些比较重要的知识, 其他的都去看这个大牛的博客
1 : 01背包
问题描述: 有n中物品, 背包容量是tot, 每种物品都有一个体积和价值, 常规询问tot容量的背包最多能得到多少价值.
最优复杂度做法O(NV).
最基础的二维表示状态 dp[i][j] 代表前i件物品当前的j体积下的最优值.
转移方程为 : dp[i][j] = max(dp[i-1][j], dp[i-1][j-v[i]] + w[i]); // 这个方程非常的重要.
板子(一维):

 for(int i = 1 ; i <= n ; i ++)
    for(int j = tot ; j >= v[i] ; j --)   //tot表示总体积.
       dp[j]=max(dp[j], dp[j-v[i]] + w[i]);
//dp[i]表示背包体积为i时的最大价值,只有一层层的dp下去就可以找到体积最大时价值最高为多少.

二维
dp[i][v]=max( dp[i-1][j],dp[i-1][j-w[i]]+v[i] )
// 这个方程几乎是背包问题中最核心的方程了!!! 一定要理解好它的代表意义….. 以及它是有哪些状态更新过来的…….
对于这个方程其实并不难理解,方程之中,现在需要放置的是第i件物品,这件物品的体积是w[i],价值是v[i],因此f[i-1][j]代表的就是不将这件物品放入背包,而dp[i-1][j-w[i]]+v[i]则是代表将第i件放入背包之后的总价值,比较两者的价值,得出最大的价值存入现在的背包之中。

 for(int i = 1 ; i <= n ; i ++) {
        for(int j = 0 ; j <= tot ; j ++) {
            if(j >= v[i])//如果放的下的话才放.
                dp[i][j] = max(dp[i-1][j], dp[i-1][j-v[i]] + w[i]);//如果可以更新就更新.
            else dp[i][j] = dp[i-1][j]; //否则就不更新.
        }
}

二维滚动数组背包(因为当前状态只被上一层的状态所影响, 所以可以用滚动数组)

int dp[2][maxn];
 for(int i = 1 ; i <= n ; i ++) {
    for(int j = 0 ; j <= tot ; j ++) {
       if(j>=w[i])
          dp[i%2][j]=max(dp[(i-1)%2][j],dp[(i-1)%2][j-w[i]]+v[i]);
       else dp[i%2][j]=dp[(i-1)%2][j];
    }
}
printf("%d\n",dp[m%2][tot]);

说一个注意点就是区分两个问题:
就是询问“恰好装满背包”时的最优解,有的题目则并没有要求必须把背包装满. 一种区别这两种问法的实现方法是在初始化的时候有所不同.
如果是第一种问法,要求恰好装满背包,那么在初始化时除了f[0]为0其它f[1..V]均设为-∞,这样就可以保证最终得到的f[N]是一种恰好装满背包的最优解.
如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将f[0..V]全部设为0.
为什么呢?可以这样理解:初始化的f数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为0的背包可能被价值为0的nothing“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,它们的值就都应该是-∞了。如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的价值为0,所以初始时状态的值也就全部为0了.

其实分别把这两种类型的dp数组打印出来就可以理解了.

模板题

2: 完全背包
问题描述: 有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大.

最优复杂度做法O(NV).

for(int i = 1 ; i <= n ; i ++)
   for(int j = v[i] ; j <= tot ; j ++)   //tot表示总体积.
      dp[j]=max(dp[j], dp[j-v[i]] + w[i]);

首先想想为什么01背包中要按照v = V..0的逆序来循环。这是因为要保证第i次循环中的状态f[i][v]是由状态f[i-1][v-c[i]]递推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第i件物品”这件策略时,依据的是一个绝无已经选入第i件物品的子结果f[i-1][v-c[i]]。而现在完全背包的特点恰是每种物品可选无限件,所以在考虑“加选一件第i种物品”这种策略时,却正需要一个可能已选入第i种物品的子结果f[i][v-c[i]],所以就可以并且必须采用v=0..V的顺序循环。这就是这个简单的程序为何成立的道理

3 :多重背包
问题描述: 有N种物品和一个容量为V的背包。第i种物品最多有n[i]件可用,每件费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大

最优复杂度做法O(NV)但是需要用单调队列进行优化, 所以这里就不讲了. 说说其他常规做法.
O(V * sigma(Ci)) 或者 O(V * sigma(logCi));
根据复杂度来适当选择, 其实前者就是每一样物品都单独取出来, 即全部分散出来做01背包, 而后者是进行了一定的优化后再做的01背包.
多重背包推荐博客
后者: 我们把第i种物品看成单个的,一个一个的,我们想想二进制,任何一个数都可以由二的幂表示。

我们试试看,比如Ci = 14,我们可以把它化成如下4个物品:

重量是Wi,体积是Vi
重量是2 * Wi , 体积是2 * Vi
重量是4 * Wi , 体积是4 * Vi
重量是7 * Wi , 体积是7 * Vi

注意最后我们最后我们不能取,重量是8 * Wi , 体积是8 * Vi 因为那样总的个数是1 + 2 + 4 + 8 = 15个了,我们不能多取对吧?

我们用这4个物品代替原来的14个物品,大家可以试试原来物品无论取多少个,重量和体积都可以靠我们这几个物品凑出来,这说明我们这种分配方式和原来是等价的。

我们转化为一般方法,对于Ci ,我们的拆分方法是:

1,2,4,8…… 同时Ci减去这些值,如果Ci不够减了,则把最后剩余的算上,同时我们的价值和体积也对应乘以这些系数. 这样Ci个同一种物品,被我们变成了logCi个物品了. 于是按照0-1背包的做法,时间复杂变为O(V * sigma(logCi))了,降了很多.

前者复杂度可以过模板题
代码参考:

const int maxn = 1e3+5;
int num[maxn], v[maxn], w[maxn];
int dp[maxn];
void solve()
{
    int n, tot;
    scanf("%d%d", &tot, &n);
    for (int i = 1 ; i <= n ; i ++) {
        cin >> v[i] >> w[i] >> num[i];
    }
    Fill(dp, 0); // 实际思想就是把这些一起做01背包即可.
    for (int i = 1 ; i <= n ; i ++) {
        for (int k = 1 ; k <= num[i] ; k ++) {
            for (int j = tot ; j >= v[i] ; j --) {
                dp[j] = max(dp[j], dp[j-v[i]] + w[i]);
            }
        }
    }
    printf("%d\n", dp[tot]);
}

后者复杂度才能过模板题
(当然可以过上面那道题)
代码参考:

const int maxn = 5e4+5;
int num[maxn], v[maxn], w[maxn];
int dp[maxn];
void solve()
{
    int n, tot;
    scanf("%d%d", &n, &tot); int cc = 0;
    for (int i = 1 ; i <= n ; i ++) {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        for (int j = 1 ; ; j <<= 1) {
            if (c >= j) {
                v[++cc] = j*a;
                w[cc] = j*b;
                c -= j;
            }
            else {
                v[++cc] = c*a;
                w[cc] = c*b;
                break;
            }
        }
    }
    Fill(dp, 0);
    for (int i = 1 ; i <= cc ; i ++) {
        for (int j = tot ; j >= v[i] ; j --) {
            dp[j] = max(dp[j], dp[j-v[i]] + w[i]);
        }
    }
    printf("%d\n", dp[tot]);
}

4:01, 完全, 多重背包混合题型.
我们只需要根据每种物品的类型. 采用相应的算法去解决就OK啦. 伪代码如下:

for i = 1...N
    ifi件物品是01背包
       for j = tot....v[i]
    else ifi件物品是完全背包
       for j = v[i]...tot
    else ifi件物品是多重背包
       用第二种复杂度做就好了(实际上我们可以先扫一遍,先把多重的先处理好log个即可)

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

算法:
费用加了一维,只需状态也加一维即可。设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采用逆序的循环,当物品有如完全背包问题时采用顺序的循环。当物品有如多重背包问题时拆分物品.

物品总个数的限制

有时,“二维费用”的条件是以这样一种隐含的方式给出的:最多只能取M件物品。这事实上相当于每件物品多了一种“件数”的费用,每个物品的件数费用均为1,可以付出的最大件数费用为M。换句话说,设f[v][m]表示付出费用v、最多选m件时可得到的最大价值,则根据物品的类型(01、完全、多重)用不同的方法循环更新,最后在f[0..V][0..M]范围内寻找答案

模板题

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

算法

这个问题变成了每组物品有若干种策略:是选择本组的某一件,还是一件都不选。也就是说设f[k][v]表示前k组物品花费费用v能取得的最大权值,则有:

f[k][v]=max{f[k-1][v],f[k-1][v-c[i]]+w[i]|物品i属于第k组}

使用一维数组的伪代码如下:

for 所有的组k
    for v = V..0
        for 所有的i属于组k
            f[v] = max{f[v], f[v-c[i]]+w[i]}

注意这里的三层循环的顺序, “for v=V..0”这一层循环必须在“for 所有的i属于组k”之外. 这样才能保证每一组内的物品最多只有一个会被添加到背包中.

模板题

7: 有依赖性的背包问题

简化的问题
这种背包问题的物品间存在某种“依赖”的关系。也就是说,i依赖于j,表示若选物品i,则必须选物品j。为了简化起见,我们先设没有某个物品既依赖于别的物品,又被别的物品所依赖;另外,没有某件物品同时依赖多件物品。

算法
将不依赖于别的物品的物品称为“主件”,依赖于某主件的物品称为“附件”。由这个问题的简化条件可知所有的物品由若干主件和依赖于每个主件的一个附件集合组成.

按照背包问题的一般思路,仅考虑一个主件和它的附件集合。可是,可用的策略非常多,包括:一个也不选,仅选择主件,选择主件后再选择一个附件,选择主件后再选择两个附件……无法用状态转移方程来表示如此多的策略(事实上,设有n个附件,则策略有2^n+1个,为指数级)

再考虑P06中的一句话: 可以对每组中的物品应用P02中“一个简单有效的优化”。 这提示我们,对于一个物品组中的物品,所有费用相同的物品只留一个价值最大的,不影响结果。所以,我们可以对主件i的“附件集合”先进行一次01背包,得到费用依次为0..V-c[i]所有这些值时相应的最大价值f’[0..V-c[i]]。那么这个主件及它的附件集合相当于V-c[i]+1个物品的物品组,其中费用为c[i]+k的物品的价值为f’[k]+w[i]。也就是说原来指数级的策略中有很多策略都是冗余的,通过一次01背包后,将主件i转化为V-c[i]+1个物品的物品组,就可以直接应用P06的算法解决问题了.

较一般的问题

更一般的问题是:依赖关系以图论中“森林”的形式给出(森林即多叉树的集合),也就是说,主件的附件仍然可以具有自己的附件集合,限制只是每个物品最多只依赖于一个物品(只有一个主件)且不出现循环依赖。

解决这个问题仍然可以用将每个主件及其附件集合转化为物品组的方式。唯一不同的是,由于附件可能还有附件,就不能将每个附件都看作一个一般的01背包中的物品了。若这个附件也有附件集合,则它必定要被先转化为物品组,然后用分组的背包问题解出主件及其附件集合所对应的附件组中各个费用的附件所对应的价值。

事实上,这是一种树形DP,其特点是每个父节点都需要对它的各个儿子的属性进行一次DP以求得自己的相关属性。这已经触及到了“泛化物品”的思想。看完P08后,你会发现这个“依赖关系树”每一个子树都等价于一件泛化物品,求某节点为根的子树对应的泛化物品相当于求其所有儿子的对应的泛化物品之和。

第8到第9讲就鸽了呀……………. (详情可以参照最先给出的那个大牛的博客说明)

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值