题目描述
思路解析
一句话答案:LeetCode 738 单调递增的数字:贪心找 ≤n 的最大各位非递减整数。从右往左扫,遇左位>右位就把左位减 1、其后全填 9,变小到 ≤n 又尽量大,时间 O(len)。
求 ≤n 的最大数,还要各位从左到右不下降
给一个整数 n,要返回不超过 n、且各位数字单调递增的最大整数——单调递增就是从左到右每一位都不大于右边那位,像 1234、1359 都算。题面给 n=332,答案是 299:332 自己中间的 3 比右边的 2 大、不满足,而 299 是 ≤332 的递增数里最大的一个。
从 n 往下一个个试,为什么会超时
n 能到 10 位数,要是从 n 开始一个一个往下减、每个都拆开检查是不是递增,最坏得试上亿次才碰到答案,交上去必然超时。所以不能挨个验,得看出数字本身的规律,一遍扫描就定位到答案。
出现下降,该把哪位动手、动成什么样
盯住相邻两位:只要左边一位比右边大,这个数在这里就下降了,必须改。改法是把左边那位减 1,减 1 是为了把整个数压到 ≤n;可光减 1 会让它变得没必要地小,所以从被减的这位往后,全部填成 9——在已经比 n 小的前提下,每位取到最大的 9,整个数才尽量大。这种见下降就把左位借走 1、其后全填 9 顶到最大的改法,就是贪心:每一处改动都在当前局面下把数做到能做的最大。
为什么这一遍必须从右往左扫
把 n 拆成一排数字,从最右边相邻的一对开始、向左逐对检查:遇到左位>右位就把左位减 1、并记下这个位置 mark(表示从这位起后面都要填 9)。为什么方向不能反?因为减 1 只会在更左边引出新的下降:332 把中间的 3 减成 2 后,最左的 3 又比它大了。从右往左走,新冒出来的下降都落在还没检查的左侧,会被接着处理;要是从左往右,改了前面、后面已检查过的又被破坏,来不及补救。扫完再从 mark 一路填 9 到末尾。
332 是怎么一步步变成 299 的
拆成 [3, 3, 2],下标 0、1、2,mark 先记成 3(3 已越过最后一位、代表眼下没有要填 9 的位置)。从右往左:先看下标 1 和 2,3>2 是下降,把下标 1 的 3 减成 2、mark=2。再往左看下标 0 和 1,此时下标 1 已是 2,3>2 还是下降,下标 0 的 3 减成 2、mark=1。到最左没有邻居了,扫描停,数字成了 [2, 2, 2]、mark=1。接着从 mark=1 起填 9:下标 1、下标 2 都改成 9,得到 [2, 9, 9],转成整数就是 299。
这题最常见的三种写错
方向反了最隐蔽:从左往右扫,减 1 引出的连锁下降会被漏掉,332 就算不出 299,只能从右往左。填 9 也别抠着减 1 那一位的右边只补一格,减 1 发生在最左时,它右边每一位都得填成 9,整个数才最大,所以要从 mark 铺到末尾。还有 n=10 这种,中途会得到「09」,别慌着特判,最后统一转成整数,前导 0 自然消失、结果就是 9。复杂度上只扫数字的位数、最多十来位,时间 O(len);把数字拆成字符存下来占 O(len) 空间。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
n 可能很大(到 10 位数),一个个往下试要试几亿次,必然超时。得找规律。
把「左位减 1、右边全填 9」记牢。减 1 是为了变小到 ≤n,填 9 是为了在变小的前提下尽量大。下面一帧帧演给你看。
起步 · n = 332:把 332 拆成三位数字 [3, 3, 2] 排成一排,下标 0、1、2。我们要从最右边的相邻对开始,一对对往左查「有没有下坡」。
看最右一对 · 下标 1 与 2:指针落在最右位 下标 2,看它和左边邻居 下标 1 这一对。问:左边 3 是不是大于右边 2?
3 > 2 · 是下坡!:3 > 2,是下坡!按规律:左边这位 3 减 1 变成 2(数字真的变了),并记下 mark=2——意思是「从下标 2 开始往后都要填 9」。
继续往左 · 下标 0 与 1:指针往左挪到 下标 1,看它和更左的 下标 0 这一对。注意此刻 s[1] 已经是刚减过的 2。再问:3 > 2 吗?
又是 3 > 2 · 再下坡!:又是下坡!s[0] 由 3 减为 2,mark 更新成 1(因为减 1 发生在更左边,填 9 的起点也要跟着左移)。数字现在是 [2, 2, 2]。
到最左了 · 扫描结束:指针到了最左位,再没有「左边邻居」可比,比较阶段结束。现在数字是 [2, 2, 2],记着的 mark = 1。下一步:从 mark 开始填 9。
填 9 · 下标 1 → 9:进入填 9 阶段:从 mark=1 这位开始,下标 1 改成 9。为什么填 9?因为前面减了 1 已经保证比 n 小,后面就该尽量大,9 最大。
填 9 · 下标 2 → 9:再把 下标 2 也改成 9。填到末尾,数字变成 [2, 9, 9],也就是 299——正是答案!
332 → 299 ✓:检验:2 ≤ 9 ≤ 9 确实单调递增,且不超过 332。一次从右往左的扫描就拿到了答案 299。
再演一例 · n = 1234:换个数 1234 看看「本来就递增」会怎样。同样从最右一对开始往左查下坡。
3 < 4 · 不是下坡:最右一对 3 和 4:3 < 4,不是下坡,什么都不改,指针往左挪。
2 < 3 · 不是下坡:再往左一对 2 和 3:还是 2 < 3,不是下坡,继续左移。
1 < 2 · 不是下坡:最左一对 1 和 2:依旧 1 < 2。全程没找到下坡,mark 始终为「无」。
1234 → 1234 ✓:既然没下坡,就不减也不填,1234 本身已经单调递增,直接返回 1234。
易错例 · n = 10:看一个易错例 10:最右一对 1 和 0,1 > 0 是下坡。左位要减 1。
10 · 减 1 + 填 9:s[0] 减成 0,mark=1 让 s[1] 填 9,字符串变成 "09"。注意最高位成了 前导 0!
10 → 9 ✓:把 "09" 转回整数,前导 0 自动消失(下标 0 那位被丢弃,变灰),得到 9。所以代码最后用「转整数」一步天然处理了前导 0,不用特判。
再演一例 · n = 100:再看 100。最右一对是 0 和 0,相等不算下坡(要求是「左 > 右」),不改,指针左移。
100 · 下标 0 与 1:往左一对 1 和 0:1 > 0 是下坡!左位要减 1,并记 mark。
100 · 减 1 + 记 mark:s[0] 由 1 减成 0,记下 mark=1。数字暂时是 [0, 0, 0]。
100 · 从 mark 填 9:从 mark=1 起把下标 1、2 都填 9,得到 "099",转整数去掉前导 0 → 99。
如果从左往右,改了前面的位,后面已经检查过的就可能又被破坏、来不及补救。从右往左保证每次改动只影响「更左、还没查的」位,一遍扫描就够。
边界三连:先想前导 0(10)、已递增(1234)、连锁减 1(332)三种,代码就稳了。
面试追问:把「贪心为何对」「为何从右往左」「前导 0 怎么处理」讲清楚,是这题面试的得分点。
参考代码
class Solution: def monotoneIncreasingDigits(self, n): s = list(str(n)) # 拆成各位数字字符 mark = len(s) # 从这一位起填 9, 初始无 for i in range(len(s) - 1, 0, -1): # 从右往左 if s[i - 1] > s[i]: # 发现下坡 s[i - 1] = str(int(s[i - 1]) - 1) # 左位减 1 mark = i # 记下填 9 起点 for i in range(mark, len(s)): # 从 mark 起全填 9 s[i] = '9' return int(''.join(s)) # 转整数, 前导 0 自然消失复杂度
- 时间复杂度:O(L),L 是数字位数(int 范围内 ≤ 10)。从右往左扫一遍 + 从 mark 填一遍,都是线性 → O(L)
- 空间复杂度:O(L),把数字转成字符数组/字符串存了 L 个字符 → O(L)
易错点
面试追问把动画讲成自己的话
追问为什么贪心一定正确?
追问为什么从右往左而不是从左往右?
追问n=10 这种会出前导 0,怎么办?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
划分字母区间
LeetCode 763 · 中等 · 沿着 贪心套路 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题