zoukankan      html  css  js  c++  java
  • 背包九讲理解(代码上的区别)

    1. 01背包
    问题:N种物品,每种物品1个,第i个物品重量wi,价值vi。背包容量M。选择装入背包的物品使得价值最大。
    /*
    二维状态:f[i][j] 表示对于前 i 个物品,总体积为 j 的情况下可以获得的最大价值。
    结果:max(f[N][M])。
    状态转移:
    1. 不选择第 i 个物品:f[i][j] = f[i - 1][j];
    2. 选择第 i 个物品:f[i][j] = f[i - 1][j - w[i]] + v[i];   注意这里状态是从第 i - 1 个物品的状态转移过来的,因为对于01背包来说如果当前要选择第 i 个物品,则代表之前一定不能选择过第 i 个物品(只能选一次)。
    f[i][j] = max(1., 2.);
    即:f[i][j] = max(f[i - 1][j], f[i - 1][j - w[i]] + v[i]);
    初始化:f[0][0] = 0;
    */
     
    class solution {
    public:
        int change(int N, int M, vector<int>& w, vector<int>& v) {
            vector<vector<int>> f(N + 1, vector<int>(M + 1));
            for(int i = 1; i <= N; i++) {
                for(int j = 0; j <= M; j++) {
                    f[i][j] = f[i - 1][j];
                    if(j >= w[i - 1]) f[i][j] = max(f[i][j], f[i - 1][j - w[i - 1]] + v[i - 1]);
                }
            }
            int res = 0;
            for(int i = 0; i <= M; i++) res = max(res, dp[N][i]);
            return res;
        }
    }
     
    /*
    一维状态:f[M]
    状态转移:f[j] = max(f[j], f[j - w[i]] + v[i]);   这里的f[j - w[i]]是旧值,上一轮的状态。
    因为到达判断第 i 个物品的状态时只与前一个物品(第 i - 1个物品)的状态有关,因此二维状态中的物品维度可以省略。但是因为原来二维状态时保留了所有前i - 1个物品选择时的状态,在计算第二个维度体积时是可以小到大计算的。但是在一维状态表示时,f[M]表示第i - 1个物品在各个体积下的状态,如果还是从小到大计算的话,第 i 个物品在某个体积下的状态将会覆盖掉第 i - 1个物品的状态,而计算f[i]的转移方程时就会出错。因为f[i][j]需要的是f[i - 1][j]和f[i - 1][j - w[i]]的状态。因此需要从大体积向小体积遍历,保证上一轮的状态没有被覆盖。
    */
     
    class solution {
    public:
        int change(int N, int M, vector<int>& w, vector<int>& v) {
            vector<int> f(M + 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 - 1]] + v[i - 1]);
                }
            }
            return f[M];
        }
    }
    

      

    2. 完全背包
    问题:N种物品,每种物品无限个,第 i 个物品的体积wi,价值vi,背包容量M。选择装入背包的物品使得价值最大。
    /*
    朴素算法。转成01背包问题。
    二维状态:f[i][j]
    结果:
    状态转移:
    1. 不选第 i 个物品,f[i][j] = f[i - 1][j];
    2. 选择 k 个第 i 个物品,f[i][j] = (f[i - 1][j - k * w[i]] + k * v[i]), 0 <= k * w[i] <= j
    f[i][j] = max(1., 2.)
    即:f[i][j] = max(f[i - 1][j], f[i - 1][j - k * w[i]] + v[i]);
     
    一维状态:f[j]
    结果:
    状态转移:
    */
     
    class solution {
    public:
        int change(int N, int M, vector<int>& w, vector<int>& v) {
            vector<vector<int>> f(N + 1, vector<int>(M + 1));
            for(int i = 1; i <= N; i++) {
                for(int j = 1; j <= M; j++) {
                    f[i][j] = f[i - 1][j];
                    for(int k = 1; k * w[i - 1] <= j; k++) {
                        f[i][j] = max(f[i][j], f[i - 1][j - k * w[i - 1]] + k * v[i - 1]);
                }
            }
            int res = 0;
            for(int i = 0; i <= M; i++) res = max(res, dp[N][i]);
            return res;
        }
    }
     
    // 一维
    class solution {
    public:
        int change(int N, int M, vector<int>& w, vector<int>& v) {
            vector<int> f(M + 1);
            for(int i = 1; i <= N; i++) {
                for(int j = M; j >= 1; j—-) {
                    for(int k = 1; k * w[i - 1] <= j; k++) {
                        f[j] = max(f[j], f[j - k * w[i - 1]] + k * v[i - 1]);
                }
            }
            int res = 0;
            for(int i = 0; i <= M; i++) res = max(res, dp[N][i]);
            return res;
        }
    }
     
    /*
    二维状态:f[i][j]表示对于前 i 个物品,体积为 j 可以获得的最大价值。
    结果:f[N][M]
    状态转移:
    1. 不选第 i 个物品,f[i][j] = f[i - 1][j];
    2. 至少选择1个第 i 个物品,f[i][j] = f[i][j - w[i]] + v[i]; 注意这里是从第 i 个物品的状态转移的,而不是而不是第 i - 1 个物品的状态转移过来,因为不知道当前选择的是第几个 i 物品,之前有个可能已经选择过第 i 个物品了。
    f[i][j] = max(1., 2.)
    即:f[i][j] = max(f[i - 1][j], f[i][j - w[i]] + v[i]);
    初始化:f[0][j] = 0
    */
     
    class solution {
    public:
        int change(int N, int M, vector<int>& w, vector<int>& v) {
            vector<vector<int>> f(N + 1, vector<int>(M + 1));
            for(int i = 1; i <= N; i++) {
                for(int j = 1 j <= M; j++) {
                    f[i][j] = f[i - 1][j];
                    if(j >= w[i - 1]) f[i][j] = max(f[i][j], f[i][j - w[i - 1]] + v[i - 1]);
                }
            }
            int res = 0;
            for(int i = 0; i <= M; i++) res = max(res, dp[N][i]);
            return res;
        }
    }
     
    /*
    一维状态:f[i]表示体积为 i 的情况下,可以获得的最大价值。
    结果:res = max(f[ 0 … M]);
    状态转移:f[j] = max(f[j], f[j - w[i]] + v[i]);    这里的f[j - w[i]] 是新值,当前轮的状态。
    关键:与01背包问题的一维解法的区别。多维背包中f[i]是用第 i 个物品的在体积 j - w[i] 的状态更新后面在第 i 个物品的在体积 j 的状态。而01背包中f[i]是用第 i - 1个物品在体积 j - w[i] 的状态来更新在第 i 个物品的在体积 j 的状态。因为01背包每个物品只能选一次,因此如果要选择第 i 个物品,则一定是从第 i - 1个物品的状态转移过来的,而完全背包对于一个物品可以选择多次,因此需要从第 i 个物品在体积 j - w[i] 的状态转移,因为f[j - w[i]]先于f[i]计算,所以f[j - w[i]]状态可能已经包含了若干个 i 物品(具体包含了多少个 i 物品又之前的状态转移过程决定)。因此一维解法中,体积维度的遍历,01背包问题需要逆序遍历(需要的是考虑上一轮第 i - 1 个物品的更小体积的状态,逆序计算可以避免在使用之前被覆盖);而完全背包问题需要顺序遍历(需要的是考虑本轮第 i 个物品更小体积的状态,顺序计算保证使用的是更新后的状态)
    */
     
    class solution {
    public:
        int change(int N, int M, vector<int>& w, vector<int>& v) {
            vector<int> f(M + 1);
            for(int i = 1; i <= N; i++) {
                for(int j = v[i]; j <= M; j++) {
                    f[j] = max(f[j], f[j - w[i - 1]] + v[i - 1]);
                }
            }
            return f[M];
        }
    }
    
    3. 多重背包
    问题:N种物品,第i种物品有si个,第 i 个物品的体积wi,价值vi,背包容量M。选择装入背包的物品使得价值最大。
    /*
    朴素算法。转为01背包问题。
    一维状态:f[i]表示体积为 i 的情况下,获得的最大价值。
    */
    class solution {
    public:
        int change(int N, int M, vector<int>& w, vector<int>& v, vector<int>& s) {
            vector<int> f(M + 1);
            for(int i = 1; i <= N; i++) {
                for(int j = M; j >= 1; j—-) {
                    // 第 i 个物品选择 k 个
                    for(int k = 0; k <= s[i - 1] && k * w[s - 1] <= j; k++) { 
                        f[j] = max(f[j], f[j - k * w[i - 1]] + k * v[i - 1]);
                    }            
                }
            }
            return f[M];
        }
    }
     
    /*
    多重背包转化为01背包问题求解。
    1. 二进制拆分优化。
    将原来的 si 个第 i 种物品,根据二进制拆分为ceil(log(si))种物品。得到新的物品种类。
    e.g.:
    第 1 种物品有 s1 = 10 个,体积 w1 = 2,价值 v1 = 3。
    则可以将 10 个第 1 种物品转换成 4 种01背包的物品:
    包含第1种物品的个数:1,2,4,3     // 选择其中几个可以组合出任意<=10的数,因此可以用该方法优化
    体积分别是:       2,4,8,6
    价值分别是:       3,6,12,9
    第 2 种物品有 s2 = 3 个,体积 w2 = 1,价值 v2 = 4。
    则可以将 3 个第 2 种物品转换成 2 种01背包的物品:
    包含第1种物品的个数:1,2
    体积分别是:       1,2
    价值分别是:       4,8
    */
     
    class solution {
    public:
        int change(int N, int M, vector<int>& w, vector<int>& v, vector<int>& s) {
            vector<int> new_v, new_w;
            // 二进制优化
            for(int i = 0; i < N; i++) {
                int s = s[i];
                for(int j = 1; j <= s; j <<= 1) {
                    new_v.push_back(v[i] * j);
                    new_w.push_back(w[i] * j);
                    s -= j;
                }
                if(s) {
                    new_v.push_back(v[i] * s);
                    new_w.push_back(w[i] * s);
                }
            }
            N = new_v.size();
            vector<int> f(M + 1);
            for(int i = 1; i <= N; i++) {
                for(int j = M; j >= new_w[i]; j—-) {
                    f[j] = max(f[j], f[j - new_w[i - 1]] + new_v[i - 1]);
                }
            }
            return f[M];
        }
    }
     
    /*
    2. 单调队列优化。
    更新状态转化为:滑动窗口内最大值。可以使用单调队列优化。
    根据观察可以发现状态在跟新的时候 f 数组只和与对当前第 i 个物品的体积同余的前状态有关。可以根据该特性在每一轮状态更新时将状态 f[0 … M] 根据当前物品的体积 wi 划分为 wi 类。 
    e.g.:
    N, M: 2, 9
    第 1 个物品w1, v1, s1: 3, 5, 2
    f[9] = 5  f[6] + 5 = 5
    f[9] = 10 f[3] + 10 = 10
    f[8] = 5  f[5] + 5 = 5
    f[8] = 10 f[2] + 10 = 10
    f[7] = 5  f[4] + 5 = 5
    f[7] = 10 f[1] + 10 = 10
    f[6] = 5  f[1] + 5 = 5
    f[6] = 10 f[0] + 10 = 10
    f[5] = 5  f[2] + 5 = 5
    f[4] = 5  f[1] + 5 = 5
    f[3] = 5  f[0] + 5 = 5
    第 2 个物品w2, v2, s2: 2, 4, 3
    // 左边是新值(当前轮的状态),由右边是旧值(上一轮的状态)计算而来的
    f[9] = 14  f[7] + 4 = 14    // 1 个 第2个物品
    f[9] = 14  f[5] + 8 = 13    // 2 个 第2个物品
    f[9] = 17  f[3] + 12 = 17   // 3 个 第2个物品
    f[8] = 14  f[6] + 4 = 14    
    f[8] = 14  f[4] + 8 = 13
    f[8] = 14  f[2] + 12 = 12
    f[7] = 10  f[5] + 4 = 9
    f[7] = 13  f[3] + 8 = 13
    f[7] = 13  f[1] + 12 = 12
    f[6] = 10  f[4] + 4 = 9
    f[6] = 10  f[2] + 8 = 8
    f[6] = 12  f[0] + 12 = 12
    f[5] = 9   f[3] + 4 = 9
    f[5] = 9   f[1] + 8 = 8
    f[4] = 5   f[2] + 4 = 4
    f[4] = 8   f[0] + 8 = 8
    f[3] = 5   f[1] + 4 = 4
    f[2] = 4   f[0] + 4 = 4
    根据对第 2 个物品体积 2 的余数分为了 2 类状态。
    f[0], f[2], f[4], f[6], f[8]. 
    f[1], f[3], f[5], f[7], f[9]. f[j]可以由前面不超过数量 s 的同余状态递推获得。
    这相当于从前面宽度为 s 的窗口挑选最大值来更新当前状态。
    转化成问题:求滑动窗口最大值?-> 单调队列维护窗口最大值!
    从而可以将更新f[j]的次数缩减到1次。
    因为f[j]是由前面的状态递推更新的,因此需要保存上一轮的状态。使用备份数组g。
    answer = 17
    */
     
    class solution {
    public:
        int change(int N, int M, vector<int>& w, vector<int>& v, vector<int>& s) {
            deque<int> Q;
            vector<int> f(M + 1);
            for(int i = 1; i <= N; i++) {   // 第 i 个物品
          // f[k]是顺序更新的,旧值会被先覆盖,而在更新f[j]时用到的是旧值,因此需要备份数组保存上一轮的状态
                vector<int> g(f.begin(), f.end());  
                int w = w[i - 1], v = v[i - 1], s = s[i - 1];
                for(int j = 0; j < v; j++) {  // 按照体积 vi 将状态分为 vi 类
                    for(int k = j; k <= M; k += w) {   //  对每个类使用单调队列找到窗口内最大值
                        // 判断队头元素是否还在窗口[k - s * w, k - w]内
                        if(!Q.empty() && Q.front() < k - s * w) Q.pop_front();
                        // 更新状态1.不选,2.选几个第 i 个物品的价值最大
                        if(!Q.empty()) f[k] = max(g[k], g[Q.front()] + (k - Q.front()) / w * v);
    //若用g[k]比用g[Q.back()]更新f[x]能获得更大的价值,则应有:g[k] + (x - k) / w * v >= g[Q.back()] + (x - Q.back()) / w * v
                        while(!Q.empty() && g[k] >= g[Q.back()] + (k - Q.back()) / w * v)) Q.pop_back();
                        // while(!Q.empty() && g[k] - (k - j) / w * v >= g[Q.back()] + (k - Q.back()) / w * v)) Q.pop_back();
                        Q.push_back(k);
                }
            }
            return f[M];
        }
    }
    
    4. 混合背包
    问题:N种物品,背包容量M。每种物品体积 wi,价值 vi。求背包可以装入物品的最大价值。
    第一类物品只能用一次(01背包),第二类物品可以无限使用(完全背包),第三类物品至多只能使用 si 次(多重背包)。
    /*
    预处理转成两类背包问题处理:
    1.01背包。
    2.完全背包。
    */
     
    class Solution {
    private:
        struct Thing {
            int kind;
            int v, w;
        };
    public:
        int change(int N, int M, vector<int>& w, vector<int>& v, vector<int>& s) {
            vector<Thing> things;
            vector<int> f(M + 1, 0);
            for(int i == 0; i < s.size(); i++) {
                if(s[i] == 0) things.push_back({0, v[i], w[i]});
                else if(s[i] == -1) things.push_back({-1, v[i], w[i]});
                else {
                    int s = s[i];
                    for(int k = 1; k <= s; k <<= 1) {
                        things.push_back({-1, v[i] * k, w[i] * k});
                        s -= k;
                    }
                    if(s > 0) things.push_back({-1, v[i] * s, w[i] * s});    
                }    
            }
            for(auto thing : things) {
                if(thing.kind < 0) {  // 01背包状态转移
                    for(int j = M; j >= thing.w; j—-) 
                        f[j] = max(f[j], f[j - thing.w] + thing.v);
                }
                else {   // 完全背包状态转移
                     for(int j = thing.w; j <= M; j++) 
                        f[j] = max(f[j], f[j - thing.w] + thing.v]);
                }
            }
            return f[M];
        }
    }
    

     

    5. 二维费用背包
    问题:N种物品,背包容量M,承重C。每个物品只能用一次,第 i 个物品体积wi,价值vi,重量ci。
    /*
    由一维01背包推广。
    一维01背包状态:f[j]表示背包容量为 j 可以获得的最大价值。 结果为 f[M]。
    二维01背包状态:f[j][k]表示背包容量为 j,承重为 k 可以获得的最大价值。结果为 f[M][C]。
     
    类似可以推广得到二维完全背包问题,三维01背包问题…
    */
     
    class solution {
    public:
        int change(int N, int M, int C, vector<int>& w, vector<int>& v, vector<int>& c) {
            vector<vector<int>> f(M + 1, vector<int>(C + 1, 0));
            for(int i = 1; i <= N; i++) {
                for(int j = M; j >= w[i]; j—-) {
                    for(int k = C; k >= 1; k—-) {
                        f[j][k] = max(f[j][k], f[j - w[i - 1]][k - c[i - 1]] + v[i - 1]);
                    }
                }
            }
            return f[M][C];
        }
    }
    

      

    6. 分组背包
    问题:N组物品,每组物品至多只能选择1个。
    /*
    转成01背包问题。
    */
     
    7. 背包问题求方案数
    8. 求背包问题的具体方案
    9. 有依赖的背包问题
     
    动规:
    • 状态定义
    • 状态转移
    • 结果
    • 初始化
    时间:C++评测里1s差不多10^7~10^8个测试。
     
  • 相关阅读:
    Unity 深入解析合批(Batching)原理及失败原因
    ILRuntime作者林若峰分享:优化 Dots URP 性能优化 (干货满满)
    unity Mesh绘制网格线
    Shder基础知识
    UnityEditor工具链开发的常用小技巧与示例
    磁盘文件分析清理工具
    波函数坍塌算法(Wave Collapse Function)实现随机地图
    视觉小说 文字游戏引擎 ink 可配合Unity
    c# 字典键值排序
    unity文件夹复制
  • 原文地址:https://www.cnblogs.com/tristatl/p/15120629.html
Copyright © 2011-2022 走看看