题目描述
https://leetcode-cn.com/problems/ones-and-zeroeshttps://leetcode-cn.com/problems/ones-and-zeroes给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。
如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
难度 中等 来源:力扣(LeetCode)
示例 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 。
方法名称
public int findMaxForm(String[] strs, int m, int n) {
//在此写出你的代码
}
理解:
这个题中,题目解释得到子集,说明每一个字符串只能取一次。根据条件最多有m个0和n个1。而每一个字符串只包含‘0’和‘1’,说明对于每一个字符串中的1和零,要么取,要么不取。对于取还是不取,这就是动态规划要解决的问题。动态规划的思路是:物品一个一个尝试,容量一点一点尝试,每个物品分类讨论的标准是:选与不选。因为条件限制了0和1的数量,每一个字符串的0和1要么取,要么不取,是01背包的问题。
本题为了理解,不使用优化。下面介绍没有经过优化的01背包模板。
01背包模板
状态转移方程定义
dp[i][j],表示从0-i个物品中选择一个物品,把它放在承重为j的背包中的最大价值
状态转移方程实现
假设需要用一个背包去背不同重量的砖块,不同重量的砖块挣的钱也不一样。背包中已经放了一些砖块。现在要新放入一个砖块:
(1)如果放入的砖块重量加上原来的重量大于了背包的承受重量,背包就会被弄破,所以不能放入这个砖块。背包里面的砖块还是那些砖块。
(2)如果放入的砖块重量加上原来的重量小于等于了背包的承受重量,这个时候,如果放入这个砖块,那么背包里面多了一个砖块,但是背包里还能放进去的砖块的重量就少了。同时,放进去了一个砖块,意味了能挣到这个砖块重量的钱了;如果嫌弃钱太少,就不放入这个砖块,那么背包中的砖块还是那些,重量还是那么重。
(1)不能取,背包的状态不更新
dp[i][j] = dp[i - 1][j];背包中还是那些物品,背包还能承受的重量为j,价值没有变。
(2)能取
1)产生的价值太少,不想取,背包的状态不更新
dp[i][j] = dp[i - 1][j];背包中还是那些物品,背包还能承受的重量为j,价值没有变。
2)取,背包的状态更新
dp[i][j] = value[i] + dp[i - 1][j - weight[i]];新加入的物品产生了价值(间接相当于加入了这个物品),背包中还有原来的物品。新加入的物品减少了背包还能承受的重量。价值是新加入的物品的价值加旧的包的价值。
这种情况中需要选择一个产生最大价值的情况。
状态转移方程初始化
dp[i][0] = 0;当背包不能承受重量的时候,什么物品都不能加,没有价值。
本题步骤
套入模板,要格外注重状态方程的定义,它是对于整个题目的理解。虽然有模板,但是不熟练的时候,仍然是套不上的,所以要多多的练习,同时,具体问题具体分析,没必要严格套模板。这个模板是没有优化的。
状态转移方程的定义
题目对于0和1都有限制,那么0和1都是我们的背包承受重量。这种情况也不需要害怕,在二维数组的基础上再加一个维度就可以了。定义的状态转移方程如下:
dp[k][i][j]:从0-k个字符串中选择一个字符串,把它放在i(限制0的容量)和j(限制1的容量)容量下的最大子集数量。
这样定义之后,我们肯定是要知道每一个字符串中1和0的数量。所以要先进行一个预备工作,创建一个二维数组,计算每一个字符串中0的数量,放在0下标,1的数量放在1下标。
int len = strs.length;
//获得1和0的二维数组
int[][] cnt = new int[len][2];
for (int i = 0; i < len; i++) {
int zero = 0;
int one = 0;
for (char ch : strs[i].toCharArray()) {
if (ch == '0') {
++zero;
} else {
++one;
}
}
cnt[i] = new int[] {zero, one};
}
状态转移方程实现
dp[k][i][j]
(1)当取不了这个字符串中的1和0,也就是说,这个字符串中的1和0数量大于了题目限制的1和0数量或者总的1和0数量大于了题目的限制,这个时候,不更新包的状态。
if (i < zero || j < one), 有dp[k][i][j] = dp[k - 1][i][j];
(2)能取,又分成两种情况
1)要取这个字符串,得到它的1和0的数量后,背包中还能放1和0要减少,同时,多了一个子集(注意对这个三维数组的定义)。
dp[k][i][j] = dp[k - 1][i - zero][j - one] + 1;
2)不取这个字符串,那么就不需要更新包的状态。
dp[k][i][j] = dp[k - 1][i][j];
(2)中取一个最大的值。
状态转移方程初始化
dp[k][0][0] = 0;当1被限制在0数量,0被限制在0数量的时候,啥都放不了,自然是0。不过Java在创建数组的时候,默认是0,所以可以不用写。
特殊的,当这个字符串数组是空的或它的长度是0,就直接返回0,不用走下面的步骤了。
public int findMaxForm(String[] strs, int m, int n) {
if (strs == null || strs.length == 0) {
return 0;
}
int len = strs.length;
int[][] cnt = new int[len][2];
for (int i = 0; i < len; i++) {
int zero = 0;
int one = 0;
for (char ch : strs[i].toCharArray()) {
if (ch == '0') {
++zero;
} else {
++one;
}
}
cnt[i] = new int[] {zero, one};
}
int[][][] dp = new int[len + 1][m + 1][n + 1];
for (int k = 1; k <= len; k++) {
int zero = cnt[k - 1][0];
int one = cnt[k - 1][1];
for (int i = 0; i <= m; i++) {
for (int j = 0; j <= n; j++) {
if (i < zero || j < one) {
dp[k][i][j] = dp[k - 1][i][j];
} else {
dp[k][i][j] = Math.max(dp[k - 1][i][j], dp[k - 1][i - zero][j - one] + 1);
}
}
}
return dp[len][m][n];
}
可以看出,这样写出来的速度比较慢,但是胜在好理解。