【专题】背包问题及其子问题详解

导读

背包问题是动态规划中非常经典的一类问题,需要仔细地思考背包问题的每一个状态转移方程。

背包问题大致有以下几大类:

  • 01背包问题
  • 完全背包问题
  • 多重背包问题
  • 混合背包问题
  • 二位费用的背包问题
  • 分组背包问题
  • 背包问题求方案数
  • 求背包问题的方案
  • 有依赖的背包问题

下面开始讨论背包问题的这些子问题:

*本文不会讨论后面三种子问题!

01背包问题

模板

N N N 件物品和一个容量是 V V V 的背包。每件物品只能使用一次。第 i i i 件物品的体积是 v [ i ] v[i] v[i],价值是 w [ i ] w[i] w[i]

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。

解法分析

这是背包问题中最简单的一种子问题。其特点是: 每种物品仅有一件,可以选择放或不放

我们首先确定状态: f [ i ] [ j ] f[i][j] f[i][j] 表示前 i i i 个物品在背包容量 j j j 的情况下能获得的最大价值。则其状态转移方程便是: f [ i ] [ j ] = s t d : : m a x ( f [ i − 1 ] [ j ] , f [ i − 1 ] [ j − v [ i ] ] + w [ i ] ) f[i][j]=std::max(f[i-1][j], f[i-1][j-v[i]]+w[i]) f[i][j]=std::max(f[i1][j],f[i1][jv[i]]+w[i])

这个方程非常重要,几乎所有的跟背包问题相关的问题的方程都是由它衍生出来的。希望读者能够完全理解它。

这里由于篇幅有限~~(其实就是懒)~~,给读者提供一个视频链接。这个老师讲的非常清楚。bilibili@codereasy 背包问题

本文将在上面的视频的基础上优化空间复杂度。

以上算法的时间复杂度为 O ( N ∗ V ) O(N*V) O(NV),空间复杂度也是 O ( N ∗ V ) O(N*V) O(NV)。其中,时间复杂度已经基本不能再优化了,但是空间复杂度可以继续优化到 O ( V ) O(V) O(V)

在每一次第二维的循环中, f [ i ] [ j ] f[i][j] f[i][j] 是均由 i − 1 i-1 i1 的状态得来的。 f [ i ] [ j ] f[i][j] f[i][j] f [ i − 1 ] [ j ] f[i-1][j] f[i1][j] 是相互独立的。因此,我们可以考虑将 i i i 这一维舍去。需要注意的是,舍弃掉第一维后,我们必须采用倒序循环的方式。如果仍然使用正序循环,那么就是从 f [ 较 小 体 积 ] f[较小体积] f[] 更新到 f [ 较 大 体 积 ] f[较大体积] f[]。当循环到较大体积时,可能用的是第 i i i 轮的状态而不是第 i − 1 i-1 i1 轮的状态。

举个例子:当我们对一个体积为 8 8 8 的物体进行决策时, f [ 19 ] f[19] f[19] 应该由 f [ 11 ] f[11] f[11] 更新得到。但此时的 f [ 11 ] f[11] f[11] 却不是第 i − 1 i-1 i1 轮的 f [ 11 ] f[11] f[11] ,而是这一轮刚刚更新过的 f [ 11 ] f[11] f[11] 。说的通俗一点,即 f [ 11 ] f[11] f[11] 已经被污染了。而如果使用逆序循环,则不会有这样的问题。

最后,我们的状态转移方程就顺水推舟地写出来了: f [ j ] = f [ j − v [ i ] ] + w [ i ] f[j]=f[j-v[i]]+w[i] f[j]=f[jv[i]]+w[i]

代码:

#include <iostream>
const int N = 39, M = 209;
int m, n;
int w[N], c[N], f[M];
// f[j] 表示 N 件物品,背包容量 j 下的最优解
int main()
{
    scanf("%d%d", &m, &n);
    for (int i = 1; i <= n; i++)
        scanf("%d%d", w + i, c + i);
    for (int i = 1; i <= n; i++)
        for (int j = m; j >= w[i]; j--)
            f[j] = std::max(f[j], f[j-w[i]] + c[i]);
    printf("%d", f[m]);
    return 0;
}

例题:[NOIP2005 普及组] 采药

完全背包问题

模板

N N N 种物品和一个容量是 V V V 的背包,每种物品都有无限件可用。第 i i i 种物品的体积是 v [ i ] v[i] v[i],价值是 w [ i ] w[i] w[i]

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。

解法分析

完全背包的状态转移方程可以由01背包的状态转移方程演变而来,即 f [ i , j ] = m a x { f [ i − 1 , j ] , f [ i − 1 , j − v ] + w , f [ i − 1 , j − 2 ∗ v ] + 2 ∗ w , f [ i − 1 , j − 3 ∗ v ] + 3 ∗ w , . . . } f[i,j]=max\{f[i-1,j],f[i-1,j-v]+w,f[i-1,j-2*v]+2*w,f[i-1,j-3*v]+3*w,...\} f[i,j]=max{f[i1,j],f[i1,jv]+w,f[i1,j2v]+2w,f[i1,j3v]+3w,...} 其中, j > = k ∗ v j>=k*v j>=kv

代码:

#include <iostream>
const int N = 1010;
int n, m, f[N][N], v[N], w[N];
int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++)
		scanf("%d%d", v + i, w + i);
    for (int i = 1; i <= n; i++)
        for(int j = 0; j <= m; j++)
            for (int k = 0; k*v[i] <= j; k++)
                f[i][j] = std::max(f[i][j], f[i-1][j-k*v[i]] + k*w[i]);
	printf("%d", f[n][m]);
    return 0;
}

这样的代码可能会 TLE, 所以我们得想办法优化。

我们将两个方程放在一起看:

f [ i , j ] = m a x { f [ i − 1 , j ] , f [ i − 1 , j − v ] + w , f [ i − 1 , j − 2 ∗ v ] + 2 ∗ w , f [ i − 1 , j − 3 ∗ v ] + 3 ∗ w , . . . } f[i,j]=max\{f[i-1,j],f[i-1,j-v]+w,f[i-1,j-2*v]+2*w,f[i-1,j-3*v]+3*w,...\} f[i,j]=max{f[i1,j],f[i1,jv]+w,f[i1,j2v]+2w,f[i1,j3v]+3w,...}

f [ i , j − v ] = m a x {    f [ i − 1 , j − v ] , f [ i − 1 , j − 2 ∗ v ] + w , f [ i − 1 , j − 3 ∗ v ] + 2 ∗ w , . . . } f[i , j-v]= max\{\qquad \ \ f[i-1,j-v] , f[i-1,j-2*v] + w , f[i-1,j-3*v]+2*w, ...\} f[i,jv]=max{  f[i1,jv],f[i1,j2v]+w,f[i1,j3v]+2w,...}

将两方程整理后得到: f [ i ] [ j ] = m a x ( f [ i , j − v ] + w , f [ i − 1 ] [ j ] ) f[i][j]=max(f[i,j-v]+w,f[i-1][j]) f[i][j]=max(f[i,jv]+w,f[i1][j])

那么,代码就可以优化成这样:

#include <iostream>
const int N = 1010;
int n, m, f[N][N], v[N], w[N];
int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++)
		scanf("%d%d", v + i, w + i);
    for (int i = 1; i <= n; i++)
        for (int j = 0; j <= m; j++)
        {
            f[i][j] = f[i-1][j];
            if(j - v[i] >= 0)
                f[i][j]=max(f[i][j], f[i][j-v[i]]+w[i]);
        }
	printf("%d", f[n][m]);
    return 0;
}

与01背包一样,我们可以将空间复杂度优化成 O ( V ) O(V) O(V)

最终代码:

#include <iostream>
const int N = 209;
int m, n, f[N], w[N], c[N];
int main()
{
	scanf("%d%d", &m, &n);
    for (int i = 1; i <= n; i++)
        scanf("%d%d", w + i, c + i);
    for (int i = 1; i <= n; i++)
        for (int j = w[i]; j <= m; j++)
            f[j] = std::max(f[j], f[j-w[i]] + c[i]);
    printf("%d", f[m]);
    return 0;
}

例题:[AHOI2001]质数和分解

多重背包

模板

N N N 种物品和一个容量是 V V V 的背包。第 i i i 种物品最多有 s [ i ] s[i] s[i] 件,每件体积是 v [ i ] v[i] v[i],价值是 w [ i ] w[i] w[i]

求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。

解法分析

第一种:朴素解法

代码好理解,就不做解释了。只要掌握了01背包的状态转移方程,这个代码问题不大。

#include <iostream>
const int N = 509;
int n, m, f[N];
int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++)
    {
        int s, v, w;
        scanf("%d%d%d", &s, &v, &w);
        for (int j = m; j >= v; j--)
            for (int k = 0; k<=s and j>=k*v; k++)
                f[j] = std::max(f[j], f[j-k*v] + k*w);
    }
    printf("%d", f[m]);
    return 0;
}

注:这个代码是后来写的,将 s, v, w 数组改成了变量,应该能够看懂。

这个代码时间复杂度较高( O ( V ∗ Σ i = 1 N C i ) O(V*\Sigma^N_{i=1}C_i) O(VΣi=1NCi)),当数据较小时,该代码可以通过。但是,当数据很大时,这个代码就一定会 TLE。

所以,我们考虑改善时间复杂度。应当使用二进制拆分法来将多重背包转换成01背包,再求解,速度将大幅提高。

二进制拆分法的原理:从 2 0 , 2 1 , 2 2 , . . . , 2 k − 1 2^0,2^1,2^2,...,2^{k-1} 20,21,22,...,2k1 k k k 2 2 2 的整数次幂中选出若干个相加,可以表示出 0 ∼ 2 k − 1 0\sim 2^k-1 02k1 之间任何一个整数。

例如: 1 , 2 , 4 1,2,4 124可以表示 1 1 1 7 7 7 内所有的正整数。即
1 = 1 2 = 2 3 = 1 + 2 4 = 4 5 = 1 + 4 6 = 2 + 4 7 = 1 + 2 + 4 1=1\\ 2=2\\ 3=1+2\\ 4=4\\ 5=1+4\\ 6=2+4\\ 7=1+2+4 1=12=23=1+24=45=1+46=2+47=1+2+4
按照这个思路,我们可以把数量为 c [ i ] c[i] c[i] 的第 i i i 种物品拆成若干个由二进制数组成的物品数量。例如,有一种物品有 10 10 10 个,每个物品的价值为 100 100 100,体积为 20 20 20。那么,我们就要存:

第一轮: w [ + + c o u n t e r ] = 1 ∗ 100 ,   v [ c o u n t e r ] = 1 ∗ 20 w[++counter]=1*100,\ v[counter]=1*20 w[++counter]=1100, v[counter]=120

第二轮: w [ + + c o u n t e r ] = 2 ∗ 100 ,   v [ c o u n t e r ] = 2 ∗ 20 w[++counter]=2*100,\ v[counter]=2*20 w[++counter]=2100, v[counter]=220

第三轮: w [ + + c o u n t e r ] = 4 ∗ 100 ,   v [ c o u n t e r ] = 4 ∗ 20 w[++counter]=4*100,\ v[counter]=4*20 w[++counter]=4100, v[counter]=420

第四轮: w [ + + c o u n t e r ] = 3 ∗ 100 ,   v [ c o u n t e r ] = 3 ∗ 20 w[++counter]=3*100,\ v[counter]=3*20 w[++counter]=3100, v[counter]=320

这样,我们就把多重背包问题转化成了01背包问题,时间复杂度 O ( l o g   C i ) O(log\ C_i) O(log Ci)

#include <iostream>
const int N = 509, M = 26009;
int n, nn, m, f[N], v[M], w[M];
int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1, s, x, y; i <= n; i++)
    {
        int t = 1;
        scanf("%d%d%d", &s, &x, &y);
        while (s >= t)
        {
            v[++nn] = t * x, w[nn] = t * y;
            s -= t, t <<= 1;
        }
        if (s) { v[++nn] = s * x, w[nn] = s * y; }
    }
    for (int i = 1; i <= nn; i++)
        for (int j = m; j >= v[i]; j--)
            f[j] = std::max(f[j], f[j-v[i]] + w[i]);
    printf("%d", f[m]);
    return 0;
}

混合背包

混合背包即将上述的三种最基础的背包问题体型放在一种问题里。先来看模板题。

模板

N N N 种物品和一个容量是 V V V 的背包。

物品一共有三类:

  • 第一类物品只能用1次(01背包);
  • 第二类物品可以用无限次(完全背包);
  • 第三类物品最多只能用 s [ i ] s[i] s[i] 次(多重背包);

每种体积是 v [ i ] v[i] v[i],价值是 w [ i ] w[i] w[i]。求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。

解法分析

当该物品有无限个时,则按照完全背包的解法来处理;

当该物品有有限个时,则按照多重背包或者01背包的解法来处理。

代码:

#include <iostream>
const int N = 209;
int v, n, f[N];
int main()
{
    scanf("%d%d", &v, &n);
    for (int i = 1, w, c, p; i <= n; i++)
    {
        scanf("%d%d%d", &w, &c, &p);
        if (p == 0)
            for (int j = w; j <= v; j++)
                f[j] = std::max(f[j], f[j-w] + c);
        else
            for (int k = 1; k <= p; k++)
                for (int j = v; j >= w; j--)
                    f[j] = std::max(f[j], f[j-w] + c);
    }
    printf("%d", f[v]);
    return 0;
}

二维费用的背包问题

模板

N N N 件物品和一个容量是 V V V 的背包,背包能承受的最大重量是 M M M

每件物品只能用一次。体积是 v i v_i vi,重量是 m i m_i mi,价值是 w i w_i wi

求解将哪些物品装入背包,可使物品总体积不超过背包容量,总重量不超过背包可承受的最大重量,且价值总和最大。

解法分析

这种题与前三种常规背包问题几乎相同,只是多了一个限定条件而已。

回顾一维费用的背包问题,状态准是: f [ j ] f[j] f[j] 表示在背包容量为 j j j 的情况下能获得的最大价值

此时,多了一个限定条件,那么我们就自然想到多增一维,即: f [ j ] [ k ] f[j][k] f[j][k] 表示在第一个限制条件为 j j j ,第二个限制条件为 k k k 的情况下能获得的最大价值

同样类比一维费用的背包问题的状态转移方程,二维的状态转移方程应该这么写:
f j , k = m a x ( f j − v i ,   k − m i ,   f j ,   k ) f_{j, k} = max(f_{j − v_i,\ k − m_i},\ f_{j,\ k}) fj,k=max(fjvi, kmi, fj, k)
状态转移方程出来了,那么写代码也就轻松了。

#include <iostream>
const int N = 400;
int g, v, n, f[N][N];
int main() {
    scanf("%d%d%d", &g, &v, &n);
    for (int i = 1, value, w1, w2; i <= n; i++)
    {
        scanf("%d%d%d", &value, &w1, &w2);
        for (int j = g; j >= w1; j--)
            for (int l = v; l >= w2; l--)
                f[j][l] = std::max(f[j][l], f[j - w1][l - w2] + value);
    }
    printf("%d", f[g][v]);
    return 0;
}

分组背包

模板

N N N 组物品和一个容量是 V V V 的背包。

每组物品有若干个,同一组内的物品最多只能选一个。每件物品的体积是 v i , j v_{i,j} vi,j,价值是 w i , j w_{i,j} wi,j,其中 i i i 是组号, j j j 是组内编号。求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。

解法分析

这个问题变成了每组物品有若干个策略:是选择本组的某一件,还是不选。

状态转移方程: f [ k ] [ v ] = m a x { f [ k − 1 ] [ v ] ,   f [ k − 1 ] [ v − w [ i ] ] + c [ i ] ∣ 物 品 i ∈ 第 k 组 } f[k][v]=max\{f[k-1][v],\ f[k-1][v-w[i]]+c[i]\bigg| 物品i\in 第k组 \} f[k][v]=max{f[k1][v], f[k1][vw[i]]+c[i]ik}

代码:

#include <iostream>
const int N = 39, M = 209;
int v, n, t;
int w[N], c[N], a[11][N], f[M];
int main() {
    scanf("%d%d%d", &v, &n, &t);
    for (int i = 1, p; i <= n; i++)
    {
        scanf("%d%d%d", w + i, c + i, &p);
        a[p][++a[p][0]] = i; // a[p][0] is the number of items in group p
    }
    for (int k = 1; k <= t; k++)
        for (int j = v; j >= 0; j--)
            for (int i = 1; i <= a[k][0]; i++)
                if (j>=w[a[k][i]] and f[j]<f[j-w[a[k][i]]]+c[a[k][i]])
                    f[j] = f[j-w[a[k][i]]] + c[a[k][i]];
    printf("%d", f[v]);
    return 0;
}

注意: for (int j) 必须放在 for (int i) 的外面, 这样才能保证每一组内的物品最多只有一个会被添加到背包中.

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值