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


https://leetcode-cn.com/problems/minimum-increment-to-make-array-unique/
在这里插入图片描述

我写leetcode相关的博文不是写那种大家看得题解,只是记录自己解题过程中的思路和想法,加深印象,所以会写的比较乱

暴力

哇昨天做这题真的心态崩了,暴力都不会暴力 😒
其实暴力解法第一次做的时候在纸上已经推算出来了
例如1,1,1,2,4,5这种情况
1,1,1,2,4,5,每个重复的数字,只保留一个,然后其余的都往后加就行了
1,2,2,2,4,5
1,2,3,3,4,5
1,2,3,4,4,5
1,2,3,4,5,5
1,2,3,4,5,6
一共需要7次
于是我按照这个思路尝试了第一次,注意要先排序保证有序

class Solution:
    def minIncrementForUnique(self, A: List[int]) -> int:
        size=len(A)
        dct=dict()
        for num in A:
            dct[num]=dct.get(num,0)+1
        ans=0
        while len(dct)!=size:
            for k,v in dct.items():
                if v!=1:	# 这一步太耗时了
                    fk,fv=k,v
                    break
            cnt=fv-1
            dct[fk]=1
            dct[fv+1]=dct.get(fv+1,0)+cnt
            ans+=cnt
        return ans

超 时 大 成 功

第二次尝试
看到第一次尝试超时了,丝毫没有意识到是寻找第一个重复数的操作耗时太多,反而想了个更超时的操作 😂
用一个足够大的空数组,填满数字
对每个重复的数字,计算它与下一个0的距离,然后标记下一个0的值为1
重复以上行为,直到start与size相等
这个操作看起来很美好啊
对于1,1,1,2,4,6,9
重复的两个1,直接填到空缺的3和5中,并且距离也很好算,一个是3-1,一个是5-1,一下子就得出移动次数了
变成1,2,3,4,5,6,9
但是这个寻找下一个0的操作实在是耗时太长了。。。

class Solution:
    def minIncrementForUnique(self, A: List[int]) -> int:
        size=len(A)
        ans=0
        dct=[0]*80000  #存放数字的映射
        start=0 #初始数字长度
        lst=[]  #存放那些有重复的数字
        for num in A:
            if dct[num]==0:
                start+=1
            dct[num]+=1
            if dct[num]==2: #标记有重复的元素,并且只标记一次
                lst.append(num)
        lst.sort()        
        ind=0
        zero=dct.index(0)   #第一个不为0下标
        while start!=size:
            # 统计有多少个重复数,只保留一个,移动其它的重复数
            t=dct[lst[ind]]-1
            for i in range(t):
                while zero<lst[ind] or dct[zero]!=0:   #只往后寻找0 *这一步太耗时了*
                    zero=zero+1+dct[zero+1:].index(0)
                ans+=zero-lst[ind]
                dct[zero]=1
            ind+=1
            start+=t
        return ans

又一次超时大成功
这里解释下为什么dct的大小为80000,因为最差的情况就是40000个40000,往后移动刚好填满40000~79999

看了一下别人的暴力,真的完爆我了

class Solution:
    def minIncrementForUnique(self, A: List[int]) -> int:
        A.sort()
        dct=[0]*800001
        ans=0
        for num in A:
            dct[num]+=1
        for i in range(len(dct)):
            if dct[i]>1:
                ans+=dct[i]-1
                dct[i+1]+=dct[i]-1
        return ans

在这里插入图片描述
多么朴素的解法啊,只需要一个排序,然后从前往后的遍历,碰到重复的就往后移动,跟我纸上推出来的规律是一样的,不过我的解法超时了
其余的解法我也不会了,,看下别人的题解吧

官方题解

在这里插入图片描述
我第二次的尝试和这个方法一很像啊…不过我是每次碰到重复元素都找一次0…超时了

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

重点是怎么找到这些没出现过的2,4,6
在这里插入图片描述
噢。。可以在读数据的时候就进行移动操作啊…😅
尝试着自己重新写了一下

class Solution:
    def minIncrementForUnique(self, A: List[int]) -> int:
        dct=[0]*80001
        lst=[]	#存放重复数
        for num in A:
            dct[num]+=1
        ans=0
        for i in range(len(dct)):
            if dct[i]>1:
                lst.append(i)
            elif dct[i]==0: #找到一个0
                if lst==[]:	#没有重复数则跳过
                    continue
                num=lst[0]	#取出最小的重复数
                dct[lst[0]]-=1
                ans+=i-lst[0]
                if dct[lst[0]]==1:	#移动lst
                    lst=lst[1:]
        return ans

在这里插入图片描述
通过了
照着优化又改进了一下,优化先减再加的原因就是ans总是等于新的0的下标-旧的重复的下标,那么先减再加完全没有问题

不用存重复的数字了,很方便,但是我们还要存目前有多少个数字重复了,这样在遇到0的时候再加上

class Solution:
    def minIncrementForUnique(self, A: List[int]) -> int:
        dct=[0]*80001
        ans=0
        cnt=0	#记录有多少个重复数字
        for num in A:
            dct[num]+=1
        for i in range(len(dct)):
            if dct[i]>1:
                ans-=(dct[i]-1)*i	#先减
                cnt-=(dct[i]-1)		#记录重复数字
            elif dct[i]==0:
                if cnt==0:	#当前并没有重复数字
                    continue
                ans+=i
                cnt+=1
        return ans

在这里插入图片描述
更快了

方法二 排序
在这里插入图片描述
这个解法很妙啊,特别是这一点

如果 A[i - 1] < A[i],则区间 \big[A[i - 1] + 1, A[i] - 1\big][A[i−1]+1,A[i]−1] 里的数都是没有出现过的

那每次碰到一个空的区间,就可以把之前重复的数填进来了
尝试着自己写一下

class Solution:
    def minIncrementForUnique(self, A: List[int]) -> int:
        A.sort()
        ans=0
        cnt=0
        for i in range(1,len(A)):
            if A[i-1]==A[i]:
                ans-=A[i]
                cnt+=1
            elif A[i-1]<A[i] and cnt:
                #尝试填满[A[i-1]+1,A[i]]区间的数
                for num in range(A[i-1]+1,A[i]):
                    if cnt==0:
                        break
                    ans+=num
                    cnt-=1
        # 遍历到最后一个数A[-1],如果还有重复数
        # 那么需要把所有重复数移动到最后一个数上面,然后再往后移动,直到没有重复数
        # 往后移动的次数可以用求和公式来计算
        if cnt:
            ans+=A[-1]*cnt+int((cnt+1)*cnt/2)
        return ans

在这里插入图片描述
求和公式的出镜率很高啊

官方题解的做法更加精简了(这就是强者的世界吗)
主要还是这个公式
在这里插入图片描述
用求和公式来替换,左边的A[i-1]*give,是因为有可能A[i-1]并非重复数,例如

1 1 1 1 2 6

2并没有重复,之前重复的3个1要移动3次来移动到2,然后再往后移动

class Solution:
    def minIncrementForUnique(self, A: List[int]) -> int:
        A.sort()
        A.append(100000)
        ans = taken = 0

        for i in range(1, len(A)):
            if A[i-1] == A[i]:
                taken += 1
                ans -= A[i]
            else:
                give = min(taken, A[i] - A[i-1] - 1)
                ans += give * (give + 1) // 2 + give * A[i-1]
                taken -= give

        return ans

作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/minimum-increment-to-make-array-unique/solution/shi-shu-zu-wei-yi-de-zui-xiao-zeng-liang-by-leet-2/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

在这里插入图片描述

贴一个别人的题解
https://leetcode-cn.com/problems/minimum-increment-to-make-array-unique/solution/ji-shu-onxian-xing-tan-ce-fa-onpai-xu-onlogn-yi-ya/#%E4%B8%80%E3%80%81%E6%8E%92%E5%BA%8Fonlogn
这里面的线性探测法很妙

下面的内容摘自上面的链接

线性探测法

在这里插入图片描述
确实是这样的
在这里插入图片描述
重点就是这个压缩变成4的过程
在这里插入图片描述
这样能节省很多查找比较的时间,真强
自己尝试一下实现

class Solution:
    def minIncrementForUnique(self, A: List[int]) -> int:
        dct=[-1]*80000
        ans=0
        for num in A:
            if dct[num]!=-1: #产生冲突
                #做法:按照下标走到第一个为空的位置,记录增加的次数,同时更新走过的下标为当前的位置
                lst=[]  #存放需要更新下标的数字
                start=num #记录初始的位置
                while dct[num]!=-1:
                    lst.append(num)
                    num=dct[num]+1
                ans+=num-start    #找到了空位,记录移动次数
                for n in lst:
                    dct[n]=num
            dct[num]=num
        return ans

哎,这个方法真的很棒
需要注意的地方是,表里面存放的是下标的值,也就是dct[num]=num,表示它指向的最后路径(初始为本身)
dct的值初始为-1,因为数据会出现0

太秀了,还是递归

class Solution {
    int[] pos = new int [80000];
    public int minIncrementForUnique(int[] A) {
        Arrays.fill(pos, -1); // -1表示空位
        int move = 0;
        // 遍历每个数字a对其寻地址得到位置b, b比a的增量就是操作数。
        for (int a: A) {
            int b = findPos(a); 
            move += b - a;
        }
        return move;
    }
    
    // 线性探测寻址(含路径压缩)
    private int findPos(int a) {
        int b = pos[a];
        // 如果a对应的位置pos[a]是空位,直接放入即可。
        if (b == -1) { 
            pos[a] = a;
            return a;
        }
        // 否则向后寻址
        // 因为pos[a]中标记了上次寻址得到的空位,因此从pos[a]+1开始寻址就行了(不需要从a+1开始)。
        b = findPos(b + 1); 
        pos[a] = b; // 寻址后的新空位要重新赋值给pos[a]哦,路径压缩就是体现在这里。
        return b;
    }
}
作者:sweetiee
链接:https://leetcode-cn.com/problems/minimum-increment-to-make-array-unique/solution/ji-shu-onxian-xing-tan-ce-fa-onpai-xu-onlogn-yi-ya/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

照着这个思路改造了一下自己的解法

class Solution:
    def minIncrementForUnique(self, A: List[int]) -> int:
        self.dct=[-1]*80000
        ans=0
        for num in A:
            ans+=self.findPos(num)-num
        return ans

    def findPos(self,pos):
        #找到第一个为-1的下标,同时压缩路径
        t=self.dct[pos]
        #如果当前就是为空的下标,直接更新当前的pos,并返回找到了
        if t==-1:
            self.dct[pos]=pos   #更新下标
            return pos
        # 没找到,则递归,这里的t最终会等于那个值为1的下标
        t=self.findPos(t+1)
        self.dct[pos]=t #更新走过的下标
        return t

主要是这个递归不太好写
编写思路是这样的
findPos的一个功能是返回第一个pos,使得dct[pos]=-1
根据这个特点我们可以直接这样写

	def findPos(self,pos):
		if self.dct[pos]==-1:# 假如当前就是最终的pos,直接返回
			self.dct[pos]=pos
			return pos

那么找不到的话,一直递归即可

	def findPos(self,pos):
		if self.dct[pos]==-1:# 假如当前就是最终的pos,直接返回
			self.dct[pos]=pos
			return pos
		pos=findPos(pos+1)
		return pos

findPos的另一个功能是更新遍历过程中的下标t,使得dct[t]=pos
显然我们只会在找不到的时候更新下标

	def findPos(self,pos):
		if self.dct[pos]==-1:# 假如当前就是最终的pos,直接返回
			self.dct[pos]=pos
			return pos
		pos=findPos(pos+1)	#pos是最终的下标
		# self.dct[寻找过程中的下标]=pos
		return pos

于是我们上面的递归代码就需要加上一个临时变量来存储寻找过程中的下标

    def findPos(self,pos):
        t=self.dct[pos]
        #如果当前就是为空的下标,直接更新当前的pos,并返回找到了
        if t==-1:
            self.dct[pos]=pos   #更新下标
            return pos
        # 没找到,则递归,这里的t最终会等于那个值为1的下标
        t=self.findPos(t+1)
        self.dct[pos]=t #更新走过的下标
        return t

可行做法总结

第一个就是暴力了,每次碰到相同的数,就往后搬

class Solution:
    def minIncrementForUnique(self, A: List[int]) -> int:
        A.sort()
        dct=[0]*800001
        ans=0
        for num in A:
            dct[num]+=1
        for i in range(len(dct)):
            if dct[i]>1:
                ans+=dct[i]-1
                dct[i+1]+=dct[i]-1
        return ans

第二个就是一次遍历,记录有多少个重复的数字,如果碰到一个为0的位置,就把之前的数移动到当前位置上来
而且还有优化过了,先减去重复的数字,然后再加上移动的当前位置

class Solution:
    def minIncrementForUnique(self, A: List[int]) -> int:
        dct=[0]*80001
        ans=0
        cnt=0	#记录有多少个重复数字
        for num in A:
            dct[num]+=1
        for i in range(len(dct)):
            if dct[i]>1:
                ans-=(dct[i]-1)*i	#先减
                cnt-=(dct[i]-1)		#记录重复数字
            elif dct[i]==0:
                if cnt==0:	#当前并没有重复数字
                    continue
                ans+=i
                cnt+=1
        return ans

方法三 排序后利用求和公式

class Solution:
    def minIncrementForUnique(self, A: List[int]) -> int:
        A.sort()
        A.append(100000)
        ans = taken = 0

        for i in range(1, len(A)):
            if A[i-1] == A[i]:
                taken += 1
                ans -= A[i]
            else:
                give = min(taken, A[i] - A[i-1] - 1)
                ans += give * (give + 1) // 2 + give * A[i-1]
                taken -= give

        return ans

方法四就是上面的路径压缩了

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值