题目描述
思路解析
一句话答案:LeetCode 769 最多能完成排序的块:数组是 0 到 n−1 的排列,从左扫维护前缀最大值,它一追上当前下标就说明这段自成一块、切一刀,时间 O(n)、空间 O(1)。
把 0 到 n−1 的排列切块,最多能切几块
给一个长度为 n 的数组 arr,它恰好是 0 到 n−1 这 n 个数各出现一次的排列。把它切成若干连续的块,每块单独排序后再按原顺序拼起来,要让整个数组变成升序,问最多能切成多少块。题面例子 arr=[1,0,2,3,4],答案是 4,切法是 [1,0] | [2] | [3] | [4]:头两个数排完序变 [0,1],后面三个本来就各就各位,拼起来正好升序。
枚举每个切点验一遍,为什么会慢
能想到的笨办法是枚举所有切法:每两个数之间切或不切,切完把每段排序、拼回去,看整体是否升序。切点组合有 2 的 n−1 次方种,逐段排序验证,n 稍大就跑不动。得找只扫一遍、当场判断某位能不能切的办法。
前缀最大值追上下标,为什么就能放心切一刀
先抓住排列这个条件:数组是 0 到 n−1 的排列,整体排好序后,下标 i 上站的必然是数字 i。所以一个位置能不能当切口,就看它左边这一段排完序能不能刚好填回自己的位置。
维护一个前缀最大值 maxv,也就是从头扫到当前位置为止见过的最大数字。扫到下标 i 时,若 maxv 正好等于 i,说明 0 到 i 这 i+1 个数字已经全部出现在前 i+1 格里、正好填满 0 到 i 这些位置,左边这段排完序就是 0 到 i、和右边彻底分家,这一刀就能落下。贪心在这里落在前缀最大值这把尺子上:maxv 一追平当前下标就立刻封块切一刀、往后再攒下一段,切出的块数自然最多。
一个变量记前缀最大值,一趟扫完数块数
代码就两个变量:maxv 记前缀最大值、初值设 0,chunks 记切出的块数。从左往右扫,每到一格先用当前数字更新 maxv(谁大取谁),再判断 maxv 是否等于当前下标,相等就 chunks 加一。扫到头,chunks 就是答案。初值设 0 是因为排列里一定含 0,第一格若是 0 就能立刻切;更新和判断的先后也不能颠倒,先更新 maxv 再比下标,错一步就会多切或少切一块。
[1,0,2,3,4] 五个位置,maxv 各追到哪
下标 0,数字 1,maxv 更新成 1;maxv 是 1、下标是 0,不相等,数字 0 还没露面,这块封不了。下标 1,数字 0,maxv 还是 1;这回 maxv 等于下标 1,{0,1} 正好填满前两格,[1,0] 自成一块,chunks 记 1。下标 2,数字 2,maxv 变 2、等于下标 2,[2] 单独一块,chunks 记 2。下标 3、下标 4 同理,数字各是 3 和 4,maxv 每次都追平下标,各切一刀。一趟扫完,chunks 停在 4。
光比相邻两格,为什么判不出切点
常见的写错是只看相邻两个数的大小,或者拿最小值去判切点。可 [1,0,2,3,4] 里下标 0 不能切、下标 1 能切,靠相邻比较说不清缘由——真正决定能否封块的是左边整段的最大值有没有落回自己的位置,这是从头累计的全局信息,相邻两格看不出来。另一个坑是 maxv 初值:设成 arr[0] 或负数,再配上先比后更新,下标就会错开一位,多切或漏切。整套逻辑只扫一遍、只用两个变量,时间 O(n)、空间 O(1)。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
为什么?排好序后下标 i 上应该是数字 i。如果前 i+1 个数的最大值恰好是 i,说明 0..i 这些数字正好填满了 0..i 这些位置,左边自成一块、和右边再不纠缠,所以能切。
准备 · arr = [1,0,2,3,4]:我们让一个指针从左往右扫,一路维护「到目前为止见过的最大值」maxv。每到一格,先更新 maxv,再看 maxv 是否等于当前下标。
i=0 · 更新 maxv:指针落在下标 0,值是 1。更新前缀最大值:maxv = 1。
i=0 · maxv=1 ≠ 0 · 不能切:maxv(1) ≠ i(0)。出现了比 0 大的数,说明数字 0 还在后面没遇到,这一块没法封口,不能切。
i=1 · 更新 maxv:指针挪到下标 1,值是 0。maxv 取较大者还是 1(没变大)。
i=1 · maxv=1 == 1 · 切!:maxv(1) == i(1)!前两格里的数 {0,1} 正好填满位置 {0,1},左边 [1,0] 自成一块。切一刀,块数 = 1。
i=2 · 更新 maxv:指针到下标 2,值是 2。maxv 被刷新成 2。
i=2 · maxv=2 == 2 · 切!:maxv(2) == i(2)!到这里 0..2 的数正好填满 0..2 的位置,[2] 单独成块。块数 = 2。
i=3 · 更新 maxv:指针到下标 3,值是 3。maxv 刷新成 3。
i=3 · maxv=3 == 3 · 切!:maxv(3) == i(3)![3] 又是独立一块。块数 = 3。
i=4 · 更新 maxv:指针到最后一格下标 4,值是 4。maxv 刷新成 4。
i=4 · maxv=4 == 4 · 切!完成:maxv(4) == i(4)!最后一刀,[4] 独立成块。扫完一遍,总共切出 4 块,这就是答案。
结果 · 4 块:每个「maxv == i」的位置都是一个切口。整趟一次遍历、一个变量,O(n) 就数完了块数。
下面用倒序数组走一遍,体会「前面只要冒出一个大数,后面就被它牢牢绑住切不开」。
反例 · arr = [4,3,2,1,0]:换成完全倒序的 [4,3,2,1,0],规则不变,看 maxv 怎么一路被 4 卡住。
i=0 · 更新 maxv:指针落在下标 0,第一格就是最大值 4,maxv 直接被顶到 4。
i=0 · maxv=4 ≠ 0 · 不切:maxv(4) ≠ i(0)。这个 4 要等到下标 4 才安顿好,在那之前哪也切不了。
i=1 · maxv=4 ≠ 1:下标 1,值 3,比 4 小,maxv 不变,仍是 4。4 ≠ 1,切不开。
i=2 · maxv=4 ≠ 2:下标 2,值 2,maxv 还是 4。4 ≠ 2,继续不能切。
i=3 · maxv=4 ≠ 3:下标 3,值 1,maxv 仍 4。4 ≠ 3,还差一步。
i=4 · maxv=4 == 4 · 只切这一刀:直到最后一格 i=4,maxv 才等于下标。整个数组只能算 1 块——前面的大数把后面全锁死了,这正是贪心判据的威力。
雷区实演 · 看相邻会切错:假如只盯相邻两格的大小关系,就解释不清「为什么 i=0 不能切、i=1 能切」。真正的依据是前缀最大值 maxv 何时追上下标,这是全局信息,相邻比较替代不了。
边界三连:想清楚「全升序切满 n 块」「全倒序只切 1 块」「最小乱序切 1 块」三种极端,代码就稳了。
面试追问:把「贪心为何对」和「推广到 LC768」讲清楚,是这题面试的高分点。
参考代码
class Solution: def maxChunksToSorted(self, arr): max_so_far = 0 chunks = 0 for i in range(len(arr)): if arr[i] > max_so_far: max_so_far = arr[i] if max_so_far == i: # 前缀最大值追平下标 → 切 chunks += 1 return chunks复杂度
- 时间复杂度:O(n),只从左到右扫一遍数组,每格做一次取最大值和一次相等判断,共 n 次 → O(n)
- 空间复杂度:O(1),只用了 max_so_far 和 chunks 两个变量,不随数组规模增长 → O(1)
易错点
面试追问把动画讲成自己的话
追问为什么贪心(前缀最大值==下标)一定对?
追问如果数组不是排列,而是任意可重复整数(LC768)怎么办?
追问为什么是 O(n) 而不能更快?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
保持城市天际线
LeetCode 807 · 中等 · 沿着 贪心套路 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题