跳转至

背包问题 3

归纳状态表示:

  1. 体积至多是 j: f[0] = f[1] = ... = 0 && 更新时V >= 0.
  2. 体积恰好是 j: f[0] = 0 && f[1] = f[2] = ... = +INF && 更新时V >= 0.
  3. 体积最少是 j: f[0] = 0 && f[1] = f[2] = ... = +INF.
    1. 1020 潜水员

1020 潜水员

动态规划:

  • 状态表示: f[i,j,k]
    • 集合: 所有从前i个物品中选,且氧气含量至少是j,氮气含量至少是k 的所有选法
    • 属性: min
  • 状态计算:
C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <cstring>
#include <iostream>

using namespace std;

const int N = 22, M = 80;

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

int main()
{
    cin >> n >> m >> K;

    memset(f, 0x3f, sizeof f);
    f[0][0] = 0;

    while (K -- )
    {
        int v1, v2, w;
        cin >> v1 >> v2 >> w;
        // (1) 从i-1层转移而来,所以是从大到小循环
        // (2) 当 i-v1 < 0 OR j-v2 <0 时, 仍成立, 等价f[0][0].
        // 注意: 对应于 "体积最少是 j" 的情况!!!
        for (int i = n; i >= 0; i -- )
            for (int j = m; j >= 0; j -- )
                f[i][j] = min(f[i][j], f[max(0, i - v1)][max(0, j - v2)] + w);
    }


    cout << f[n][m] << endl;

    return 0;
}

12 背包问题求具体方案

以01背包为例:

Text Only
1
2
3
4
5
6
f[i,j] = max ( f[i-1][j], f[i-1][j-Vi] + Wi )

本质上我只需要追踪 f[i,j] 是由这二者中的哪一个转移而来

- f[i-1][j] != f[i-1][j-Vi] + Wi: 选等于 f[i,j] 者
- f[i-1][j] == f[i-1][j-Vi] + Wi: 任选一个

题面:

\(N\) 件物品和一个容量是 \(V\) 的背包。每件物品只能使用一次

\(i\) 件物品的体积是 \(v_i\),价值是 \(w_i\)

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

输出 字典序最小的方案。这里的字典序是指:所选物品的编号所构成的序列。物品的编号范围是 \(1…N\)

注意需要“字典序最小”

比如从前往后看, 对于1号物品

  1. 只能选择: 必须选
  2. 只能不选择: 必须不选
  3. 可选可不选: 一定要选 ⚠️

由于正常循环, f[n,m] 是 “由n-1推的” , 因此需要逆向循环 !!! (有点绕)

[!tip] 背包问题进行方案选择 <-> 追踪转移 - 先完成所有DP计算:首先,完整地计算出整个 f 数组,f[total_n][total_v] 会得到最终的最大价值 - 再反向推导方案:根据计算好的 f 数组,从 f[total_n][total_v] 开始,一步步反推,判断每个物品当初是否被选中

标准的0/1背包问题,如果只要求输出任意一个最优方案,我们可以从 i=Ni=1 反向推导

但是,题目要求 字典序最小。这意味着,如果同样能达到最大价值,我们应优先选择编号小的物品。例如,方案 {2, 5}{3, 4} 价值相同,我们必须选择 {2, 5}

  1. N1 反向推导,会优先对编号大的物品做决策,这和字典序的要求是相反的。
  2. 为了优先对编号小的物品做决策,我们必须 正向 推导路径。

而正向推导路径,又需要我们预先知道“如果我当前选了物品 i,那么用剩下 i+1...N 的物品能否填满剩余容量并达到最优”

这就引出了解决此问题的经典方法:反向DP,正向选路 !!!!!!

[!danger] 反向DP, 正向选路 (1) 状态定义: f[i][j]: “从第 i 件到第 N 件物品中选择,放入容量为 j 的背包中所能获得的最大价值 (2) 状态转移: f[i][j] = max(f[i+1][j], f[i+1][j - v[i]] + w[i])

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

const int maxn = 1e3+10;

vector<int> path;

int v[maxn], w[maxn];
// 从第i件到第N件物品中选择, 占总容量为j -> 获得的最大价值
int f[maxn][maxn];
int total_n, total_w;

int main()
{
    cin >> total_n >> total_w;

    for (int i=1; i<=total_n; i++) cin >> w[i] >> v[i];

    // [反向DP]
    for (int i=total_n; i>=1; i--)
    {
        for (int j=1; j<=total_w; j++)
        {
            f[i][j] = f[i+1][j];
            if (j - w[i] >= 0)
            {
                f[i][j] = max(f[i][j], f[i+1][j-w[i]] + v[i]);
            }   
        }
    }

    // for (int i=1; i<=total_n; i++)
    // {
    //     for (int j=1; j<=total_w; j++)
    //         cout << f[i][j] << " ";
    //     cout << endl;
    // }

    // [正向寻路]
    // 检查是否应该选择物品 i
    // 1. 当前容量必须能装下物品 i
    // 2. 选择物品 i 后的总价值等于从当前状态 f[i][current_w] 出发能达到的最大价值
    int current_w = total_w;
    for (int i=1; i<=total_n; i++)
    {
        if (current_w >= w[i]) // 条件(1)
        {
            if (f[i][current_w] == f[i+1][current_w-w[i]] + v[i])
            {
                path.push_back(i);
                current_w -= w[i]; // 更新剩余容量
            }
        }
    }

    for (auto i : path) cout << i << " ";

    return 0;
}

保存路径, 本质上可以使用 dfs的"最短路" 回溯

9 分组背包问题

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

每组物品有若干个,同一组内的物品最多只能选一个

每件物品的体积是 vij,价值是 wij,其中 i 是组号,j 是组内编号

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

输出最大价值:

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include<iostream>
#include<algorithm>
using namespace std;

/*
分组背包:

f[i][j]: 只在前i组物品中选,总体积不超过j的选法 -> 对应的总价值max

递推式:

f[i][j] = max (
        f[i-1][j],
        ...
        f[i-1][j-v[i,k]] + w[i,k]
    )

遍历考察第i组选择第k个物件的情况
*/

const int maxn = 110;

int v[maxn][maxn], w[maxn][maxn];
int s[maxn];
int f[maxn][maxn];
int N, V;

int main()
{
    cin >> N >> V;
    for (int i=1; i<=N; i++) // 组
    {
        cin >> s[i];
        // 组内件
        for (int j=1; j<=s[i]; j++) cin >> v[i][j] >> w[i][j];
    }

    // // 初始化f[0][j]为0
    // for (int j=0; j<=V; j++) f[0][j] = 0;

    for (int i=1; i<=N; i++)
    {
        for (int j=0; j<=V; j++)
        {
            f[i][j] = f[i-1][j];
            for (int k=1; k<=s[i]; k++)
            {
                if (j - v[i][k] >= 0) // 注意这里的下标是v[i][k]
                    f[i][j] = max(f[i][j], f[i-1][j-v[i][k]] + w[i][k]);
            }
        }
    }

    cout << f[N][V] << endl;
    return 0;
}

1013 机器分配

总公司拥有 M 台 相同 的高效设备,准备分给下属的 N 个分公司

各分公司若获得这些设备,可以为国家提供一定的盈利。盈利与分配的设备数量有关

问:如何分配这M台设备才能使国家得到的盈利最大?

求出最大盈利值

分配原则:每个公司有权获得任意数目的设备,但总台数不超过设备数 M

转化思路: "组内互斥"

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <iostream>

using namespace std;

const int N = 20;

int n, m;
int w[N][N];
int f[N][N];
int path[N], cnt;

/*
"动态规划求状态转移路径" 等价于在 "拓扑图中找最短路径"

可以直接沿用 "最短路" 输出路径的方法就可以找出 "状态的转移"
*/
void dfs(int i, int j)
{
    if (!i) return;
    //寻找当前状态f[i][j]是从上述哪一个f[i-1][k]状态转移过来的
    for (int a = 0; a <= j; ++ a)
    {
        if (f[i - 1][j - a] + w[i][a] == f[i][j])
        {
            path[cnt ++ ] = a;
            dfs(i - 1, j - a);
            return;
        }
    }
}


int main()
{
    //input
    cin >> n >> m;
    for (int i = 1; i <= n; ++ i)
        for (int j = 1; j <= m; ++ j)
            cin >> w[i][j];

    //dp
    for (int i = 1; i <= n; ++ i)
        for (int j = 1; j <= m; ++ j)
            for (int k = 0; k <= j; ++ k)
                f[i][j] = max(f[i][j], f[i - 1][j - k] + w[i][k]);
    cout << f[n][m] << endl;

    //find path
    dfs(n, m);
    for (int i = cnt - 1, id = 1; i >= 0; -- i, ++ id)
        cout << id << " " << path[i] << endl;
    return 0;
}

489 金明的预算方案

思路: 主+副 = 一个组

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <cstring>
#include <iostream>
#include <algorithm>
#include <vector>

#define v first
#define w second

using namespace std;

typedef pair<int, int> PII;

const int N = 60, M = 32010;

int n, m;
PII master[N];
vector<PII> servent[N];
int f[M];

int main()
{
    cin >> m >> n;

    for (int i = 1; i <= n; i ++ )
    {
        int v, p, q;
        cin >> v >> p >> q;
        p *= v;
        if (!q) master[i] = {v, p};
        else servent[q].push_back({v, p});
    }

    for (int i = 1; i <= n; i ++ )
        for (int u = m; u >= 0; u -- )
        {
            /*
            二进制枚举, 有点牛b:

            servent[i].size() 取值范围为[0, 2],j的取值范围[0, 3], k的取值范围[0,1]
                - 当 j = 0 时:k 无论为0还是1,都不取
                - 当 j = 1 时:k = 0 取,k = 1 不取
                - 当 j = 2 时:k = 0 不取,k = 1 取
                - 当 j = 3 时:k = 0 || k = 1 都取
            分别对应四种互斥情况
            */
            for (int j = 0; j < 1 << servent[i].size(); j ++ )
            {
                int v = master[i].v, w = master[i].w;
                for (int k = 0; k < servent[i].size(); k ++ )
                    if (j >> k & 1)
                    {
                        v += servent[i][k].v;
                        w += servent[i][k].w;
                    }
                if (u >= v) f[u] = max(f[u], f[u - v] + w);
            }
    }

    cout << f[m] << endl;

    return 0;
}

426 开心的金明

简单的01背包

将原问题做如下转化

  • 总钱数相当于背包总容量
  • 每件物品的价格相当于体积
  • 每件物品的价格乘以重要度相当于价值
C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <algorithm>

using namespace std;
const int N = 30010;

int n, m;
int f[N];

int main()
{
    cin >> m >> n;

    for (int i = 0; i < n; i ++ )
    {
        int v, w;
        cin >> v >> w;
        w *= v;
        for (int j = m; j >= v; j -- )
            f[j] = max(f[j], f[j - v] + w);
    }

    cout << f[m] << endl;

    return 0;
}