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方法。

 

這樣的代碼,如果理解的話,代碼量小,同時思路清晰,可讀性強,易於維護,由此可見:好的數據結構和算法,能減少很多複雜繁多的工作量。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章