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

786. 第 K 个最小的素数分数

原题出处:

https://leetcode-cn.com/problems/k-th-smallest-prime-fraction/

一个已排序好的表 A,其包含 1 和其他一些素数.  当列表中的每一个 p

那么第 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 kthSmallestPrimeFraction(vector& A, int K) {
		priority_queue 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

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 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小的分数值为 f(K)=p/q;此题A的分数组合中小于等于x的分数有 g(x)个。

用二分法,比较g(x)和K,试出f(K)≤ 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
class Solution {
public:
	vector kthSmallestPrimeFraction(vector& 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]

总结:

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

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

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

你可能感兴趣的:(动态规划,LeetCode,分治,Python)