LeetCode 945. 使数组唯一的最小增量

余生长醉 提交于 2020-03-23 01:57:44

我的LeetCode刷题源码[GitHub]:https://github.com/izhoujie/Algorithmcii

LeetCode 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

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/minimum-increment-to-make-array-unique
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解题思路

思路1-先排序,再从左向右累加每两个临近数需要的+1操作数;

  1. Arrays.sort(A)排序;
  2. 顺次比较并记录将后一个数变为前一个数+1数所需要的操作数;

例子:假如排序后是1123455那么从第二个数开始:

  • 第一次:1223455 此时move+=2-1
  • 第二次:1233455 此时move+=3-2
  • 第三次:1234455 此时move+=4-3
  • 第四次:1234555 无需操作
  • 第五次:1234565 此时move+=6-5
  • 第六次:1234567 此时move+=7-6
    其中:
  • 时间复杂度:O(NlogN) N=A.length
  • 空间复杂度:O(1)

思路2-先统计顺次进行操作数的累加,每次需要累加+1操作的次数是相同数的个数;

  1. 创建新数组new int[40001],然后将A中每个数作为下标进行统计;
  2. 遍历新数组,1中统计到的相同数大于0的,其-1后的数就是这些数需要进行+1操作的数,并把这些+1操作后的数累加给下一个统计数,通过每次-1来使得最终数都不相同;
  3. 遍历完后需要再检查一下最大下标数的个数,若大于1,其中-1个数都需要进行+1操作,直接使用1-n的求和公式即可;
  • 时间复杂度:O(N) N=max(A.length,max(A))
  • 空间复杂度:O(40001)即O(1)

Tips:第一步的统计其实隐含了排序,利用了自然数的特性,下标天然有序是数组很容易被忽略的一个特性,比如字母(通过char的 -'a'操作)转数组去统计就避免了额外排序;

思路3-路径压缩;(来自LeetCode评论区,很秀...)

  1. 创建新数组new int[80000](初始化值-1),因为路径压缩不同于方法2中的统计(或者说是统计压缩),这里压缩的是+1的操作,但是+1后的数需要新数组去记录,若A中所有值都是39999,最后的最大数将是79999;
  2. 开始遍历并进行路径点位记录findPath,这是个递归方法,可能比较绕,单独分析下:
private int findPath(int i) {
	// 初次遇到点位,记录值并返回,此时j=0
	if (path[i] == -1) {
		path[i] = i;
		return i;
	} else {
		// 若i有记录,则向后找path[i] + 1位置的值,并最终递归更新路径值
		path[i] = findPath(path[i] + 1);
		return path[i];
	}
}

对于例子:A{1,1,2,3,5,5,2},对应的路径数组初始化的路径值均为-1

  • 0下标1的路径值为-1,执行后更新为1并返回1,此时move+=1-1,对应1不需要+1操作;
  • 1下标1的路径值因为已经被标记为1了,所以往后找1+1的路径值,此时找到的2的路径值为-1,更新路径1和2的值都为2并返回,最后move+=2-1,对应1需要1次+1操作;
  • 2下标2的路径值为2,往后找2+1的路径值得到-1,此时将路径1,2,3的路径值都更新为3,最后move+=3-2,对应2需要1次+1操作;
  • 3下标3的路径值为3,往后找3+1的路径值得到-1,此时将路径1,2,3,4的路径值都更新为4,最后move+=4-3,对应3需要1次+1操作;
  • 4下标5的路径值为-1,执行后更新为5并返回5,此时move+=5-5,对应5不需要+1操作;
  • 5下标5的路径值为5,往后找5+1的路径值得到-1,此时将路径1,2,3,4,5,6的路径值都更新为6,最后move+=6-5,对应5需要1次+1操作;
  • 6下标2的路径值为6,往后找6+1的路径值得到-1,此时将路径1,2,3,4,5,6,7的路径值都更新为7,最后move+=7-2,对应2需要5次+1操作;

所谓的路径压缩其实是记录了当前已遍历数经过+1操作后中的最大数,便于后面根据路径直接找到最大数并在此基础上计算需要+1的次数

算法源码示例

package leetcode;

import java.util.Arrays;

/**
 * @author ZhouJie
 * @date 2020年3月22日 下午8:04:55 
 * @Description: 945. 使数组唯一的最小增量
 *
 */
public class LeetCode_0945 {

}

class Solution_0945 {
	/**
	 * @author: ZhouJie
	 * @date: 2020年3月22日 下午8:08:06 
	 * @param: @param A
	 * @param: @return
	 * @return: int
	 * @Description: 1-先排序,再从左向右累加每两个临近数需要的+1操作数;
	 * 				时间复杂度:O(NlogN) N=A.length
	 * 				空间复杂度:O(1)
	 *
	 */
	public int minIncrementForUnique_1(int[] A) {
		int len = 0, move = 0;
		if (A == null || (len = A.length) < 2) {
			return move;
		}
		Arrays.sort(A);
		for (int i = 1; i < len; i++) {
			// 若当前值小于等于前一个值,说明需要进行+1操作,+1操作的次数就等于差值再+1,此外还需要更新当前值为前一个值+1
			if (A[i] <= A[i - 1]) {
				move += A[i - 1] - A[i] + 1;
				A[i] = A[i - 1] + 1;
			}
		}
		return move;
	}

	/**
	 * @author: ZhouJie
	 * @date: 2020年3月22日 下午8:15:17 
	 * @param: @param A
	 * @param: @return
	 * @return: int
	 * @Description: 2-先统计再由小到大进行操作数的累加,每次需要累加+1次数的是相同数的个数-1;
	 * 				其实统计这一步隐含了排序(自然数的特性),这也是比1方法快的关键原因
	 * 				时间复杂度:O(N) N=max(A.length,max(A))
	 * 				空间复杂度:O(40001)即O(1)
	 */
	public int minIncrementForUnique_2(int[] A) {
		int move = 0;
		if (A == null || A.length < 2) {
			return move;
		}
		// 因为最大数是3999,若+1为40000,需要用到40000索引
		int[] statistics = new int[40001];
		// 记录最大数,用作遍历statistics的右边界
		int max = 0;
		for (int i : A) {
			statistics[i]++;
			max = Math.max(max, i);
		}
		max++;
		// max是A中最终可能的最大值
		for (int i = 0; i < max; i++) {
			// 若A中statistics[i]的个数大于1,说明statistics[i]-1个数需要进行+1操作,
			// 这一步只是给statistics[i]-1个数各进行了一次+1操作,后续的+1交给statistics[i+1]去完成(递归)
			if (statistics[i] > 1) {
				move += statistics[i] - 1;
				statistics[i + 1] += statistics[i] - 1;
			}
		}
		// 若statistics[max]的个数大于1,则statistics[max]-1个数(记为n个)需要进行+1操作;
		// 这n个数依次需要进行+1的次数为1、2、3、4....n,即对1-n求和,直接使用求和公式
		if (statistics[max] > 1) {
			int n = statistics[max] - 1;
			move += n * (n + 1) / 2;
		}
		return move;
	}

	/**
	 * @author: ZhouJie
	 * @date: 2020年3月22日 下午8:36:13 
	 * @param: @param A
	 * @param: @return
	 * @return: int
	 * @Description: 1-路径压缩;(来自LeetCode评论区,很秀...)
	 * 				时间复杂度:O(N) N=A.length
	 * 				空间复杂度:O(80000)即O(1)
	 * 				因为findPath每次可能更改多个点位的值,所以效率没有方法2高
	 */

	// 若A中所有数都相等且都为39999,则+1操作完成时,最大值将为79999
	int[] path = new int[80000];

	public int minIncrementForUnique_3(int[] A) {
		int move = 0;
		if (A == null || A.length < 2) {
			return move;
		}
		// -1为空地址标记,与A中数不同即可
		Arrays.fill(path, -1);
		for (int i : A) {
			int j = findPath(i);
			move += j - i;
		}
		return move;
	}

	/**
	 * @author: ZhouJie
	 * @date: 2020年3月22日 下午8:49:55 
	 * @param: @param i
	 * @param: @return
	 * @return: int
	 * @Description: 路径压缩核心
	 *
	 */
	private int findPath(int i) {
		// 初次遇到点位,记录值并返回,此时j=0
		if (path[i] == -1) {
			path[i] = i;
			return i;
		} else {
			// 若i有记录,则向后找path[i] + 1位置的值,并最终递归更新路径值
			path[i] = findPath(path[i] + 1);
			return path[i];
		}
	}
}

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!