LeetCode 786. 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

本人比较愚笨,想到的方法是这样的:

1.比如A = [1, 2, 3, 5],则先拼出一个数组A1:1/5, 1/3, 1/2,这样的数列是天然有序的;

2.再拼出一个有序数组A2:2/5,2/3,A3:3/5,然后合并A1,A2,A3;

3.最后获取对应下标的数据即可。

但是这样实践后证明,进行了太多的数组之间的赋值、比较和copy,效率很低下,同时代码量大,可读性差,因此参考了一下别人的答案。有兴趣的可以去看一下,

链接:http://bookshadow.com/weblog/2018/02/18/leetcode-k-th-smallest-prime-fraction/

 

看完之后研究了好久才看懂,原文中没有讲原理,所以这里将原理记录一下。

这道题首先涉及到一个分数的比较,当然可以将两个int类型的数强转为double类型进行除法运算然后比较,还有一种巧妙的方法是交叉相乘法:

原理就是:a/b > c/d,且a,b,c,d都大于0,则((a/b) * (d/c)) > ((c/d) * (d/c)),后面的式子可以约分掉,则ad/bc > 1,则ad > bc。

 

上代码:

public static int[] kthSmallestPrimeFraction1(final int[] A, int K) {

        class Pair implements Comparable<Pair>{
            public int x;
            public int y;
            public Pair(int x, int y) {
                this.x = x;
                this.y = y;
            }
            @Override
            public int compareTo(Pair p) {
                return A[x] * A[p.y] - A[y] * A[p.x];
            }
        }

        PriorityQueue<Pair> pq = new PriorityQueue<>();
        for (int i = 1; i < A.length; i++) {
            pq.add(new Pair(0, i));
        }
        Pair top = null;
        for (int i = 0; i < K; i++) {
            top = pq.poll();
            if (top.x + 1 < top.y) {
                pq.add(new Pair(top.x + 1, top.y));
            }
        }
        return new int[]{A[top.x], A[top.y]};
    }

PriorityQueue 一个基于优先级的无界优先级队列。优先级队列的元素按照其自然顺序进行排序,或者根据构造队列时提供的 Comparator 进行排序,具体取决于所使用的构造方法。该队列不允许使用 null 元素也不允许插入不可比较的对象(没有实现Comparable接口的对象)。
PriorityQueue 队列的头指排序规则最小那个元素。如果多个元素都是最小值则随机选一个。

 

该方法定义了一个内部类Pair,类中定义了两个属性,分子x和分母y,并且实现了Comparator接口,重写了compareTo方法,compareTo方法的比较规则就是刚才说的“交叉相乘”法。

接着新建了一个PriorityQueue队列,接着将新建的Pair对象加入到队列中,新建的Pair对象x都为0,y是1~A.length-1,

举例:如果A=[1, 2, 3, 5],则队列中的Pair对象是:Pair(0,1) Pair(0,2) Pair(0,3),并且已经是按大小顺序排好的1/5,1/3,1/2,也就是Pair(0,3),Pair(0,2),Pair(0,1)的顺序。

        PriorityQueue<Pair> pq = new PriorityQueue<>();
        for (int i = 1; i < A.length; i++) {
            pq.add(new Pair(0, i));
        }

还是之前的例子,A = [1, 2, 3, 5],则经过刚才的代码后,pq中的元素为:

1/5

1/3

1/2

 

接着就是这个算法的核心,PriorityQueue先将已经排好序的最小的一个元素取出来(原队列中该元素就没有了,也就是原队列长度减一),pq中的元素变为:

1/3

1/2

        Pair top = null;
        for (int i = 0; i < K; i++) {
            top = pq.poll();
            if (top.x + 1 < top.y) {
                pq.add(new Pair(top.x + 1, top.y));
            }
        }

top=Pair(0,3),x=0,y=3,我们发现0+1=1<3,所以会进入条件语句中,pq中增加新的元素:Pair(1,3),也就是2/5,这时pq中的元素为:

1/3

2/5

1/2

接着代码循环该步骤直到i=K,即此时的top元素即为第K个最小的分数。

 

这个解决思路的前提就是已知A数组是有序的,且第一位是1,所以1/5是能组成的最小的分数,然后继续将2/5和1/3比较,也就是5为分母的第二小的和3为分母的第一小的比较。队列中始终有最小的元素存在(已经弹出的不算)。

再通俗一点:A = [1, 2, 3, 5],则可以组成的分数有如下:(5为分母为一列,3为分母一列,2为分母一列)

 

pq队列中一开始把最左侧一列加入队列,弹出一个最小的,再把最小值挨着的右侧的一个元素加入队列(如果有的话),add时不需要考虑排序,因为队列重写了compareTo方法。

 

这样的代码,如果理解的话,代码量小,同时思路清晰,可读性强,易于维护,由此可见:好的数据结构和算法,能减少很多复杂繁多的工作量。

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值