• 背包DP全类型


    AcWing 2. 01背包问题

    #include <iostream>
    #include <algorithm>
    
    using namespace std;
    
    const int N = 1010;
    
    int n, m;
    int v[N], w[N];
    int f[N];
    
    int main()
    {
        cin >> n >> m;
        for(int i = 1; i <= n; i++) cin >> 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]);
            }
        }
    
        cout << f[m] << endl;
    
        return 0;
    }
    

    AcWing 3. 完全背包问题

    状态表示:(f(i,j)) 其中的(i)表示只从前(i)种物品选,(j)表示选出来的物品总体积 (leq) j,因此(f(i,j))就表示从前(i)种物品中选出总体积不超过(j)的所有选法。
    状态计算:集合划分
    (i)种物品选(0)(1)(2)(3)(……)(k-1)(k)个。
    如果第(i)种物品,一个也不选,则说明我们只考虑前(i-1)种物品,即:(f(i-1, j))
    如果选第(i)种物品选(k)个,则我们可以采取与(01)背包类似的思路,去掉已经确定了价值的(k)个第(i)种物品,因为这并不会影响前(i-1)种物品我们选法的最大价值,因此得到方程:

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

    这里注意,第(i)种物品如果我们不选的话,就等价于(f(i-1, j)),因此不选的情况我们也可以并到上式子中,因此我们得到状态转移方程:

    [f(i, j) = max(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) + w, f(i-1, j-2*v) + 2*w, f(i-1, j-3*v) + 3*w, ...) ]

    而:

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

    观察上下两式,我们看出式子(1)(f(i-1, j-v) + w, f(i-1, j-2*v) + 2*w, f(i-1, j-3*v) + 3*w, ...)可以由式子(2):(f(i, j-v)) (+) (w)得到,于是我们就可以进行合并,我们可以得到这样的式子:

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

    因此这样就可以不用再枚举(k)了,只需要枚举两个状态即可。
    (f(i, j) = max(f(i-1, j), f(i, j-v) + w))(01)背包的状态转移方程并不同,(01)背包需要从(i-1)转移而来,而完全背包是从(i)转移而来。

    #include <iostream>
    #include <algorithm>
    
    using namespace std;
    
    const int N = 1010;
    int n, m;
    int f[N], w[N], v[N];
    
    int main() {
        cin >> n >> m;
        for (int i = 1; i <= n; i++) {
            cin >> 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]);
            }
        }
    
        cout << f[m] << endl;
        return 0;
    }
    

    AcWing 4. 多重背包问题 1

    #include <iostream>
    #include <algorithm>
    
    using namespace std;
    
    const int N = 110;
    int n, m;
    int f[N][N];
    int v[N], w[N], s[N];
    
    int main() {
        cin >> n >> m;
        for (int i = 1; i <= n; i++) {
            cin >> 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]);
                }
            }
        }
        cout << f[n][m] << endl;
        return 0;
    }
    

    AcWing 5. 多重背包问题 II

    二进制优化

    这个如果再用朴素做法就不行了,(O(NVS)) 大约要(4*10^9),肯定不行,而若我们对(S)进行二进制拆开然后将整个问题当成(01)背包来做,那么复杂度就被有优化为了(O(NVlogS)) 大约为(2*10^7),优化了两个数量级。
    那为什么我们不用优化完全背包的方法来优化多重背包呢?先从状态转移方程出发

    [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-s*v]+s*w) ag{1} ]

    而:

    [f[i][j-v] = max(f[i-1][j-v], f[i-1][j-2*v]+w, ... ,f[i-1][j-s*v]+(s-1)*w, f[i-1][j-(s+1)*v]+s*w) ag{2} ]

    可见(2)式比(1)式多了一项,因此不能这样优化。
    进入二进制优化正题
    假设有件物品的数量为(200),那么我们可以分成(1, 2, 4, 8, 16, 32, 64)这么几组,还剩下一个(73)不再拆分,分成的每一组我们最多只能拿一次那么就可以按照(01)背包做了。
    那会出现什么问题吗,例如我们想拿3件但是并没有啊,这问题并不会出现,(1 + 2 = 3)啊, 对于例子,(1,2)可以凑出(1-3)的任何一个数字,同理(1, 2)加一个4就可以凑出(1-7)任何一个数字,再同理,假设我们可以把(n)分出一组(1, 2, ..., 2^k, C)(C)不足以(2^{k+1}),否则我们就是(1, 2, ..., 2^k, 2^{k+1}),我们可以用(1, 2, ..., 2^k)凑出(2^{k+1} - 1)(根据等差数列求和)中的任何一个数字,那么再加一个(C)就可以凑出(1-n)中任何一个数。那么这样再做一遍(01)背包即可,复杂度就由(O(NVS))优化到了(O(NVlogS))

    #include <iostream>
    #include <algorithm>
    
    using namespace std;
    
    const int N = 12000;
    int v[N], w[N];
    int n, m, f[N];
    
    int main() {
        cin >> n >> m;
        int cnt = 0;
        for (int i = 1; i <= n; i++) {
            int a, b, s;
            cin >> a >> b >> s;
            int k = 1;
            while (k <= s) {
                cnt++;
                v[cnt] = a * k, w[cnt] = b * k; //每个物品a 价值为b
                s -= k;
                k *= 2;
            }
            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]);
            }
        }
        cout << f[m] << endl;
        return 0;
    }
    

    AcWing 6. 多重背包问题 III
    留个坑 等我学了单调队列DP 再来填。

    AcWing 7. 混合背包问题
    分开分析即可。

    #include <bits/stdc++.h>
    
    using namespace std;
    
    const int N = 1010;
    int f[N];
    int n, m;
    
    int main() {
        cin >> n >> m;
        for (int i = 0; i < n; i++) {
            int v, w, s;
            cin >> v >> w >> s;
            if (s == 0) {
                for (int j = v; j <= m; j++) f[j] = max(f[j], f[j - v] + w);
            } else {
                if (s == -1) s = 1;
                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);
                    }
                }
            }
        }
        
        cout << f[m] << endl;
        
        return 0;
    }
    

    AcWing 8. 二维费用的背包问题
    在01背包基础上加了一维。

    #include <iostream>
    #include <cstring>
    #include <algorithm>
    
    using namespace std;
    
    const int N = 110;
    int n, V1, V2;
    int f[N][N];
    
    int main() {
        cin >> n >> V1 >> V2;
        for (int i = 1; i <= n; i++) {
            int v1, v2, w;
            cin >> v1 >> v2 >> w;
            for (int j = V1; j >= v1; j--) {
                for (int k = V2; k >= v2; k--) {
                    f[j][k] = max(f[j][k], f[j - v1][k - v2] + w);
                }
            }
        }
    
        cout << f[V1][V2] << endl;
    
        return 0;
    }
    

    AcWing 9. 分组背包问题

    #include <iostream>
    
    using namespace std;
    
    const int N = 110;
    int s[N], w[N][N], v[N][N];
    int n, m, f[N];
    
    int main() {
        cin >> n >> m;
        for (int i = 1; i <= n; i++) {
            cin >> s[i];
            for (int j = 0; j < s[i]; j++) {
                cin >> 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 (v[i][k] <= j) {
                        f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
                    }
                }
            }
        }
        cout << f[m] << endl;
        return 0;
    }
    

    AcWing 10. 有依赖的背包问题

    #include <iostream>
    #include <algorithm>
    #include <cstring>
    
    using namespace std;
    
    const int N = 110;
    int f[N][N]; //表示从以u根的子树中选,总体积不超过j的最大价值
    int h[N], e[N], ne[N], idx;
    int n, m;
    int v[N], w[N];
    
    void add(int a, int b) {
        e[idx] = b, ne[idx] = h[a], h[a] = idx++;
    }
    
    void dfs(int u) {
        for (int i = h[u]; i != -1; 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]; //选择v[u]
        for (int i = 0; i < v[u]; i++) f[u][i] = 0;
    }
    
    int main() {
        cin >> n >> m;
        int root;
        memset(h, -1, sizeof h);
        for (int i = 1; i <= n; i++) {
            int p;
            cin >> v[i] >> w[i] >> p;
            if (p == -1) root = i;
            else add(p, i);
        }
        
        dfs(root);
        
        cout << f[root][m] << endl;
        
        return 0;
    }
    

    AcWing 11. 背包问题求方案数
    类似于最短路的计数问题。

    #include <iostream>
    #include <algorithm>
    
    using namespace std;
    
    const int N = 1010, Mod = 1e9 + 7;;
    int f[N], cnt[N];
    int n, m;
    
    int main() {
        cin >> n >> m;
        for (int i = 0; i <= m; i++) cnt[i] = 1;
        
        for (int i = 1; i <= n; i++) {
            int v, w;
            cin >> v >> w;
            for (int j = m; j >= v; j--) {
                int x = f[j - v] + w;
                if (x > f[j]) {
                    f[j] = x;
                    cnt[j] = cnt[j - v];
                } else if (x == f[j]) {
                    cnt[j] = (cnt[j - v] + cnt[j]) % Mod;
                }
            }
        }
        
        cout << cnt[m] << endl;
        
        return 0;
    }
    

    AcWing 12. 背包问题求具体方案
    对于方案的求解,我们可以类似于迷宫求最短路径时候的记录方案。
    就是判断一下每一步是从哪一步转移过去的,对于背包来说,f[i][j]可能由两个状态转移过来:

    1. 如果f[i][j] == f[i - 1][j]:表示从不选第i个物品转移到了f[i][j],那么记录转移状态的时候肯定必选第i个物品
    2. 如果f[i][j] == f[i - 1][j - v] + w:表示选第i个物品转移到了f[i][j],那么记录路径的时候肯定必然不能选第i个物品。
    3. 还有一种特例,f[i - 1][j] == f[i - 1][j - v] + w表示第i个物品可选可不选都可以转移到f[i][j],那么此是我们肯定是要选的,因为题目中还要保证字典序最小,所以选上肯定比不选的字典序要小。

    综上,能选必选。

    还有就是对于我们求转移状态的时候其实是倒着往前求得,类比于迷宫,而我们要获得字典序最小需要从前往后求,那么我们解决办法就是再求f的时候直接从ni枚举即可。

    #include <iostream>
    #include <cstring>
    #include <algorithm>
    
    using namespace std;
    
    const int N = 1010;
    
    int n, m;
    int 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 j = m; 
        for (int i = 1; i <= n; i++) {
            if (j >= v[i] && f[i][j] == f[i + 1][j - v[i]] + w[i]) {
                cout << i << " ";
                j -= v[i];
            }
        }
        
        return 0;
    }
    

    关于记录方案还有另外一种方式:
    这种转移方法是来自迷宫求最短路的记录方案的灵感,这个题里y总只是提了一下,没有具体讲。

    开一个PII g[i][j]数组来记录,表示first表示(i, j)是由first这个点转移而来,second表示由first转移而来的时候第first物品是否被选.

    g[i][j] = {i + 1, false}:表示(i,j)这个状态是从没选第i + 1个物品转移而来。
    g[i][j] = {i + 1, true}:表示(i,j)这个状态是从选第i + 1个物品转移而来。

    然后边DP边记录即可。

    对于输出的过程:首先要判断一下g[i][j].second是否为true如果为true,那么表示是由选择一个物品转移而来,那么当前体积需要跟着变化,那就是减去当前的v,反之如果为false,那么就不必减去当前的v,还有注意这两种情况下i都需要进行转移。

    还要注意的就是求解的时候还是要倒序求,因为需要保证字典序最小,又因为这个WA了一发。

    int i = 1, j = m;
    while (i <= n && j) {
        if (g[i][j].second == true) {
            int t = i;
            cout << i << " ";
            i = g[i][j].first;
            j -= v[t];
        } else {
            i = g[i][j].first;
        }
    }
    

    详细的代码有一些细节都在注释里了:

    #include <iostream>
    #include <cstring>
    #include <algorithm>
    #include <vector> 
    
    using namespace std;
    
    typedef pair<int, bool> PII;
    const int N = 1010;
    
    int n, m;
    int f[N][N];
    int v[N], w[N];
    PII g[N][N]; //first表示第几件物品 second表示选没选
    vector<int> res;
    
    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];
                g[i][j] = {i + 1, false}; 
                if (j >= v[i]) {
                    if (f[i + 1][j - v[i]] + w[i] >= f[i][j]) { //注意这里一定要大于等于,因为为了保证字典序,能选必选
                        g[i][j] = {i + 1, true};
                        f[i][j] = f[i + 1][j - v[i]] + w[i];    
                    }
                }
            }
        }
        
        int i = 1, j = m;
        while (i <= n && j) { //这里一定是&& 一开始写成了||,调了半个多小时
            if (g[i][j].second == true) {
                int t = i; //这里一定要临时存一下i,因为下边i接着变成了它的上一个状态,而j要减去的是当前状态的v
                cout << i << " ";
                i = g[i][j].first;
                j -= v[t];
            } else {
                i = g[i][j].first;
            }
        }
        
        return 0;
    }
    
  • 相关阅读:
    进程,线程,协程,异步IO知识点
    Socket网络编程知识点
    面向对象编程知识点
    Zabbix系列之七——添加磁盘IO监测
    WARNING: 'aclocal-1.14' is missing on your system.
    tomcat的catalina.out日志按自定义时间日式进行分割
    Plugin with id 'com.novoda.bintray-release' not found.的解决方案
    MaterialCalendarDialog【Material样式的日历对话框】
    导入项目报错【Minimum supported Gradle version is 3.3. Current version is 2.14.1】
    通过Calendar简单解析Date日期,获取年、月、日、星期的数值
  • 原文地址:https://www.cnblogs.com/ZhengLijie/p/14838926.html
Copyright © 2020-2023  润新知