背包九讲 学习笔记

背包是动态规划专题中较为特殊也是最有规律的一类问题,刚系统的学习了一遍背包问题,做一个简单的总结

01背包

题面就不解释了,根据一般的动态规划知识,要先找出转移的方程。

我们可以设 f[i][j] 为取前 i 个物品,当前容量为 j 时的最大价值

对于一般的情况,我们有两种选择,一种是不选择选择当前的第 i 个物品,那么f[i][j] = f[i-1][j], 另一种是选择了当前第 i 件物品,f[i][j] = f[i-1][j-v]+w。

我们发现,影响后一个状态的永远是前一项,因此我们考虑将f[i][j]压成一维的数组,并且满足f[i][j] = max(f[i-1][j], f[i-1][j-v]+w)。所以枚举容量时要从m到0倒着枚举,为了不影响更新的顺序。

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]);

完全背包
和01背包的题面类似,只是物品的数量变成无限制取用。

这时我们再来看转移的方程

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…] (1)

f[i][j - v] = max(f[i-1][j-v], f[i-1][j-2v]+w, f[i-1][j-3v]+2w…) (2)

可以发现,我们给第二项加上w就与第一项的后半部分完全相同

我们替换一下变成f[i][j] = max(f[i-1][j], f[i][j-v]+w)

结果非常amazing,与01背包的不同点只在于f[i][j-v]+w,也就是说,完全背包的更新操作是会涉及到同一层之间的更新的,所以在压缩成一维的时候要注意容量应该从0到m的枚举

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]);

二维费用背包

其实与之前的模型没有过多的区别,只不过在物品属性的限制之上又增加了一维,但并不影响。

f[i][j][k]可以代表前i个物品在体积为j,重量为k时的最大价值,当然只需要枚举三层循环即可表示出来,可以用相同原理的滚动数组优化成二维。

f[i][j] = max(f[i][j], f[i-a]+f[j-b] + w);

分组背包

最经典的题型就是将物品分成 c 个组,每个组内允许选择一个,求最大价值

f[i][j] = max(f[i-1][j], f[i-1][j-v1]+w1, f[i-1][j-v2]+w2, f[i-1][j-v3]+w3…)

v1…n代表v组的第一个数的体积,w1代表那个数的价值

对于分组背包,总结之后就是
1.枚举组
2.枚举体积
3.枚举组内元素

多重背包

题面增加了每种物品的个数

根据朴素的思想
1.枚举物品
2.枚举体积
3.枚举取用的个数

for (int i = 1; i <= n; i++) 
    for (int j = m; j >= 0; j--) 
        for (int k = 1; k <= cnt[i] && k * v[i] <= j; k++) 
            f[j] = max(f[j], f[j-k*v[i]]+k*w[i]);  

这样在O(n^3)的时间复杂度里可以完成,但效率过低

接着介绍两种优化的方案

二进制优化

我们考虑到一个数都可以被拆成2进制数

例如 7 = 1 + 2 + 4

那么将 7 变成 三位2进制数1 1 1,0代表选,1代表不选,是不是看到了01背包的影子

那么就会有人问了,那要不能刚好拆成2的平方数相加怎么办呢

例如10 = 1 + 2 + 4 + 3

那么我们可以把3单独拎出来,同样,这四位数也可以做到1 ——10之内的数全覆盖
例如 8 = 1 + 4 + 3
9 = 2 + 3 + 4

#include <bits/stdc++.h>
using namespace std;

const int N = 100010;

struct Good{
    
    
    int v, w;
};

int n, m;
int f[N];

int main() {
    
    
    vector<Good> goods;
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
    
    
        int v, w, s;
        cin >> v >> w >> s;
        for (int k = 1; k <= s; k *= 2) {
    
    
            s -= k;
            goods.push_back({
    
    k*v, k*w});
        }
        if (s > 0) goods.push_back({
    
    s*v, s*w});
    }
    for (auto x : goods) {
    
    
        for (int j = m; j >= x.v; j--) {
    
    
            f[j] = max(f[j], f[j - x.v] + x.w);
        }
    }
    cout << f[m] << endl;
}

这样时间复杂度就变成了O(nm*log(k))

当然这还不是最优做法,利用单调队列可以优化到O(nm)

单调队列优化
我们发现,每次在更新容量时,其实满足一个线性的关系

举例说

我们先更新m,然后更新的m-v,然后更新的m-2v…
接着更新m-1, 然后更新m-1-v, 然后更新的m-1-2v…
我们发现,其实他们之间是可以做到互不影响的

f[0] = max(f[0], f[v] + w, f[2v] + 2w, f[3v] + 3w…)
f[1] = max(f[1], f[1+v] + w, f[1+2v] + 2w, f[1+3v] + 3w…)
f[2] = max(f[2], f[2+v] + w, f[2+2v] + 2w, f[2+3v] + 3w…)

f[v-1] = max(f[v-1], f[v-1+v] + w, f[v-1+2v] + 2w, f[v-1+3v] + 3w…)

我们枚举0到v-1的数作为起点,然后每次递增一个v

可以写成 c + k*v 的方程形式,而且所有的容量都可以写成这个形式

我们设 j 为起点,v为增量
f[j] = f[j]
f[j+v] = max(f[j+v], f[j] + w)
f[j+2v] = max(f[j+2v], f[j+v] + w, f[j] + 2w)
f[j+3v] = max(f[j+3v], f[j+2v]+w, f[j+v]+2w, f[j] + 3w)

感觉没法找到通式?我们转换一下

f[j] = f[j]
f[j+v] = max(f[j+v] - w, f[j]) + w
f[j+2v] = max(f[j+v] - 2w, f[j+v] - w, f[j]) + 2w
f[j+3v] = max(f[j+3v] - 3w, f[j+2v] - 2w, f[j+v] - w, f[j]) + 3w

f[j + kv] - kv就是通式
至于为什么用单调队列维护,因为我们背包取用个数的限制,列队的长度不能超过个数s+1,即从f[j],只能到f[j-k*v]为止的这段区间,而我们需要维护的就是区间的最值,所以单调队列恰好帮助我们解决了问题

因此存入队列时比较的是f[j+kv] - kw的值,取出更新答案时再加上尾部的k*w

#include <bits/stdc++.h>
using namespace std;

const int N = 20010;

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

int main() {
    
    
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
    
    
        int v, w, s;
        cin >> v >> w >> s;
        memcpy(g, f, sizeof f);
        for (int j = 0; j < v; j++) {
    
    
            int hh = 1, tt = 0;
            for (int k = j; k <= m; k += v) {
    
    
                while (hh <= tt && k - q[hh] > s*v) hh++; //队列中最多只能有s+1个值
                if (hh <= tt) f[k] = max(f[k], g[q[hh]] + ( k - q[hh]) / v * w);
                while (hh <= tt && g[q[tt]] - (q[tt] - j) / v * w <= g[k] - (k - j) / v * w) tt--;
                q[++tt] = k;
            }
        }
    }
    cout << f[m] << endl;
} 

混合背包

顾名思义,同时存在各种类型的物品,有1个的,k个的和无限个的。

我们完全可以用二进制优化将k个的转化为01背包来做,下面就按照01和完全背包的模型取做就可以了。

有依赖的背包

概念就是,每一个物品都有一个先前的依赖关系,比如先选择了a才能选b之类的

根据关系可以推断出这是个树形的结构,有点类似于树形dp和分组背包(每个子树都是一组)

(代码中详解)

#include <bits/stdc++.h>

using namespace std;

const int N = 100 + 5;

struct edge{
    
    
    int to, next;
}e[N << 1];

int n, m, f[N][N*N], h[N], cnt, rt, v[N], w[N];

void add(int u, int v) {
    
    
    e[cnt].to = v;
    e[cnt].next = h[u];
    h[u] = cnt++;
}

void dfs(int x, int fa) {
    
    
    for (int i = h[x]; ~i; i = e[i].next) {
    
    
        int y = e[i].to;
        if (y == fa) continue;
        dfs(y, x);
        for (int j = m - v[x]; j >= 0; j--) //默认已经选择了当前节点的物品
            for (int k = 0; k <= j; k++) //枚举容量的转移,从子节点到当前节点
                f[x][j] = max(f[x][j], f[x][j-k] + f[y][k]);
    }
    for (int i = m; i >= v[x]; i--) f[x][i] = f[x][i-v[x]]+w[x];//强制选择当前节点的物品
    for (int i = 0; i < v[x]; i++) f[x][i] = 0;//如果连当前物品都放不下,直接初始化为0
} 

int main() {
    
    
    memset(h, -1, sizeof h);
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
    
    
        int id;
        cin >> v[i] >> w[i] >> id;
        if (id == -1) rt = i;//找到一个根,如果可能有多个根,那就建立一个虚根
        else add(id, i), add(i, id);
    }
    
    dfs(rt, 0);
    
    cout << f[rt][m] << endl;
}

背包计数


我们设f[j]代表当前最大价值,g[j]代表方案数

  1. f[j-v]+w > f[j] 时,我们应该用g[j-v]的方案数更新g[j]
  2. f[j-v]+w = f[j] 时,我们应该用g[j-v]和g[j]同时更新g[j]
#include <bits/stdc++.h>

using namespace std;

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

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

int main() {
    
    
    cin >> n >> m;
    for (int i = 0; i <= m; i++) g[i] = 1;
    
    for (int i = 1; i <= n; i++) {
    
    
        int v, w;
        cin >> v >> w;
        for (int j = m; j >= v; j--) {
    
    
            int temp = f[j-v] + w;
            if (temp > f[j]) {
    
    
                g[j] = g[j-v] % mod;
                f[j] = temp;   
            }
            else if (temp == f[j])
                g[j] = (g[j] + g[j-v]) % mod;
        }
    }    
    cout << g[m] << endl;
}

背包最优方案

注意题中要求字典序最小

所以我们要正着扫一遍输出答案, 这就要求我们逆着求背包

看代码应该很好理解

#include <bits/stdc++.h>

using namespace std;

const int N = 1000 + 5;

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

int main() {
    
    
    cin >> n >> m;
    for (int i = 1; i <= n; i++) cin >> 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 cur = m;
    for (int i = 1; i <= n; i++) {
    
    
        if (cur - v[i] < 0) continue;
        if (f[i][cur] == f[i+1][cur-v[i]] + w[i]) {
    
    //如果是最优方案直接输出
            cout << i << " ";
            cur -= v[i];
        }
    }
}

猜你喜欢

转载自blog.csdn.net/kaka03200/article/details/108489878