使二进制数组全部等于 1 的最少操作次数 I 图解题解
这道题到底在问什么
- 输入
- nums = [0,1,1,1,0,0]
- 输出
- 3
- 输入
- nums = [0,1,1,1]
- 输出
- -1
最优解:一步一步想明白
- 3记住这一句:从左往右,谁是 0 就把从它开始的连续三个翻过来,数一次操作。为什么必须从左往右、为什么这样一定最省,下面用画面讲给你看。
- 4先看这个二进制数组 [0,1,1,1,0,0]。红色标出来的是 0,我们的任务是把所有 0 都变成 1。每次操作能任选连续的三个数,把它们同时翻转,0 变 1、1 变 0。
- 5像这样框住连续的三个位置,一按开关,这三个数同时翻个面。有个关键想法:最左边那个 0,只有以它为左端的这一个框能翻到它,再往左没有数了。所以处理顺序只能从左往右,一个都躲不掉。
- 6指针走到第 0 位,这里是 0,必须动手。能翻到它、又不去动它左边已经弄好的部分的,只有以它为左端的那个框。
- 7把框放在第 0 到第 2 位这三个数上。因为下标 i 加 2 等于 2,还落在数组里,框放得下,可以翻。
- 8开关一按,这三个数全翻了面。第 0 位如愿变成 1,后面两位也跟着翻。操作次数加一,现在是 1 次。第 0 位从此定死是 1,指针放心往右挪。
- 9指针走到第 1 位,这里是 0,必须动手。能翻到它、又不去动它左边已经弄好的部分的,只有以它为左端的那个框。
- 10把框放在第 1 到第 3 位这三个数上。因为下标 i 加 2 等于 3,还落在数组里,框放得下,可以翻。
- 11开关一按,这三个数全翻了面。第 1 位如愿变成 1,后面两位也跟着翻。操作次数加一,现在是 2 次。第 1 位从此定死是 1,指针放心往右挪。
- 12指针走到第 2 位,这里已经是 1 了,不用管它,直接往右走。
- 13指针走到第 3 位,这里是 0,必须动手。能翻到它、又不去动它左边已经弄好的部分的,只有以它为左端的那个框。
- 14把框放在第 3 到第 5 位这三个数上。因为下标 i 加 2 等于 5,还落在数组里,框放得下,可以翻。
- 15开关一按,这三个数全翻了面。第 3 位如愿变成 1,后面两位也跟着翻。操作次数加一,现在是 3 次。第 3 位从此定死是 1,指针放心往右挪。
- 16指针走到第 4 位,这里已经是 1 了,不用管它,直接往右走。
- 17指针走到第 5 位,这里已经是 1 了,不用管它,直接往右走。
- 18扫到头了,整个数组变成了 [1,1,1,1,1,1],全是 1。一路上一共动手 3 次,这就是最少操作次数,答案 3。
- 19再看一个反例 [0,1,1,1]。只有开头一个 0,看着好像很好办,咱们按同样的规矩走一遍,看会遇到什么。
- 20第 0 位是 0,右边还够三个,框住第 0 到第 2 位翻转。
- 21翻完第 0 位变成 1,可注意后面两位也被翻了,说不定又冒出新的 0,接着往下看。
- 22第 1 位是 0,右边还够三个,框住第 1 到第 3 位翻转。
- 23翻完第 1 位变成 1,可注意后面两位也被翻了,说不定又冒出新的 0,接着往下看。
- 24第 2 位是 1,略过,继续往右。
- 25走到第 3 位,它还是 0,可它右边只剩不到两个数了,下标 i 加 2 等于 5,已经超出数组。没有以第 3 位为左端、长度为三的合法窗口;能盖住它的窗口都得从更左边伸过来,一翻就会破坏已经固定好的前缀,所以在保持前缀全 1 的前提下这个 0 变不成 1。
- 26所以这组数据没救,直接返回 -1。这也是判无解的唯一情形:某个 0 落在末尾不足三个的位置上,凑不出以它为左端的合法窗口,而任何盖住它的窗口都得从左边伸过来、又会翻坏已固定的前缀。
⚠️ 容易写错的地方
✗ 错:暴力枚举所有操作组合去搜最优
✓ 对:从左往右贪心,遇 0 就翻它开头的三个
操作可以无限次做,组合是指数级的,枚举必然超时;而每个 0 的处理其实唯一确定,贪心一遍就够
✗ 错:从右往左扫,或把窗口套在 0 的中间、右边
✓ 对:必须让当前的 0 落在窗口最左端
若窗口不以当前 0 为左端,就会去翻左边已经处理好的位置,把先前的成果破坏掉
✗ 错:忘了判右边是否够三个就直接翻
✓ 对:先检查下标 i 加 2 是否越界,越界即返回 -1
右边不足三个时凑不出以它为左端的合法窗口,只能从左边伸窗口过来又会翻坏已固定的前缀,是唯一的无解情形
✗ 错:以为必须把 nums[i] 也显式翻回 1 才对
✓ 对:代码可以不动 nums[i],只翻后两个
指针只右移、不再回看第 i 位,它留成 0 也不影响后续判断,少写一步而已
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def minOperations(self, nums: List[int]) -> int:
ans = 0
for i, x in enumerate(nums):
if x == 0:
if i + 2 >= len(nums):
return -1
nums[i + 1] ^= 1
nums[i + 2] ^= 1
ans += 1
return ansC++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
int minOperations(vector<int>& nums) {
int ans = 0;
int n = nums.size();
for (int i = 0; i < n; ++i) {
if (nums[i] == 0) {
if (i + 2 >= n) {
return -1;
}
nums[i + 1] ^= 1;
nums[i + 2] ^= 1;
++ans;
}
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int minOperations(int[] nums) {
int ans = 0;
int n = nums.length;
for (int i = 0; i < n; ++i) {
if (nums[i] == 0) {
if (i + 2 >= n) {
return -1;
}
nums[i + 1] ^= 1;
nums[i + 2] ^= 1;
++ans;
}
}
return ans;
}
}复杂度
时间
O(n)
n 是数组长度。指针从左到右只走一遍,每个位置最多做一次翻转和一次判断,都是常数操作,总量随长度线性增长
空间
O(1)
按峰值算。直接在原数组上做异或翻转,不额外开辅助数组,只用了答案计数和下标这几个变量,占用是常数
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 使二进制数组全部等于 1 的最少操作次数 I 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这题为什么能用贪心,而且一定最优?+
从左往右看,当前最左边那个 0 只能被以它为左端的窗口翻转,更左边没有元素了,所以这一步操作是被逼的、没有第二种选择。每个 0 的处理都唯一确定,一路贪心下来自然就是最少次数。
怎么判断无法全部变成 1?+
扫到一个 0 时,如果它右边不足两个元素,也就是下标 i 加 2 超出了数组,就凑不出以它为左端的窗口,而任何盖住它的窗口都得从左边伸过来、会翻坏已固定的前缀,直接返回 -1。这是唯一的无解情况。
复杂度是多少,能不能优化空间?+
从左到右扫一遍,每个位置常数操作,时间 O(n);直接在原数组上异或翻转,不开额外数组,空间 O(1)。已经是最优,没有更省的做法。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 使二进制数组全部等于 1 的最少操作次数 I 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。