将字符串翻转到单调递增 图解题解
这道题到底在问什么
- 输入
- s="00110"
- 输出
- 1 (把第 3 位的 1 翻成 0 → 00010?不,翻末位前的 1,得 00011)
最优解:为什么这么做
一句话答案:LeetCode 926 将字符串翻转到单调递增:从左扫一遍,见 1 就把前缀里 1 的个数 ones 加一,见 0 就让翻转数 flips 在『翻它自己』和『翻光前面的 1』间取小。一趟扫描的滚动动态规划,时间 O(n)、空间 O(1)。
单调递增到底长什么样,这题要最少翻几次
给一个只由 0 和 1 组成的字符串 s,每次能把某位翻转(0 改成 1 或 1 改成 0)。目标是让整串单调递增:出现 1 后右边不准再有 0,即形如 0…01…1(前段全 0、后段全 1,两段可空)。求最少翻几位。比如 s="00110",最少 1 次就变单调,答案是 1,翻哪一位后面逐位算。
把每个切点都试一遍,为什么会慢下来
长 n 的 s,单调结果无非在某处切一刀:左边全是 0、右边全是 1,切点有 n+1 个候选(含全 0、全 1 两个极端)。n 个切点逐个试就是 O(n²)(大 O 记号,描述规模变大时操作数怎么涨):每个切点把左边每个 1 翻成 0、右边每个 0 翻成 1,扫左右两侧数出翻转数取小,串一长就拖不动。
一趟扫描要盯住哪两笔账
其实不必为每个切点重新数。从左往右扫一遍、维护两笔账就够——这就是动态规划(把大问题拆成一串能顺推的小问题,前面算好的直接喂给后面、不重算)的思路。第一笔账 ones:前缀(前缀 = 从头到当前这一段)里到眼下有几个 1;把这段前缀压成全 0,就得翻掉这些 1,代价正好是 ones。第二笔账 flips:把眼下前缀改成单调最少翻几次。扫完整串的 flips 就是答案。
见 0 时的那行取小,到底在挑什么
每读一位,两笔账各自更新。读到 1:它接在已有的 1 后面不破坏『0 在前、1 在后』,不用翻,flips 不变,只 ones 加一。读到 0 才是关键:它破坏了单调,补救对应两种切法。一是把它划进后段、翻成 1,代价是原 flips 加一,即 flips+1;二是让切点落在它之前,前缀作全 0 段收尾,把前面所有 1 翻成 0,代价正好是 ones。取便宜的,于是 flips = min(flips+1, ones)——这行 min 就是在『顶当前 0』和『压光前面的 1』里挑省的,枚举切点被压进这一步。
拿 s=00110 逐位把两笔账走一遍
开局 ones=0、flips=0。第 0 位是 0,前面没有 1,flips=min(0+1, 0)=0。第 1 位也是 0,flips 仍 0。第 2、3 位都是 1,不用翻,ones 加到 2,flips 仍 0。第 4 位是 0,前面攒了 2 个 1:翻它自己要 flips+1=1 次,翻光前面 2 个 1 要 ones=2 次,取小 flips=min(1, 2)=1。扫完返回 flips=1,即把最后这个 0 翻成 1、得 00111,正对上题面答案 1。
见 0 只顾着翻它自己,长串上为什么会平白多翻
复杂度:只扫一遍,每位一次加法或取小,时间 O(n);只留 ones、flips 两个数(只留最近几笔账、循环覆盖旧值,叫滚动变量),空间 O(1)。真正易错的是那行取小:见 0 只记得 flips+1 把它顶成 1,漏了『翻光前面的 1』这条路。碰上前面 1 多、后面又连来一串 0 的串就吃亏:一味翻 0 越翻越多,其实把那几个 1 压成全 0 更省,漏掉 ones 答案会偏大。边界顺带想清:空串、全 0、全 1 本就单调,一次都不用翻。
▶ 动画逐步走查(共 25 步)——想跟着动画一帧帧对照就展开
- 3记住「见 1 则 ones+1;见 0 则 flips = min(flips+1, ones)」,下面每帧都在套它。
- 4开局两个计数都为 0。我们从左往右逐位读:见 1 累加 ones,见 0 在「翻自己」与「翻光前面的 1」之间取小。
- 5读到第 0 位是 0,但前面一个 1 都还没有,它天然就在合法位置,不构成任何冲突。
- 6结算:前面无 1,这个 0 不用动,flips 维持 0。
- 7读到第 1 位是 1(紫色)。1 能不能直接留下?能——1 接在已有的 1 后面不破坏「0 在前 1 在后」的单调。
- 8结算:这个 1 不用翻,ones 累加到 1。flips 保持 0。继续下一位。
- 9读到第 2 位是 0(红色)。可前面已经有 1 个 1 了,0 出现在 1 后面破坏了单调,必须想办法补救。
- 10结算:两条补救路取小——翻它自己要 1 次,翻光前面 1 个 1 要 1 次,于是 flips = 1(两者代价一样,任选其一,这里按翻当前 0 来讲)。
- 11读到第 3 位是 1(紫色)。1 能不能直接留下?能——1 接在已有的 1 后面不破坏「0 在前 1 在后」的单调。
- 12结算:这个 1 不用翻,ones 累加到 2。flips 保持 1。继续下一位。
- 13读到第 4 位是 1(紫色)。1 能不能直接留下?能——1 接在已有的 1 后面不破坏「0 在前 1 在后」的单调。
- 14结算:这个 1 不用翻,ones 累加到 3。flips 保持 1。继续下一位。
- 15读到第 5 位是 0(红色)。可前面已经有 3 个 1 了,0 出现在 1 后面破坏了单调,必须想办法补救。
- 16结算:两条补救路取小——翻它自己要 2 次,翻光前面 3 个 1 要 3 次,于是 flips = 2(翻当前 0 更省)。
- 17读到第 6 位是 0(红色)。可前面已经有 3 个 1 了,0 出现在 1 后面破坏了单调,必须想办法补救。
- 18结算:两条补救路取小——翻它自己要 3 次,翻光前面 3 个 1 要 3 次,于是 flips = 3(两者代价一样,任选其一,这里按翻当前 0 来讲)。
- 19读到第 7 位是 0(红色)。可前面已经有 3 个 1 了,0 出现在 1 后面破坏了单调,必须想办法补救。
- 20结算:两条补救路取小——翻它自己要 4 次,翻光前面 3 个 1 要 3 次,于是 flips = 3(翻光前面的 1 更省)。
- 21读到第 8 位是 1(紫色)。1 能不能直接留下?能——1 接在已有的 1 后面不破坏「0 在前 1 在后」的单调。
- 22结算:这个 1 不用翻,ones 累加到 4。flips 保持 3。继续下一位。
- 23读到第 9 位是 1(紫色)。1 能不能直接留下?能——1 接在已有的 1 后面不破坏「0 在前 1 在后」的单调。
- 24结算:这个 1 不用翻,ones 累加到 5。flips 保持 3。继续下一位。
- 25读到第 10 位是 0(红色)。可前面已经有 5 个 1 了,0 出现在 1 后面破坏了单调,必须想办法补救。
- 26结算:两条补救路取小——翻它自己要 4 次,翻光前面 5 个 1 要 5 次,于是 flips = 4(翻当前 0 更省)。
- 27全串扫完,flips = 4 就是把 01011000110 变成单调递增所需的最少翻转次数。整个过程一遍扫描、只维护 ones 和 flips 两个数。
⚠️ 容易写错的地方
✗ 错:见 0 只想到「翻自己」
✓ 对:还要和「翻光前面所有 1」比较取小
当前面 1 很少时,把它们全翻成 0 反而更省
✗ 错:见 1 时去动 flips
✓ 对:见 1 只累加 ones,flips 不变
1 接在 1 后面天然合法,无需任何翻转
✗ 错:用二维 DP 开数组
✓ 对:两个滚动变量即可
flips 只依赖前一状态,无需保存整张表
完整代码(Python / C++ / Java)
Python
class Solution:
def minFlipsMonoIncr(self, s: str) -> int:
ones = 0
flips = 0
for ch in s:
if ch == '1':
ones += 1
else:
flips = min(flips + 1, ones)
return flipsC++
#include <string>
#include <algorithm>
using namespace std;
class Solution {
public:
int minFlipsMonoIncr(string s) {
int ones = 0, flips = 0;
for (char ch : s) {
if (ch == '1') ++ones;
else flips = min(flips + 1, ones);
}
return flips;
}
};Java
import java.util.*;
class Solution {
public int minFlipsMonoIncr(String s) {
int ones = 0, flips = 0;
for (int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
if (ch == '1') ones++;
else flips = Math.min(flips + 1, ones);
}
return flips;
}
}复杂度
时间
O(n)
扫一遍字符串
空间
O(1)
只用 ones、flips 两个变量
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 将字符串翻转到单调递增 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么读到 1 的时候 flips 完全不用动?+
因为 1 天生能合法地接在单调串的尾部。单调递增要求『0 在前、1 在后』,前缀若已经是合法的单调段,末尾再添一个 1,它落在 1 区里,不会制造任何『1 后面跟着 0』的违规,所以维持前缀单调一次都不用多翻,flips 保持原值。变的只有 ones:前缀里多了一个 1,将来若要把这段前缀整体压成全 0,需要翻掉的 1 就多了一个,代价随之涨一。
这段代码看着只是两个计数器,凭什么算动态规划?+
它是把两状态的动态规划压扁了。规范写法会维护两笔前缀账:一笔是『把前缀整段变成全 0』的最少翻转,另一笔是『把前缀变成单调、允许以 1 结尾』的最少翻转;每读一位,前者见 1 加一,后者在两笔账里取小、再看当前位是不是 0 决定加不加一。观察下来每步只用得上前一位的两笔账,于是第一笔账正好等于前缀里 1 的个数,收成计数器 ones;第二笔账收成 flips,两个数滚动前进,空间从 O(n) 降到 O(1)。数组版和计数器版每步算出的值完全一致。
如果要求的是单调递减(1 在前、0 在后),做法要大改吗?+
不用大改,把方向对称过来即可。单调递减等价于先把字符串整个反转、再求单调递增;或者干脆从右往左扫,把 ones 换成统计 0 的个数,见 1 时做取小。核心那行取小(在『翻当前位』和『翻光前面某一类字符』之间挑便宜的)结构一点不变,只是前段和后段的角色对调了一下。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 将字符串翻转到单调递增 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。