👋 欢迎阅读

🎯 欢迎来到「单调递增的数字」题解之旅! 本文将带你从"找不超过给定数字的最大单调递增数"这一数字处理问题出发,深入理解贪心算法 的巧妙运用,并掌握如何通过从高位向低位扫描、定位下降点来高效构造结果。
在开始之前,建议你先:
-
了解题目背景 :这是 LeetCode 738 题,给定一个非负整数 nn,要求返回小于或等于 nn 的最大数字 ,且该数字的每一位从左到右呈非递减 (单调递增)。本质上,我们需要在数字的十进制表示上做局部调整,将某个高位减1,后续全部变为9,以保证结果尽量大且满足单调性。
-
明确学习目标 :掌握贪心核心步骤 ------将 nn 转为字符数组,从高位向低位找到第一个下降位置 ,然后从该位置向前回溯,将前面相等的数字减1(处理借位),最终将该位置之后的所有数字置为9。理解为什么这样能得到最大且合法的结果(贪心选择:在尽可能高的位置减小一位,后面全部用9填充,既保证最大又保证单调)。
-
准备好环境:建议在本地 IDE 或 LeetCode 在线编辑器中打开代码,边看边运行。
本文将从问题转化、贪心策略设计(定位下降点 + 回溯借位 + 填充9)、正确性直觉 到代码实现,层层递进。即使你对贪心算法还不熟悉,我们也会从"让高位尽量大,后面全用9来补"这一直觉出发,让你轻松抓住核心思想 ------一旦发现上升趋势被破坏,就减小当前位,并把后面所有位变成9,这样既保证最大又满足单调。现在,让我们一起调整数字的每一位,找出那个最大的单调递增数吧! 🔢✨
一、题目

二、做题思路
1. 问题分析(前置分析)
给定一个整数 n,要求找到 小于或等于 n 的最大数字 ,且该数字的每一位从高位到低位 单调非递减 (即 x <= y)。
核心贪心思想 :从高位到低位扫描,找到第一个 下降点 (即 当前位 > 下一位),将 该位数字减 1 ,并将 该位之后的所有位都变为 9 ,这样得到的数一定是满足条件且最大的。
特殊处理 :若下降点前有连续相等数字,需要 回退到相等段的起始位 进行减 1,以保证结果最大且不破坏单调性。
2. 贪心策略(核心决策规则)
-
将
n转换为字符串ret,便于逐位操作。 -
使用两个指针:
left指向当前相等段的起始位置 ,right指向当前待比较的下一位。 -
从左到右扫描:
-
若
ret[left] < ret[right],说明严格递增,更新left = right,right++继续向后。 -
若
ret[left] == ret[right],说明相等,left保持不变 ,right++继续向后。 -
若
ret[left] > ret[right],找到下降点:-
将
ret[left]减 1 (字符减 1,如'3'变为'2')。 -
将
left+1及其之后所有位都设为'9'。 -
直接返回修改后的整数 (
stoi(ret)可自动去除前导零)。
-
-
-
若整个扫描未发现下降点,说明原数字本身已单调递增,直接返回
n。
3. 正确性说明(简单版本)
为了得到不超过 n 的最大单调递增数,必须在第一个可能导致递减的位置进行调整。将第一个下降点的高位减 1,并将后面所有位变成 9 ,是使数字尽可能大且满足单调性的唯一最优方式。若将更低位减 1,则高位不变但后面变为 9 会超过 n;若将更高位减 1,则数字会过小,不是最大。回退到相等段起始位 是为了避免 332 这样的情况:直接处理 3>2 得到 329,但 329 仍不是单调递增(3>2),必须将第一个 3 减 1 变为 2,后面全为 9,得到 299,这才是正确结果。因此贪心策略正确。
4. 实现细节(边界防护)
-
使用 字符串
ret存储n的每一位,便于修改和比较。 -
初始
left = 0,right = 1。 -
扫描过程中,
left始终指向当前相等段的第一个位置,当遇到下降时,该位置就是要减 1 的位置。 -
找到下降点后,将
left之后的字符全部置为'9',然后用stoi转回整数返回。 -
若
n为个位数,直接返回原数(循环不执行,进入最后的return n)。
5. 返回值(目标映射)
返回修改后的整数,即 小于或等于 n 的最大单调递增数字。
四、代码
cpp
class Solution
{
public:
int monotoneIncreasingDigits(int n)
{
// 贪心策略:从高位到低位寻找第一个下降点(当前位 > 下一位),
// 将该位数字减1,并将之后所有位变为9,即可得到最大的单调递增数。
// 注意:如果前面存在连续相等的情况,需要回退到相等段的起始位进行减1,
// 以确保结果最大。本算法通过 left 指针始终指向当前相等段的起始位来实现。
string ret = to_string(n); // 将整数转为字符串,便于逐位操作
int len = ret.size();
int left = 0; // 当前比较的左边界(指向当前相等段的起始位置)
int right = 1; // 当前比较的右边界(指向待比较的下一位)
// 从左到右扫描,寻找第一个下降点
while (right < len)
{
// 情况1:左位 < 右位,说明是严格递增,更新左边界为当前右位,
// 并继续向后移动右指针。
if (ret[left] < ret[right])
{
left = right;
right++;
}
// 情况2:左位 == 右位,说明相等,左边界不变(保持为相等段的起点),
// 只移动右指针继续向后比较。
else if (ret[left] == ret[right])
{
right++;
}
// 情况3:左位 > 右位,找到下降点。
// 将相等段起始位(left)减1,并将该位之后的所有位置设为'9',
// 然后返回结果。
else
{
ret[left] = ret[left] - 1; // 减1操作(注意字符减1,如'3'变为'2')
int index = left + 1;
while (index < len)
{
ret[index++] = '9';
}
// 将修改后的字符串转回整数并返回(自动忽略前导零)
return stoi(ret);
}
}
// 如果整个字符串原本就是单调递增的,则直接返回原数
return n;
}
};
五、流程图
六、正确性说明(详细版)
步骤 1:符号与问题建模
+----------------------------------------------------+
| 输入:整数 n,转为字符串 s(高位到低位) |
| 定义:单调递增数满足 s[i] <= s[i+1] 对所有 i。 |
| 目标:返回不大于 n 的最大单调递增整数。 |
+----------------------------------------------------+
|
v
+----------------------------------------------------+
| 贪心策略:从左到右扫描,维护 left 指向当前相等段 |
| 的起始位置(初始0),right 指向下一位。 |
| ① 若 s[left] < s[right],则更新 left=right; |
| ② 若 s[left] == s[right],则保持 left 不动; |
| ③ 若 s[left] > s[right](找到下降点), |
| 将 s[left] 减1,并将 left 之后所有位设为9, |
| 返回结果。 |
| 贪心实质:在保证尽可能大的前提下,遇到下降必须 |
| 退一步(减1)并填充9,以维持单调递增性质。 |
+----------------------------------------------------+
-
将整数
n转为字符串,便于逐位操作。 -
核心观察 :任何小于
n且尽量大的数,必然是在某个位置比n小一位,后面全为9。为了最大,这个位置应尽量靠右(即最早出现的下降点的前一位,且若是相等段则需要回退到该段的最左端)。
步骤 2:关键性质 ------ 下降点与相等段处理的正确性(反证法)
+----------------------------------------------------+
| 性质 1:若 s[i] > s[i+1],则任何大于将 s[i] 减1 |
| 且后面全9的数都不可能满足单调递增。 |
| 反证:若 s[i] 不减1,则为了不大于原数,后面必 |
| 然有某位小于原数,形成新的下降,无法保持递增。 |
| 因此最优解必须将 s[i] 减1,后续全置9。 |
+----------------------------------------------------+
|
v
+----------------------------------------------------+
| 性质 2(相等段回退):若存在连续相等段 |
| s[left..right-1] 都相等,且 s[right] 更小, |
| 则必须将 **整个相等段的最左端** 减1, |
| 因为若只在最后一位减1(如332 → 322), |
| 则前一位仍大于下一位(3 > 2),仍不满足递增, |
| 且后续无论怎么补9都无法补救。 |
+----------------------------------------------------+
详细论证:
-
性质1(下降点处理) :设首次出现下降的位置为
i,即s[i] > s[i+1]。为了得到不大于原数的最大单调递增数,该位置之前的数字必须保持不变 (否则会变得更小,损失太大)。若s[i]不变,则无论i之后如何调整,s[i] > s[i+1]的约束无法满足,因此必须将s[i]减1。减1后,为了让数尽可能大且保持递增,i之后的所有位都应设为9(这是最大的可行选择)。 -
性质2(相等段回退) :当遇到
s[left] == s[right]时,left保持不动,直到找到一个更小的数字或字符串结束。例如332:扫描过程left=0, right=1(3==3),right=2发现 3>2,此时left仍为0,将s[0]减1得2,后面全9得299。若将left更新为1再减1,则得到322,并非单调递增。因此 必须回退到相等段的最左端进行减1,才能保证减1后前缀仍满足递增。
步骤 3:归纳证明 ------ 扫描过程得到全局最优
+----------------------------------------------------+
| 初始:left=0, right=1,扫描前两位。 |
| 归纳假设:在处理到位置 i 时,left 指向当前相等段 |
| 起点,且 s[left] 是前缀中需要保留的参考值, |
| 之前部分已经满足单调递增。 |
+----------------------------------------------------+
|
v
+----------------------------------------------------+
| 处理 s[i] 与 s[i+1]: |
| - 若 s[i] < s[i+1],则前缀递增,更新 left=i+1。 |
| - 若 s[i] == s[i+1],保持 left,继续。 |
| - 若 s[i] > s[i+1],由性质1和性质2,将 s[left] |
| 减1,后面全9,所得结果即为最大可行数。 |
+----------------------------------------------------+
|
v
+----------------------------------------------------+
| 若扫描结束未发现下降,则原数本身单调递增, |
| 直接返回 n。 |
| 否则,修改后的字符串是最大且唯一的解。 |
+----------------------------------------------------+
详细论证:
-
归纳基础 :初始时
left=0,前缀为空,无任何约束。 -
归纳步骤 :假设已处理到位置
i,且之前所有位都满足单调递增。考虑s[i]与s[i+1]的关系:-
若
s[i] < s[i+1],则已满足递增,且s[i]作为新的相等段起点(因为下一个可能相等),更新left=i+1。 -
若
s[i] == s[i+1],则当前相等段延续,left保持,继续向后检查。 -
若
s[i] > s[i+1],出现下降。由性质2,必须将相等段起点(left位置)减1,因为任何更靠右的修改都会导致前缀或自身出现新的下降。减1后,为了最大化,将后续所有位设为9。此时得到的结果是不大于原数的最大单调递增数。
-
-
终止 :若整个字符串扫描结束未发现下降,则
n本身满足单调递增,直接返回n。若发现下降并修改,则修改后的字符串满足单调递增(因为前缀不变且递增,减1位变小,后续全9,必不小于前一位),且是唯一能达到的最大值(任何其他改法都会更小或违反单调性)。
🎯 闭幕
🎉 恭喜你完成了「单调递增的数字」问题的学习!
为了巩固知识并进一步拓展,建议你:
🚀 动手实践
在 LeetCode 上提交代码,尝试不同的测试用例。
💡 深入思考
-
本题的核心是 贪心 + 字符串操作 :从高位向低位寻找第一个 下降点 (即
ret[i] > ret[i+1]),然后向前回退到 相等段的起始位 ,将该位减1,后面全部置为9。为什么这样的操作能得到"小于等于 n 的最大单调递增数"? 如果直接找到下降点就处理,不处理相等段,可能会错过更大的结果,你能举例说明吗? -
代码中维护了
left指针指向当前相等段的起始位置。为什么需要记录相等段? 如果只有下降点而前面没有相等,比如332,找到下降点3>2,但前面的两个3相等,需要把第一个3减1得到299;如果不回退到最前面,只把第二个3减1会得到329,比299大吗?实际上329不是单调递增(3>2),所以必须回退到相等段起点。你能理解这个回退的必要性吗?
📚 延伸挑战
-
将题目改为 单调递减的数字 (每位
x >= y),求小于等于n的最大递减数,你只需要修改哪些判断条件? -
如果要求 严格单调递增 (每位必须
<而不是<=),算法需要怎么调整?
如果你觉得本文对你有所帮助,欢迎:
👍 点赞 / 收藏
👤 关注作者,获取更多题解
💬 留言交流你的疑问或优化思路
祝你在 算法之路 上越走越稳,早日攻克每一道难题!下次见 🚀✨