• 多重背包问题(三阶段)


    多重背包问题

    这是个很老很老很早很早的知识点, 也是最为经典的DP(背包)模型之一。

    题意: 给出物品个数 n,背包容积 V,以及每个物品的体积 v、价值 w、数量 s,求最大价值和。

    今天没事做就写了写多重背包,三道模板,对应不同算法。

    前置芝士:01背包、完全背包的学习,DP基础入门

    多重背包1((N leq 100, V leq 100)

    (0 < v_i, w_i, s_i leq 100)

    简单的背包,直接枚举取几个物品即可。

    物品的体积、价值、数量分别用 V[i]、W[i]、C[i] 表示。

    设 f[j] 表示使用了 j 的容积时,能取到的最大价值。以物品为阶段,我们考虑枚举取的物品个数, 显然有转移方程:

    [f_j = max_{1 leq k leq C_i}(f_{j - k imes V_i} + k imes W_i)[k imes V_i leq j] ]

    其中 k 表示枚举的 选取物品的个数。

    时间复杂度约为 (mathrm{O(N^2V)})

    退化:当物品个数全为1时就退化成01背包;当物品个数都远远大于“全取该物品”能取到的个数时,相当于完全背包。

    关于本题:数据非常之小,以至于你可以将所有物品都拆出来,然后转化成01背包去做,但是显然这样对于稍微大一点的数据,比如物品可以非常之多的情况,就不适用了。

    (mathrm{Code:})(码风略微鬼畜)

    #include <iostream>
    #define FOR(i, a, b) for (int i = (a), bb = (b); i <= bb; ++i) 
    #define DOWN(i, a, b) for (int i = (a), bb = (b); i >= bb; --i)
    const int N = 101, M = 101;
    int n, m, f[M], V[N], W[N], C[N], ans = 0;
    inline int read() {
    	int s = 0, _ = 1; char c = getchar();
    	for (; !isdigit(c) && c != '-'; c = getchar());
    	(c == '-' ? _ = -1, c = getchar() : 0);
    	for (; isdigit(c); c = getchar()) s = (s << 3) + (s << 1) + c - 48;
    	return s * _; 
    }
    template <typename T>
    inline void write(T x) {
    	if (x < 0) x = ~x + 1, putchar('-');
    	if (x > 9) write(x / 10);
    	return putchar(x % 10 + 48), void();
    }
    signed main() {
    	n = read(), m = read();
    	FOR(i, 1, n) V[i] = read(), W[i] = read(), C[i] = read();
    	FOR(i, 1, n) DOWN(j, m, V[i]) FOR(k, 1, C[i])
    		if (j - k * V[i] >= 0) ans = std ::max(ans, f[j] = std ::max(f[j], f[j - k * V[i]] + k * W[i]));
    	write(ans);
    	return 0;
    }
    

    多重背包2 ((N leq 1000, V leq 2000)

    (0 < v_i, w_i, s_i leq 2000)

    这个数据范围,单纯的多重背包已经不再适用,我们考虑小 Trick,优化 01背包。

    我们需要的是能够选择任意数量的物品,而 01 背包的局限在于只能选择单个物品。

    那么通过上一阶段数据对于物品拆分的灵感,我们着手优化拆分。

    能够表示 0 ~ n 中任意整数的拆分方案?二进制拆分

    众所周知,二进制是十分特殊的进制,对于任意整数 x,都有拆分:

    [x = b_1 imes2^0 + b_2 imes2^1 + b_3 imes2^2... ]

    b[i] 是该整数的二进制形式的从右往左第 i 位, 数值只有 0 或 1。 对于其他进制,我们都要考虑系数的问题,而二进制并不需要。

    我们一路按照 2 的整次幂拆出物品,直到剩下的数量达不到 2 的整次幂, 则单独成一个物品

    假如说一个物品个数为 11,那么它就可以拆成 (2^0,2^1,2^2,4) 这四个物品,你会发现对于 0 ~ 11 中的任意一数,都可以通过这三个物品的 “选” 与 “不选” 拼凑而成。

    而 01 背包会帮你”智能“地选择拼成哪个数最好。

    所以把物品拆开直接 01 背包就好了

    类贪心:本题的 Acwing 评论区中提出了一些贪心选择优化的DP,虽说存在反例,但是就效率而言不错,链接

    PS: 当然,结合了之前提到的完全背包退化,该做法会有更大的提升,甚至能水过一些 OJ 第三阶段的模板。

    (mathrm{Code:})

    #include <bits/stdc++.h>
    #define FOR(i, a, b) for (int i = (a), bb = (b); i <= bb; ++i)
    #define DOWN(i, a, b) for (int i = (a), bb = (b); i >= bb; --i)
    const int N = 1e6 + 10, M = 4e6 + 10;
    int n, m, w[N], v[N];
    struct Production { int s, w, v; } a[N];
    inline int read() {
    	int s = 0, _ = 1;
    	char c = getchar();
    	while ((c < '0' || c > '9') && c != '-') c = getchar();
    	if (c == '-') c = getchar(), _ = -1;
    	while (isdigit(c)) 
    		s = (s << 1) + (s << 3) + c - 48, c = getchar();
    	return s * _;
    }
    template <class T>
    inline void write(T x) {
    	if (x < 0) x = ~x + 1, putchar('-');
    	if (x > 9) write(x / 10);
    	return putchar(x % 10 + 48), void();
    }
    int f[M], len = 0;
    void Spilt() {
    	FOR(i, 1, n) {
    		int t = 1;
    		while (t <= a[i].s)
    			w[++len] = t * a[i].w, v[len] = t * a[i].v, a[i].s -= t, t <<= 1;
    		if (a[i].s) w[++len] = a[i].s * a[i].w, v[len] = a[i].s * a[i].v;
    	}
    }			// 二进制拆分
    signed main() {
    	n = read(), m = read();
    	FOR(i, 1, n) 
    		a[i].w = read(), a[i].v = read(), a[i].s = read();
    	Spilt();
    	FOR(i, 1, len) DOWN(j, m, w[i])
    		f[j] = std ::max(f[j], f[j - w[i]] + v[i]);
        		// 裸 01 背包
    	write(f[m]);
    	return 0;
    }
    

    多重背包3 ((N leq 1000, V leq 20000)

    (0 < v_i, w_i, s_i leq 20000)

    这个数据加强了体积与物品数量,拆分做法不再适用。想要通过这样的数据量,接下来我们就要通过 DP优化 获得提升。

    本题为单调队列优化,前置芝士:单调队列。

    观察 我们每次转移的 决策。即我们是从哪些地方获取的最优值更新当前最优值。

    (f_j) 是从 ({f_k| k in j - h imes V_i}) 转移的,如图:

    我们发现黄色、橙色的值各自继承,没有干扰,那么就可以按照 (j \% V_i) 的余数进行分类,分别计算答案,因为我们发现一个同余的类是可以优化的。

    所以我们把方程变一变,用 u + p * V[i] 表示某一个 j。那么

    [egin{aligned} f_{u + p imes V_i} &= max_{p - C_i leq k leq p - 1} (f_{u + k imes V_i} + (p - k) imes V_i) end{aligned} ]

    (不知为何打不出花括号,也没办法,凑活着看吧)

    红色的点答案是由某一个黄色的点转移而来,这个黄色的点显然具有一些最优值,那么我们是不是只需要能实时维护这样的最优值,就可以避免枚举了呢?答案是肯定的。

    我们将 DP 方程中只和 i 有关、只和 j 有关的项裂开,得到:

    [egin{aligned} f_{u + p imes V_i} &= max_{p - C_i leq k leq p - 1} ( f_{u + k imes V_i} + (p - k) imes V_i) \ & = max_{p - C_i leq k leq p - 1} (f_{u + k imes V_i} - k imes V_i) + p imes V_i end{aligned} ]

    在枚举 i 和 u 的情况下可以视其为常数,我们要维护的就是 max 中的值。

    大家都知道,单调队列可以及时排除劣项,以维护集合单调有序。而我们通过这点维护 决策集合(即上图黄色决策点的集合)单调性。

    建立一个单调队列 q,开始时为空。再枚举 p,每次操作:

    • 先排除不可用决策,即如果决策点小于最低线 (p - C_i),即需排除当前决策。
    • 用最优决策更新当前点,即用 q[h] 求 (f_{u + p imes V_i}),可以通过单调队列保证决策为最优。
    • 用新决策点 (f_{u + p imes V_i}) 及时排除队尾不优决策,具体方式为比较 (f_{u + k imes V_i} - k imes V_i)这一值的大小,排除该值比当前值小的决策。

    (mathrm{Code:})(为了不写滚动数组改得 十 分 鬼 畜,更加舒适的体验还请移步 Acwing某题解

    #include <iostream>
    #define FOR(i, a, b) for (int i = (a), bb = (b); i <= bb; ++i)
    #define DOWN(i, a, b) for (int i = (a), bb = (b); i >= bb; --i)
    const int N = 1e3 + 10, M = 2e4 + 10;
    int n, m, f[M], q[M], V[N], W[N], C[N];
    inline int read() {
        int s = 0, w = 1;
        char c = getchar();
        for (; !isdigit(c) && c != '-'; c = getchar());
        (c == '-' ? w = -1, c = getchar() : 0);
        for (; isdigit(c); c = getchar()) s = (s << 3) + (s << 1) + c - 48;
        return s * w;
    }
    template <typename T>
    inline void write(T x) {
        if (x < 0) x = ~x + 1, putchar('-');
        if (x > 9) write(x / 10);
        return putchar(x % 10 + 48), void();
    }
    inline int Calc(int i, int u, int x) { return f[u + x * V[i]] - x * W[i]; }
    signed main() {
        n = read(), m = read();
        FOR(i, 1, n) V[i] = read(), W[i] = read(), C[i] = read();
        FOR(i, 1, n) FOR(u, 0, V[i] - 1) {
            int h = 1, t = 0, maxn = (m - u) / V[i];
            DOWN(k, maxn - 1, std ::max(maxn - C[i], 0)) {
                while (h <= t && Calc(i, u, q[t]) < Calc(i, u, k)) --t;
                q[++t] = k;
            }
            DOWN(p, maxn, 0) {
                while (h <= t && q[h] > p - 1) ++h;
                if (h <= t) f[u + p * V[i]] = std ::max(f[u + p * V[i]], Calc(i, u, q[h]) + p * W[i]);
                if (p - C[i] - 1 >= 0) {
                    while (h <= t && Calc(i, u, q[t]) < Calc(i, u, p - C[i] - 1)) --t;
                    q[++t] = p - C[i] - 1;
                }
            }
        }
        int ans = 0;
        FOR(i, 1, m) ans = std ::max(ans, f[i]);
        write(ans);
        return 0;
    }
    
  • 相关阅读:
    性能测试,负载测试,压力测试,容量测试的区别
    全文检索引擎Solr系列——整合MySQL、MongoDB
    全文检索引擎Solr系列——整合中文分词组件IKAnalyzer
    全文检索引擎Solr系列——整合中文分词组件mmseg4j
    全文检索引擎Solr系列——Solr核心概念、配置文件
    全文检索引擎Solr系列—–全文检索基本原理
    ES之一:Elasticsearch6.4 windows安装 head插件ik分词插件安装
    FTP,FTPS,FTPS与防火墙
    NGINX通过Stream转发ftp请求
    nginx 的第三方模块ngx_http_accesskey_module 来实现下载文件的防盗链步骤(linux系统下)
  • 原文地址:https://www.cnblogs.com/yywxdgy/p/13325290.html
Copyright © 2020-2023  润新知