[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
原文地址:https://www.cnblogs.com/zhcpku/p/14600369.html