1. 程式人生 > >LeetCode : 502. IPO 分析與解答

LeetCode : 502. IPO 分析與解答

502. IPO

假設 LeetCode 即將開始其 IPO。為了以更高的價格將股票賣給風險投資公司,LeetCode希望在 IPO 之前開展一些專案以增加其資本。 由於資源有限,它只能在 IPO 之前完成最多 k 個不同的專案。幫助 LeetCode 設計完成最多 k 個不同專案後得到最大總資本的方式。

給定若干個專案。對於每個專案 i,它都有一個純利潤 Pi,並且需要最小的資本 Ci 來啟動相應的專案。最初,你有 W 資本。當你完成一個專案時,你將獲得純利潤,且利潤將被新增到你的總資本中。

總而言之,從給定專案中選擇最多 k 個不同專案的列表,以最大化最終資本,並輸出最終可獲得的最多資本。

示例 1:

輸入: k=2, W=0, Profits=[1,2,3], Capital=[0,1,1].

輸出: 4

解釋:
由於你的初始資本為 0,你儘可以從 0 號專案開始。
在完成後,你將獲得 1 的利潤,你的總資本將變為 1。
此時你可以選擇開始 1 號或 2 號專案。
由於你最多可以選擇兩個專案,所以你需要完成 2 號專案以獲得最大的資本。
因此,輸出最後最大化的資本,為 0 + 1 + 3 = 4。

注意:

  1. 假設所有輸入數字都是非負整數。
  2. 表示利潤和資本的陣列的長度不超過 50000。
  3. 答案保證在 32 位有符號整數範圍內。

解答

題目分析:

首先,由於每個專案都是獲得純利潤,即不存在資本的損耗,所以我們總是去尋找當前資本下可以獲得利潤最高的專案即可,再將獲取的利潤加入到資本 W 中。

這是典型的 “貪婪演算法”,我們每次都是尋找當前的最優解,並在投資 k 個不同專案後得到最優解。

思路1:

先說一個同樣可以實現,但是效率不怎麼高的實現吧(最終在輸入陣列長度為 50000 的 case 下執行超時了)。

  1. 首先,將所有專案根據利潤從大到小排序,使用快速排序,排序時間複雜度 O(n * lg n)。
  2. 其次,我們從頭開始遍歷排序後的集合,找到第一個當前資本能夠啟動的專案,將其利潤加入到資本 W 中,並將這個專案剔除。在這一步中,每一次我們都可以獲得一個最優解。
  3. 重複上一步,迴圈 k 次後得到最終最優解。

一切看起來合乎情理,然而提交程式碼出現了超時的情況。再回顧上述實現,發現查詢過程其實效率很低,最糟糕的情況下總是需要遍歷完整個集合才找到當前最優解(資金 W 能夠啟動的專案總是在最後一個)。這樣迴圈 k 次時間複雜度就達到了 O(k * n) 了。

思路2:

既然上述情況的效率卡在了查詢環節,我們就得想辦法優化這一塊了。仔細回顧原題,我們只有 W 的資本,大可不必在整個集合中依次查詢,假設我們有一個這樣的集合,它只包含我們能夠啟動的專案,那我們的搜尋範圍就小很多了,並在獲得利潤後再將當前 W 能夠啟動的專案加入到這個小集合中。

  1. 我們選擇使用一個優先佇列 profitQueue,按照利潤 Pi 從高到低,將我們當前 W 資本能夠啟動的專案新增進入
  2. 我們在選擇使用一個優先佇列 capitalQueue,按照啟動資本 Ci 從低到高,將剩餘的專案新增進入
  3. 我們在 profitQueue 取出利潤最大者剔除,然後更新當前資本 W,資本 W 更新了就需要將當前資本 W 能夠啟動的專案從 capitalQueue 中移到 profitQueue
  4. 重複上一步,執行 k 次後最終獲得最優解

選擇使用兩個佇列的好處是,我們不在需要完整的查詢了,profitQueue 中一定都是我們可以啟動的專案,且最大利潤的專案總是在隊首,取出來的時間複雜度為 O(1),而插入新專案的時間複雜度也僅僅只有 O(lg n)。

實現:

import java.util.PriorityQueue;

/**
 * Copyright © 2018 by afei. All rights reserved.
 * 
 * @author: afei
 * @date: 2018年11月2日
 */

public class Solution {

    public static void main(String[] args) {
        int k = 2;
        int W = 0;
        int[] Profits = { 1, 2, 3 };
        int[] Capital = { 0, 1, 1 };
        System.out.println(findMaximizedCapital(k, W, Profits, Capital));
    }

    public static int findMaximizedCapital(int k, int W, int[] Profits, int[] Capital) {
        // 這個佇列按照收益從大到小排序,且只包含能啟動的專案
        PriorityQueue<Project> profitQueue = new PriorityQueue<>((o1, o2) -> o2.profits - o1.profits);
        // 這個佇列按照啟動資本從小到大排序,且只包含目前資本 W 不能啟動的專案
        PriorityQueue<Project> capitalQueue = new PriorityQueue<>((o1, o2) -> o1.capital - o2.capital);
        for (int i = 0; i < Profits.length; i++) {
            if (Capital[i] > W) {
                capitalQueue.add(new Project(Profits[i], Capital[i]));
            } else {
                profitQueue.add(new Project(Profits[i], Capital[i]));
            }
        }
        for (int i = 0; i < k; i++) {
            if (profitQueue.isEmpty()) {
                break; // profitQueue 為空,即沒有能夠啟動的專案了,結束迴圈
            }
            W += profitQueue.poll().profits; // 取出收益最大的元素,並更新資本 W
            while (!capitalQueue.isEmpty() && W >= capitalQueue.peek().capital) {
                // 將 capitalQueue 能夠啟動的專案加入到 profitQueue 中
                profitQueue.offer(capitalQueue.poll());
            }
        }
        return W;
    }

    static class Project {
        int profits;
        int capital;

        public Project(int profits, int capital) {
            this.profits = profits;
            this.capital = capital;
        }
    }
}

專案地址

原題地址