Offer必备算法29_其它背包问题_二维费用背包+似包非包+卡特兰数

目录

①力扣474. 一和零(二维费用背包)

解析代码

代码优化

②力扣879. 盈利计划(二维费用背包)

解析代码

代码优化

③力扣377. 组合总和 Ⅳ(似包非包)

解析代码

④力扣96. 不同的二叉搜索树(卡特兰数)

解析代码

本篇完。


①力扣474. 一和零(二维费用背包)

474. 一和零

难度 中等

给你一个二进制字符串数组 strs 和两个整数 m 和 n 。

请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。

如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。

示例 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 。

示例 2:

输入:strs = ["10", "0", "1"], m = 1, n = 1
输出:2
解释:最大的子集是 {"0", "1"} ,所以答案是 2 。

提示:

  • 1 <= strs.length <= 600
  • 1 <= strs[i].length <= 100
  • strs[i] 仅由 '0' 和 '1' 组成
  • 1 <= m, n <= 100
class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {

    }
};

解析代码

先看能不能将问题转化成熟悉的题型。

        在一些物品中挑选一些出来,然后在满足某个限定条件下,解决一些问题,大概率是背包模型, 由于每一个物品都只有 1 个,因此是一个01 背包问题。 但是发现这一道题里面有两个限制条件。因此是一个二维费用的 01 背包问题。那么定义状态表示的时候,创建一个三维 dp 表,把第二个限制条件加上即可。

        二维费用的背包问题是指:对于每件物品,具有两种不同的费用,选择这件物品必须同时付出这两种代价,对于每种代价都有一个可付出的最大值(例:背包容量,问怎样选择物品可以得到最大的价值。设这两种代价分别为代价1和代价2,第i件物品所需的两种代价分别为a[i]和b[i]。两种代价可付出的最大值(两种背包容量)分别为V和U。物品的价值为w[i]。)

        故:对于01背包问题、完全背包问题和多重背包问题的方法都完全可以使用,只不过增加一个代价。

状态表示:dp[i][j][k] 表示:从前 i 个字符串中挑选,字符 0 的个数不超过 j ,字符 1 的个数不超过 k ,所有的选法中,最大的长度。

状态转移方程:

        线性 dp 状态转移方程分析方式,一般都是根据最后一步的状况,来分情况讨论。为了方便叙述,记第 i 个字符中,字符 0 的个数为 a ,字符 1 的个数为 b :

  • 不选第 i 个字符串:相当于就是去前 i - 1 个字符串中挑选,并且字符 0 的个数不超过 j ,字符 1 的个数不超过 k 。此时的最大长度为 dp[i][j][k] = dp[i - 1] [j][k] 。
  • 选择第 i 个字符串:那么接下来仅需在前 i - 1 个字符串里面,挑选出来字符 0 的 个数不超过 j - a ,字符 1 的个数不超过 k - b 的最长长度,然后在这个长度后面加上字符串 i 即可。此时 dp[i][j][k] = dp[i - 1][j - a][k - b] + 1 。 但是这种状态不一定存在,因此需要特判一下。

综上,状态转移方程为:dp[i][j][k] = max(dp[i - 1][j][k], dp[i - 1][j - a] [k - b] + 1) ;

初始化: 每一维多开一个空间方便初始化,当第一维 i 为0。即没有字符串的时候,没有长度,因此初始化为 0 即可。

填表顺序: 保证第一维 i 从小到大即可。

返回值: 根据状态表示,返回 dp[len][m][n] ,其中 len 表示字符串数组的长度。

class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        // dp[i][j][k] 表示:从前 i 个字符串中挑选,字符 0 的个数不超过 j ,
        // 字符 1 的个数不超过 k ,所有的选法中,最大的长度
        int len = strs.size();
        vector<vector<vector<int>>> dp(len + 1, vector<vector<int>>(m + 1, vector<int>(n + 1, 0)));
        for(int i = 1; i <= len; ++i)
        {
            int a = 0, b = 0;
            for(auto& e : strs[i - 1]) // 统计0和1的个数
            {
                if(e == '0')
                    ++a;    
                else
                    ++b;
            }
            for(int j = 0; j <= m; ++j)
            {
                for(int k = 0; k <= n; ++k)
                {
                    if(j >= a && k >= b)
                        dp[i][j][k] = max(dp[i - 1][j][k], dp[i - 1][j - a][k - b] + 1) ;
                    else
                        dp[i][j][k] = dp[i - 1][j][k];
                }
            }
        }
        return dp[len][m][n];
    }
};

代码优化

        所有的背包问题,都可以进行空间上的优化。 对于二维费用的 01 背包问题,优化还是和之前的01背包类似,删掉第一维,然后修改其他维度的遍历顺序:

class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        // dp[i][j][k] 表示:从前 i 个字符串中挑选,字符 0 的个数不超过 j ,
        // 字符 1 的个数不超过 k ,所有的选法中,最大的长度
        int len = strs.size();
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
        for(int i = 1; i <= len; ++i)
        {
            int a = 0, b = 0;
            for(auto& e : strs[i - 1]) // 统计0和1的个数
            {
                if(e == '0')
                    ++a;    
                else
                    ++b;
            }
            for(int j = m; j >= 0; --j)
            {
                for(int k = n; k >= 0; --k)
                {
                    if(j >= a && k >= b)
                        dp[j][k] = max(dp[j][k], dp[j - a][k - b] + 1) ;
                    else
                        dp[j][k] = dp[j][k];
                }
            }
        }
        return dp[m][n];
    }
};


②力扣879. 盈利计划(二维费用背包)

879. 盈利计划

难度 困难

集团里有 n 名员工,他们可以完成各种各样的工作创造利润。

第 i 种工作会产生 profit[i] 的利润,它要求 group[i] 名成员共同参与。如果成员参与了其中一项工作,就不能参与另一项工作。

工作的任何至少产生 minProfit 利润的子集称为 盈利计划 。并且工作的成员总数最多为 n 。

有多少种计划可以选择?因为答案很大,所以 返回结果模 10^9 + 7 的值

示例 1:

输入:n = 5, minProfit = 3, group = [2,2], profit = [2,3]
输出:2
解释:至少产生 3 的利润,该集团可以完成工作 0 和工作 1 ,或仅完成工作 1 。
总的来说,有两种计划。

示例 2:

输入:n = 10, minProfit = 5, group = [2,3,5], profit = [6,7,8]
输出:7
解释:至少产生 5 的利润,只要完成其中一种工作就行,所以该集团可以完成任何工作。
有 7 种可能的计划:(0),(1),(2),(0,1),(0,2),(1,2),以及 (0,1,2) 。

提示:

  • 1 <= n <= 100
  • 0 <= minProfit <= 100
  • 1 <= group.length <= 100
  • 1 <= group[i] <= 100
  • profit.length == group.length
  • 0 <= profit[i] <= 100
class Solution {
public:
    int profitableSchemes(int n, int minProfit, vector<int>& group, vector<int>& profit) {
        
    }
};

解析代码

        题目结合例子多读几遍,就会发现是一个经典二维费用的背包问题。因此可以仿照二维费用的背包问题来定义状态表示。

状态表示:dp[i][j][k] 表示:从前 i 个计划中挑选,总人数不超过 j ,总利润至少为 k ,一共有多少种选法。

        这道题里面出现了一个至少,和之前做过的背包问题不一样。因此在分析状态转移程的时候要结合实际情况考虑一下。

状态转移方程:

        线性 dp 状态转移方程分析方式,一般都是根据最后一步的状况,来分情况讨论。结合题目的要求,有选择最后一个元素或者不选择最后一个元素两种策略:

  • 不选 i 位置的计划:只能去前 i - 1 个计划中挑选,总人数不超过 j ,总利润至少为 k 。此时一共有 dp[i - 1][j][k] 种选法
  • 选择 i 位置的计划:在前 i - 1 个计划中挑选的时候,限制就变成了,总人数不超过 j - g[i] ,总利润至少为 k - p[i] 。此时一共有 dp[i - 1][j - g[i]] [k - p[i]] 。

第二种情况下有两个细节需要注意:

  1. j - g[i] < 0 :此时说明 g[i] 过大,也就是人数过多。因为状态表示要求人数是不能超过 j 的,因此这个状态是不合法的,需要舍去。
  2. k - p[i] < 0 :此时说明 p[i] 过大,也就是利润太高。但是利润高正是我们想要的。所以这个状态不能舍去。但是问题来了,dp 表是没有负数的下标的,这就意味着这些状态无法表示。其实根本不需要负的下标,根据实际情况来看,如果这个任务的利润已经能够达标了,仅需在之前的任务中,挑选出来的利润至少为 0 就可以了。因为实际情况不允许是负利润,那么负利润就等价于利润至少为 0 的情况。所以说这种情况就等价于 dp[i][j][0] ,可以对 k - p[i] 的结果与 0 取一个 max 。

综上,状态转移方程为:dp[i][j][k] = dp[i - 1][j][k] + dp[i - 1][j - g[i - 1]][max(0, k - p[i - 1])] ;

初始化: 当没有任务的时候,利润为 0 ,此时无论人数限制为多少,都能找到一个空集的方案。 因此初始化 dp[0][j][0] 的位置为 1 ,其中 0 <= j <= n 

填表顺序: 保证第一维i 从小到大即可。

返回值: 根据状态表示,返回 dp[len][n][minProfit] ,其中 len 表示字符串数组的长度。

class Solution {
public:
    int profitableSchemes(int n, int minProfit, vector<int>& group, vector<int>& profit) {
        // dp[i][j][k] 表示:从前i个计划中挑选,总人数不超过j,总利润至少为k,一共有多少种选法
        const int MOD = 1e9 + 7; // 注意结果取模
        int len = group.size();
        vector<vector<vector<int>>> dp(len + 1, vector<vector<int>>(n + 1, vector<int>(minProfit + 1)));
        for(int j = 0; j <= n; j++)
        {
            dp[0][j][0] = 1; // 初始化
        }
        for(int i = 1; i <= len; ++i)
        {
            for(int j = 0; j <= n; ++j)
            {
                for(int k = 0; k <= minProfit; ++k)
                {
                    dp[i][j][k] = dp[i - 1][j][k];
                    if(j >= group[i - 1])
                        dp[i][j][k] += dp[i - 1][j - group[i - 1]][max(0, k - profit[i - 1])];
                    dp[i][j][k] %= MOD; // 注意结果取模
                }
            }
        }
        return dp[len][n][minProfit];
    }
};

代码优化

        所有的背包问题,都可以进行空间上的优化。 对于二维费用的 01 背包问题,优化还是和之前的01背包类似,删掉第一维,然后修改其他维度的遍历顺序:

class Solution {
public:
    int profitableSchemes(int n, int minProfit, vector<int>& group, vector<int>& profit) {
        // dp[i][j][k] 表示:从前i个计划中挑选,总人数不超过j,总利润至少为k,一共有多少种选法
        const int MOD = 1e9 + 7; // 注意结果取模
        int len = group.size();
        vector<vector<int>> dp(n + 1, vector<int>(minProfit + 1));
        for(int j = 0; j <= n; j++)
        {
            dp[j][0] = 1; // 初始化
        }
        for(int i = 1; i <= len; ++i)
        {
            for(int j = n; j >= group[i - 1]; --j)
            {
                for(int k = minProfit; k >= 0; --k)
                {
                    dp[j][k] += dp[j - group[i - 1]][max(0, k - profit[i - 1])];
                    dp[j][k] %= MOD; // 注意结果取模
                }
            }
        }
        return dp[n][minProfit];
    }
};


力扣377. 组合总和 Ⅳ(似包非包)

377. 组合总和 Ⅳ

难度 中等

给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。

题目数据保证答案符合 32 位整数范围。

示例 1:

输入:nums = [1,2,3], target = 4
输出:7
解释:
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。

示例 2:

输入:nums = [9], target = 3
输出:0

提示:

  • 1 <= nums.length <= 200
  • 1 <= nums[i] <= 1000
  • nums 中的所有元素 互不相同
  • 1 <= target <= 1000

进阶:如果给定的数组中含有负数会发生什么?问题会产生何种变化?如果允许负数出现,需要向题目中添加哪些限制条件?

class Solution {
public:
    int combinationSum4(vector<int>& nums, int target) {

    }
};

解析代码

        需要知道的是背包问题本质上求的是组合数问题,而这一道题虽然题目是组合总数,但求的是排列数问题(排列是无序的,组合是有序的),题目感觉不太严谨。(虽然不能用背包问题解决,但代码类似背包问题,所以称为似包非包)用常规的 dp 思想来解决这道题。

        这道题的状态表示就是根据拆分出相同子问题的方式,抽象出来一个状态表示: 当在求 target 这个数一共有几种排列方式的时候,对于最后一个位置,如果拿出数组中的一个数 x ,接下来就是去找 target - x 一共有多少种排列方式。 因此可以抽象出来一个状态表示:

dp[i] 表示:总和为 i 的时候,一共有多少种排列方案

状态转移方程:

        线性 dp 状态转移方程分析方式,一般都是根据最后一步的状况,可以选择数组中的任意一个数 nums[j] ,其中 0 <= j <= n - 1 。

        当 nums[j] <= target 的时候,此时的排列数等于先找到 target - nums[j] 的方案数,然后在每t一个方案后面加上一个数字 nums[j] 即可。 因为有很多个 j 符合情况,因此状态转移方程为: dp[i] += dp[target - nums[j]] ,其中 0 <= j <= n - 1 。

初始化: 和为 0 的时候,可以什么都不选,空集一种方案,因此 dp[0] = 1 。

填表顺序: 根据状态转移方程易得从左往右。

返回值: 根据状态表示,返回 dp[target]。

class Solution {
public:
    int combinationSum4(vector<int>& nums, int target) {
        // dp[i] 表示:总和为 i 的时候,一共有多少种排列方案
        vector<double> dp(target + 1, 0); // double防溢出
        dp[0] = 1;
        int sz = nums.size();
        for(int i = 1; i <= target; ++i)
        {
            for(int j = 0; j < sz; ++j)
            {
                if(i >= nums[j])
                    dp[i] += dp[i - nums[j]];
            }
        }
        return dp[target];
    }
};


④力扣96. 不同的二叉搜索树(卡特兰数)

96. 不同的二叉搜索树

难度 中等

给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。

示例 1:

输入:n = 3
输出:5

示例 2:

输入:n = 1
输出:1

提示:

  • 1 <= n <= 19
class Solution {
public:
    int numTrees(int n) {

    }
};

解析代码

        这道题属于卡特兰数的一个应用,同样能解决的问题还有合法的进出栈序列、括号匹配的括号序列、电影购票等等。如果感兴趣的可以继续百度搜索卡特兰数,会有很多详细的介绍。

        这道题的状态表示就是根据拆分出相同子问题的方式,抽象出来一个状态表示: 当在求个数为 n 的 BST(二叉搜索树) 的个数的时候,当确定一个根节点之后,左右子树的结点个数也确定了。此时左右子树就会变成相同的子问题,因此可以这样定义状态表示: dp[i] 表示:当结点的数量为 i 个的时候,一共有多少颗 BST 。 难的是如何推导状态转移方程,因为它跟之前常见的状态转移方程不是很像。

状态转移方程:

        对于 dp[i] ,此时已经有 i 个结点了,为了方便叙述,将这 i 个结点排好序,并且编上 1、2、3、4、5.....i 的编号。那么对于所有不同的 BST ,可以按照下面的划分规则,分成不同的 i 类:按照不同的头结点来分类。分类结果就是:

  1. 头结点为 1 号结点的所有 BST
  2. 头结点为 2 号结点的所有 BST
  3. ......

如果能求出每一类中的 BST 的数量,将所有类的 BST 数量累加在一起,就是最后结果。

        接下来选择头结点为 j 号的结点,来分析这 i 类 BST 的通用求法。如果选择 j 号结点来作为头结点,根据 BST 的定义:

  • j 号结点的左子树的结点编号应该在 [1, j - 1] 之间,一共有 j - 1 个结点。那么 j 号结点作为头结点的话,它的左子树的种类就有 dp[j - 1] 种(回顾此 dp 数组的定义)。
  • j 号结点的右子树的结点编号应该在 [j + 1, i] 之间,一共有 i - j 个结点。那么 j 号结点作为头结点的话,它的右子树的种类就有 dp[i - j] 种。

        根据排列组合的原理可得: j 号结点作为头结点的 BST 的种类一共有 dp[j - 1] *dp[i - j] 种。因此只要把不同头结点的 BST 数量累加在⼀起,就能得到 dp[i] 的值: dp[i] += dp[j - 1] * dp[i - j] ( 1 <= j <= i) (这个公式就是卡特兰数的递推公式)。注意用的是 += ,并且 j 从 1 变化到 i 。

初始化: 注意到每一个状态转移里面的 j - 1 和 i - j 都是小于 i 的,并且可能会用到前一个的状态(当 i = 1,j = 1 的时候,要用到 dp[0] 的数据)。因此要先把第一个元素初始化。 当 i = 0 的时候,表示一颗空树,空树也是一颗二叉搜索树,因此 dp[0] = 1 。

填表顺序: 根据状态转移方程易得从左往右。

返回值: 根据状态表示,返回 dp[n]。

class Solution {
public:
    int numTrees(int n) {
        vector<int> dp(n + 1, 0);
        // dp[i] 表示:当结点的数量为 i 个的时候,一共有多少颗 BST 
        dp[0] = 1; // 空树也是二叉搜索树
        for(int i = 1; i <= n; ++i) // 枚举结点个数
        {
            for(int j = 1; j <= i; ++j) // 枚举每⼀个根节点
            {
                dp[i] += dp[j - 1] * dp[i - j];
            }
        }
        return dp[n];
    }
};


本篇完。

下一篇:BFS解决拓扑排序类型的OJ。

下下篇是递归中偏难的递归搜索与回溯类型的OJ。

  • 42
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
好的,我了解了你的问题。下面是使用分支限界法解决01背包问题的Java代码示例: ```java import java.util.*; public class KnapsackProblem { public static void main(String[] args) { int[] weight = {2, 2, 4, 6, 3}; // 物品重量 int[] value = {3, 4, 8, 9, 6}; // 物品价值 int capacity = 9; // 背包容量 int maxValue = knapsack(weight, value, capacity); System.out.println("背包能装的最大价值为:" + maxValue); } /** * 分支限界法解决01背包问题 * * @param weight 物品重量数组 * @param value 物品价值数组 * @param capacity 背包容量 * @return 背包能装的最大价值 */ public static int knapsack(int[] weight, int[] value, int capacity) { int n = weight.length; PriorityQueue<Node> queue = new PriorityQueue<Node>(n, new Comparator<Node>() { @Override public int compare(Node o1, Node o2) { return o2.upperBound - o1.upperBound; } }); // 优先队列,按照上界排序 Node u = new Node(); Node v = new Node(); int[] x = new int[n]; // 当前解 int maxValue = 0; // 背包能装的最大价值 u.level = -1; u.profit = 0; u.weight = 0; while (true) { if (u.level == -1) { // 第一步,初始化u节点 v.level = 0; } else if (u.level == n - 1) { // 最后一步,已经遍历完了所有物品 break; } else { // 中间步骤 v.level = u.level + 1; v.weight = u.weight + weight[v.level]; v.profit = u.profit + value[v.level]; } // 计算v节点的上界 v.upperBound = bound(v, weight, value, capacity, n); // 如果v节点的上界比当前最大价值更大,则将v节点加入优先队列 if (v.upperBound > maxValue) { queue.offer(v); } // 取出优先队列中上界最大的节点作为下一个节点 u = queue.poll(); if (u == null) { break; } } // 计算最大价值 for (int i = 0; i < n; i++) { if (x[i] == 1) { maxValue += value[i]; } } return maxValue; } /** * 计算节点的上界 * * @param node 节点 * @param weight 物品重量数组 * @param value 物品价值数组 * @param capacity 背包容量 * @param n 物品数量 * @return 节点的上界 */ public static int bound(Node node, int[] weight, int[] value, int capacity, int n) { int j, k; int totWeight; int bound; if (node.weight >= capacity) { // 物品重量大于等于背包容量,无法放入 return 0; } else { bound = node.profit; j = node.level + 1; totWeight = node.weight; while ((j < n) && (totWeight + weight[j] <= capacity)) { // 贪心选择物品 totWeight += weight[j]; bound += value[j]; j++; } if (j < n) { // 不能装满背包,计算上界 bound += (capacity - totWeight) * value[j] / weight[j]; } return bound; } } /** * 节点类 */ public static class Node { int level; // 节点所在层数 int weight; // 节点对应的物品总重量 int profit; // 节点对应的物品总价值 int upperBound; // 上界 public Node() { } public Node(int level, int weight, int profit, int upperBound) { this.level = level; this.weight = weight; this.profit = profit; this.upperBound = upperBound; } } } ``` 希望这段代码能够帮助到你,如果还有任何问题,请随时提出。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GR鲸鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值