题目描述
给你一个按递增顺序排序的数组 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(n−1)种可能,并将其排序,然后寻找第 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[j−1]
显而易见,这些列表是递增的。此时,我们要找到这
n
−
1
n-1
n−1个列表中第
k
k
k个最小的分数,可以采用优先队列来解决。初始时,优先队列里包含着
n
−
1
n-1
n−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
=
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 j−1,并且移动的过程中,还要保证 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)。