演算法細節系列(21) 貪心有理?

2021-08-01 09:49:01 字數 4497 閱讀 9036

詳細**可以fork下github上leetcode專案,不定期更新。

題目摘自leetcode:

1. leetcode 502: ipo

2. leetcode 055: jump game

3. leetcode 330: patching array

刷完挑戰,繼續刷leetcode,遇到的第乙個題就是ipo,而這恰巧是貪心系列,那就順便把貪心給學了,多門技術,多條生路。

《演算法導論》也有關於貪心演算法的相關章節,但我還不敢看,無非怕被書中的論述思維給限制住了。所以先刷點題,對貪心有了基本了解後,回過頭來再看它的論證。

我所理解的貪心:

貪心,每一步決策都是區域性最優的?一種短視的行為?好吧,這是我對貪心最真切的認識了,沒有其他。日後,刷題時逐一完善加深對貪心的理解,話不多說,直接開始。

求n個專案所能累加的最大profit。

呵呵噠,leetcode的題目有個很大的特色,很多題在解釋中把思路明確告訴你了,我一開始就納悶了,直接找符合capital中的最大profit累加即可?好像也符合貪心的策略,選擇區域性最優。

思路:

沒錯,按照題目的意思來就可以了,**如下:

public

intfindmaximizedcapital(int k, int w, int profits, int capital) }}

if (index == -1) return total;

capital[index] = integer.max_value;

total += maxprofit;

}return total;

}

tle了,如果有n個專案,那麼上述**時間複雜度為o(

n2) 。嘿,其實在它貪心的背後,它是一道資料結構題,用到了優先佇列。

提到優先佇列,相信你能很快想出了思路,但這對我來說,不夠完美,如果不提優先佇列這想法,我就很難想到用這資料結構了。所以,我慢慢分析下為啥用到了優先佇列。

之前的博文中,我有提過所有的迴圈遍歷,如果沒有容器記錄狀態,都是無記憶遍歷,它們是一種非常低階的手段。就拿上述**:

for (int j = 0; j < capital.length; j++)

}}

遍歷整個capital陣列,只為找到最大值?更何況,當我使用完該最大值,我還得從capital陣列中把它標識為不可使用狀態,所以這樣乙個迴圈遍歷,每次都得遍歷整個陣列,然後找出乙個次大的。

嘿,這個特徵比較符合優先佇列了,用過的元素直接poll出去,而在隊頭的元素是次大的,直接把它poll出來。而構建優先佇列的時間複雜度只有o(

logn

) ,這是高階資料結構本身的特點,在構建之初就把大小關係維護進去,讓它再插入新元素時,能夠以較快的速度篩選出最大or最小。

思路:

1. 構造乙個pair對,把profit和capital關聯起來,兩者有著一一對應的關係。

2. 篩選資產,在當前總資產下,把所有capital[i]小於等於當前總資產取出,並存入另外乙個優先佇列中。

3. 該優先佇列維護profit的大小關係,隊頭永遠是符合資產中的最大profit(一種貪心策略)

總結:

看到刪除+最大or最小,想想優先佇列。

**如下:

private

class

pair

}public

intfindmaximizedcapital(int k, int w, int profits, int capital)

priorityqueueq1 = new priorityqueue<>((o1,o2) -> (o1.capital - o2.capital));

priorityqueueq2 = new priorityqueue<>((o1,o2) -> (o2.profit - o1.profit));

for (int i = 0; i < n; i++)

int total = w;

for (int i = 0; i < k; i++)

if (q2.isempty()) return total;

total += q2.poll().profit;

}return total;

}

這道題可謂是麻雀雖小,五臟俱全啊,貪心的味道很濃,得深入分析分析。

首先,看到這道題的第一眼,我想到了遞迴,思路如下:

根據當前能夠jump的步數,選擇後續的位置,這樣就變成了相同的子問題,而只要最終pos能夠抵達陣列末端就能返回true。可謂是信心滿滿啊,為了防止tle,還加了記憶化手段,**如下:

public

boolean

canjump(int nums)

private

boolean

canjump(int nums, int pos,boolean dp)}}

dp[pos] = true;

return

false;

}

呵呵噠,stackoverflow了,這只能說明我還太嫩,遞迴顯然無法解決該問題了,那這樣,就用動規咯,所以又想了個動規的方案。**如下:

public

boolean

canjump(int nums)

}return dp[nums.length - 1];

}

結果tle了,tle的原因在於step,上述**,i每遞增一次,都會更新step步的dp,如:

nums = [25000,25000,24000,1]

顯然沒必要更新step步,25000能走的位置涵蓋了所有位置,應該直接返回true即可。

此時,有了貪心,該貪心的含義是說,每到乙個新的位置時,更新我能覆蓋的所有範圍(取最大),這就意味著,dp的狀態沒必要全部更新,因為我們知道在該範圍內的dp都可以是true,換句話說,我們只需要知道乙個邊界即可。

所以**如下:

public

boolean

canjump(int nums)

return

true;

}

結構比起動規簡單很多,只需要o(

n)的時間複雜度。

這道題還未理解它,它的思路嘗試來證明下,幫助理解這道題為什麼是貪心。

思路:

patching array該問題的關鍵點在於用nums原有的資料集去構造0~n的數,舉個最簡單的例子:

nums = [1,2,5] n = 7

如何構造所有的和數?我們知道它們所有的和可以用三位1來表示:

111 表示 1+2+5=8

110 表示 1+2 = 3

所以總共有8種表示方法,如下:

000,001,010,011,100,101,110,111

得到的和從小到大排列為:

0,1,2,3,5,6,7,8

此處,我們可以明顯看到當n=7時,缺了乙個4,所以我們必須得補上。所以,

nums = [1,2,4,5] n = 7

以同樣的方式構造所有和,得到:

0,1,2,3,4,5,6,7 (由1,2,4得)

在此處,我們還發現乙個規律,當打乙個補丁滿足連續的數後,

我們在構造5的所有和時,可以直接在原來構造的和上加個5,所以有:

0,1,2,3,4,5,6,7

5,6,7,8,9,10,11,12

所以n在0~12之內都能滿足條件,此時我們再來nums = [1,2,4,5,23], n = 100

我們知道:[1,2,4,5]構造的連續和為:

0,1,2,3,4,5,6,7,8,9,10,11,12

而為了能夠構造盡可能多的和,構造的補丁一定為13,因為:

0,1,...,12

| 13,14,...,25

在這裡你看到了貪心,nums = [1,2,4,5],但我們沒有構造7的原因是因為構造7所能覆蓋的範圍非常少,如下:

0,1,2,...,6,...,12

| 7,8,9,10,...,19

構造7得到的範圍為0-19,構造13能夠得到的範圍為0-25,你選誰?

在這裡明確乙個補丁的性質,在已有的連續和上,新的連續和是【補丁+已有連續和】,想想000-111,擴充套件到4位的情況。

而連續和我們只要維護乙個界即可,所以有了網上大多數的做法,對具體做法感興趣的,可以搜搜。

**如下:

public

intminpatches(int nums, int n)

else

}return added;

}

注意miss的long,防止溢位,進入死迴圈。

貪心演算法 1 演算法導論 21

我們先來看看乙個適應貪心演算法求解的問題 選擇活動問題。假定有乙個 n 個活動的集合 s 每個活動 a i 的舉辦時間為 s i,f i 0 leqslant s i f i 並且假定集合 s 中的活動都已按結束時間遞增的順序排列好。由於某些原因,這些活動在同一時刻只能有乙個被舉辦,即對於任意兩個活...

面試衝刺演算法系列 21

看到公升序,降序等代表有序的詞就可以優先考慮二分法。設定兩個指標,分別指向第乙個k和最後乙個k,最後乙個k減去第乙個k的索引即可獲得k的個數。public class solutionif firstk 1 lastk 1 return0 public intfindfirstk int array...

演算法細節系列(26) 區間

詳細 可以fork下github上leetcode專案,不定期更新。題目摘自leetcode 思路 該開始使用了for迴圈中加入了stack的結構,但發現這種思路很難逼近答案。正確的思路應該為 for 如下 public listmerge listintervals else 合併到一半的區間最後...