王清欢Randy 王清欢Randy
首页
  • 编程语言

    • C/C++ 学习笔记
    • Golang 学习笔记
  • 算法分析

    • LeetCode 刷题笔记
  • 操作系统

    • Linux 基础
    • Vim 实用技巧
    • Shell 脚本编程
    • GDB 学习笔记
  • 开发工具

    • Git 学习笔记
  • 分布式理论

    • 共识算法
    • 分布式事务
  • 数据库内核

    • PostgreSQL
    • Postgres-XL
  • hidb
  • pgproxy
  • 实用技巧
  • 学习方法
  • 资源分享
GitHub (opens new window)
首页
  • 编程语言

    • C/C++ 学习笔记
    • Golang 学习笔记
  • 算法分析

    • LeetCode 刷题笔记
  • 操作系统

    • Linux 基础
    • Vim 实用技巧
    • Shell 脚本编程
    • GDB 学习笔记
  • 开发工具

    • Git 学习笔记
  • 分布式理论

    • 共识算法
    • 分布式事务
  • 数据库内核

    • PostgreSQL
    • Postgres-XL
  • hidb
  • pgproxy
  • 实用技巧
  • 学习方法
  • 资源分享
GitHub (opens new window)
  • 基础算法

    • 双指针

      • 双指针基础
      • 碰撞指针
      • 快慢指针
      • 滑动窗口
    • 二分查找

      • 基础应用
      • 边界收缩
      • 局部有序
    • 排序算法

      • 01八大排序
      • 02快速排序
      • 03归并排序
      • 04桶排序
      • 05堆排序
    • 优先搜索

      • 01递归
      • 02网格结构深度优先搜索
      • 03树结构深度优先搜索
      • 04图结构深度优先搜索
      • 05网格结构广度优先搜索
      • 06树结构广度优先搜索
      • 07图结构广度优先搜索
    • 回溯算法

      • 01递归
      • 02 子集问题
      • 03 全排列问题
      • 04 组合问题
      • 05 回溯搜索问题
  • 基础数据结构

    • 线性表与哈希表

      • 01数组
      • 02栈和队列
      • 03单调栈
      • 04优先队列
      • 05双端队列
      • 06哈希表
      • 07多重集合
      • 08前缀和
      • 09数据结构设计
    • 字符串

      • 01字符串比较
      • 02回文字符串
      • 03字符串匹配
      • 04字符串算术表达式
    • 单链表

      • 01链表基础操作
      • 02链表遍历
    • 二叉树

      • 01二叉树的属性
      • 02二叉树的操作
      • 03层次遍历
      • 04前中后序遍历
      • 05二叉搜索树的属性
      • 06二叉搜索树的操作
      • 07字典树
      • 08二叉搜索树BST
    • 图

      • 01二分图
      • 02拓扑排序
      • 03并查集
      • 04最小生成树
      • 05最短路径
  • 进阶算法

    • 贪心算法

      • 01跳跃游戏
      • 02分配问题
      • 03区间问题
    • 分治策略

    • 动态规划

      • 01一维动态规划
      • 02二维动态规划
      • 03分割型动态规划
      • 04子序列问题
      • 05背包问题
        • 05 背包问题
          • 416 分割等和子集
          • 494 目标和
          • 474 一和零
          • 322 零钱兑换
      • 06字符串编辑问题
      • 07股票交易问题
  • 其他内容

    • 数学问题

      • 01公倍数与公因数
      • 02质数问题
      • 03进制转换问题
      • 04数字字符串求和问题
      • 05众数问题
      • 06中位数问题
      • 07数字处理问题
      • 08随机数问题
    • 位运算

      • 01位运算基础
      • 02妙用异或运算
      • 03二进制特性
  • LeetCode刷题笔记
  • 进阶算法
  • 动态规划
王清欢
2023-03-24
目录

05背包问题

# 05 背包问题

​ 背包问题:有 N 个物品和容量为 W 的背包,每个物品都有自己的体积 w 和价值 v,求拿哪些物品可以使得背包所装下物品的总价值最大。如果限定每种物品只能选择 0 个或 1 个,则问题称为 0-1 背包问题;如果不限定每种物品的数量,则问题称为完全背包问题。

​ 0-1 背包问题,我们可以定义一个二维数组 dp 存储最大价值,其中 dp[i][j] 表示前 i 件物品体积不超过 j 的情况下能达到的最大价值。在我们遍历到第 i 件物品时,在当前背包总容量为 j 的情况下,分为两种情况:(1)如果我们不将物品 i 放入背包,即当前背包容量不足或这放入当前物品无法达到最大价值,那么 dp[i][j]= dp[i-1][j],即前 i 个物品的最大价值等于只取前 i -1 个物品时的最大价值;(2)如果我们将物品 i 放入背包,假设第 i 件物品体积为 w,价值为 v,那么我们得到 dp[i][j] = dp[i-1][j-w] + v。

int knapsack(vector<int> weights, vector<int> values, int N, int W) {
	vector<vector<int>> dp(N+1,vector<int>(W+1));
    // 放置第i个物品
    for(int i=1;i<=N;++i){
        // 第 i 个物品的体积w和价值v
        int w = weights[i-1], v = values[i-1];
        // 遍历容量
        for(int j=1;j<=W;++j){
            if(j>=w){
                dp[i][j] = max(dp[i-1][j],dp[i-1][j-w]+v);
            }else{
                dp[i][j] = dp[i-1][j];
            }
        }
    }
    return dp[N][W];
}

​ 在完全背包问题中,一个物品可以拿多次。这里直接给出完全背包问题的状态转移方程dp[i][j] = max(dp[i-1][j], dp[i][j-w] + v),其与 0-1 背包问题的差别仅仅是把状态转移方程中的第二个 i-1 变成了 i。

int knapsack(vector<int> weights, vector<int> values, int N, int W) {
	vector<vector<int>> dp(N+1,vector<int>(W+1));
    // 放置第i个物品
    for(int i=1;i<=N;++i){
        // 第 i 个物品的体积w和价值v
        int w = weights[i-1], v = values[i-1];
        // 遍历容量
        for(int j=1;j<=W;++j){
            if(j>=w){
                dp[i][j] = max(dp[i-1][j],dp[i][j-w]+v);
            }else{
                dp[i][j] = dp[i-1][j];
            }
        }
    }
    return dp[N][W];
}

# 416 分割等和子集 (opens new window)

给定一个正整数数组,求是否可以把这个数组分成和相等的两部分。

输入是一个一维正整数数组,输出时一个布尔值,表示是否可以满足题目要求。

输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。

解析:

​ 本题等价于 0-1 背包问题,设所有数字和为 sum,我们的目标是选取一部分物品,使得它们的总和为 target = sum/2,及用部分物品填满容量为target的背包。同时本题不需要考虑价值,因此我们只需要通过一个布尔值矩阵来表示状态转移矩阵。

​ 设置状态: dp[i][j] 表示 nums 的前 i 个整数是否能够组合成和为 j

​ 状态转移方程:(01背包问题的子问题都很简单只用区分选择当前物品或者不选择当前物品的情况)

  • 不选择 nums[i]:nums[i] 大于于当前容量 j 时,容量不够不选择该物品,dp[i][j] = dp[i-1][j]

  • 选择 nums[i]:

    • nums[i] 恰好等于当前容量 j,即nums[i]==j时,只放该物品就可以填满容量为 j 的背包,dp[i][j] =true
    • nums[i] 小于当前容量 j,即nums[i]<j时,只放该物品不够填满容量为 j 的背包,检测放入该物品后dp[i-1][j-nums[i]]的情况下是否可以填满背包

    总的说,当容量足够时,物品nums[i] 可选可不选(在有价值的情况下就需要选择价值较大的情况),则dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i]]

​ 初始情况:容量为0的时候,不选择任何物品,dp[i][0] = true;只有一个物品的时候,当容量为j == nums[0]时恰好满足,即dp[0][nums[0]] = true

​ 示例的状态转移矩阵根据上述状态转移方程可以得到如下表格:

0 1 2 3 4 5 6 7 8 9 10 11
[0] = 1 T T F F F F F F F F F F
[1] = 5 T T F F F T T F F F F F
[2] = 11 T T F F F T T F F F F T
[3] = 5 T T F F F T T F F F T T
class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int sum = accumulate(nums.begin(),nums.end(),0);
        auto maxPos = max_element(nums.begin(),nums.end());
        int target = sum/2, len = nums.size();
        // 注意特殊情况,和为奇数或者元素个数小与2都无法进行有效分割,而元素值大于target直接导致越界
        if(len<2 || sum&1 || *maxPos > target){
            return false;
        }
        vector<vector<bool>> dp(len,vector<bool>(target+1,false));
        // 初始情况容量为0
        for(int i=0;i<len;++i){
            dp[i][0] = true;
        }
        // 初始情况物品只有一个
        dp[0][nums[0]] = true;
        for(int i=1;i<len;++i){
            for(int j=1;j<=target;++j){
                // 背包容量大于当前扫描物品,可选可不选
                if(j >= nums[i]){
                    dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i]];
                // 背包容量小于当前扫描物品,没法选
                }else{
                    dp[i][j] = dp[i-1][j];
                }
            }
        }
        return dp[len-1][target];
    }
};

# 494 目标和 (opens new window)

给定义一个整数数组 nums 和一个整数 target 。向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式,返回可以完成表达式构造的、运算结果等于 target 的不同 表达式 的数目。

输入一个一维数组和整数,输出一个整数表示构造表达式结果为target的方法数

输入:nums = [1,1,1,1,1], target = 3 输出:5 解释:一共有 5 种方法让最终目标和为 3 。 -1 + 1 + 1 + 1 + 1 = 3 +1 - 1 + 1 + 1 + 1 = 3 +1 + 1 - 1 + 1 + 1 = 3 +1 + 1 + 1 - 1 + 1 = 3 +1 + 1 + 1 + 1 - 1 = 3

解析:

​ 本题可以转化为 01 背包问题,假设数组元素总和为 sum,添加 '+'的元素和为 x,那么添加 '-'的元素和为 sum - x。由此可以得到 x - (sum - x) = target,即为x = (target + sum) / 2,那么本题就可以转化为使用 nums中的N个物品装满容量为 x 的背包,共有多少种方法,可以看出这时一个组合问题。

​ 设置状态:使用一个二维数组 dp[i][j] 表示数组 nums 中从开头到以 i 位置结尾的所有物品装满容量为 j 的背包的方法数。

​ 状态转移方程:考虑第 i 个元素,如果当前背包容量 j<nums[i],则不能放入第 i 个元素 dp[i][j] = dp[i-1][j];如果当前背包容量j >= nums[i],不放入第 i 个元素的方法数为dp[i][j] = dp[i-1][j],放入第 i 个元素的方法数为dp[i][j] = dp[i-1][j-nums[i]],那么总的方法数为这两中情况之和即dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i]]

​ 边界情况:如果(target + sum)不为偶数是无解情况,如果 abs(target) > sum 也是无解情况,没有任何构造方式能够满足题目条件。当数组为空,背包容量为0时dp[0][0] == 1

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int len = nums.size();
        int sum = accumulate(nums.begin(),nums.end(),0);
        if((target+sum)&1 || abs(target)>sum){
            return 0;
        }
        int weight = (target + sum)/2;
        vector<vector<int>> dp(len+1,vector<int>(weight+1));
        dp[0][0] = 1;
        for(int i=1;i<=len;++i){
            for(int j=0;j<=weight;++j){
                dp[i][j] = dp[i-1][j];
                if(j>=nums[i-1]){
                    dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i-1]];
                }
            }
        }
        return dp[len][weight];
    }
};

# 474 一和零 (opens new window)

​ 给定 m 个数字 0 和 n 个数字 1,以及一些由 0-1 构成的字符串,求利用这些数字最多可以构成多少个给定的字符串,字符串只可以构成一次。

​ 输入两个整数 m 和 n,表示 0 和 1 的数量,以及一个一维字符串数组,表示待构成的字符串;输出是一个整数,表示最多可以生成的字符串个数。

输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3 输出:4 解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。

解析:

​ 本题也是一个01背包问题,而其特点在于它有两个背包,一个装0,另一个装1。

​ 设置状态:dp[i][j][k] 表示装0背包容量为 j,装1背包容量为k的情况下,能够装入前 i 个物品中的几个

​ 状态转移方程:仍旧划为装入当前物品和不装入当前物品两个子问题,不装入则dp[i][j][k]=dp[i-1][j][k],装入则dp[i][j][k]=max(dp[i-1][j][k],dp[i-1][j-w0[i]][k-w1[i]] + 1),其中w0表示物品中0的体积,w1表示物品中1的体积。

class Solution {
public:

    // 计算每个str中0和1的数量
    pair<int,int> countWeight(string str){
        int count0 = 0, count1 = 0;
        for(int i=0;i<str.length();++i){
            if(str[i] == '0'){
                ++count0;
            }else{
                ++count1;
            }
        }
        return make_pair(count0,count1);
    }

    int findMaxForm(vector<string>& strs, int m, int n) {
        int len = strs.size();
        // 用一个三维数组表示在前 i 个字符串中,使用 j 个 0 和 k 个 1 的情况下最多可以得到的字符串数量
        vector<vector<vector<int>>> dp(len+1,vector<vector<int>>(m+1,vector<int>(n+1)));
        for(int i=1;i<=len;++i){
            auto count = countWeight(strs[i-1]);
            // j 和 k 都从零开始,因为str存在'0'或'1'这种只有一种元素构成的情况
            for(int j=0;j<=m;++j){
                for(int k=0;k<=n;++k){
                    if(j >= count.first && k >= count.second){
                        dp[i][j][k] = max(dp[i-1][j][k],dp[i-1][j-count.first][k-count.second] + 1);
                    }else{
                        dp[i][j][k] = dp[i-1][j][k];
                    }
                }
            }
        }
        return dp[len][m][n];
    }
};

# 322 零钱兑换 (opens new window)

​ 给定一些硬币的面额,求最少可以用多少颗硬币组成给定的金额。

​ 输入一个一维整数数组,表示硬币的面额;以及一个整数,表示给定的金额。输出一个整数,表示满足条件的最少的硬币数量。若不存在解,则返回-1。

输入:coins = [1, 2, 5], amount = 11
输出:3 
解释:11 = 5 + 5 + 1

解析:

​ 因为每个硬币可以用无限多次,这道题本质上是完全背包问题。完全背包问题还是没搞懂,记录一下,以后仔细琢磨。

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        int Max = amount + 1;
        vector<int> dp(amount + 1, Max);
        dp[0] = 0;
        for (int i = 1; i <= amount; ++i) {
            for (int j = 0; j < (int)coins.size(); ++j) {
                if (coins[j] <= i) {
                    dp[i] = min(dp[i], dp[i - coins[j]] + 1);
                }
            }
        }
        return dp[amount] > amount ? -1 : dp[amount];
    }
};
上次更新: 2023/11/19, 12:55:48
04子序列问题
06字符串编辑问题

← 04子序列问题 06字符串编辑问题→

Theme by Vdoing | Copyright © 2023-2024 Wang Qinghuan | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式