前言
动态规划专题,从简到难通关动态规划。
每日一题
今天的题目是 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 <= 6001 <= strs[i].length <= 100strs[i]仅由'0'和'1'组成1 <= m, n <= 100
题解
01背包
本题可以使用 0-1 背包的思想进行求解,将每一个字符串看作是一个物品,0 和 1 的数量就是物品的价值,需要在 m 个 0 和 n 个 1 的空间限制下获取最多的价值。我们用动态规划五部曲来解决一下这道题。
- 定义 dp 数组以及下标的含义
本题中,由于需要限制 0 和 1 的个数,因此可以将 dp 数组定义为一个二维数组,即 dp[i][j] 表示最多使用 i 个 0 和 j 个 1 最多可以表示多少个字符串。 例如,dp[1][1] 表示最多使用 1 个 0 和 1 个 1 能够表示多少个字符串。
- 确定递推公式
其次,我们需要确定递推公式。由于是一个 0-1 背包问题,因此我们使用动态规划来处理。对于 dp[i][j],表示使用前 i 个字符串能够组成的最大字符串集合,则根据题意,有如下递推公式:
dp[i][j] = Math.max(dp[i][j], dp[i - zeroCount][j - oneCount] + 1)
其中 zeroCount 是当前字符串中 0 的个数,oneCount 是当前字符串中 1 的个数。即,我们要求解在使用前 i 个字符串时,限制 0 和 1 的数量下能够得到的最多字符串集合数。对于每一个字符串,如果它的 0 和 1 的数量都不超过限制 m 和 n,那么我们可以选择该字符串,且该选择带来的最大收益为 1,因此最终的结果就是能够获得的最大收益数。
- 如何初始化 dp 数组
由于根据递推公式,dp[0][0] 需要赋值为 0,因此我们可以初始化为一个全为 0 的二维数组。
- 如何确定遍历顺序
在对每一个字符串进行背包更新时,需要注意要倒序遍历(从大到小遍历),这是由于前面的更新值会被后面的值更新,因此要从大到小遍历以便于进行正确的更新。
- 举例推导 dp 数组
| dp | 0 | 1 | 2 | 3 |
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 |
| 1 | 0 | 1 | 1 | 1 |
| 2 | 0 | 1 | 2 | 2 |
| 3 | 0 | 1 | 2 | 3 |
根据上面的递推公式,我们可以写出函数:
function findMaxForm(strs: string[], m: number, n: number): number {
const dp = new Array(m + 1).fill(0).map(() => new Array(n + 1).fill(0));
for (const str of strs) {
let zeroCount = 0, oneCount = 0;
for (const c of str) {
if (c === '0') {
zeroCount++;
} else {
oneCount++;
}
}
for (let i = m; i >= zeroCount; i--) {
for (let j = n; j >= oneCount; j--) {
dp[i][j] = Math.max(dp[i][j], dp[i - zeroCount][j - oneCount] + 1);
}
}
}
return dp[m][n];
};