1. 题目解析与核心思路
这道LeetCode 995题要求我们找到使二进制数组中所有元素都变为1的最小翻转次数。每次翻转必须选择长度为k的子数组进行整体取反操作。我们先来看题目给出的函数签名:
public int minKBitFlips(int[] nums, int k)1.1 问题重述
给定一个二进制数组nums和一个整数k,我们需要确定最少需要进行多少次翻转操作才能使数组中所有元素都变为1。每次翻转操作必须选择数组中连续的k个元素,并将这些元素的值取反(0变1,1变0)。
1.2 示例分析
假设输入:
- nums = [0,1,0]
- k = 1
解决方案:
- 翻转第一个元素:[1,1,0]
- 翻转第三个元素:[1,1,1] 总共需要2次翻转。
1.3 暴力解法思路
最直观的解法是遍历数组,遇到0时就翻转从该位置开始的k个元素:
public int minKBitFlips(int[] nums, int k) { int count = 0; for (int i = 0; i < nums.length; i++) { if (nums[i] == 0) { if (i + k > nums.length) return -1; for (int j = i; j < i + k; j++) { nums[j] ^= 1; // 取反操作 } count++; } } return count; }这种方法的时间复杂度是O(n*k),在n和k较大时效率很低,无法通过LeetCode的所有测试用例。
2. 优化思路与差分数组
2.1 翻转标记的优化
观察到每次翻转操作实际上会影响后续元素的判断,我们可以使用一个标记数组来记录翻转操作的影响,而不是每次都实际修改数组元素。
public int minKBitFlips(int[] nums, int k) { int n = nums.length; int[] diff = new int[n + 1]; int res = 0, revCnt = 0; for (int i = 0; i < n; i++) { revCnt += diff[i]; if ((nums[i] + revCnt) % 2 == 0) { if (i + k > n) return -1; res++; revCnt++; diff[i]++; diff[i + k]--; } } return res; }2.2 差分数组原理
差分数组的核心思想是:
- 使用
revCnt记录当前位置经历的翻转次数 diff数组标记翻转操作的开始和结束位置- 当
(nums[i] + revCnt) % 2 == 0时,说明当前元素需要翻转
这种方法将时间复杂度优化到O(n),空间复杂度O(n)。
3. 最优解:滑动窗口法
3.1 滑动窗口实现
我们可以进一步优化空间复杂度,使用一个变量代替差分数组:
public int minKBitFlips(int[] nums, int k) { int n = nums.length; int res = 0, revCnt = 0; for (int i = 0; i < n; i++) { if (i >= k && nums[i - k] > 1) { revCnt ^= 1; nums[i - k] -= 2; // 恢复原值 } if ((nums[i] ^ revCnt) == 0) { if (i + k > n) return -1; res++; revCnt ^= 1; nums[i] += 2; } } return res; }3.2 算法分析
这种方法的关键点:
- 利用原数组存储翻转标记(通过+2操作)
- 使用
revCnt记录当前窗口的翻转状态 - 当窗口滑动时,检查是否需要减少翻转计数
时间复杂度O(n),空间复杂度O(1)。
4. 边界条件与异常处理
4.1 无法完成的情况
当剩余元素不足k个且仍有0存在时,返回-1:
if (i + k > n) return -1;4.2 特殊测试用例
考虑以下特殊情况:
- 全1数组:直接返回0
- k=1:相当于逐个翻转0元素
- k=n:只能整体翻转一次或零次
5. 完整代码实现
class Solution { public int minKBitFlips(int[] nums, int k) { int n = nums.length; int res = 0, revCnt = 0; for (int i = 0; i < n; i++) { if (i >= k && nums[i - k] > 1) { revCnt ^= 1; nums[i - k] -= 2; } if ((nums[i] ^ revCnt) == 0) { if (i + k > n) return -1; res++; revCnt ^= 1; nums[i] += 2; } } return res; } }6. 复杂度分析与对比
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 暴力法 | O(n*k) | O(1) | 小规模数据 |
| 差分数组 | O(n) | O(n) | 通用解法 |
| 滑动窗口 | O(n) | O(1) | 最优解 |
7. 实际应用与变种
7.1 实际应用场景
这种翻转操作的思想可以应用于:
- 数据纠错编码
- 硬件电路设计
- 网络协议中的错误恢复
7.2 题目变种
可以考虑以下变种问题:
- 允许任意长度的翻转操作
- 翻转操作有成本,求最小总成本
- 目标状态不是全1,而是任意给定模式
8. 常见错误与调试技巧
8.1 常见错误
- 忘记处理无法完成的情况
- 翻转计数更新不及时
- 数组越界访问
8.2 调试建议
使用以下测试用例进行验证:
- [0,0,0,1,0,1,1,0], k=3
- [1,1,0], k=2
- [0,1,0], k=1
9. 性能优化技巧
- 使用位运算代替算术运算
- 尽量减少条件判断
- 合理利用原数组空间存储状态
10. 扩展思考
这个问题可以引申出更广泛的滑动窗口应用,例如:
- 最大连续子数组和
- 最长不重复子串
- 最小覆盖子串
掌握这种翻转标记的思想对于解决类似的位操作问题非常有帮助。在实际面试中,面试官可能会要求解释算法的工作原理或进行适当修改,因此理解核心思想比记住代码更重要。