Algorithm-Pattern
  • Introduction
  • C & C++
    • C语言
      • C/C++编译器
      • 宏的使用
      • 编译过程
      • 指针 & 数组
      • 柔性数组
      • 函数指针 & 回调函数
      • C标准库之stdio
      • C标准库之string
      • C标准库之errno
      • C标准库之stdarg
      • C标准库之regex
    • C++基础语法
      • 自增(++) & 自减(--)
      • c语言到c++
      • 可变模板参数
      • 强制类型转换
      • C/C++类型转换的本质
      • 指针 & 引用
      • const的用法
      • static的用法
      • 重要的关键字(一)
      • 重要的关键字(二)
      • 内存申请和释放
      • 内联函数
      • 函数 & 运算符重载
      • 面向对象之封装
      • 构造函数 & 析构函数
      • 面向对象之继承
      • 面向对象之多态
      • 泛型编程
      • 异常
      • 再谈函数指针
    • C++并发编程
      • C++的锁
      • 并发与多线程
    • C++高级特性
      • 函数对象
      • 移动语义 & 完美转发
      • lambda表达式
      • RTTI技术
      • RAII技术
      • 智能指针
      • 模板的特化
      • C++静态库和动态库
      • 内存溢出和内存泄漏
    • STL基础
      • String
      • array/vector/list
      • deque/priority_queue
      • set/map
      • unordered_set/unordered_map
      • algorithm_1
      • functional
      • allocator
    • C++标准库
      • IO
      • Tuple
      • regex
      • bitset
      • numeric
    • STL深入源码
      • vector内部实现
      • deque内部实现
      • sort函数实现
    • 第三方库
      • JsonCpp
      • ProtoBuf
  • 数据结构
    • 线性表
    • 字符串
    • 栈和队列
    • 二叉树
    • 平衡二叉树
    • 平衡多路搜索树
    • 树结构的延申
    • 图
    • 二进制
    • 散列表
  • 算法基础
    • 排序算法
    • 查找算法
    • 数学问题
    • 并查集
    • 递归算法
    • 附加——主定理
    • Catalan数
  • 算法设计思想
    • 滑动窗口思想
    • BFS/DFS
    • 二分法
    • 回溯法
    • 贪心算法
    • 分治法
    • 动态规划
    • 分支限界算法
    • 有限状态机(FSM)
  • LeetCode系列
    • 死磕二叉树
    • 股票买卖问题
    • 打家劫舍问题
    • 跳跃游戏问题
    • 括号匹配问题
    • 石子游戏问题
    • 子序列问题
    • 数组 & 矩阵
    • 排列 & 组合
  • 经典算法问题
    • 几何问题
    • 区间问题
    • 背包问题
    • 石子堆问题
    • 表达式求值
  • 面试题
    • 数据结构和算法基础
    • 程序设计题
      • 实现双线程交替打印
      • C++实现读写锁
      • 实现阻塞队列
      • 实现环形队列
      • 实现线程池
      • 实现智能指针
      • 实现string类
      • 实现高性能local cache
      • 实现内存池
      • 生产者-消费者模型
      • 设计定时器
    • 经典的算法题
    • C++面试题总结
    • 面试算法题总结
由 GitBook 提供支持
在本页
  • 1、0-1背包【链接】
  • Partition Equal Subset Sum
  • 2、完全背包【链接】
  • Coin Change
  • Coin Change 2
  • 3、多重背包【链接】(京东笔试)
  • 优化:
  • Ones and Zeroes

这有帮助吗?

  1. 经典算法问题

背包问题

上一页区间问题下一页石子堆问题

最后更新于4年前

这有帮助吗?

有 nnn 种物品,物品 jjj 的重量和价值分别为 wjw_jwj​ 和 vjv_jvj​ ,如果背包的最大容量限制是 mmm ,怎么样选择放入背包的物品以使得背包的总价值最大?

组合优化问题,设 xjx_jxj​ 表示装入背包的第 jjj 个物品的数量,解可以表示为 <x1,x2,…,xn><x_1,x_2,\ldots,x_n><x1​,x2​,…,xn​> 。那么目标函数和约束条件是:

如果组合优化问题的目标函数和约束条件都是线性函数,称为线性规划。如果线性规划问题的变量 xjx_jxj​ 都是非负整数,则称为整数规划问题。背包问题就是整数规划问题。限制所有的 xj=0orxj=1x_j = 0 or x_j = 1xj​=0orxj​=1 时称为0-1背包。

有n件物品和容量为m的背包,给出每件物品的重量 wiw_iwi​ 以及价值 viv_ivi​ ,求解让装入背包的物品重量不超过背包容量,且价值最大 。特点:每个物品只有一件供你选择放还是不放。

二维解法:设 f[i][j]f[i][j]f[i][j] 表示前 iii 件物品 总重量不超过 jjj 的最大价值,可得出状态转移方程:

f[i][j]=max{f[i−1][j−w[i]]+v[i], f[i−1][j]}f[i][j]=max\{f[i-1][j-w[i]]+v[i],\ f[i-1][j]\}f[i][j]=max{f[i−1][j−w[i]]+v[i], f[i−1][j]}

一维解法:设 f[j]f[j]f[j] 表示重量不超过 jjj 公斤的最大价值,可得出状态转移方程:

f[j]=max{f[j], f[j−w[i]]+v[i]}f[j]=max\{f[j],\ f[j−w[i]]+v[i]\}f[j]=max{f[j], f[j−w[i]]+v[i]}

int maxValue(std::vector<int>& values, std::vector<int>& weight, int n, int m){
    int dp[n + 1][m + 1];
    memset(dp, 0, sizeof(dp));
    for(int i = 1; i <= n; ++i){
        for(int j = 1; j <= m; ++j){
            if(weight[i] <= j)
                dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + values[i]);
            else
                dp[i][j] = dp[i - 1][j];
        }
    }
    return dp[n][m];
}
int maxValue(std::vector<int>& values, std::vector<int>& weight, int n, int m){
    int dp[m + 1];
    memset(dp, 0, sizeof(dp));
    for(int i = 1; i <= n; ++i){
        for(int j = m; j >= 1; --j){
            if(weight[i] <= j)
                dp[j] = max(dp[j], dp[j - weight[i]] + values[i]);
        }
    }
    return dp[m];
}

有 nnn 件物品和容量为 mmm 的背包,给出每件物品的重量 wiw_iwi​ 以及价值 viv_ivi​ ,求解让装入背包的物品重量不超过背包容量,且价值最大 。特点:每个物品可以无限选用。

一维解法: 设 f[j]f[j]f[j] 表示重量不超过 jjj 公斤的最大价值,可得出状态转移方程:

f[j]=max{f[j], f[j−w[i]]+v[i]}f[j]=max\{f[j],\ f[j−w[i]]+v[i]\}f[j]=max{f[j], f[j−w[i]]+v[i]}

int maxValue(std::vector<int>& values, std::vector<int>& weight, int n, int m){
    int dp[m + 1];
    memset(dp, 0, sizeof(dp));
    for(int i = 1; i <= n; ++i){
        for(int j = weight[i]; j <= m; ++j){
            dp[j] = max(dp[j], dp[j - weight[i]] + values[i]);
        }
    }
    return dp[m];
}

有 nnn 件物品和容量为 mmm 的背包。给出每件物品的数量 sis_isi​ 、重量 wiw_iwi​ 以及价值 viv_ivi​ ,求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。特点: 每个物品都有了一定的数量。

二维解法:把 sis_isi​ 个物品逐个拆分,则得到 ∑si\sum s_i∑si​ 个物品,原问题转化为01背包问题,时间复杂度为 O(m×∑si)O(m\times \sum s_i)O(m×∑si​) 。也可以在转移得过程中枚举 kkk ,代表第 iii 个物品选取的数量,则转移方程为:

f[i][j]=max0≤k≤s[i]{f[i−1][j−k×v[i]]+k×w[i]}f[i][j]=\mathop{max}\limits_{0\le k \le s[i]}\{f[i - 1][j - k\times v[i]] + k\times w[i]\}f[i][j]=0≤k≤s[i]max​{f[i−1][j−k×v[i]]+k×w[i]}

一维解法:设 f[j]f[j]f[j] 表示重量不超过 jjj 公斤的最大价值,可得出状态转移方程:

f[j]=max0≤k≤s[i]{f[j−k×w[i]]+k×v[i]}f[j] = \mathop{max}\limits_{0\le k \le s[i]}\{f[j−k\times w[i]]+k\times v[i]\}f[j]=0≤k≤s[i]max​{f[j−k×w[i]]+k×v[i]}

int maxValue(std::vector<int>& values, std::vector<int>& weight, 
             std::vector<int>& count, int n, int m){
    int dp[m + 1];
    memset(dp, 0, sizeof(dp));
    for(int i = 1; i <= n; ++i){
        for(int j = m; j >= weight[i]; --j){
            for(int k = 0; k <= count[i]; k++){
                if(j - k * weight[i] < 0)
                    break;
                dp[j] = max(dp[j], dp[j - k * weight[i]] + k * values[i]);
            }
        }
    }
    return dp[m];
}

优化:

1、二进制优化

将第 iii 种物品分成若干件物品,可以有 (vi,wi),(vi×2,wi×2),(vi×4,wi×4)…(v_i,w_i),(v_i\times 2,w_i\times 2),(v_i\times 4,w_i\times 4)\ldots(vi​,wi​),(vi​×2,wi​×2),(vi​×4,wi​×4)… 等等,每件物品有一个系数,分别为 1,2,4,…,2k−1,n−2k+11,2,4,\ldots,2^{k - 1},n-2^k+11,2,4,…,2k−1,n−2k+1 , kkk 是满足的最大整数。例如, nnn 为13,拆分成 1,2,4,61,2,4,61,2,4,6 四件物品,根据二进制的性质, 0…130\ldots 130…13 都可以由这四件物品组合得到。于是, nnn 件物品,就拆分成至多 lognlognlogn 件物品。再转化为01背包问题求解,复杂度降至 O(m×∑logn)O(m\times \sum logn)O(m×∑logn) 。

int maxValue(std::vector<int>& values, std::vector<int>& weight, 
             std::vector<int>& count, int n, int m){
    vector<pair<int, int> > lis;
    lis.push_back({0, 0});
    int idx, c;
    for(int i = 1; i <= n; ++i){
        c = 1;
        while(count[i] - c > 0){
            count[i] -= c;
            lis.push_back({c * values[i], c * weight[i]});
            c *= 2;
        }
        lis.push_back({count[i] * values[i], count[i] * weight[i]});
    }
    int dp[m + 1];
    memset(dp, 0, sizeof(dp));
    for(int i = 1; i <= lis.size(); ++i){
        for(int j = m; j >= lis[i].second; --j){
            dp[j] = max(dp[j], dp[j - lis[i].second] + lis[i].first);
        }
    }
    return dp[m];
}

2、单调队列优化

将背包的容量按照的剩余分成组:

仔细观察转移方程,会发现真正的转移只会发生在同一组内,不同的组是不可能转移的可以改一下上面的转移方程:

f[b+x×v[i]]=max0≤k≤s[i]{f[b+(x−k)×v[i]]+k×w[i]}f[b+x\times v[i]] = \mathop{max}\limits_{0\le k \le s[i]}\{f[b+(x-k)\times v[i]]+k\times w[i]\}f[b+x×v[i]]=0≤k≤s[i]max​{f[b+(x−k)×v[i]]+k×w[i]}

令 t=x−kt = x - kt=x−k ,状态转移方程变成:

f[b+x×v[i]]=maxx−s[i]≤t≤x{f[b+t×v[i]]−t×w[i]}+x×w[i]f[b+x\times v[i]] = \mathop{max}\limits_{x-s[i]\le t \le x}\{f[b+t\times v[i]]-t\times w[i]\} + x\times w[i]f[b+x×v[i]]=x−s[i]≤t≤xmax​{f[b+t×v[i]]−t×w[i]}+x×w[i]

这里的转移实际上一对 (b,x)(b,x)(b,x) 唯一对应一个 jjj 。我们固定 bbb ,那么对于 xxx 的决策 ttt 的集合正好是一个滑动区间,而 maxmaxmax 里面是一个和 xxx 无关的式子,所以可以用单调队列来维护。具体算法如下:

  • 枚举一个 iii 和 b(0≤b≤v[i])b(0\le b\le v[i])b(0≤b≤v[i]) ;

  • 从小到大枚举 xxx ,并且用单调队列来维护 f[b+t×v[i]]−t×w[i]f[b+t\times v[i]]-t\times w[i]f[b+t×v[i]]−t×w[i] 关于 ttt 单调下降,更新 f[b+x×v[i]]f[b+x\times v[i]]f[b+x×v[i]] 的值。

由于每个剩余类最多只有 mv[i]\frac{m}{v[i]}v[i]m​ 个元素,那么对于一个 iii ,转移的总时间为 O(mv[i]×v[i])=O(m)O(\frac{m}{v[i]} \times v[i]) = O(m)O(v[i]m​×v[i])=O(m) ,所以算法的时间复杂度为 O(n×m)O(n\times m)O(n×m) ,这样我们就又优化掉了多重背包中的物品个数这一维。

int maxValue(std::vector<int>& values, std::vector<int>& weight, 
             std::vector<int>& count, int n, int m){
    int dp[m + 1];
    int q[m + 1];
    int num[m + 1];
    memset(dp, 0, sizeof(dp));
    int l, r;
    for(int i = 1; i <= n; ++i){
        int c = min(count[i], m / weight[i]);
        for(int b = 0; b < weight[i]; b++){
            l = r = 1;
            for(int t = 0; t <= (m - b) / weight[i]; t++){
                int tmp = dp[t * weight[i] + b] - t * values[i];
                while(l < r && q[r - 1] <= tmp)
                    r--;
                q[r] = tmp;
                num[r++] = t;
                // 滑动区间长度不大于c,因为dp[t * weight[i] + b] - t * values[i]既然存在,
                // 那么再加c区间的t * values[i]的值肯定能取到
                while(l < r && t - num[l] > c)
                    l++;
                // 因为dp中的是t * weight[i] + b,所以是q[l] + t * values[i]
                dp[t * weight[i] + b] = max(dp[t * weight[i] + b], q[l] + t * values[i]);
            }
        }
    }
    return dp[m];
}

1、0-1背包【】

2、完全背包【】

3、多重背包【】(京东笔试)

✏️
链接
Partition Equal Subset Sum
✏️
链接
Coin Change
Coin Change 2
✏️
链接
Ones and Zeroes