[LeetCode 786.] K-th Smallest Prime Fraction
LeetCode 786. K-th Smallest Prime Fraction
一道经典题,给出两种经典解法。
题目描述
You are given a sorted integer array arr containing 1 and prime numbers, where all the integers of arr are unique. You are also given an integer k.
For every i and j where 0 <= i < j < arr.length, we consider the fraction arr[i] / arr[j].
Return the kth smallest fraction considered. Return your answer as an array of integers of size 2, where answer[0] == arr[i] and answer[1] == arr[j].
Example 1:
Input: arr = [1,2,3,5], k = 3
Output: [2,5]
Explanation: The fractions to be considered in sorted order are:
1/5, 1/3, 2/5, 1/2, 3/5, and 2/3.
The third fraction is 2/5.
Example 2:
Input: arr = [1,7], k = 1
Output: [1,7]
Constraints:
- 2 <= arr.length <= 1000
- 1 <= arr[i] <= 3 * 104
- arr[0] == 1
- arr[i] is a prime number for i > 0.
- All the numbers of arr are unique and sorted in strictly increasing order.
- 1 <= k <= arr.length * (arr.length - 1) / 2
解题思路
第一种解法是使用堆,类似 LeetCode 373. Find K Pairs with Smallest Sums,需要自定义 comparator,参考前面的【priority_queue 自定义 comparator】这种写法,重点在于会自定义 comparator。
这俩有一个重点优化在于我们不需要把所有合法分数入队一遍,只需要根据分数的有序性质,入队头,然后按多路排序的思路每次出队再把自己的后继入队即可。
堆解法的空间复杂度 O(K)
,时间复杂度 O(N+K+K*logK)
。
第二种解法是使用二分查找,可能不是那么直观就能想到。首先分数的取值范围是 [0, 1],我们用浮点数来搜索一个可能的上限值,使其满足比这个值小的分数恰好是 k 个。在统计比浮点数小的值的过程中,同时找出一个比浮点数小但距离最近的分数,这个分数就是所求。
二分查找空间复杂度 O(1),时间复杂度是多少?每一轮确定搜索区间后的统计阶段,时间复杂度是 O(N*N)
,那么搜索区间最多有几轮?
O(log1.0) 显然是错误的。要注意,这并不是一个整数区间,所以并不能认为是进行了O(区间长)的轮数。
我们注意到,我们搜索的 [l, r] 然后统计小于 m 的分数个数,本质上m的取值是在两个候选分数之间的“空隙”中,而每个“空隙”实际上最多进一次,空隙个数本质上看的是候选分数个数,所以轮数是 O(log(N*N))=O(logN)
,总体时间复杂度 O(N*N*logN)
。
参考代码
堆自定义 comparator 的解法
/*
* @lc app=leetcode id=786 lang=cpp
*
* [786] K-th Smallest Prime Fraction
*/
// @lc code=start
class Solution {
public:
vector<int> kthSmallestPrimeFraction(vector<int>& arr, int k) {
using PII = pair<int,int>;
auto cmp = [&](const PII& a, const PII& b) {
auto [i1, j1] = a;
auto [i2, j2] = b;
return (double)arr[i1]/arr[j1] > (double)arr[i2]/arr[j2];
};
priority_queue<PII, deque<PII>, decltype(cmp)> q(cmp);
int n = arr.size();
for (int j=n-1; j>=1 && n-j<=k; j--) {
q.emplace(0, j);
}
while (--k) {
auto [i, j] = q.top();
q.pop();
// if (i+1 < n) {
if (i+1 < j) {
q.emplace(i+1, j);
}
}
auto [i, j] = q.top();
return {arr[i], arr[j]};
} // AC
};
// @lc code=end
二分查找的解法
这里其实还可以进一步优化,比如统计 cnt 的时候每个分子对应的一系列分数是有序的,遇到临界值就可以跳出循环了。更进一步,搜索临界值也不必线性扫描,二分查找就可以,每一轮统计只需要 O(N*logN)
时间就可以了。
/*
* @lc app=leetcode id=786 lang=cpp
*
* [786] K-th Smallest Prime Fraction
*/
// @lc code=start
class Solution {
public:
vector<int> kthSmallestPrimeFraction(vector<int>& arr, int k) {
int n = arr.size();
int p, q;
double l = 0.0, r = 1.0;
while (l < r) {
double m = l + (r - l) / 2.0;
int cnt = 0;
double res = 0.0;
for (int i=0; i<n; i++) {
for (int j=i+1; j<n; j++) {
double f = (double)arr[i] / arr[j];
if (f < m) {
cnt ++;
if (res < f) {
res = f;
p = arr[i];
q = arr[j];
}
}
}
}
if (cnt == k) break;
else if (cnt < k) l = m;
else r = m;
}
return {p, q};
} // AC, vital
};
// @lc code=end