使二进制数组全部等于 1 的最少操作次数 I 图解题解
这道题到底在问什么
- 输入
- nums = [0,1,1,1,0,0]
- 输出
- 3
- 输入
- nums = [0,1,1,1]
- 输出
- -1
最优解:为什么这么做
一句话答案:LeetCode 3191 使二进制数组全部等于 1 的最少操作次数用贪心扫一遍:最左的 0 只能靠以它为首的三连翻转来救,从左到右遇 0 就翻并计数,末尾凑不出三个即无解,时间 O(n)、空间 O(1)。
翻转连续三个数,要把数组凑成全 1
给一个只含 0 和 1 的数组 nums,一次操作能任选连续的三个元素、把它们同时反转(0 变 1、1 变 0),操作次数不限。要用最少操作把整个数组变成全 1,做不到就返回 -1。题面 nums=[0,1,1,1,0,0] 答案是 3,而 nums=[0,1,1,1] 怎么翻都会留下一个 0,只能返回 -1。
操作次数不限,为什么不能把所有翻法都试一遍
操作次数不限、每一步又能落在任意连续三个位置上,可拼出的序列是指数级的,真去枚举所有序列再挑最短的,数组稍大就跑不完。搜索空间里还塞满冗余:同一处翻两次等于没翻,分支互相抵消。穷举走不通,得看清每一步'该怎么翻'其实并不自由、而是被前面结果锁死。
最左边那个 0,其实只有一种翻法能救它
把目光放到从左数第一个 0 上。能盖住它的三连窗口有好几个,可只要不是以它为最左端,窗口就必然向左伸出去、把已经弄成 1 的位置又翻回 0。而它再往左没有元素了,不破坏左侧的选择就独剩一个:以它为首、连翻它和右边两位。这一步是被逼的,没有第二种走法。既然每个 0 怎么处理都被它左边已固定的部分锁定,从左往右照此翻,总次数就压到了最少。
从左扫到右、遇 0 就翻,什么时候会翻不下去
做法很直白:指针从下标 0 往右走,遇 1 跳过、遇 0 就以它为左端翻转连续三个并计一次数。翻转会连带改动右边两位,可能又冒出新的 0,指针继续右移照常处理即可。真正会卡住的只有一种情形:扫到一个 0 时,它右边不足两个元素、下标 i 加 2 已越出数组,凑不出以它为左端的合法窗口,只能判无解、返回 -1。
把题面两组数据的翻转一步步走出来
手算这两组时老实翻整段三位、盯着数组一步步变;代码只翻右边两位是个结果等价的简写,留到末尾再说。先走 nums=[0,1,1,1,0,0]。第 0 位是 0,翻下标 0、1、2,得 [1,0,0,1,0,0],一次。第 1 位又是 0,翻 1、2、3,得 [1,1,1,0,0,0],两次。第 2 位是 1,跳过。第 3 位是 0,翻 3、4、5,得 [1,1,1,1,1,1],三次。第 4、5 位都已是 1,扫完,答案 3。再走反例 nums=[0,1,1,1]:第 0 位翻出 [1,0,0,1]、一次,第 1 位翻出 [1,1,1,0]、两次,第 2 位是 1 跳过,第 3 位却又是 0,而它右边只剩不到两个数、下标加 2 等于 5 已越界,凑不出以它为左端的窗口,返回 -1。
别急着翻,先问一句右边还够不够三个
复杂度上,指针从头到尾只走一遍、每处最多一次翻转加一次判断,时间 O(n);就地异或翻转、只用计数和下标几个变量,空间 O(1)。几处易错:翻转前先查 i 加 2 是否越界,右边不足三个还硬翻,会把本该 -1 的算成操作数;窗口必须严格贴着当前 0 的左端,套到它中间或右侧就翻坏左边已固定成 1 的位置。还有个反直觉处:参考代码只翻右边两位、没动 nums[i],因为指针只右移不回看,这个 0 留着也不干扰后面的判断,翻不翻它答案都一样。本就全是 1 时答案 0,[0,0,0] 一次翻转搞定。
▶ 动画逐步走查(共 24 步)——想跟着动画一帧帧对照就展开
- 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 只能被以它为左端的窗口翻到,更左边没有元素了,换别的窗口一碰就会破坏已经弄成 1 的前缀。也就是说这一步操作是被逼的、没有第二种选法。既然每个 0 该怎么处理都被它左边固定好的部分唯一确定,一路贪心翻下来,次数就没法再少。
怎么判断这个数组根本变不成全 1?+
只有一种无解情形:从左扫到某个 0 时,它右边不足两个元素、下标 i 加 2 超出了数组,凑不出以它为左端的合法三连窗口。而任何能盖住它的窗口都得从左边伸过来、会把已固定的前缀翻坏。碰到这种 0 直接返回 -1,除此之外的情况都能翻成全 1。
翻转时要不要把 nums[i] 自己也翻回 1?+
不用。参考代码只对 nums[i+1] 和 nums[i+2] 做异或,nums[i] 保持原样。原因是指针从左往右走、翻过第 i 位后就再不回看它,它留着 0 也不影响对后面每一位的判断。当然把它一起翻成 1 也完全正确,只是多做一次没必要的异或——两种写法答案相同。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 使二进制数组全部等于 1 的最少操作次数 I 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。