Loading [MathJax]/jax/output/HTML-CSS/jax.js

数据结构与算法教程之动态规划(2)——背包 dp

数据结构与算法教程之动态规划(2)——背包 dp

动态规划

背包 dp

背包问题是动态规划中入门级别的题目,其应用还算广泛。

一般背包就是指一个容器,然后装载物品,使得期望的某些价值最大等等。

01 背包

问题模型

n 个价值和体积分别为 v[i],w[i] 的物品(每个物品都只有一件),有一个大小为 W 的背包,求问最多能在背包中装多少价值的物品。

分析

根据上讲的分析,我们先来思考状态的表示。

题目相当于算体积和不超过 W 的物品的最大价值和。

不超过看上去不太好写,我们可以考虑计算体积和恰为 W 的物品的最大价值和。

之后只需要对体积和为 0W 求得其中的最大值即可。

dp[i][j] 表示从前 i 件物品选出体积和为 j 的物品集合的最大价值。

转移十分自然:dp[i][j]=max(dp[i1][j],dp[i1][jw[i]]+v[i])

所以核心代码就是:

for (int i=1;i<=n;i++)
    for (int j=0;j<=W;j++)
        if (j>=w[i]) dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
        else dp[i][j]=dp[i-1][j];
C++

时间复杂度与空间复杂度均为 O(nW)

优化

这样子用一个二维数组来表示状态显得冗余,我们考虑用一维数组 dp[i] 表示到当前体积和为 j 的物品集合的最大价值。

那么转移本身还是差不多:dp[j]=max(dp[j],dp[jw[i]]+v[i])

但我们需要保证每件物品最多用了一次。

所以有一个巧妙的方法,就是倒着枚举 j,这样小的 j 就不会更新给大的。

代码会变为:

for (int i=1;i<=n;i++)
    for (int j=W;j>=w[i];j--)
        dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
C++

甚至变得更为简单。

这样时间复杂度不变,空间复杂度降为了 O(W)

完全背包

问题模型

n 类价值和体积分别为 v[i],w[i] 的物品(每个物品有无限件),有一个大小为 W 的背包,求问最多能在背包中装多少价值的物品。

分析

跟上一个的区别就是每个物品可以取多次。

所以我们只需要将上面的优化算法的 j 的枚举顺序重新变为正序即可。

我写的代码:

for (int i=1;i<=n;i++)
    for (int j=w[i];j<=W;j++)
        dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
C++

复杂度跟上面完全相同。

多重背包

问题模型

n 类价值和体积分别为 v[i],w[i] 的物品(每个物品 m[i] 件),有一个大小为 W 的背包,求问最多能在背包中装多少价值的物品。

分析

有点像上述两个的中间体,既不是不能多次,又不是无限次使用。

看起来十分难以直接表示状态。所以需要更进一步的状态。

考虑到每个非负整数都能用二进制表示,我们可以将 m[i] 件物品分为若干部分。

ti=log2(m[i]+1),可以将其分为 20,21,,2ti1,m[i]2ti+1 这么几个部分。

这样每一个 0m[i] 的整数,一定能被分成这些部分的和。

也就是说,用这些每个部分作为整体(价值和体积都乘上若干倍数),其选择方案肯定覆盖了原有的所有可能。

所以这之后就重新变为了 01 背包问题。

具体代码不再演示。

时件复杂度为 O(nWlogM),空间复杂度为 O(W)。(M=nmaxi=1mi

优化

这种方法不是一定要掌握。

如果用 dp[i][j] 表示同样的含义,容易写出转移方程:

dp[i][j]=max{dp[i1][jk×w[i]]+k×v[i]},0km[i]

这个算法的复杂度大概高的可怕。

d=w[i],a=jd,b=ja×d

则转移方程可改写为:

dp[i][j]=max{dp[i1][b+k×d]k×v[i]}+a×v[i],am[i]ka

(相信大家可以自己看出来是怎么化简的)

dp[i][j] 就是求 j 的前面 m[i]+1 个数 dp[i1][b+k×d]k×v[i] 的最大值,然后加上 a×v[i]

如果将 dp[i][j] 前面所有的 dp[i1][b+k×d]k×v[i] 放入一个队列,那么 dp[i][j] 就是求这个队列长度不超过 m[i]+1 时,元素的最大值,再加上一个常数。所以我们希望用 O(1) 的时间算出一个队列的最大值。

而这也正是单调队列所能做到的。

具体关于单调队列的内容先不讲,留待之后数据结构部分细说。

这里给一下代码:

pair<int,int> Q[M];
for (int i=1;i<=n;i++)
{
    m[i]=min(m[i],W/w[i]);
    for (int j=0;j<w[i];j++)
    {
        int head=0,tail=1;
        for (int k=0;k<=(W-j)/w[i];k++)
        {
            int y=dp[k*w[i]+j]-k*v[i];
            while (head<=tail&&Q[tail].second<=y) tail--;
            Q[++tail]=make_pair(k,y);
            if (Q[head].first<k-m[i]) head++;
            dp[k*w[i]+j]=Q[head].second+k*v[i];
        }
    }
}
C++

学过单调队列再来看这题,我们可以发现,这样做的时间复杂度应当是 O(nW),空间复杂度是 O(W),成功将时间复杂度优化掉了一个 log

混合背包

问题模型

每个物品都有前三个物品的三种可能数量要求,其他不变。

分析

显然可以使用多重背包直接做。

当然,如果想常数优秀,你可能还可以对于每个 i,特判掉 m[i]=1(01 背包)和 m[i]×w[i]W(完全背包)的情况。

多重背包的两种方法往往都已经足够优秀。

二维费用背包

问题模型

与前面的背包问题不同的是,每个物品的体积限制变为了两维。

也就是有两个属性 w[i]g[i],其总和分别不超过 WT

分析

二维版 01 背包显然可以通过加一维状态解决。

dp[i][j][k] 表示从前 i 件物品选出 w[i] 之和为 jg[i] 之和为 k 的物品集合的最大价值和。

则转移显然是:

dp[i][j][k]=max(dp[i1][j][k],dp[i1][jw[i]][kg[i]]+v[i])

当然也可以倒序使得维数下降。

完全背包同理使用顺序即可,多重背包使用二进制拆分即可。

当然这里原来的优先队列方法就较难类比了。

时间复杂度应该是 O(nWT)O(nWTlogM)

空间复杂度应该是 O(WT) 或者 O(nWT)

分组背包

问题模型

在 01 背包的问题基础上,对于物品有一个分组,对于同一组中的物品,至多选择一件。

分析

其实只有把原来的前 i 件,换为前 i 组,并且在最后枚举第 i 组中的每个物品即可。

大致核心代码:

for (int i=1;i<=m;i++)//m 为组数
    for (int j=W;j>=0;j--)
        for (int k=1;k<=size[i];k++)
            if (j>=w[i][k]) dp[j]=max(dp[j],dp[j-w[i][k]]+v[i][k]);
C++

时空复杂度还是 O(nW)O(W)

有依赖背包

简化问题模型

在 01 背包的问题基础上,物品间存在一些依赖关系,比如选择物品 i 就必须选择物品 j

这里我们先假设没有一个物品既被依赖,又依赖别的物品,并且一个物品至多依赖一个物品。

分析

对于不依赖其他物品的物品,称之为”主件“,依赖其他物品的物品,称之为”附件“。

则对于每一个主件及依赖其的 m 个附件,构成了一个组合。

对于这个组合,我们有 2m 种选择方案,也就是如果转化为分组背包,这个分组中将有 2m 个物品。

但是实际上,在体积相同的情况下,我们只需要知道最大的价值即可。

那么我们可以对每个主件 i 的所有附件进行一次体积上限为 Ww[i] 的 01 背包得到 dp[0Ww[i]],那么转化后的分组就是 Ww[i]+1 件物品,体积分别为 k+w[i],价值分别为 dp[k]+v[i]

这之后再进行分组背包即可。

时间复杂度大概是 O(nW2),空间复杂度大概是 O(nW)

一般问题模型

一个物品可以既被依赖又依赖别的物品,也就是这些依赖关系以森林形式给出。

可以考虑从最底层开始不断向上合并,方法与简化问题模型类似,然后用这一层的 dp 值和 v[i],w[i] 的和作为下一层的新的一些物品。

因为这里涉及到了树形 dp 的思想,而且每个子树的物品组都类似于一个泛化物品,所以不细讲,可以在讲完树形 dp 后自行探究。

泛化物品

问题模型

每个物品的价值变为与其体积也就是费用相关的一个函数。

每次选择物品时还要选择它的体积。

也就是说一个泛化物品是一个数组 v[i][0W],给出费用(体积)c,可得到其价值 v[i][c]

可以发现,上面所有的背包问题,都可以转化成这里的泛化物品。

具体转化可以自行探究一下。

分析

对于两个泛化物品,如何选择分别的大小,使其总费用为 j 时总价值最大呢?

设两个泛化物品的函数关系 f[i]g[i],设最终合并两个泛化物品的结果为 h[i]

可以轻松得到转移:

h[j]=jmaxi=0{f[i]+h[ji]}

而事实上,原问题其实就相当于求解 n 个泛化物品的和。

所以自然只需要重复这个过程就能得出泛化物品下的背包问题做法。

一般对于较为复杂的类背包问题,就可以尝试转化为泛化物品解决。

背包问题的变化

求方案

总体思想是记录是从哪一个状态转移而来转移,如通过 g[i] 表示 dp[i] 的状态通过 dp[iw[g[i]]]+v[i] 转移而来。

这样只要对最终状态倒序循环,就能找出每一步的转移,即得方案。

字典序最小方案

也就是需要在转移时,对于价值相同的情况优先选择序号小的。

对于选择了物品 1,则问题转化为体积上限为 Ww[1] 的物品 2n 的背包,否则就是体积不变的物品 2n 的背包。

可以发现问题总是变为 in 的子问题,所以我们可以倒序加入物品。

特别注意,当 dp[i][j]=dp[i1][j]dp[i][j]=dp[i1][jw[i]]+v[i] 同时成立,优先选择后者,也就是选择物品 i

求方案总数

其实就是把 max 换成 即可。

初始条件 dp[0][0]=1

求最优方案数

直接加一个数组,如果更新了最大值,同步更新方案数。

如果与原来最大值相同,则加上新的方案数。

k 优解

无非就是用一个大小不超过 k 的单调下降数组来代替原来的 dp[i][j] 或者 dp[j]

合并时可以类比归并排序。

时间复杂度也就是乘了一个 k

习题

Luogu 2925 干草出售

Luogu 1853 投资的最大效益

Luogu 1776 宝物筛选

Luogu 1833 樱花

Luogu 1507 NASA的食物计划

Luogu 1757 通天之分组背包

Luogu 1064 金明的预算方案

HDU 2639 Bone Collector II

HDU 3810 Magina

 

点赞 0

No Comments

Add your comment

世界是一场空虚的宇宙,有时候仍像是一个梦。