786. 第 K 个最小的素数分数
原题出处:
https://leetcode-cn.com/problems/k-th-smallest-prime-fraction/
一个已排序好的表 A,其包含 1 和其他一些素数. 当列表中的每一个 p<q 时,我们可以构造一个分数 p/q 。
那么第 k 个最小的分数是多少呢? 以整数数组的形式返回你的答案, 这里 answer[0] = p 且 answer[1] = q.
示例:
输入: A = [1, 2, 3, 5], K = 3
输出: [2, 5]
解释:
已构造好的分数,排序后如下所示:
1/5, 1/3, 2/5, 1/2, 3/5, 2/3.
很明显第三个最小的分数是 2/5.
输入: A = [1, 7], K = 1
输出: [1, 7]
注意:
A 的取值范围在 2 — 2000.
每个 A[i] 的值在 1 —30000.
K 取值范围为 1 —A.length * (A.length - 1) / 2
暴力解法:
先来个笨办法,就是枚举出所有的分数,再用堆排序找出第K小的,时间复杂度为 O(max{A.length^2, K*logK})。C++版本代码如下:
//超出时间限制!
class Solution {
struct Fraction{
int p, q; //分子,分母
Fraction(int p, int q) :p(p), q(q) {}
bool operator<(const Fraction& r)const { //用于小根堆
return this->p*r.q > this->q*r.p;
}
};
public:
vector<int> kthSmallestPrimeFraction(vector<int>& A, int K) {
priority_queue<Fraction> Q; //功能同堆
size_t N_1 = A.size() - 1;
for (size_t i = 0; i < N_1; i++){
for (size_t j = i+1; j <= N_1; j++){
Q.push(Fraction(A[i], A[j]));
}
}
while (K-- > 1)Q.pop(); //取出K-1个堆顶
return{ Q.top().p,Q.top().q }; //剩下的就是第K小的数
}
};
果不其然,这个程序是超时的!对于困难的题目,这么简单暴力的解法是不行的。
分析:
N=A.length个非合数有 个组合即分数,要从中找出第K小的分数。要节省计算量,就必须找处批量比较这些分数的方法。题中已经说明数列A是有序的,提醒我们要利用A可以有序排列的特征。
设 0<= i <j < N,则p=A[i],q=A[j]分别表示分子、分母(∵题中要求分子p<分母q)。那么当i,j增大时,分数分别是增大和减少。那么就可以利用此规律,快速的找出比x小的分数有多少个。
Python代码如下:(后面还有C++版本)
class Solution:
#计数A中组成的分数中,≤x的有多少个,并返回其中的最大分数
def count_less_than(self,A,x):
cnt,j=0,1
maxf=[0.0,0,0]
for i,p in enumerate(A[0:-1]):#分子递增
j=max(j,i+1) #每一轮取更大分子后,分母不可能减少,否则不可能满足 frac<=x。又分母必须>分子。
while j<len(A): #分母下标
frac=p/A[j] #分数值 frac
if frac <=x:
if frac> maxf[0]: #在所有 frac<=x 中记录最大的 frac
maxf= [frac,p,A[j]]
cnt+=len(A)-j #累计满足 frac<=x 的个数
break
j+=1 #分母递增
if j>=len(A): break #n作为分子,分母取到最大都不满足“frac<=x”了,则n增大也必不满足。
return cnt,maxf[1],maxf[2] #返回:满足 frac<=x 的个数 , 其中最大分数的 分子、分母
#LeetCode规定的主函数
def kthSmallestPrimeFraction(self, A: List[int], K: int) -> List[int]:
l,r=0.0,1.0 #依据题意,分数必介于开区间(0,1)
while True:
m=(l+r)/2 #二分法
cnt,p,q=self.count_less_than(A,m)
if cnt==K: return [p,q] #恰有K个 ≤ x的分数,其中最大分数即为所求。
elif cnt<K: l=m
else: r=m
设第K小的分数值为 f(K)=p/q;此题A的分数组合中小于等于x的分数有 g(x)个。
用二分法,比较g(x)和K,试出f(K)≤ x <f(K+1)中的x。在求g(x)的同时,记录≤x的分数中最大者即为所求。
而求g(x)的时间复杂度至多为O(N),基本思路如下:
- 以分子递增外循环,分母递增内循环,一旦内循环出现≤x的分数,则更大的分母必然也满足≤x,记录信息并跳出内循环。
- 外循环分子增大后,要满足分数≤x,则分母不可能减少,故分母可接上一循环的值开始(但须分母>分子)。
所以,分子A[i]和分母A[j]都至多只有一趟遍历。
二分法试出介于第K小和第K+1小分数值之间的x,其时间复杂度为,若近似f(i)在0~1的分布均匀,则时间复杂度近似为 O(log(N))。
则该解法总的时间复杂度为 O(N·logN),而且空间复杂度为O(1),完胜暴力枚举法。
C++,追求执行时间版本:
#include<math.h>
class Solution {
public:
vector<int> kthSmallestPrimeFraction(vector<int>& A, int K) {
int p, q = 1, N_1 = A.size()-1;
if (1 == K)return{ A[0],A[N_1] };
double left = 0, right = 1;
while (true) {
double mid = (right + left) / 2.0; //预估第K小的分数值
int cnt = 0; p = 0; // p/q 与 mid 比较,初始置 p/q=0,即p=0
for (int i = 0, j = 1; i < N_1; ++i) {
//if (j <= i)j = i + 1; 没必要
int mq = ceil(A[i] / mid);
if (mq <= A[N_1]) {
while (A[j]<mq) ++j; //找出使得 A[i]/A[j]<=mid 成立的最小j
cnt += A.size() - j; //累计所有 ≤ mid的分数组合个数
if (j <= N_1 && p * A[j] < q * A[i]) { //★当前p/q比上一次的更大
p = A[i]; //意味着 当前的p/q更接近 mid,故需更新。
q = A[j];
}
}else break; //A[i]/A[N_1] 都>mid 了,无需继续遍历!
}
if (cnt == K) return{ p, q };
else if (cnt < K) left = mid;
else right = mid;
}
}
};
static const auto __ = []() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
return nullptr;
}();
总结:
对于有序序列中找满足某条件的元素时,应利用其有序的规律,采用分治的思想,逐渐缩小搜索范围,最终确定所需的元素。
因为比较大小的是分数的值,所以应以分数的值作为二分法的关键词。
如有不恰当之处,欢迎高手指教。