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

方法一:计数
思路
由于 A[i] 的范围为 [0, 40000),我们可以用数组统计出每个数出现的次数,然后对于每个重复出现的数,我们暴力地将它递增,直到它增加到一个没有重复出现的数为止。但这样的方法的时间复杂度较大,可以达到 O(N^2),例如数组 A 中所有元素都是 1 的情况。

因此,我们不能对重复出现的数暴力的进行递增,而是用以下的做法:当我们找到一个没有出现过的数的时候,将之前某个重复出现的数增加成这个没有出现过的数。注意,这里 「之前某个重复出现的数」 是可以任意选择的,它并不会影响最终的答案,因为将 P 增加到 X 并且将 Q 增加到 Y,与将 P 增加到 Y 并且将 Q 增加到 X 都需要进行 (X + Y) - (P + Q) 次操作。

例如当数组 A 为 [1, 1, 1, 1, 3, 5] 时,我们发现有 3 个重复的 1,且没有出现过 2,4 和 6,因此一共需要进行 (2 + 4 + 6) - (1 + 1 + 1) = 9 次操作。

算法
首先统计出每个数出现的次数,然后从小到大遍历每个数 x:

  1. 如果 x 出现了两次以上,就将额外出现的数记录下来(例如保存到一个列表中);
  2. 如果 x 没有出现过,那么在记录下来的数中选取一个 v,将它增加到 x,需要进行的操作次数为 x - v。
    我们还可以对该算法进行优化,使得我们不需要将额外出现的数记录下来。还是以 [1, 1, 1, 1, 3, 5] 为例,当我们发现有 3 个重复的 1 时,我们先将操作次数减去 1 + 1 + 1。接下来,当我们发现 2,4 和 6 都没有出现过时,我们依次将操作次数增加 2,4 和 6。这种优化方法在方法二中也被使用。

注意事项
虽然 A[i] 的范围为 [0, 40000),但我们有可能会将数据递增到 40000 的两倍 80000。这是因为在最坏情况下,数组 A 中有 40000 个 40000,这样要使得数组值唯一,需要将其递增为 [40000, 40001, …, 79999],因此用来统计的数组需要开到 80000。

class Solution {
public:
    int minIncrementForUnique(vector<int>& A) {
        int cnt[8000] = {0};
        int maxValue = -1;
        //获取A中每个数字出现的次数
        printf("%d\n", A.size());
        for(int i=0; i<A.size(); i++){
            cnt[A[i]]++;
            if (A[i] > maxValue){
                maxValue = A[i];
            }
        }
        //处理边界问题
        maxValue = (maxValue+1)*2;
        printf("%d\n", maxValue);
        //0-maxValue每个数字出现的次数
        int ans = 0, taken = 0;
        for(int i=0; i<maxValue; i++){
            if(cnt[i]>=2){
                //cnt[i] - 1 个 i 需要操作
                taken += cnt[i] - 1;
                //有多少个基础数字需要减
                ans -= i*(cnt[i] - 1);
            }
            else if(taken>0 && cnt[i]==0){
                taken--;
                ans += i;
            }
        }
        
        return ans;
    }
};

复杂度分析

时间复杂度:O(L)O(L),其中 LL 的数量级是数组 A 的长度加上其数据范围内的最大值,因为在最坏情况下,数组 A 中的所有数都是数据范围内的最大值。

空间复杂度:O(L)O(L),需要长度 LL 的数组统计每个数出现的次数。

方法二:先计数再遍历
例如输入 [3, 2, 1, 2, 1, 7],计数之后有两个 1 和两个 2。我们先看最小的数,两个 1 重复了,需要有一个增加到 2,这样 2 的数量变成了三个。在三个 2 中,又有两个需要增加到 3,然后又出现了两个 3…… 以此类推,可以计算出需要增加的次数。

我们可以用 map(如 C++ 的 unordered_map,Java 的 HashMap)来做计数。不过既然题目中说明了整数的范围在 0 到 40000 之间,我们不妨直接用一个大小为 40000 的数组做计数。

需要注意的是,虽然整数的范围是 0 到 40000,但是由于整数还会因为增加而变大,超出 40000 的范围。例如极端的情况:所有数都是 39999。所以需要对整数中最大的数单独处理。

class Solution {
public:
    int minIncrementForUnique(vector<int>& A) {
        int cnt[4000] = {0};
        int maxValue = -1;
        //获取A中每个数字出现的次数
        printf("%d\n", A.size());
        for(int i=0; i<A.size(); i++){
            cnt[A[i]]++;
            if (A[i] > maxValue){
                maxValue = A[i];
            }
        }
        //0-maxValue每个数字出现的次数
        int ans = 0;
        for(int i=0; i<maxValue; i++){
            if(cnt[i]>=2){
                // 有 cnt[i] - 1 个数需要增加
                ans += cnt[i] - 1; 
                // i+1 出现的次数
                cnt[i+1] += cnt[i] - 1;
            }
        }

        // cnt[maxValue] 单独计算,是因为可能超出 40000 的边界
        if (cnt[maxValue] > 1) {
            // 计算多少个数字需要处理
            int d = cnt[maxValue] - 1; 
            // 有 d 个数需要增加
            // 分别增加为 maxValue + 1, maxValue + 2, ... maxValue + d
            // 使用等差数列公式求和
            ans += (1 + d) * d / 2;
        }
        
        return ans;
    }
};

这种解法的时间复杂度不能简单地写成 O(n)O(n)。设 nn 为数组元素的个数,kk 为数组元素的可能取值个数(本期中 k = 40000k=40000),这个算法的时间复杂度是 O(n + k)O(n+k)。

方法三:先排序再遍历
首先将数组进行排序,然后从左到右遍历数组:

如果当前元素大于上一个元素,保持不变;
如果当前元素小于等于上一个元素,就需要增加当前元素,直到大于上一个元素。
例如输入 [3, 2, 1, 2, 1, 7],排序后为 [1, 1, 2, 2, 3, 7]。

class Solution {
public:
    int minIncrementForUnique(vector<int>& A) {
        sort(A.begin(), A.end());
        int curmax = -1; // 当前数组最大值
        int res = 0;
        for (int i = 0; i < A.size(); i++) {
            if (A[i] <= curmax) {
                int ai = curmax + 1; // 当前元素 A[i] 需要增加到 curmax + 1
                res += (ai - A[i]); // 记录自增次数
                A[i] = ai; // 增加当前元素
            }
            //计算此时的最大值
            if(A[i] > curmax){
                curmax = A[i];
            }
        }
        return res;
    }
};

时间复杂度:O(n \log n)O(nlogn),主要的复杂度在排序上。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值