[dp22_二维背包] 一和零 | 盈利计划
目录
1.一和零
题解
2.盈利计划
题解
1.一和零
链接: 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 。
- 找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1
- 也就说找到的子集 字符 0 的个数 小于等于 m,字符 1 的个数 小于等于 n。
如果对背包问题敏感,这就是一个背包问题。
- 背包问题问的是给一堆物品,让在这堆物品中选一些物品,在满足某个限定条件下,问一些东西,要么问最大价值,要么问多少种选法。
- 这道题其实就是背包问题,就是在字符串数组中挑一些字符串出来,需要满足的是两个条件
- 字符 0 的个数小于等于5,字符 1 的个数小于等于3。
- 像这种满足两个条件的背包问题,我们称之为二维费用背包问题。
二维费用背包问题:
之前我们遇到的01背包问题,完全背包问题都只有一个限定条件,二维费用背包就是多了一个限制变成了两个限制条件。
- 二维费用背包问题也分为二维费用的01背包问题和二维费用的完全背包问题。
- 这这道题因为每个字符串都面临选or不选两种情况,所以是二维费用的01背包问题。
题解
之前说过所有其余分类的背包问题都是从01背包问题哪里延申出来的。
- 所以我们尝试用01背包问题分析思路来解决我们这里的问题。
1.状态表示
- 01背包状态表示:
- dp[i][j] 表示:从前 i 个物品中挑选,总体积不超过 j,所有选法中,最大的价值
- 二维费用的01背包问题:其实就只是多加了一个限制条件,之前就只有体积限制,现在不仅有体积还有重量。所以多加一维。
- dp[i][j][k] 表示:从前 i 个字符串中挑选,字符 0 的个数不超过 j,字符 1 的个数不超过 k,所有选法中,最长的长度。
2.状态转移方程
- 本质还是一个01背包问题,所以我们还可以借用之前01背包状态转移方程的思路,根据最后一个位置,划分情况
- 设最后一个位置的字符串中,字符 0 的个数为 a,字符 1 的个数为 b。
- 不选 i 位置这个字符串,那就从 0 ~ i - 1 区间内字符串中挑,因为 i 位置都没选,所以0 ~ i - 1 区间内字符串中选 字符 0 的个数不超过 j,字符 1 的个数不超过k,在这两个限制条件下要的最大的长度。就是dp[i-1][j][k]
选 i 位置这个字符串,整体要保证字符 0 的个数不超过 j,字符 1 的个数不超过k,又因为选了 i 位置这个字符串,接下去取 0 ~ i - 1挑的时候,就要去掉 i 这个字符串中 字符串 0 的个数 和 字符串 1 的个数。然后加上选 i 位置字符串的个数。
- 注意 j - a,k - b 不一定存在,所以用之前要判断一下。
- 我们要的是最大长度,因此是两种情况的最大值
- 如果01背包问题都熟悉了,二维费用的01背包问题就是多加一个限制条件其他都一样。
3.初始化
- 初始化和之前一样,仅需考虑 i = 0 的情况就可以了,k = 0,j = 0 我们会特判不会越界的。
- 当 i = 0 表示字符串数组里没有字符串,如果数组没有字符串,想凑成字符串 0 的个数不超过 j,字符串 1 的个数不超过 k ,根本不能做到,不能做到就是一个空集
- 如果是空集,那最大个数全都是0
4.填表顺序
- 填dp[i][j][k]状态,要保证填 i 这一面 , i - 1 那一面的值已经填好了
- 因此仅需保证 i 从小到大。j、k无论怎么变化什么顺序都可以。
5.返回值
- dp[i][j][k] 表示:从前 i 个字符串中挑选,字符 0 的个数不超过 j,字符 1 的个数不超过 k,所有选法中,最长的长度。
- 我们要的是从整个字符串中选,字符 0 的个数不超过 j,字符 1 的个数不超过 k,所有选法中,最长的长度。所以返回的是
dp[len][m][n],len是字符串数组长度。
class Solution {
public:int findMaxForm(vector<string>& strs, int m, int n) {int len=strs.size();vector<vector<vector<int>>> dp(len+1,vector<vector<int>>(m+1,vector<int>(n+1,0)));//0 个 元素 初始化为0for(int i=1;i<=len;i++){for(int j=0;j<=m;j++){for(int k=0;k<=n;k++){int a=0,b=0;//下标映射for(char& c:strs[i-1]){if(c=='0')a++;if(c=='1')b++;}dp[i][j][k]=dp[i-1][j][k];if(j>=a && k>=b)dp[i][j][k]=max(dp[i][j][k],dp[i-1][j-a][k-b]+1);}}}return dp[len][m][n];}
};
- 子集 要是 连续的,所以可以这么写
优化
- 只看 上一行一列
- 01 背包,避免覆盖,要从后往前
class Solution {
public:int findMaxForm(vector<string>& strs, int m, int n) {int len=strs.size();vector<vector<int>> dp(m+1,vector<int>(n+1,0));//0 个 元素 初始化为0for(int i=1;i<=len;i++){for(int j=m;j>=0;j--){for(int k=n;k>=0;k--){int a=0,b=0;//下标映射for(char& c:strs[i-1]){if(c=='0')a++;if(c=='1')b++;}if(j>=a && k>=b) dp[j][k]=max(dp[j][k],dp[j-a][k-b]+1);}}}return dp[m][n];}
};
2.盈利计划
链接: 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) 。
这道题读起来很难读,但是结合示例我们能明白这道题的意思:
- 集团有n名员工,还有一些计划,每种计划都有对应的利润以及人数。
- 问从这些计划种挑选一些计划,要求人数不超过n,利润至少为minProfit
- 一共有多少种计划可以选择。
当明白这道题的意思之后,我们就应该想到这是一道背包问题。
- 背包问题就是给我们一些东西,让我们在这些东西里挑一些东西出来,在满足某个限定条件下,问一些东西。这道题问的是有多少计划可以选择。
- 这道题有两个限定条件,因此是一个二维费用的背包问题,又因为每种工作只能面临选or不选两个情况,所以这是一个二维费用的01背包问题。
题解
1.状态表示
- dp[i][j][k] 表示:从前 i 个计划中挑选,总人数不超过 j,总利润至少为 k,一共有多少种选法。
- 不超过j -> 小于等于j,至少为k -> 大于等于k
- 等会状态转移方程你会发现不超过和至少状态转移方程是不一样的。
2.状态转移方程
- 还是根据最后一个位置,划分情况
- 不选 i ,相当于去 0 ~ i - 1 去看看总人数不超过 j,利润不超过 k,在这两种情况下,一共有多少种情况
选 i,要求总人数不超过 j,如果选 i 位置的计划,i 位置里面有个人数就是group[i]里面存着,那去 0 ~ i - 1里面挑的时候总人数是不能超过 j - group[i]的,
- 先别着急分析下一个,这里思考一下 j - g[i] 能小于0吗?不能!计划就已经超过总人数了,那前面在怎么挑也不行。
- 在分析第二个限定条件,总利润至少为k,选 i 位置的时候,i 有一个利润在profit[i]里存着,也就是说在 0 ~ i -1 里面去选总利润至少为 k - p[i]就够了,看似和上面一样,但是k - p[i] 能小于 0 吗?
- 但是要注意,刚才的状态表示,一个是不超过 j,一个是至少为 k。其实 k - p[i] 能小于 0!也就相当于 k < p[i] ,也就是说在 0 ~ i 里挑总利润大于等于K,利润多了就多了呗
但是问题来了dp是一个数组,数组下标能小于0吗?不能!也就是说 k - p[i] 是可以存在的,但是在数组中找不到。
- 所以我们要根据实际情况想一个折中的想法来表示这种情况。
- i 位置利润就已经超过k了,那说明前面 0 ~ i -1 里面的利润挑多少都可以,所以我们可以认为前面利润只要大于等于0就可以了。
问一共多少选法,所以这两种情况加起来
3.初始化
这里不在画图了,可以根据实际情况来初始化。
- 这里我们仅需出来当没有任务的时候,dp[i][j][k] 应该等于多少。
- 如果没有任务,那也没有利润,此时不管有多少人,只要不选就好了
- 也就是选出来一个空集也就是一种选法,因此dp[0][j][0] = 1 (0 <= j <= n)
4.填表顺序
- 填 i 需要 i - 1,所以仅需要保证 i 从小到大
5.返回值
- dp[i][j][k] 表示:从前 i 个计划中挑选,总人数不超过 j,总利润至少为 k,一共有多少种选法。
- 我们要的是从所有计划中挑,总人数不超过 n,总利润至少为 m,一共有多少种选法。
- 因此返回 dp[len][n][m], len是数组的长度
class Solution {typedef long long ll;
public:int profitableSchemes(int n, int m, vector<int>& group, vector<int>& profit) {int w=group.size();vector<vector<vector<ll>>> dp(w+1,vector<vector<ll>>(n+1,vector<ll>(m+1)));//针对 dp[0][j][0]=1 //没工作 有人 没利润 //不要人 也是一种选法for(int j=0;j<=n;j++)dp[0][j][0]=1;for(int i=1;i<=w;i++){//xia biaofor(int j=0;j<=n;j++){for(int k=0;k<=m;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]%=(ll)(1e9+7);}}}return dp[w][n][m]; }
};
优化
- 01 背包,倒着优化
class Solution {typedef long long ll;
public:int profitableSchemes(int n, int m, vector<int>& group, vector<int>& profit) {int w=group.size();vector<vector<ll>> dp(n+1,vector<ll>(m+1));//针对 dp[0][j][0]=1 //没工作 有人 没利润for(int j=0;j<=n;j++)dp[j][0]=1;for(int i=1;i<=w;i++){//xia biaofor(int j=n;j>=group[i-1];j--){for(int k=m;k>=0;k--){dp[j][k]+=dp[j-group[i-1]][max(0,k-profit[i-1])];dp[j][k]%=(ll)(1e9+7);}}}return dp[n][m]; }
};