这一节我们再来看一个使用贪心算法解决的问题。
这道题是「力扣」第 945 题:使数组唯一的最小增量。
给定整数数组 A
,每次 move
操作将会选择任意 A[i]
,并将其递增 1
。
返回使 A 中的每个值都是唯一的最少操作次数。
示例 1:
输入:[1,2,2]
输出:1
解释:经过一次 move 操作,数组将变为 [1, 2, 3]。
示例 2:
输入:[3,2,1,2,1,7]
输出:6
解释:经过 6 次 move 操作,数组将变为 [3, 4, 1, 2, 5, 7]。
可以看出 5 次或 5 次以下的 move 操作是不能让数组的每个值唯一的。
提示:
0 <= A.length <= 40000
0 <= A[i] < 40000
分析:
题目说:
消除重复元素的办法,只能自增。
那么在这个数组里,最小的那个元素(如果唯一的话),一定不需要自增。例如:示例 1 最小元素 1
。
同理,最大的那个元素(如果唯一的话),也不需要自增。例如:示例 2 最大元素 7
(这句话回过头来看是不严谨的,但是不妨碍我们找到求解这个问题的思路)。
题目又说:
返回使 A 中的每个值都是唯一的最少操作次数。
这句话告诉我们对于相同的数,自增 1 的操作次数是有上限的,加到一定程度就没有必要再加了。
因此,对原始数组进行排序,大概可以解决这个问题。我们看示例 2。
这个算法每一步只用看当前的情况,当前最优,全局就最优,所以是「贪心算法」。证明贪心算法主要有 2 个途径:一是数学归纳法,二是反证法。
这里用反证法相对容易。
「贪心算法」最优性证明:
假设当前算法的决策不是最优解。
现在假设一个新算法得到的解是最优解,这个新算法是:
-
如果新算法是:修改原始算法的第 1 个步骤,将 move 数 - 1甚至减去更大的数,其它步骤不变,肯定不符合要求,因为题目说了,只能增加。
-
那么这个新算法就只能在原始算法的第 1 个步骤里,再多增加 1,由于这个问题的特点,之前的数多了 1,它会影响到后面所有的阶段的决策。
例如:原来被 2 占的位置,现在被 3 占用了,那么原来 3 占用的位置,就得换成 4。
由于这种连锁的效应,这个新算法一定不会比原始算法得到的解更好,与最开始的假设新算法的最优性矛盾。因此原始算法是最优解。
参考代码 1:
import java.util.Arrays;
public class Solution {
public int minIncrementForUnique(int[] A) {
int len = A.length;
if (len == 0) {
return 0;
}
Arrays.sort(A);
// 打开调试
// System.out.println(Arrays.toString(A));
int preNum = A[0];
int res = 0;
for (int i = 1; i < len; i++) {
// preNum + 1 表示当前数「最好」是这个值
if (A[i] == preNum + 1) {
preNum = A[i];
} else if (A[i] > preNum + 1) {
// 当前这个数已经足够大,这种情况可以合并到上一个分支
preNum = A[i];
} else {
// A[i] < preNum + 1
res += (preNum + 1 - A[i]);
preNum++;
}
}
return res;
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] A = new int[]{1, 2, 2};
// int[] A = new int[]{3, 2, 1, 2, 1, 7};
int res = solution.minIncrementForUnique(A);
System.out.println(res);
}
}
复杂度分析:
- 时间复杂度: O ( N log N ) O(N\log N) O(NlogN),这里 N N N 是数组的长度。主要消耗在排序算法上。
- 空间复杂度: O ( 1 ) O(1) O(1)。
我们已经和大家讲解了「贪心算法」的 3 个问题,依然是要和大家再强调一下,如果只是应付平时的面试和笔试,是没有必要去把「贪心算法」的证明弄清楚。只需要有一个感性的认识。
我们学习过的「选择排序」其实就应用了「贪心算法」,在基础算法领域的「选择排序」、「合并 K 个链表」、「哈夫曼编码(Huffman Coding)」、「最小生成树」问题都是经典的应用「贪心算法」解决的问题。
学习「贪心算法」,还是重在使用上,在使用的过程中去理解「局部最优,整体最优」的思想,通过「贪心」的策略,又快又好地解决这问题。
同时还是要特别强调一点,贪心是有前提的,贪心虽然有时候看起来显而易见,但不是每个问题都能用。
练习
1、「力扣」第 23 题:合并 K 个排序链表。