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。
注意:
- 假設所有輸入數字都是非負整數。
- 表示利潤和資本的陣列的長度不超過 50000。
- 答案保證在 32 位有符號整數範圍內。
解答
題目分析:
首先,由於每個專案都是獲得純利潤,即不存在資本的損耗,所以我們總是去尋找當前資本下可以獲得利潤最高的專案即可,再將獲取的利潤加入到資本 W 中。
這是典型的 “貪婪演算法”,我們每次都是尋找當前的最優解,並在投資 k 個不同專案後得到最優解。
思路1:
先說一個同樣可以實現,但是效率不怎麼高的實現吧(最終在輸入陣列長度為 50000 的 case 下執行超時了)。
- 首先,將所有專案根據利潤從大到小排序,使用快速排序,排序時間複雜度 O(n * lg n)。
- 其次,我們從頭開始遍歷排序後的集合,找到第一個當前資本能夠啟動的專案,將其利潤加入到資本 W 中,並將這個專案剔除。在這一步中,每一次我們都可以獲得一個最優解。
- 重複上一步,迴圈 k 次後得到最終最優解。
一切看起來合乎情理,然而提交程式碼出現了超時的情況。再回顧上述實現,發現查詢過程其實效率很低,最糟糕的情況下總是需要遍歷完整個集合才找到當前最優解(資金 W 能夠啟動的專案總是在最後一個)。這樣迴圈 k 次時間複雜度就達到了 O(k * n) 了。
思路2:
既然上述情況的效率卡在了查詢環節,我們就得想辦法優化這一塊了。仔細回顧原題,我們只有 W 的資本,大可不必在整個集合中依次查詢,假設我們有一個這樣的集合,它只包含我們能夠啟動的專案,那我們的搜尋範圍就小很多了,並在獲得利潤後再將當前 W 能夠啟動的專案加入到這個小集合中。
- 我們選擇使用一個優先佇列
profitQueue
,按照利潤 Pi 從高到低,將我們當前 W 資本能夠啟動的專案新增進入 - 我們在選擇使用一個優先佇列
capitalQueue
,按照啟動資本 Ci 從低到高,將剩餘的專案新增進入 - 我們在
profitQueue
取出利潤最大者剔除,然後更新當前資本 W,資本 W 更新了就需要將當前資本 W 能夠啟動的專案從capitalQueue
中移到profitQueue
中 - 重複上一步,執行 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;
}
}
}