题目描述
思路解析
一句话答案:LeetCode 945 使数组唯一的最小增量:排序后从左到右,每个数至少比前一个占用值大 1,不够就抬到刚好够,抬升量累加就是最少操作次数,时间 O(n log n)、空间 O(1)。
每次把某个数加 1,最少几次能让值都不同
给整数数组 nums,一次操作把某个下标的值加 1,只能加不能减。要让每个值都唯一,问最少操作多少次。题面例子 nums=[1,2,2] 只需 1 次,把一个 2 加成 3;nums=[3,2,1,2,1,7] 要 6 次,最终变成 [1,2,3,4,5,7]。难点在于重复的数不止一个,往哪顶、顶到几还互相牵扯。
见一个重复就去找空位,为什么会越找越慢
先想到的多半是扫出重复的数,挑一个没被占用的新值。可「没被占用」得对着整个数组查,通常拿集合记下已用的值,遇到重复就从它往上一格格试到空位。数据一大,最坏每个重复数都要探很多格,退化成平方级,几万个数就超时。而且顶到哪个值才不多花,散着看理不清。
先排好序,每个数只跟前一个占用值较劲
把 nums 从小到大排序,相同的数就挨在一起,重复只发生在相邻位置。这样每个数不必顾全局,只要保证比左边刚定下来的占用值大就行。
记左边最后占用的值为 y,轮到当前数 x:若 x 已经比 y 大,它自己就够,原地占用、一步不花;若 x ≤ y,会撞上前面,得把它抬到 y+1,这是躲开重复的最小合法值。为什么抬到刚好 y+1、不多抬?多抬一格,当前多花代价,还把后面所有数的下限顶高,只会更贵;抬到 y+1 既躲开重复,又给后面留最大空间。每步取最小可用值,合起来抬升量最小。
把「抬到刚好」压成一行递推
用变量 y 记上一个占用值,初始设成 -1,让第一个数的可用下限落在 0,非负的数都不用抬。ans 记累计代价从 0 起。遍历排序后每个 x:占用值 y = max(y+1, x),要么被前一个逼到 y+1、要么就是 x 自己,取大的那个;代价 y − x 累加进 ans。走完返回 ans。x > y 时代价 0,x ≤ y 时补上和 y+1 的差距。
[3,2,1,2,1,7] 排完序,6 是怎么一步步加出来的
排序后是 [1,1,2,2,3,7],起手 y=-1、ans=0。第一个 1:y=max(0,1)=1,代价 1−1=0,ans=0。第二个 1:y=max(2,1)=2,顶到 2,代价 2−1=1,ans=1。
第一个 2:y=max(3,2)=3,顶到 3,代价 3−2=1,ans=2。第二个 2:y=max(4,2)=4,顶到 4,代价 4−2=2,ans=4。3:y=max(5,3)=5,顶到 5,代价 5−3=2,ans=6。7:y=max(6,7)=7,自己够大,代价 0,ans 停在 6。数组落成 [1,2,3,4,5,7],六个值互不相同,累计正是 6。
三个会让你多花步数或直接超时的地方
不排序直接贪心是头号坑:一个数得和前面所有已定的值逐一比较,逻辑绕不清、容易漏判撞车。抬过头是第二个:只要抬到 max(y+1, x),多抬哪怕一格,当前多花一步,后面所有数的下限被顶高,代价滚成雪球。第三个是拿集合暴力找空位,一格格往上探,最坏退化成平方级就卡死。
时间上,排序 O(n log n),之后只线性扫一趟,仍是 O(n log n);除了 y 和 ans 没另开空间,O(1)。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
记住这套「新占用值 = max(上一个+1, x),代价 = 新值 - x」,下面每一帧都在套它。
先看原始数组 [3,2,1,2,1,7],里面有两个 1、两个 2,存在重复,需要把它们抬开。
排序后变成 [1,1,2,2,3,7]。相同的数挨在一起,从左往右处理就只需盯住「前一个占用值」。y 先设成 -1,这样第 0 个数的下一个可用值是 0,不会误抬。
轮到第 0 个数 1(紫色)。左边绿色的都是已经定下来、互不重复的值。前一个占用到 -1,所以现在能用的最小值是 0。
1 已经不小于 0,自己就够大,直接占用 1,这一步代价是 0,一步都不用花。
1 原地锁定为绿色,代价 0,总代价还是 0。下一个数要比 1 更大。
轮到第 1 个数 1(紫色)。左边绿色的都是已经定下来、互不重复的值。前一个占用到 1,所以现在能用的最小值是 2。
2 比 1 大,说明 1 占不下,会和前面重复(标红)。只能把它抬到刚好不撞的 2,多走 1 步。
把它从 1 抬到 2,绿色锁定。总代价累加到 1,下一个数要比 2 更大才行。
轮到第 2 个数 2(紫色)。左边绿色的都是已经定下来、互不重复的值。前一个占用到 2,所以现在能用的最小值是 3。
3 比 2 大,说明 2 占不下,会和前面重复(标红)。只能把它抬到刚好不撞的 3,多走 1 步。
把它从 2 抬到 3,绿色锁定。总代价累加到 2,下一个数要比 3 更大才行。
轮到第 3 个数 2(紫色)。左边绿色的都是已经定下来、互不重复的值。前一个占用到 3,所以现在能用的最小值是 4。
4 比 2 大,说明 2 占不下,会和前面重复(标红)。只能把它抬到刚好不撞的 4,多走 2 步。
把它从 2 抬到 4,绿色锁定。总代价累加到 4,下一个数要比 4 更大才行。
轮到第 4 个数 3(紫色)。左边绿色的都是已经定下来、互不重复的值。前一个占用到 4,所以现在能用的最小值是 5。
5 比 3 大,说明 3 占不下,会和前面重复(标红)。只能把它抬到刚好不撞的 5,多走 2 步。
把它从 3 抬到 5,绿色锁定。总代价累加到 6,下一个数要比 5 更大才行。
轮到第 5 个数 7(紫色)。左边绿色的都是已经定下来、互不重复的值。前一个占用到 5,所以现在能用的最小值是 6。
7 已经不小于 6,自己就够大,直接占用 7,这一步代价是 0,一步都不用花。
7 原地锁定为绿色,代价 0,总代价还是 6。下一个数要比 7 更大。
全部处理完,数组变成 [1,2,3,4,5,7],每个值都不一样。把每一步的代价加起来正好是 6,这就是答案。
边界先想清:已唯一就是 0;全相同时逐个抬,代价是 0,1,2 累加。
两个高频追问:计数法替代排序,以及贪心的正确性证明。
参考代码
from __future__ import annotationsfrom typing import *from collections import *from functools import *from itertools import *from math import *from heapq import *from bisect import *class Solution: def minIncrementForUnique(self, nums: List[int]) -> int: nums.sort() ans, y = 0, -1 for x in nums: y = max(y + 1, x) ans += y - x return ans复杂度
- 时间:O(n log n),排序占主导,之后一遍线性扫描
- 空间:O(1),只用 y、ans 两个变量;排序自身栈开销另算
易错点
面试追问把动画讲成自己的话
追问不排序能不能做?
追问怎么证明贪心是对的?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
行相等的最少多米诺旋转
LeetCode 1007 · 中等 · 沿着 贪心套路 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题