1. 问题背景与题目解析
LeetCode 474题"一和零"是一个经典的动态规划问题,属于中等难度。题目描述如下:
给定一个字符串数组strs和两个整数m和n,你需要找到并返回strs的最大子集的大小,该子集中最多有m个0和n个1。每个字符串中的字符只能是'0'或'1'。
这个问题的核心在于如何在有限的0和1资源下,选择最多的字符串组合。它实际上是经典的"0-1背包问题"的变种,只不过这里的"背包容量"有两个维度:0的数量和1的数量。
2. 解题思路分析
2.1 问题转化
首先我们需要将这个问题转化为更易理解的模型。每个字符串可以看作是一个物品,它有两个属性:
- 0的个数(重量1)
- 1的个数(重量2)
我们的目标是选择尽可能多的"物品",使得:
- 所有选中物品的0的总数 ≤ m
- 所有选中物品的1的总数 ≤ n
这明显是一个二维费用的背包问题。
2.2 动态规划定义
我们定义一个三维动态规划数组dp,其中:
- dp[i][j][k]表示在前i个字符串中,使用最多j个0和k个1能够组成的最大子集的大小
状态转移方程为:
- 如果不选第i个字符串:dp[i][j][k] = dp[i-1][j][k]
- 如果选第i个字符串:dp[i][j][k] = dp[i-1][j-zero][k-one] + 1 (其中zero和one分别是第i个字符串中0和1的数量)
最终结果是dp[strs.length][m][n]
2.3 空间优化
由于每次状态转移只依赖于前一行,我们可以将三维数组优化为二维数组,节省空间:
- dp[j][k]表示使用最多j个0和k个1能够组成的最大子集的大小
此时需要从后向前更新dp数组,避免覆盖还未使用的数据。
3. 详细代码实现
3.1 预处理函数
首先我们需要一个辅助函数来计算每个字符串中0和1的数量:
def count_zero_and_one(s): zero = s.count('0') one = len(s) - zero return zero, one3.2 动态规划解法
完整实现代码如下:
def findMaxForm(strs, m, n): dp = [[0] * (n + 1) for _ in range(m + 1)] for s in strs: zero, one = count_zero_and_one(s) # 从后向前遍历,避免重复计算 for i in range(m, zero - 1, -1): for j in range(n, one - 1, -1): dp[i][j] = max(dp[i][j], dp[i - zero][j - one] + 1) return dp[m][n]3.3 复杂度分析
- 时间复杂度:O(l * m * n),其中l是字符串数组的长度
- 空间复杂度:O(m * n),使用二维数组存储状态
4. 实际应用与变种
4.1 实际应用场景
这个问题虽然看起来抽象,但实际上有很多现实应用:
- 资源分配问题:在有限的两种资源下选择最优的项目组合
- 广告投放优化:在预算和展示次数限制下选择最优广告组合
- 投资组合选择:在风险和收益的双重限制下选择投资组合
4.2 问题变种
这个问题可以有多种变体:
- 每个字符串有不同的价值,求最大价值而非最大数量
- 增加第三种限制条件(如字符串长度)
- 需要输出具体的字符串组合而不仅仅是数量
5. 常见错误与调试技巧
5.1 常见错误
- 初始化错误:dp数组大小应该是(m+1)×(n+1),不是m×n
- 遍历顺序错误:必须从后向前更新dp数组,否则会重复计算
- 边界条件处理:当m或n为0时的特殊情况需要处理
5.2 调试技巧
- 打印中间状态:在循环中打印dp数组,观察状态变化
- 小规模测试:先用小的测试用例验证代码正确性
- 对比暴力解:对于小规模数据,可以对比暴力解的结果
6. 性能优化建议
- 预处理所有字符串的0和1计数,避免重复计算
- 根据实际情况,可以先对字符串进行排序(如按长度),可能有助于提前剪枝
- 对于特别大的m和n,可以考虑记忆化搜索而非动态规划
7. 扩展思考
这个问题展示了动态规划的灵活性。传统的背包问题只有一个容量限制,而这个问题有两个,体现了动态规划处理多维约束的能力。理解这个问题的解法有助于解决更复杂的资源分配问题。
在实际面试中,面试官可能会追问:
- 如何修改代码以记录选择的字符串?
- 如果字符串可以重复选择,如何修改算法?
- 如何优化算法以处理极大m和n的情况?
掌握这类问题的核心在于理解状态定义和转移方程的本质,而不是死记硬背模板。建议多练习类似的二维费用背包问题,加深理解。