动态规划 DP 讲解(三)

文章介绍了背包问题的三种经典类型:01背包、完全背包和多重背包。01背包不允许物品重复选取,完全背包允许无限次选取同一物品,多重背包则限制物品的选取次数。通过动态规划,可以优化求解过程,降低时间复杂度。文章提供了相应的状态转移方程和优化策略,并给出了代码示例。
摘要由CSDN通过智能技术生成

为了方便,我们叫它 DP


上次讲了 「 01 背包」 「01 背包」 01背包」 ,这次来说说完全背包。

这里说一句, 01 背包实际上是大部分背包问题的基础,他们都是由 01 背包演化而来的。

例如这次要说的完全背包,它和 01 背包唯一不一样的就是每一个物品能取无限次。

引入例题:P1616 疯狂的采药

PS:原题是《采药》,也可以去做做看,原题是 01 背包

我们可以考虑一个朴素的做法:对于第 i 件物品,枚举其选了多少个来转移。这样做的时间复杂度是 O ( n 3 ) O(n^3) O(n3) 的。

状态转移方程如下:
f [ i ] [ j ] = max ⁡ k = 0 + ∞ ( f [ i − 1 ] [ j − k × w [ i ] ] + v [ i ] × k ) f[i][j]=\max_{k=0}^{+\infty}(f[i-1][j-k\times w[i]]+v[i] \times k) f[i][j]=maxk=0+(f[i1][jk×w[i]]+v[i]×k)

考虑做一个简单的优化。可以发现,对于 f [ i ] [ j ] f[i][j] f[i][j],只要通过 f [ i ] [ j − w [ i ] ] f[i][j-w[i]] f[i][jw[i]] 转移就可以了。因此状态转移方程为:

f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i ] [ j − w [ i ] ] + v [ i ] ) f[i][j]=max(f[i-1][j],f[i][j-w[i]]+v[i]) f[i][j]=max(f[i1][j],f[i][jw[i]]+v[i])

理由是当我们这样转移时, f [ i ] [ j − w [ i ] ] f[i][j-w[i]] f[i][jw[i]] 已经由 f [ i ] [ j − 2 × w [ i ] ] f[i][j-2 \times w[i]] f[i][j2×w[i]] 更新过,那么 f [ i ] [ j − w [ i ] ] f[i][j-w[i]] f[i][jw[i]] 就是充分考虑了第 i i i 件物品所选次数后得到的最优结果。换言之,我们通过局部最优子结构的性质重复使用了之前的枚举过程,优化了枚举的复杂度。

与 0-1 背包相同,我们可以将第一维去掉来优化空间复杂度。

code:

#include<bits/stdc++.h>

using namespace std;

long long f[10000001],a[10000001],w[10000001];
int n,m;

int main()
{
    cin>>n>>m;
    for (int i=1;i<=m;i++)
    {
        cin>>w[i]>>a[i];
    }
    for (int i=1;i<=m;i++)
    {
        for (int j=w[i];j<=n;j++)
        {
            f[j]=max(f[j],f[j-w[i]]+a[i]);
        }
    }
    cout<<f[n]<<endl;
    return 0;
}

既然都说到了完全背包,那就再说说和它相似的多重背包。

他们两个唯一的不通点是,多重背包问题中一个物品能取的次数是有限的。

一个很朴素的想法就是:

把「每种物品选 k i k_i ki 次」等价转换为 「有 k i k_i ki 个相同的物品,每个物品选一次」。

这样就转换成了一个 0-1 背包模型,套用上文所述的方法就可已解决。状态转移方程如下:

f [ i ] [ j ] = max ⁡ k = 0 k [ i ] ( f [ i − 1 ] [ j − k × w [ i ] ] + v [ i ] × k ) f[i][j]=\max_{k=0}^{k[i]}(f[i-1][j - k \times w[i]]+v[i] \times k) f[i][j]=maxk=0k[i](f[i1][jk×w[i]]+v[i]×k)

时间复杂度 O ( W ∑ i = 1 n k i ) O(W\sum_{i=1}^nk_i) O(Wi=1nki)

考虑优化。我们仍考虑把多重背包转化成 0-1 背包模型来求解。

显然,复杂度中的 O ( n W ) O(nW) O(nW) 部分无法再优化了,我们只能从 O ( ∑ k i ) O(\sum k_i) O(ki) 处入手。为了表述方便,我们用 A i , j A_{i,j} Ai,j 代表第 i i i 种物品拆分出的第 j j j 个物品。

在朴素的做法中, ∀ j ≤ k i , A i , j \forall j\le k_i,A_{i,j} jkiAi,j 均表示相同物品。那么我们效率低的原因主要在于我们进行了大量重复性的工作。举例来说,我们考虑了「同时选 A i , 1 , A i , 2 A_{i,1},A_{i,2} Ai,1,Ai,2」与「同时选 A i , 2 , A i , 3 A_{i,2},A_{i,3} Ai,2,Ai,3」这两个完全等效的情况。这样的重复性工作我们进行了许多次。那么优化拆分方式就成为了解决问题的突破口。

我们可以通过「二进制分组」的方式使拆分方式更加优美。

具体地说就是令 A i , j ( j ∈ [ 0 , ⌊ log ⁡ 2 ( k i + 1 ) ⌋ − 1 ] ) A_{i,j}\left(j\in\left[0,\lfloor \log_2(k_i+1)\rfloor-1\right]\right) Ai,j(j[0,log2(ki+1)⌋1]) 分别表示由 2 j 2^{j} 2j 个单个物品「捆绑」而成的大物品。特殊地,若 k i + 1 k_i+1 ki+1 不是 2 2 2 的整数次幂,则需要在最后添加一个由 k i − 2 ⌊ log ⁡ 2 ( k i + 1 ) ⌋ − 1 k_i-2^{\lfloor \log_2(k_i+1)\rfloor-1} ki2log2(ki+1)⌋1 个单个物品「捆绑」而成的大物品用于补足。

举几个例子:

6=1+2+3
8=1+2+4+1
18=1+2+4+8+3
31=1+2+4+8+16

显然,通过上述拆分方式,可以表示任意 ≤ k i \le k_i ki 个物品的等效选择方式。将每种物品按照上述方式拆分后,使用 0-1 背包的方法解决即可。

时间复杂度 O ( W ∑ i = 1 n log ⁡ 2 k i ) O(W\sum_{i=1}^n\log_2k_i) O(Wi=1nlog2ki)

实现代码:

index = 0;
for (int i = 1; i <= m; i++) {
  int c = 1, p, h, k;
  cin >> p >> h >> k;
  while (k > c) {
    k -= c;
    list[++index].w = c * p;
    list[index].v = c * h;
    c *= 2;
  }
  list[++index].w = p * k;
  list[index].v = h * k;
}

好了,说了三种背包,是时候综合起来了,下面说说混合背包。

混合背包就是将前面三种的背包问题混合起来,有的只能取一次,有的能取无限次,有的只能取 k k k 次。

这种题目看起来很吓人,可是只要领悟了前面几种背包的中心思想,并将其合并在一起就可以了。下面给出代码:

for (循环物品种类) 
{
	if (01 背包) 01 背包代码;
	else if (是完全背包) 完全背包代码;
	else if (是多重背包) 多重背包代码;
}

练习题:P1833 樱花

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值