LeetCode之786:第K个最小的素数分数

本文介绍了一种更高效的方法来寻找给定数组arr中第k个最小的由素数组成的分数arr[i]/arr[j]。使用优先队列和双指针技巧,时间复杂度降低到O(n log C),空间复杂度为O(n)。通过实例演示和代码实现解析算法原理。
摘要由CSDN通过智能技术生成

题目描述

给你一个按递增顺序排序的数组 arr 和一个整数 k 。数组 arr 由 1 和若干 素数 组成,且其中所有整数互不相同。
对于每对满足 0 < i < j < a r r . l e n g t h 0 < i < j < arr.length 0<i<j<arr.length i i i j j j ,可以得到分数 a r r [ i ] / a r r [ j ] arr[i] / arr[j] arr[i]/arr[j]
那么第 k k k 个最小的分数是多少呢? 以长度为 2 的整数数组返回你的答案, 这里 a n s w e r [ 0 ] = = a r r [ i ] answer[0] == arr[i] answer[0]==arr[i] a n s w e r [ 1 ] = = a r r [ j ] answer[1] == arr[j] answer[1]==arr[j]

示例1:

输入:arr = [1,2,3,5],  k = 3
输出:[2,5]
解释:已构造好的分数,排序后如下所示: 
1/5, 1/3, 2/5, 1/2, 3/5, 2/3
很明显第三个最小的分数是 2/5。

示例2:

输入:arr = [1,7], k = 1
输出:[1,7]

题解思路

记数组 a r r arr arr的长度为 n n n。最简单粗暴的想法就是将所有的可能枚举出来,共 n ( n − 1 ) 2 \frac{n(n - 1)}{2} 2n(n1)种可能,并将其排序,然后寻找第 k k k个小的。这种解法虽然能过,但效率还是特别低的,时间复杂度为 O ( n 2 l o g n ) O(n^{2}logn) O(n2logn),空间复杂度为 O ( n 2 ) O(n^{2}) O(n2)。这问题大嘛?问题不大,能写出来就行,但咱就是说有没有看上去高级一点的方法,还是有的。

优先队列(堆)

其实得益于数组 a r r arr arr是递增的,所以在所有形成的分数中,是有顺序的。我们将分母记为 a r r [ j ] arr[j] arr[j],对于每一个分母,记其分子为 a r r [ i ] arr[i] arr[i]。我们将每一个分母看成一个长度为 j j j的列表,即:
arr ⁡ [ 0 ] arr ⁡ [ j ] , arr ⁡ [ 1 ] arr ⁡ [ j ] , ⋯   , arr ⁡ [ j − 1 ] arr ⁡ [ j ] \frac{\operatorname{arr}[0]}{\operatorname{arr}[j]}, \frac{\operatorname{arr}[1]}{\operatorname{arr}[j]}, \cdots, \frac{\operatorname{arr}[j-1]}{\operatorname{arr}[j]} arr[j]arr[0],arr[j]arr[1],,arr[j]arr[j1]
显而易见,这些列表是递增的。此时,我们要找到这 n − 1 n-1 n1个列表中第 k k k个最小的分数,可以采用优先队列来解决。初始时,优先队列里包含着 n − 1 n-1 n1个分数( arr ⁡ [ 0 ] arr ⁡ [ 1 ] , ⋯   , arr ⁡ [ 0 ] arr ⁡ [ n − 1 ] \frac{\operatorname{arr}[0]}{\operatorname{arr}[1]}, \cdots, \frac{\operatorname{arr}[0]}{\operatorname{arr}[n-1]} arr[1]arr[0],,arr[n1]arr[0]),每次从队列中选取最小的分数, arr ⁡ [ i ] arr ⁡ [ j ] \frac{\operatorname{arr}[i]}{\operatorname{arr}[j]} arr[j]arr[i],如果 i + 1 = j i+1=j i+1=j,说明这个分数是 arr ⁡ [ j ] \operatorname{arr}[j] arr[j]队列中最大的一个;如果 i + 1 < j i+1<j i+1<j,说明我们选取了 arr ⁡ [ i ] arr ⁡ [ j ] \frac{\operatorname{arr}[i]}{\operatorname{arr}[j]} arr[j]arr[i]只好要将 arr ⁡ [ i + 1 ] arr ⁡ [ j ] \frac{\operatorname{arr}[i+1]}{\operatorname{arr}[j]} arr[j]arr[i+1]再放入队列中。此时的时间复杂度为 O ( k l o g n ) O(klogn) O(klogn),单次优先队列操作的复杂度为 O ( l o g n ) O(logn) O(logn),一共要执行 k k k次,空间复杂度为 O ( n ) O(n) O(n)
代码如下:

class Frac:
    def __init__(self, idx: int, idy: int, x: int, y: int) -> None:
        self.idx = idx
        self.idy = idy
        self.x = x
        self.y = y

    def __lt__(self, other: "Frac") -> bool:
    # python中的富比较方法,用于类之间的比较,比如对类的不同实例进行sort。
        return self.x * other.y < self.y * other.x


class Solution:
    def kthSmallestPrimeFraction(self, arr: List[int], k: int) -> List[int]:
        n = len(arr)
        q = [Frac(0, i, arr[0], arr[i]) for i in range(1, n)]
        # heapq是python中的用于实现堆的一个模块heapqify是将列表具有堆特征。
        heapq.heapify(q)
		
        for _ in range(k - 1):
            frac = heapq.heappop(q)
            i, j = frac.idx, frac.idy
            if i + 1 < j:
            	# 将arr[i + 1] / arr[j]放入优先队列中
                heapq.heappush(q, Frac(i + 1, j, arr[i + 1], arr[j]))
        
        return [q[0].x, q[0].y]

二分查找 + + +双指针

如果我们能找到一个数 α \alpha α,恰好有 k k k个素数分数小于 α \alpha α,那么这些分数中最大的就是我们要找的答案。首先,对于一个 α \alpha α,我们如何找到有多少个比它小的素数分数呢?这时我们可以采用双指针。

  • 设定指针 j j j来指定分母,从左往右,每次枚举一个分母;
  • 设定指针 i i i来指定分子,从左往右移动,最小是 0 0 0,最大是 j − 1 j-1 j1,并且移动的过程中,还要保证 arr ⁡ [ i ] arr ⁡ [ j ] < α \frac{\operatorname{arr}[i]}{\operatorname{arr}[j]}<\alpha arr[j]arr[i]<α成立。当 i i i移动停止后,说明 arr ⁡ [ 0 ] , ⋯   , arr ⁡ [ i ] \operatorname{arr}[0], \cdots, \operatorname{arr}[i] arr[0],,arr[i]都可以作为分子,即分母为 arr ⁡ [ j ] \operatorname{arr}[j] arr[j]的小于 α \alpha α的素数分数一共有 i + 1 i+1 i+1个;
  • j j j向右移动的过程中,将每个 j j j对应的 i + 1 i+1 i+1累加起来,就是最终的小于 α \alpha α的素数分数的个数;
  • 在移动的时候,记录一个当前最大素数分数[x, y];

那么如何找到我们需要的 α \alpha α呢?如果上述过程最后的个数等于 k k k,那说明选的 α \alpha α刚刚好。如果个数小于 k k k,那说明当前选取的 α \alpha α过小;如果个数大于 k k k,那说明当前选取的 α \alpha α过大。这很明显,可以用二分查找来寻找最合适的 α \alpha α
代码如下:

class Solution:
    def kthSmallestPrimeFraction(self, arr: List[int], k: int) -> List[int]:
    	n = len(arr)
        left, right = 0.0, 1.0
        while True:
            mid = (left + right) / 2
            i, count = -1, 0
            x, y = 0, 1
            for j in range(1, n):
                while arr[i + 1] / arr[j] < mid:
                    i += 1
                    if arr[i] * y > x * arr[j]:
                        x, y = arr[i], arr[j]
                count += i + 1
            if count == k: return [x, y]
            if count < k:
                left = mid
            if count > k:
                right = mid

最终的时间复杂度为 O ( n l o g C ) O(nlogC) O(nlogC) C C C为数组 a r r arr arr中的元素的上界,二分查找需要 ⌈ log ⁡ C 2 ⌉ = O ( log ⁡ C ) \left\lceil\log C^{2}\right\rceil=O(\log C) logC2=O(logC)次。每一步需要 O ( n ) O(n) O(n)的时间得到小于 α \alpha α的素数分数的个数。空间复杂度为 O ( 1 ) O(1) O(1)

  • 6
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值