第五章 动态规划(3):背包模型_基础模型

背包问题
在这里插入图片描述

时间复杂度

DP时间复杂度 = 状态数量 x 转移的计算量

一 基础模型

  • 01 01 01 背包问题:每件物品只有一个,故每件物品最多只能用一次,即被称为 01 01 01 背包( 01 01 01 背包问题是一种特殊的多重背包问题);
  • 完全背包问题:每件物品有无限个;
  • 多重背包问题:每件物品的数量不同(多重背包问题是分组背包问题的特例);
  • 分组背包:物品分为 n n n 组,每组里面有若干个,但每组里面只能选择一个物品。

一个关系就是:「 01 01 01 背包」 是 「多重背包」 的特例,「多重背包」 是 「分组背包」 的特例。

1.1 01背包问题

ACWing 2

每件物品只有一个,故每件物品最多只能用一次,即被称为01背包。

集合
f [ i ] [ j ] f[i][j] f[i][j]:从前 i i i 个物品中选择,总体积小于等于 j j j 的选法的价值最大值

集合划分

  • 不选择第 i i i 个物品: f [ i ] [ j ] = f [ i − 1 ] [ j ] f[i][j] = f[i - 1][j] f[i][j]=f[i1][j]
  • 选择第 i i i 个物品。当第 i i i 个物品的体积满足 v [ i ] < = j v[i] <= j v[i]<=j 的时候: f [ i − 1 , j − v [ i ] ] + w [ i ] f[i-1, j-v[i]] + w[i] f[i1,jv[i]]+w[i]

状态表示
二维状态表示: f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i − 1 ] [ j − v [ i ] ] + w [ i ] ) f[i][j] = max(f[i-1][j],f[i-1][j-v[i]] + w[i]) f[i][j]=max(f[i1][j],f[i1][jv[i]]+w[i])
二维变一维: f [ j ] = m a x ( f [ j ] , f [ j − v [ i ] ] + w [ i ] ) f[j] = max(f[j], f[j - v[i]] + w[i]) f[j]=max(f[j],f[jv[i]]+w[i])(注意for循环是从大到小)

状态转移方程
f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] ,   f [ i − 1 ] [ j − v [ i ] ] + w [ i ] ) f[i][j] = max(f[i -1][j],\, f[i - 1][j - v[i]] + w[i]) f[i][j]=max(f[i1][j],f[i1][jv[i]]+w[i])

状态初始化
f [ 0 ] [ j ] = 0 f[0][j] = 0 f[0][j]=0:取 0 0 0 件物品且体积不超过 j j j 的取法的价值最大值。

二维DP

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1010;

int n, m;
int v[N], w[N];
int f[N][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]) f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
        }
    printf("%d\n", f[n][m]);
    return 0;
}

一维DP

状态 f [ j ] f[j] f[j] N N N 件物品,背包容量为 j j j 下的最优解

注意枚举背包容量的时候必须从 m m m 开始(从大到小)。

解释:因为在二维情况下,状态 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] 是独立的。而优化到一维后,如果我们还是正序,在 f [ 较小体积 ] f[较小体积] f[较小体积] 更新到 f [ 较大体积 ] f[较大体积] f[较大体积] 的时候,有可能本应该使用的时候第 i − 1 i-1 i1 轮的状态却用的是第 i i i 轮的状态。

比如一个例子:一维状态第 i i i 轮对体积为3的物品进行决策,如果使用顺序,会先更新 f [ 4 ] f[4] f[4] 再更新 f [ 7 ] f[7] f[7],对于这个书包问题来讲,就是有可能,在更新 f [ 4 ] f[4] f[4] 的时候,已经把这次能加的物品加进来了,然后更新 f [ 7 ] f[7] f[7] 的时候,还有可能再加一次,所以必须使用逆序,保证, f [ 4 ] f[4] f[4] 是没有加入新物品前,背包里的最优解。

这个过程还可以听一听提高课的背包模型(二)一节 2:10:00 开始的优化过程,那里也会讲解。

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1010;

int n, m;
int v[N], w[N], f[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 = m; j >= v[i]; j--)
            f[j] = max(f[j], f[j - v[i]] + w[i]);
    printf("%d\n", f[m]);
    return 0;
}

更简单的写法

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1010;

int n, m;
int f[N];

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

1.2 完全背包问题

ACWing 3

每件物品有无限个可供使用

集合
f [ i ] [ j ] f[i][j] f[i][j]:所有只考虑前 i i i 个物品,且总体积小于等于 j j j 的所有选法的价值最大值

集合划分
按第 i i i 类物品最多选择 k k k 个来划分:

  • k = 0 k=0 k=0 时,表示不选择第 i i i 类物品,这时的状态就等于 i − 1 i-1 i1 的状态: f [ i ] [ j ] = f [ i − 1 ] [ j ] f[i][j] = f[i-1][j] f[i][j]=f[i1][j]
  • k ≠ 0 k \ne 0 k=0 的时,表示选择第 i i i 类物品 k k k 件,这时候等于 i − 1 i-1 i1 时候的状态加上选择 k k k 件第 i i i 种物品的价值: f [ i ] [ j ] = f [ i − 1 ] [ j − k × v [ i ] ] + k × w [ i ] f[i][j] = f[i - 1][j - k \times v[i]] + k \times w[i] f[i][j]=f[i1][jk×v[i]]+k×w[i]

状态转移方程

f [ i ] [ j ] = m a x ( f [ i ] [ j ] , f [ i − 1 ] [ j − k × v [ i ] ] + k × w [ i ] ) f[i][j] = max(f[i][j], f[i-1][j - k\times v[i]] + k \times w[i]) f[i][j]=max(f[i][j],f[i1][jk×v[i]]+k×w[i])

三维DP

会TLE

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1010;

int n, m;
int v[N], w[N], f[N][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] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
    printf("%d\n", f[n][m]);
    return 0;
}

二维DP

优化思路:

将式子 f [ i ] [ j ] = m a x ( f [ i ] [ j ] , f [ i − 1 ] [ j − k × v [ i ] ] + k × w [ i ] ) f[i][j] = max(f[i][j], f[i-1][j - k \times v[i]] + k \times w[i]) f[i][j]=max(f[i][j],f[i1][jk×v[i]]+k×w[i]) 展开 (下面式子中使用 w w w 代替 w [ i ] w[i] w[i] v v v 代替 v [ i ] v[i] v[i])
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 − 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 − 1 ] [ j − 4 v ] + 3 w ,   ⋯   ) ⇒ f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] ,   f [ i ] [ j − v ] + w ) \begin{aligned} f[i][j] &= max(f[i - 1][j],\, &&f[i - 1][j - v] + w,\, &&f[i - 1][j - 2v] + 2w,\, &&f[i - 1][j - 3v] + 3w, \, \cdots)\\ f[i][j - v] &= max(&&f[i-1][j - v], \, &&f[i-1][j - 2v] + w,\, &&f[i-1][j-3v] + 2w,\, &&f[i-1][j-4v]+ 3w,\, \cdots)\\ \Rightarrow f[i][j] &= max(f[i-1][j], \ f[i][j -v] + w)\\ \end{aligned} f[i][j]f[i][jv]f[i][j]=max(f[i1][j],=max(=max(f[i1][j], f[i][jv]+w)f[i1][jv]+w,f[i1][jv],f[i1][j2v]+2w,f[i1][j2v]+w,f[i1][j3v]+3w,)f[i1][j3v]+2w,f[i1][j4v]+3w,)

注意与 01 01 01 背包问题二维状态表示区分:

  • 01 01 01 背包二维状态表示为 f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] ,   f [ i − 1 ] [ j − v [ i ] ] + w [ i ] ) f[i][j] = max(f[i -1][j],\, f[i - 1][j - v[i]] + w[i]) f[i][j]=max(f[i1][j],f[i1][jv[i]]+w[i])
  • 在一维状态遍历 j j j 的时候是从小到大
#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1010;

int n, m;
int v[N], w[N], f[N][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]) f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);  // 这里的i没有减1
        }
    printf("%d\n", f[n][m]);
    return 0;
}

一维DP

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1010;

int n, m;
int v[N], w[N], f[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 = v[i]; j <= m; j++)
            f[j] = max(f[j], f[j - v[i]] + w[i]);
    printf("%d\n", f[m]);
    return 0;
}

更简单的写法

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1010;

int n, m;
int f[N];

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) {
        int v, w; scanf("%d%d", &v, &w);
        for (int j = v; j <= m; j++)
            f[j] = max(f[j], f[j - v] + w);
    }
    printf("%d\n", f[m]);
    return 0;
}

1.3 多重背包问题 I

ACWing 4

每件物品可供使用的数量不同。多重背包问题是分组背包问题的特例。

集合
f [ i ] [ j ] f[i][j] f[i][j]:所有只从前 i i i 个物品中选择且总体积不超过的选 j j j 法的价值最大值

集合划分
按从第 i i i 个物品中选个 k k k 物品来划分,其中 k ∈ [ 0 , s [ i ] ] k \in [0, s[i]] k[0,s[i]]

状态转移方程
三重循环: f [ i ] [ j ] = m a x ( f [ i ] [ j ] ,   f [ i − 1 ] [ j − k × v [ i ] ] + k × w [ i ] ) f[i][j] = max(f[i][j], \, f[i - 1][j - k \times v[i]] + k \times w[i]) f[i][j]=max(f[i][j],f[i1][jk×v[i]]+k×w[i])

三重循环

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 110;

int n, m;
int v[N], w[N], s[N], f[N][N];

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) scanf("%d%d%d", v + i, w + i, s + i);
    for (int i = 1; i <= n; i++)
        for (int j = 0; j <= m; j++)
            for (int k = 0; k <= s[i] && k * v[i] <= j; k++)
                f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
    printf("%d\n", f[n][m]);
    return 0;
}

更简洁的写法

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 110;

int n, m;
int f[N][N];

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) {
        int v, w, s; scanf("%d%d%d", &v, &w, &s);
        for (int j = 0; j <= m; j++)
            for (int k = 0; k <= s && k * v <= j; k++)
                f[i][j] = max(f[i][j], f[i - 1][j - k * v] + k * w);
    }
    printf("%d\n", f[n][m]);
    return 0;
}

1.4 多重背包问题 II (二进制优化)

ACWing 5

优化思路:对每一个物品的数量进行二进制优化

时间复杂度: O ( n v s ) O(nvs) O(nvs) 降到了 O ( n v l o g s ) O(nvlogs) O(nvlogs)

二进制枚举的思路简介:假设一个数 s = 1023 s = 1023 s=1023,即物品有 1023 1023 1023 个,那么我们用上面的思路进行枚举的时候就会枚举出 k = 0 ∼ 1023 k = 0 \sim 1023 k=01023。现在换个思路,将若干个数打包一块考虑,这里将其打包成 10 10 10 组,每组分别有 1 、 2 、 4 、 8 、 16 、 32 、 64 、 128 、 256 、 512 1、2、4、8、16、32、64、128、256、512 1248163264128256512 个数,每次每组最多只能选择 1 1 1 个数,那么这从这 10 10 10 组中选择出的 10 10 10 个数便能表示出 1 ∼ 1023 1 \sim 1023 11023 中的任意一个数。比如:

  • 只有 1 1 1 时,只能表示出 1 1 1
  • 加上组 2 2 2 (包含数2和3)后, 1 、 2 1、2 12两个组便能表示出 1 ∼ 3 1\sim 3 13
  • 加上组 4 4 4 (包含数4、5、6、7)后, 1 、 2 、 4 1、2、4 124 三个组便能表示出 1 ∼ 7 1 \sim 7 17

将每一组中取出的数进行加和,便能表示出 1 ∼ 1023 1 \sim 1023 11023 的所有数。

对于每一组,我们可以将其看成是 01 01 01 背包问题,每次只选择一个,用 10 10 10 个新的物品可以表示出我们原来的所有物品,时间复杂度从 n n n 降为了 l o g n logn logn(比如 l o g ( 1024 ) = 10 log(1024) = 10 log(1024)=10)。给定一个物品数量 s s s,就可以将其拆分成 l o g s logs logs 个分组。

注意:

  • l o g log log 的底数是 2 2 2
  • s s s 是一个一般的数时,如 s = 200 s = 200 s=200 的时候,每组数据就为 1 、 2 、 4 、 8 、 16 、 32 、 64 、 73 ( 其中 73 = 200 − ( 63 + 64 ) = 200 − 127 ) 1、2、4、8、16、32、64、73 (其中 73 = 200 - (63 + 64) = 200 - 127) 124816326473(其中73=200(63+64)=200127),这里最后一组若取 128 128 128,则总共就能表示到 1   256 1 ~ 256 1 256,大于了 200 200 200

在对本题进行处理的时候,直接将所有物品看成一个整体,从第一件物品开始从头对其进行分组,直到分完所有物品为止,后续就进行 01 01 01 背包问题同样的处理即可。

注意代码中的 N = 12000 , M = 2010 N=12000,M=2010 N=12000,M=2010,这里 M M M 代表最大容量,题目中给出,对于 N N N 代表最大分组数量,由题可知最大分组数目为 N × S = 2 × 1 0 6   ( N = 1000 , S = 2000 ) N\times S=2 \times 10^6\ (N=1000, S=2000) N×S=2×106 (N=1000,S=2000),对其取优化后有 ⌈ N × l o g S ⌉ = 12000 \left \lceil N\times logS \right \rceil = 12000 N×logS=12000

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 12000, M = 2010;

int n, m;
int v[N], w[N], f[M];

int main() {
    scanf("%d%d", &n, &m);
    int cnt = 0;  // 存储新的分组编号
    for (int i = 1; i <= n; i ++ ) {
        int a, b, s; 
        scanf("%d%d%d", &a, &b, &s);
        int k = 1; 
        while (k <= s) {  // 分组
            cnt ++ ;
            v[cnt] = a * k, w[cnt] = b * k;
            s -=k, k <<= 1;
        }
        if (s > 0) {  // 处理剩余部分
            cnt ++ ;
            v[cnt] = a * s, w[cnt] = b * s;
        }
    }
    n = cnt;
    for (int i = 1; i <= n; i ++ )
        for (int j = m; j >= v[i]; j -- )
            f[j] = max(f[j], f[j - v[i]] + w[i]);
    printf("%d\n", f[m]);
    return 0;
}

更简洁的写法:直接将 k × v k \times v k×v 就是一个物品体积

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 2010;

int n, m;
int f[N];

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++ ) {
        int v, w, s;
        scanf("%d%d%d", &v, &w, &s);
        for (int k = 1; k <= s; k <<= 1) {
            for (int j = m; j >= k * v; j --) 
                f[j] = max(f[j], f[j - k * v] + k * w);
            s -= k;
        }
        if (s)
            for (int j = m; j >= s * v; j -- )
                f[j] = max(f[j], f[j - s * v] + s * w);
    }
    printf("%d\n", f[m]);
    return 0;
}

1.5 多重背包问题 III (单调队列优化)

ACwing 6

题解参考

优化思路:

将式子 f [ i ] [ j ] = m a x ( f [ i ] [ j ] ,   f [ i − 1 ] [ j − k × v [ i ] ] + k × w [ i ] ) f[i][j] = max(f[i][j], \, f[i - 1][j - k \times v[i]] + k \times w[i]) f[i][j]=max(f[i][j],f[i1][jk×v[i]]+k×w[i]) 展开(下面式子中使用 w w w 代替 w [ i ] w[i] w[i] v v v 代替 v [ i ] v[i] v[i])

f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i − 1 ] [ j − v ] + w , ⋯   , f [ i − 1 ] [ j − s v ] + s w ) f [ i ] [ j − v ] = m a x ( f [ i − 1 ] [ j − v ] , f [ i − 1 ] [ j − 2 v ] + w , ⋯   , f [ i − 1 ] [ j − ( s + 1 ) v ] + s w ) \begin{aligned} &f[i][j]=max(f[i-1][j],f[i-1][j-v]+w,\cdots,f[i-1][j-sv]+sw)\\ &f[i][j-v]=max(f[i-1][j-v],f[i-1][j-2v]+w,\cdots,f[i-1][j-(s+1)v]+sw)\\ \end{aligned} f[i][j]=max(f[i1][j],f[i1][jv]+w,,f[i1][jsv]+sw)f[i][jv]=max(f[i1][jv],f[i1][j2v]+w,,f[i1][j(s+1)v]+sw)

由于完全背包问题是将所有体积用完,而多重背包问题对于每个物品的个数是有限制的,导致等式上面与完全背包问题略有不同,但是可以将这个式子继续推导下去,直到背包体积不能用为止。

f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i − 1 ] [ j − v ] + w , ⋯   , f [ i − 1 ] [ j − s v ] + s w ) f [ i ] [ j − v ] = m a x ( f [ i − 1 ] [ j − v ] , f [ i − 1 ] [ j − 2 v ] + w , ⋯   , f [ i − 1 ] [ j − ( s + 1 ) v ] + s w ) f [ i ] [ j − 2 v ] = m a x ( f [ i − 1 ] [ j − 2 v ] , f [ i − 1 ] [ j − 3 v ] + w , ⋯   , f [ i − 1 ] [ j − ( s + 2 ) v ] + s w ) ⋯ f [ i ] [ r + s v ] = m a x ( f [ i − 1 ] [ r + s v ] , f [ i − 1 ] [ r + ( s − 1 ) v ] + w , ⋯   , f [ i − 1 ] [ r ] + s w ) f [ i ] [ r + ( s − 1 ) v ] = m a x ( f [ i − 1 ] [ r + ( s − 1 ) v ] , ⋯   , f [ i − 1 ] [ r ] + ( s − 1 ) w ) ⋯ f [ i ] [ r + 2 v ] = m a x ( f [ i − 1 ] [ r + 2 v ] , f [ i − 1 ] [ r + v ] + w , f [ i − 1 ] [ r ] + 2 w ) f [ i ] [ r + v ] = m a x ( f [ i − 1 ] [ r + v ] , f [ i − 1 ] [ r ] + w ) f [ i ] [ r ] = f [ i − 1 ] [ r ] \begin{aligned} &f[i][j]=max(f[i-1][j],f[i-1][j-v]+w,\cdots,f[i-1][j-sv]+sw)\\ &f[i][j-v]=max(f[i-1][j-v],f[i-1][j-2v]+w,\cdots,f[i-1][j-(s+1)v]+sw)\\ &f[i][j-2v] = max(f[i-1][j-2v],f[i-1][j-3v]+w,\cdots,f[i-1][j-(s+2)v]+sw)\\ &\cdots\\ &f[i][r+sv] = max(f[i-1][r+sv], f[i-1][r+(s-1)v]+w,\cdots,f[i-1][r]+sw)\\ &f[i][r+(s-1)v] = max(f[i-1][r+(s-1)v],\cdots,f[i-1][r]+(s-1)w)\\ &\cdots\\ &f[i][r+2v] = max(f[i-1][r+2v],f[i-1][r+v]+w,f[i-1][r]+2w)\\ &f[i][r+v] = max(f[i-1][r+v],f[i-1][r]+w)\\ &f[i][r] = f[i-1][r]\\ \end{aligned} f[i][j]=max(f[i1][j],f[i1][jv]+w,,f[i1][jsv]+sw)f[i][jv]=max(f[i1][jv],f[i1][j2v]+w,,f[i1][j(s+1)v]+sw)f[i][j2v]=max(f[i1][j2v],f[i1][j3v]+w,,f[i1][j(s+2)v]+sw)f[i][r+sv]=max(f[i1][r+sv],f[i1][r+(s1)v]+w,,f[i1][r]+sw)f[i][r+(s1)v]=max(f[i1][r+(s1)v],,f[i1][r]+(s1)w)f[i][r+2v]=max(f[i1][r+2v],f[i1][r+v]+w,f[i1][r]+2w)f[i][r+v]=max(f[i1][r+v],f[i1][r]+w)f[i][r]=f[i1][r]

其中 r = j   m o d   v [ i ] r = j \ mod \ v[i] r=j mod v[i],可以理解为完全背包下把当前物品选到不能再选后,剩下的余数。

得到 f [ i ] [ r ] = f [ i − 1 ] [ r ] f[i][r] = f[i-1][r] f[i][r]=f[i1][r] 之后,再利用完全背包的思路倒推回去,会发现一个滑动窗口求最大值的模型。

下面记 f [ i − 1 ] [ j ] f[i-1][j] f[i1][j] f j f_j fj

f [ i ] [ r ] = f r f [ i ] [ r + v ] = m a x ( f r + v , f r + w ) f [ i ] [ r + 2 v ] = m a x ( f r + 2 v , f r + v + w , f r + 2 v ) ⋯ f [ i ] [ r + ( s − 1 ) v ] = m a x ( f r + ( s − 1 ) v , f r + ( s − 2 ) v + w , ⋯   , f r + ( s − 1 ) w ) f [ i ] [ r + s v ] = m a x ( f r + s v , f r + ( s − 1 ) v + w , ⋯   , f r + s w )    (滑动窗口已满) f [ i ] [ r + ( s + 1 ) v ] = m a x ( f r + ( s + 1 ) v , f r + s v + w , ⋯   , f r + v + s w )    (滑动窗口已满) ⋯ f [ i ] [ j − 2 v ] = m a x ( f j − 2 v , f j − 3 v + w , ⋯   , f j − ( s + 2 ) v + s w )    (滑动窗口已满) f [ i ] [ j − v ] = m a x ( f j − v , f j − 2 v + w , ⋯   , f j − ( s + 1 ) v + s w )    (滑动窗口已满) f [ i ] [ j ] = m a x ( f j , f j − v + w , ⋯   , f j − s v + s w )    (滑动窗口已满) \begin{aligned} &f[i][r] = f_r\\ &f[i][r+v] = max(f_{r+v},f_r+w)\\ &f[i][r+2v] = max(f_{r+2v},f_{r+v}+w,f_r+2v)\\ &\cdots\\ &f[i][r+(s-1)v]=max(f_{r+(s-1)v},f_{r+(s-2)v}+w,\cdots,f_r+(s-1)w)\\ &f[i][r+sv] = max(f_{r+sv},f_{r+(s-1)v}+w,\cdots,f_r+sw)\,\,\text{(滑动窗口已满)}\\ &f[i][r+(s+1)v] = max(f_{r+(s+1)v},f_{r+sv}+w,\cdots,f_{r+v}+sw)\,\,\text{(滑动窗口已满)}\\ &\cdots\\ &f[i][j-2v] = max(f_{j-2v},f_{j-3v}+w,\cdots,f_{j-(s+2)v}+sw)\,\,\text{(滑动窗口已满)}\\ &f[i][j-v] = max(f_{j-v},f_{j-2v}+w,\cdots,f_{j-(s+1)v}+sw)\,\,\text{(滑动窗口已满)}\\ &f[i][j] = max(f_j,f_{j-v}+w,\cdots,f_{j-sv}+sw)\,\,\text{(滑动窗口已满)}\\ \end{aligned} f[i][r]=frf[i][r+v]=max(fr+v,fr+w)f[i][r+2v]=max(fr+2v,fr+v+w,fr+2v)f[i][r+(s1)v]=max(fr+(s1)v,fr+(s2)v+w,,fr+(s1)w)f[i][r+sv]=max(fr+sv,fr+(s1)v+w,,fr+sw)(滑动窗口已满)f[i][r+(s+1)v]=max(fr+(s+1)v,fr+sv+w,,fr+v+sw)(滑动窗口已满)f[i][j2v]=max(fj2v,fj3v+w,,fj(s+2)v+sw)(滑动窗口已满)f[i][jv]=max(fjv,fj2v+w,,fj(s+1)v+sw)(滑动窗口已满)f[i][j]=max(fj,fjv+w,,fjsv+sw)(滑动窗口已满)

时间复杂度 O ( N V ) O(NV) O(NV)

二维朴素版本

对代码中的一些解释:

  • 每一次更新所有 i i i 阶段里面的状态 f [ i ] [ j ] f[i][j] f[i][j],只需要额外枚举所有的余数 r r r 即可;
  • 注意在滑动窗口中比较最大值的时候还添加偏移量 w w w,即当前下标和最大值下标之间差了 x x x v v v,就要加上 x x x w w w
  • 滑动窗口的大小为 s [ i ] × v [ i ] s[i] \times v[i] s[i]×v[i]
#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1010, M = 20010;

int n, m;
int v[N], w[N], s[N];
int f[N][M];
int q[M];

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) scanf("%d%d%d", v + i, w + i, s + i);
    for (int i = 1; i <= n; i++)
        for (int r = 0; r < v[i]; r++) {
            int hh = 0, tt = -1;
            for (int j = r; j <= m; j += v[i]) {
                if (hh <= tt && j - q[hh] > s[i] * v[i]) hh++;
                while (hh <= tt && f[i - 1][q[tt]] + (j - q[tt]) / v[i] * w[i] <= f[i - 1][j]) --tt;
                q[++tt] = j;
                f[i][j] = f[i - 1][q[hh]] + (j - q[hh]) / v[i] * w[i];
            }
        }
    printf("%d\n", f[n][m]);
    return 0;
}

拷贝数组的优化版本

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;

const int N = 1010, M = 20010;

int n, m;
int v[N], w[N], s[N];
int f[M], g[M]; // g存储的f[i-1][j]层的状态
int q[M];

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) scanf("%d%d%d", v + i, w + i, s + i);
    for (int i = 1; i <= n; i++) {
        memcpy(g, f, sizeof g);
        for (int r = 0; r < v[i]; r++) {
            int hh = 0, tt = -1;
            for (int j = r; j <= m; j += v[i]) {
                if (hh <= tt && j - q[hh] > s[i] * v[i]) hh++;
                while (hh <= tt && g[q[tt]] + (j - q[tt]) / v[i] * w[i] <= g[j]) tt--;
                q[++tt] = j;
                f[j] = g[q[hh]] + (j - q[hh]) / v[i] * w[i];
            }
        }
    }
    printf("%d\n", f[m]);
    return 0;
}

滚动数组的优化版本

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1010, M = 20010;

int n, m;
int v[N], w[N], s[N];
int f[2][M];
int q[M];

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; ++i) scanf("%d%d%d", v + i, w +i, s + i);
    for (int i = 1; i <= n; ++i) {
        for (int r = 0; r < v[i]; ++r) {
            int hh = 0, tt = -1;
            for (int j = r; j <= m; j += v[i]) {
                if (hh <= tt && j - q[hh] > s[i] * v[i]) hh++;
                while (hh <= tt && f[(i - 1) & 1][q[tt]] + (j - q[tt]) / v[i] * w[i] <= f[(i - 1) & 1][j]) --tt;
                q[++tt] = j;
                f[i & 1][j] = f[(i - 1) & 1][q[hh]] + (j - q[hh]) / v[i] * w[i];
            }
        }
    }
    printf("%d\n", f[n & 1][m]);
    return 0;
}

省去V、W、S数组空间的写法(只写一种优化方法的,这里是滚动数组优化的简写)

#include <iostream>
#include <algorithm>
using namespace std;

const int M = 20010;

int n, m;
int f[2][M];
int q[M];

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; ++i) {
        int v, w, s; scanf("%d%d%", &v, &w, &s);
        for (int r = 0; r < v; ++r) {
            int hh = 0, tt = -1;
            for (int j = r; j <= m; j += v) {
                if (hh <= tt && j - q[hh] > s * v) hh++;
                while (hh <= tt && f[(i - 1) & 1][q[tt]] + (j - q[tt]) / v * w <= f[(i - 1) & 1][j]) --tt;
                q[++tt] = j;
                f[i & 1][j] = f[(i - 1) & 1][q[hh]] + (j - q[hh]) / v * w;
            }
        }
    }
    cout << f[n & 1][m] << endl;
    return 0;
}

1.6 分组背包

ACWing 9

物品分为 n n n 组,每组里面有若干个,但每组里面只能选择一个物品。

集合
f [ i ] [ j ] f[i][j] f[i][j]:只从第 i i i 组物品中选择,且总体积不大于 j j j 的所有选法的价值的最大值

集合划分(选哪个)

  • 从第 i i i 组物品中选择 0 0 0 件物品,即一个都不选: f [ i − 1 ] [ j ] f[i-1][j] f[i1][j]
  • 从第 i i i 组物品中选择第 k k k 件物品,则: f [ i − 1 ] [ j − v [ i ] [ k ] ] + w [ i ] [ k ] f[i-1][j-v[i][k]] + w[i][k] f[i1][jv[i][k]]+w[i][k]

状态转移方程
二维状态: f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i − 1 ] [ j − v [ i ] [ k ] ] + w [ i ] [ k ] f[i][j] = max(f[i-1][j],f[i-1][j-v[i][k]] + w[i][k] f[i][j]=max(f[i1][j],f[i1][jv[i][k]]+w[i][k]
一维状态: f [ j ] = m a x ( f [ j ] , f [ j − v [ i ] [ k ] ] + w [ i ] [ k ] ) f[j] = max(f[j], f[j -v[i][k]] + w[i][k]) f[j]=max(f[j],f[jv[i][k]]+w[i][k])注意for循环是从大到小

未优化版本:

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 110;

int n, m;
int v[N][N], w[N][N], s[N];
int f[N][N];

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++ ) {
        scanf("%d", s + i);
        for (int j = 0; j < s[i]; j ++ )
            scanf("%d%d", &v[i][j], &w[i][j]);
    }
    for (int i = 1; i <= n; i ++ )
        for (int j = 0; j <= m; j ++ ) {
            f[i][j] = f[i - 1][j];
            for (int k = 0; k < s[i]; k ++ )
                if (j >= v[i][k])
                    f[i][j] = max(f[i][j], f[i - 1][j - v[i][k]] + w[i][k]);
        }
    printf("%d\n", f[n][m]);
    return 0;
}

优化版本:

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 110;

int n, m;
int v[N][N], w[N][N], s[N];
int f[N];

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++ ) {
        scanf("%d", s + i);
        for (int j = 0; j < s[i]; j ++ )
            scanf("%d%d", &v[i][j], &w[i][j]);
    }
    for (int i = 1; i <= n; i ++ )
        for (int j = m; j >= 0; j -- )
            for (int k = 0; k < s[i]; k ++ )
                if (j >= v[i][k])
                    f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
    printf("%d\n", f[m]);
    return 0;
}

1.7 有依赖的背包问题

ACwing 10

集合
f [ i ] [ j ] f[i][j] f[i][j]:从所有以第 i i i 个物品为根结点的子树中选择,且选上 i i i,选法总体积不超过的 j j j 方案数的价值最大值

集合划分
每棵树按子树划分,每棵子树按体积划分,每棵子树内部就是一个分组背包问题。

金明的预算方案是按方案来划分的,因为那个题目方案数很小,这个题如果按照方案数来划分,子树下面的情况有 2 n 2^n 2n 种,太多无法保存。

  • 不选择第 i i i 个物品
  • 选择第 i i i 个物品。考虑以第 i i i 个物品为根结点的子树的体积来划分,其中 f ( s o n s i , j ) f(sons_i,j) f(sonsi,j) 表示所有子节点共用体积 j j j 的选法的最大价值

在这里插入图片描述

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;

const int N = 110;

int n, m;
int v[N], w[N];
int h[N], e[N], ne[N], idx;
int f[N][N];

inline void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

inline void dfs(int u) {
    for (int i = h[u]; ~i; i = ne[i]) {
        int son = e[i];
        dfs(e[i]);
        for (int j = m - v[u]; j >= 0; j--)
            for (int k = 0; k <= j; k++)
                f[u][j] = max(f[u][j], f[u][j - k] + f[son][k]);
    }
    for (int i = m; i >= v[u]; i--) f[u][i] = f[u][i - v[u]] + w[u];
    for (int i = 0; i < v[u]; i++) f[u][i] = 0;
}

int main() {
    scanf("%d%d", &n, &m);
    memset(h, -1, sizeof h);
    int root;
    for (int i = 1; i <= n; i++) {
        int p; scanf("%d%d%d", v + i, w + i, &p);
        if (p == -1) root = i; else add(p, i);
    }
    dfs(root);
    printf("%d\n", f[root][m]);
    return 0;
}

1.8 混合背包问题

ACwing 7

多种背包问题混合在一起

  • 状态表示: f [ i ] [ j ] f[i][j] f[i][j]
  • 集合:只从前 i i i 个物品中选择,且总体积不超过的 j j j 的选法的价值最大值
  • 状态计算:
    • 01背包问题: f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i − 1 ] [ j − v [ i ] ] + w [ i ] ) f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]) f[i][j]=max(f[i1][j],f[i1][jv[i]]+w[i])
    • 完全背包问题: f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i ] [ j − v [ i ] ] + w [ i ] ) f[i][j] = max(f[i - 1][j], f[i][j - v[i]] + w[i]) f[i][j]=max(f[i1][j],f[i][jv[i]]+w[i])
    • 多重背包问题: f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i − 1 ] [ j − v [ i ] × k ] + w [ i ] × k ) f[i][j] = max(f[i-1][j], f[i-1][j-v[i]\times k] + w[i] \times k) f[i][j]=max(f[i1][j],f[i1][jv[i]×k]+w[i]×k)

算法思路

由题意,这个题目仅限制了「有 N N N 种物品和一个容量是的 V V V 背包」,所以在对第 i i i 个物品进行状态计算的时候,第个物品 i i i 的种类与前面个物品种 i − 1 i-1 i1 类无关。因此在计算第件物品的时 i i i 候,只需要根据第个物品是什么 i i i 类型的背包问题,就是用什么类型的状态转移方程。

为了考虑时间复杂度,多重背包要使用二进制优化( 01 01 01 背包问题是一种特殊的(每个物品仅有一件)多重背包问题)

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1010;

int n, m;
int f[N];

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 0; i < n; i++) {
        int v, w, s; scanf("%d%d%d", &v, &w, &s);
        if (!s)
            for (int j = v; j <= m; j++)
                f[j] = max(f[j], f[j - v] + w);
        else {
            if (s == -1) s = 1; // 01背包转换为多重背包
            for (int k = 1; k <= s; k *= 2) {
                for (int j = m; j >= k * v; j--)
                    f[j] = max(f[j], f[j - k * v] + k * w);
                s -= k;
            }
            if (s)
                for (int j = m; j >= s * v; j--)
                    f[j] = max(f[j], f[j - s * v] + s * w);
        }
    }
    printf("%d\n", f[m]);
    return 0;
}

1.9 二维费用背包问题

ACwing 8

二维费用背包问题不仅可以和 01 01 01 背包问题结合,也可以和完全背包、多重背包、分组背包问题结合在一块。本题是和 01 01 01 背包问题结合在一起的。

集合

f [ i , j , k ] f[i, j, k] f[i,j,k]:所有制从前 i i i 个物品中选,并且总体积不超过 j j j,总重量不超过 k k k 的选法

集合划分

  • 所有不包含 i i i 的选法,只从中 1 ∼ i − 1 1 \sim i - 1 1i1 选择
  • 所有包含 i i i 的选法

状态计算

  • 左: f [ i − 1 , j , k ] f[i-1, j, k] f[i1,j,k]
  • 右: f [ i − 1 , j − v [ i ] , k − m [ i ] ] + w [ i ] f[i-1, j-v[i], k-m[i]] + w[i] f[i1,jv[i],km[i]]+w[i]
#include <iostream>
#include <algorithm>
using namespace std;

const int N = 110;

int n, V, M;
int f[N][N];

int main() {
    scanf("%d%d%d", &n, &V, &M);
    for (int i = 0; i < n; i++) {
        int v, m, w; scanf("%d%d%d", &v, &m, &w);
        for (int j = V; j >= v; j--)
            for (int k = M; k >= m; k--)
                f[j][k] = max(f[j][k], f[j - v][k - m] + w);
    }
    printf("%d\n", f[V][M]);
    return 0;
}

1.10 背包问题求具体方案(字典序最小的方案数)

ACwing 12

先求出价值,然后找方案。

01 01 01 背包问题为例, 01 01 01 背包问题状态转移方程: f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i − 1 ] [ j − v [ i ] ] + w [ i ] ) f[i][j] = max(f[i-1][j], f[i-1][j-v[i]] + w[i]) f[i][j]=max(f[i1][j],f[i1][jv[i]]+w[i])。所谓求具体方案,其实就是判断出每个物品是否被选,实质是最短路问题求路径,反推当前状态是从哪一个状态转移过来的。

下面参考了这里的题解

由于题目要求求出字典序最小的方案,如果存在一个包含第 1 1 1 个物品的方案,那么为了确保字典序最小,第 1 1 1 个物品必然要选择。接下来的问题就转换成了从第 2... N 2...N 2...N 个物品中取寻找最优解。由之前的 f [ i ] [ j ] f[i][j] f[i][j] 表示为从前 i i i 个物品中选择且体积不超过 j j j 的最优解,现在将 f [ i ] [ j ] f[i][j] f[i][j] 修改为从第 i i i 个元素到最后一个元素中选择,且体积不超过 j j j 的最优解。于是便有了下面状态转移方程: f [ i ] [ j ] = m a x ( f [ i + 1 ] [ j ] , f [ [ i + 1 ] [ j − v [ i ] ] + w [ i ] ) f[i][j] = max(f[i+1][j], f[[i + 1][j-v[i]] + w[i]) f[i][j]=max(f[i+1][j],f[[i+1][jv[i]]+w[i])

  • 前面一种表示不选择第 i i i 件物品,那么最优解便是从第 i + 1 i+1 i+1 件物品到最后一件物品中寻找的且总体积不超过 j j j 的选法;
  • 后面一种表示选择第 i i i 件物品,那么最优解就是从第 i + 1 i+1 i+1 件到最后一件物品中选择,且总体积不超过 j − v [ i ] j - v[i] jv[i] 的选法。

字典序最小问题的解决方法——贪心

由上面的状态定义, f [ 1 ] [ m ] f[1][m] f[1][m]一定是最大值,现在考虑第 1 1 1 个点能否选择:

  • 如果 f [ 1 ] [ m ] = f [ 2 ] [ m − v [ 1 ] ] + w [ 1 ] f[1][m] = f[2][m - v[1]] + w[1] f[1][m]=f[2][mv[1]]+w[1],说明选取了第 1 1 1 个物品是可以得到最优解的;
  • 如果 f [ 1 ] [ m ] = f [ 2 ] [ m ] f[1][m] = f[2][m] f[1][m]=f[2][m],说明不选择第 1 1 1 个物品依然可以得到最优解;
  • 如果 f [ 1 ] [ m ] = f [ 2 ] [ m ] = f [ 2 ] [ m − v [ 1 ] ) + w [ 1 ] f[1][m] = f[2][m] = f[2][m - v[1]) + w[1] f[1][m]=f[2][m]=f[2][mv[1])+w[1],说明选择或者不选择第 1 1 1 个物品都能得到最优解,但是为了使字典序最小,第 1 1 1 件物品依然要选择。
#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1010;

int n, m;
int v[N], w[N];
int f[N][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 = n; i >= 1; i--) // 倒序遍历方便求最小字典序
        for (int j = 0; j <= m; j++) {
            f[i][j] = f[i + 1][j];
            if (j >= v[i]) f[i][j] = max(f[i][j], f[i + 1][j - v[i]] + w[i]);
        }
    int j = m; // f[1][m] 是最大值
    for (int i = 1; i <= n; i++)
        if (j >= v[i] && f[i][j] == f[i + 1][j - v[i]] + w[i])
            printf("%d ", i), j -= v[i];
    puts("");
    return 0;
}

1.11 背包问题求方案数(最大价值的方案数)

ACwing 11

集合

  • f [ i ] [ j ] f[i][j] f[i][j]:考虑前 i i i 个物品,且当前已使用体积不超过 j j j 的方案价值 最大值
  • g [ i ] [ j ] g[i][j] g[i][j]:用于跟踪路径。考虑前 i i i 个物品,当前已使用体积恰好为 j j j 的,且价值最大的方案 数目

状态计算

  • 左边: f [ i − 1 ] [ j ] f[i-1][j] f[i1][j]

  • 右边: f [ i − 1 ] [ j − v [ i ] ] + w [ i ] f[i-1][j-v[i]]+w[i] f[i1][jv[i]]+w[i]

  • 如果左边大,则记录 g [ i − 1 ] [ j ] g[i-1][j] g[i1][j]

  • 如果右边大,则记录 g [ i − 1 ] [ j − v [ i ] ] g[i-1][j-v[i]] g[i1][jv[i]]

  • 如果左边右边一样大,则记录 g [ i − 1 ] [ j ] + g [ i − 1 ] [ j − v [ i ] ] g[i-1][j] + g[i-1][j-v[i]] g[i1][j]+g[i1][jv[i]]

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;

const int N = 1010, mod = 1e9 + 7;

int n, m;
int f[N], g[N];

int main() {
    scanf("%d%d", &n, &m);
    memset(f, -0x3f, sizeof f);
    f[0] = 0, g[0] = 1;
    for (int i = 0; i < n; i++) {
        int v, w; scanf("%d%d", &v, &w);
        for (int j = m; j >= v; j--) {
            int maxv = max(f[j], f[j - v] + w);
            int cnt = 0;
            if (maxv == f[j]) cnt += g[j];
            if (maxv == f[j - v] + w) cnt += g[j - v];
            g[j] = cnt % mod, f[j] = maxv;
        }
    }
    int res = 0;
    for (int i = 0; i <= m; i++) res = max(res, f[i]);
    int cnt = 0;
    for (int i = 0; i <= m; i++)
        if (res == f[i]) cnt = (g[i] + cnt) % mod;
    printf("%d\n", cnt);
    return 0;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值