背包九讲

preface

卧槽,发现我的命名有问题,我把 \(v\) 定义成价值(\(value\)),没什么问题。但是我把 \(w\) 定义成体积,好像有点没道理啊。
事实上,\(v=volumn, w=weight\) 比较合适。


1. \(01\) 背包

每个物品只能选或不选


2. 完全背包

每个物品可以选无数次


3. 多重背包

每个物品最多可以选 K 个,多重背包有三种做法:暴力,二进制优化,优先队列优化(\(hard\)),我们只需要使用二进制优化即可,不要使用暴力!

Code

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 2010, M = 1010 * 15;  

int n, m, s; // s表示转换为01背包之后的物品个数
int f[N], w[M], v[M]; 

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i ++ )
    {
        int _w, _v, _s;
        cin >> _w >> _v >> _s;
        // 转换为 01 背包
        int k = 1;
        while(_s >= k)
        {
            w[s] = k * _w;
            v[s] = k * _v;
            s ++ ;
            _s -= k;
            k <<= 1;
        }
        if(k)
        {
            w[s] = _s * _w;
            v[s] = _s * _v;
            s ++ ;  // 这里的 s++ 别忘了
        }
    }
    
    for(int i = 0; i < s; i ++ )
        for(int j = m; j >= w[i]; j -- )
            f[j] = max(f[j], f[j - w[i]] + v[i]);
            
    cout << f[m] << endl;
    return 0;
}

4. 分组背包

有多组物品,每个组只能选一个物品
对于分组背包问题,有一个细节点,就是对于每个组内的物品,是放在体积之前遍历,还是放在体积之后遍历,还是都可以呢?
或者说,对于这两种遍历方式,是否会造成同一组内的物品选了多个?

// 1. 物品在体积之后
for 物品
  for 体积
    for 每组内的物品

/*-------------*/

// 2. 物品在体积之前
for 物品
  for 每组内的物品
    for 体积

答案是:

  1. 对于没有使用空间优化的版本,两种遍历方式都可以
  2. 对于使用空间优化的版本,每组内的物品的遍历顺序必须放在体积的后面

解释一下第 \(2\) 点,如果我们按照下面形式遍历:

for 物品 i
  for 每组内的物品 k
    for 体积 j

其实也没那么麻烦,如果先遍历每个组的物品,在遍历体积的话,那么 f[] 就会被该物品更新掉,更新掉就意味着我们选了该物品。
例如,f[10] = max(f[10], f[10-4] + 100); 更新了 f[10]
此时,该组内的下一个物品再次遍历到 f[10],它希望使用的是上一层的 f[10]
但是因为上一层的 f[10] 已经被之前的物品更新了,所以此时的 f[10] 就该层的 f[10]
但是它不不知道啊,它只负责 max,因此它可能再次更新 f[10],这就导致,f[10] 包含了该组的两个物品。

那么为什么没有空间优化的版本可以颠倒顺序呢?这是因为它们显示指定了层,所以不会出现当前层的数据更新了上一层数据的情况。
而在空间优化版本中,如果我们先遍历体积,在遍历物品,那么我们必须制定体积从大到小遍历,因为 f[j-v] 会用到上一层的小体积。

另外,空间优化版本中,体积的遍历是从大到小的,这是因为状态转移需要用到上一层 f[i-1][] 的状态,而非当前层 f[i][] 的状态。


5. 二维费用背包

背包有多个限制,通常只有一个限制[体积],现在多了一个限制[费用]
其实也普通背包没啥区别, 就是多了一层判断

// 2023/6/7
对于二维费用的背包问题,空间优化版本中,第三位费用不用倒叙枚举,在 \(Acwing\) 测试代码通过了。
其实二维费用和 \(01\) 背包都一样,都是借用上一层的状态转移,由于我们的体积是倒序枚举的,而费用那一维在体积之后,因此只需要倒序枚举体积即可确保状态是从上一层转移的。
这是因为我们得先枚举体积,再枚举费用,而体积都还没枚举过,费用的枚举顺序如何自然也就无关紧要了(因为它也肯定没没枚举)。
当然,要是不太理解,倒序枚举呗就。

Code

// 空间优化版本
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 1010;

int n, m1, m2;
int w1[N], w2[N], v[N];
int f[N][N];

int main()
{
    cin >> n >> m1 >> m2;
    for(int i = 1; i <= n; i ++ )
        cin >> w1[i] >> w2[i] >> v[i];
    
    for(int i = 1; i <= n; i ++ )
        for(int j = m1; j >= w1[i]; j -- )
            for(int k = m2; k >= w2[i]; k -- )
                f[j][k] = max(f[j][k], f[j - w1[i]][k - w2[i]] + v[i]);
    
    cout << f[m1][m2] << endl;
    
    return 0;
}

6. 混合背包

混合背包
混合背包问题的思路很简单,既然有多种类型的背包,那么我们分类讨论即可。
麻烦的点在于,对于不同的背包模型,我们的存放数据的类型是不一样的,例如,存放分组背包和普通背包的体积的价值的数组,一个是一位数组,一个是二维数组。另外,我们还需要存储 \(flag\) 来指示,当前背包是何种类型的。
为了消除这些差异,我们当然可以既分配一个一维数组,又分配一个二维数组,但这样对空间的消耗未免也太大了!
其实我们有更好的方法,就是在输入体积和价值之后,直接 \(dp\),而不是等到输入完所有体积和价值。
这一方法对于所有背包模型都是适用的。

Code

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 1010, M = 10010;

int n, m;
int f[N];
int _v[M], _w[M];

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i ++ )
    {
        int w, v, s;
        cin >> w >> v >> s;
        if(s == 0) // 完全背包
        {
            for(int j = w; j <= m; j ++ )
                f[j] = max(f[j], f[j - w] + v);
        }
        else if(s > 0)  // 多重背包
        {
            int c = 1, k = 1;
            while(s >= k)
            {
                _v[c] = k * v;
                _w[c] = k * w;                
                s -= k;
                k <<= 1;
                c ++ ;
            }
            if(s)
            {
                _v[c] = s * v;
                _w[c] = s * w;
                s -= k;
                c ++ ;
            }
            for(int u = 1; u <= c; u ++ )
                for(int j = m; j >= _w[u]; j -- )
                    f[j] = max(f[j], f[j - _w[u]] + _v[u]);
           
        }
        else if(s == -1)// 01背包
        {
            for(int j = m; j >= w; j -- )
                f[j] = max(f[j], f[j - w] + v);
        }
        else 
        {
            cout << "Error input: " << s << endl;
            exit(0);
        }
    }
    
    cout << f[m] << endl;
    return 0;
}

7. 有依赖的背包

选物品\(A\),就必须选物品\(B\),此谓依赖
这种类型的背包问题比较特殊,其余八个都是线性 \(dp\),而这个却是树状 \(dp\)。这是因为在这种背包模型中,天然存在树形结构。
由于 \(dp\) 的类型变了,\(f[i][j]\) 的含义也要变了。
在这里,我们规定,\(f[root][m]\) 表示以 \(root\) 为根的子树,体积为 \(m\) 时的最大价值。
在求解时,有点像分组背包问题,因为我们没有制定层,所以,组的遍历必须在体积之后。

Code

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 110, M = 210;

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

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

void dfs(int u)
{
    // 自己肯定是要选的,因为根节点肯定是第一次遍历到,因此不用取 max
    for(int j = v[u]; j <= m; j ++ )    f[u][j] = w[u];
    
    for(int i = h[u]; i != -1; i = ne[i])   // 组
    {
        int son = e[i];
        
        dfs(son);   // 递归处理子节点,保证最优解
        
        for(int j = m; j >= v[u]; j -- )  // 体积    
            for(int k = 0; k <= j - v[u]; k ++ ) // 根据划分给子节点的体积来分组
                f[u][j] = max(f[u][j], f[u][j - k] + f[son][k]);    
                
    }
}

int main()
{
    memset(h, -1, sizeof h);
    
    cin >> n >> m;
    for(int i = 1; i <= n; i ++ )
    {
        int fa;
        cin >> v[i] >> w[i] >> fa;
        if(fa == -1)    root = i;
        else    add(fa, i);
    }
    
    dfs(root);
    
    cout << f[root][m] << endl;
    
    return 0;
}

8. 求方案数

背包问题求方案数
方案数的变化取决于最大价值的变化

Code

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

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

int n, m;
int f[N], v[N], w[N];
int cnt[N]; // 保存方案

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i ++ )   cin >> w[i] >> v[i];
    
    for(int i = 0; i <= m; i ++ )   cnt[i] = 1;   // 什么都不选也是一种方案
    
    for(int i = 1; i <= n; i ++ )
    {
        for(int j = m; j >= w[i]; j -- )
        {
            // 原本是:
            // f[j] = max(f[j], f[j - w[i]] + v[i]);
            int t = f[j - w[i]] + v[i];
            if(t > f[j]) 
            {
                cnt[j] = cnt[j - w[i]];
                f[j] = f[j - w[i]] + v[i];
            }
            else if(t == f[j]) 
            {
                cnt[j] = (cnt[j] + cnt[j - w[i]]) % mod;
            }
        }
    }
    cout << cnt[m] << endl;
    return 0;
}

9. 求具体方案

背包问题求方案
方案的变化取决于最大价值的变化

Code

// 按顺序遍历得到的就是最小字典序的方案
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>

using namespace std;

const int N = 1010;

int n, m;
int f[N], v[N], w[N];
vector<int> path[N];    // path[i] 表示体积为i时,最大价值的最小字典序方案

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i ++ )   cin >> w[i] >> v[i];
    
    for(int i = 1; i <= n; i ++ )
    {
        for(int j = m; j >= w[i]; j -- )
        {
            int t = f[j - w[i]] + v[i];
            if(t > f[j])
            {
                path[j] = path[j - w[i]];
                path[j].push_back(i);
                f[j] = f[j - w[i]] + v[i];
            }
            else if(t == f[j])
            {
                auto p = path[j - w[i]];
                p.push_back(i);
                if(p < path[j]) path[j] = p;
            }
        }
    }

    for(auto &x : path[m])  cout << x << ' ';
    cout << endl;
    
    return 0;
}
posted @ 2023-03-19 09:59  光風霽月  阅读(8)  评论(0编辑  收藏  举报