公司网站建设工作通知/特大新闻凌晨刚刚发生
题目描述
给你一个按递增顺序排序的数组 arr 和一个整数 k 。数组 arr 由 1 和若干 素数 组成,且其中所有整数互不相同。
对于每对满足 0<i<j<arr.length0 < i < j < arr.length0<i<j<arr.length 的 iii 和 jjj ,可以得到分数 arr[i]/arr[j]arr[i] / arr[j]arr[i]/arr[j] 。
那么第 kkk 个最小的分数是多少呢? 以长度为 2 的整数数组返回你的答案, 这里 answer[0]==arr[i]answer[0] == arr[i]answer[0]==arr[i] 且 answer[1]==arr[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]
题解思路
记数组arrarrarr的长度为nnn。最简单粗暴的想法就是将所有的可能枚举出来,共n(n−1)2\frac{n(n - 1)}{2}2n(n−1)种可能,并将其排序,然后寻找第kkk个小的。这种解法虽然能过,但效率还是特别低的,时间复杂度为O(n2logn)O(n^{2}logn)O(n2logn),空间复杂度为O(n2)O(n^{2})O(n2)。这问题大嘛?问题不大,能写出来就行,但咱就是说有没有看上去高级一点的方法,还是有的。
优先队列(堆)
其实得益于数组arrarrarr是递增的,所以在所有形成的分数中,是有顺序的。我们将分母记为arr[j]arr[j]arr[j],对于每一个分母,记其分子为arr[i]arr[i]arr[i]。我们将每一个分母看成一个长度为jjj的列表,即:
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[j−1]
显而易见,这些列表是递增的。此时,我们要找到这n−1n-1n−1个列表中第kkk个最小的分数,可以采用优先队列来解决。初始时,优先队列里包含着n−1n-1n−1个分数(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[n−1]arr[0]),每次从队列中选取最小的分数,arr[i]arr[j]\frac{\operatorname{arr}[i]}{\operatorname{arr}[j]}arr[j]arr[i],如果i+1=ji+1=ji+1=j,说明这个分数是arr[j]\operatorname{arr}[j]arr[j]队列中最大的一个;如果i+1<ji+1<ji+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(klogn)O(klogn)O(klogn),单次优先队列操作的复杂度为O(logn)O(logn)O(logn),一共要执行kkk次,空间复杂度为O(n)O(n)O(n)。
代码如下:
class Frac:def __init__(self, idx: int, idy: int, x: int, y: int) -> None:self.idx = idxself.idy = idyself.x = xself.y = ydef __lt__(self, other: "Frac") -> bool:# python中的富比较方法,用于类之间的比较,比如对类的不同实例进行sort。return self.x * other.y < self.y * other.xclass 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.idyif 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α,恰好有kkk个素数分数小于α\alphaα,那么这些分数中最大的就是我们要找的答案。首先,对于一个α\alphaα,我们如何找到有多少个比它小的素数分数呢?这时我们可以采用双指针。
- 设定指针jjj来指定分母,从左往右,每次枚举一个分母;
- 设定指针iii来指定分子,从左往右移动,最小是000,最大是j−1j-1j−1,并且移动的过程中,还要保证arr[i]arr[j]<α\frac{\operatorname{arr}[i]}{\operatorname{arr}[j]}<\alphaarr[j]arr[i]<α成立。当iii移动停止后,说明arr[0],⋯,arr[i]\operatorname{arr}[0], \cdots, \operatorname{arr}[i]arr[0],⋯,arr[i]都可以作为分子,即分母为arr[j]\operatorname{arr}[j]arr[j]的小于α\alphaα的素数分数一共有i+1i+1i+1个;
- 在jjj向右移动的过程中,将每个jjj对应的i+1i+1i+1累加起来,就是最终的小于α\alphaα的素数分数的个数;
- 在移动的时候,记录一个当前最大素数分数[x, y];
那么如何找到我们需要的α\alphaα呢?如果上述过程最后的个数等于kkk,那说明选的α\alphaα刚刚好。如果个数小于kkk,那说明当前选取的α\alphaα过小;如果个数大于kkk,那说明当前选取的α\alphaα过大。这很明显,可以用二分查找来寻找最合适的α\alphaα。
代码如下:
class Solution:def kthSmallestPrimeFraction(self, arr: List[int], k: int) -> List[int]:n = len(arr)left, right = 0.0, 1.0while True:mid = (left + right) / 2i, count = -1, 0x, y = 0, 1for j in range(1, n):while arr[i + 1] / arr[j] < mid:i += 1if arr[i] * y > x * arr[j]:x, y = arr[i], arr[j]count += i + 1if count == k: return [x, y]if count < k:left = midif count > k:right = mid
最终的时间复杂度为O(nlogC)O(nlogC)O(nlogC),CCC为数组arrarrarr中的元素的上界,二分查找需要⌈logC2⌉=O(logC)\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)。