786. 第 K 个最小的素数分数(LeetCode)C++/Python

6 篇文章 0 订阅
4 篇文章 0 订阅

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个非合数有 \frac{N\cdot (N+1)}{2}个组合即分数,要从中找出第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),基本思路如下:

  1. 以分子递增外循环,分母递增内循环,一旦内循环出现≤x的分数,则更大的分母必然也满足≤x,记录信息并跳出内循环。
  2. 外循环分子增大后,要满足分数≤x,则分母不可能减少,故分母可接上一循环的值开始(但须分母>分子)。

所以,分子A[i]和分母A[j]都至多只有一趟遍历。

二分法试出介于第K小和第K+1小分数值之间的x,其时间复杂度为O\left ( log_{2}\left [ \frac{1.0}{f(K+1)-f(K)} \right ] \right ),若近似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;
}();

总结:

    对于有序序列中找满足某条件的元素时,应利用其有序的规律,采用分治的思想,逐渐缩小搜索范围,最终确定所需的元素。

因为比较大小的是分数的值,所以应以分数的值作为二分法的关键词。

    如有不恰当之处,欢迎高手指教。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值