本部分介紹“貪心算法“ 。 接下來會介紹動態規劃。回顧一下之前脈絡:
什麼是遞歸?如何設計遞歸算法?
||
\/
常見的遞歸算法應用(快排、歸併、堆、)
||
\/
深入遞歸本質:數學歸納,遞推
||
\/
深度遍歷優先搜索(DFS)、回溯、剪枝
||
\/
貪心算法、動態規劃
那麼貪心、動規與前面這些有什麼聯繫呢?爲什麼要放在這裏介紹?
- 首先,貪心、動規和dfs這樣的搜素算法實際很相似,是爲了搜索解空間獲得(滿足條件)的解。DFS是按照一定的(深度優先)次序。逐一枚舉遍歷。相比之下:
- 動態規劃和貪心算法同樣都是一種遞推算法. 均用局部最優解來推導全局最優解,是對遍歷空間的一種優化
- 當問題具有最優子結構時,可用動態規劃,而貪心是動態規劃的特例, 特殊之處在於 眼前的最優解即是全局最終的最優解。
- 因此我們在遍歷搜索解空間時,可以按照我們所預先設定的規則,逐一進行搜尋並縮小剩餘的解的空間,直至獲得所有解集。
- 其次,貪心和dp追根溯源是從子問題的解推出更大問題規模的解,這一點上和遞歸所追求的一脈相承。不過將會看到,很多時候由於遞歸本身的複雜冗餘計算和資源消耗,很多時候會對其進行簡化(dp中的備忘錄法等),形式上可能有所不同。個人覺得放在這裏介紹應該還算合理。
1. 零錢兌換
有固定面額的零錢數組coins=[1,2,5],每種有無窮多枚。現給定一個amount,由這些conins組成amount,比如(11 = 5+ 2+2+2)。
問,在所有能構成amount的硬幣組合中,所需硬幣最少多少枚?
例:
輸入:11
輸出:3
解釋:11=5*2+1,兩枚5元的,一枚1元的
思路:我們先不忙解題,來結合前面的介紹來體會一下貪心吧!
- 一堆coins組成amount,很顯然會有很多種不同的解。如果要求我們列出所有的解,暴力搜索、dfs都是不錯的方法。
- 在這些所有解當中,一定會有一個解,這個解中所使用的硬幣數量最少。
- 如何尋找出這個解呢?可以暴力搜索所有解,然後找出len最小的那個。
- 貪心是怎麼想的呢?正如我們平時買東西一樣,我們儘量每一次用最大面額去逼近amount,比如11元就用2個5元的,而不會用5個2元的。
我們在來看看dfs與貪心的區別與聯繫(相信各位心裏已經比較清楚了)
上圖是全部解空間樹,紅色數字表示該路徑上用一個這個面額的硬幣去組成。可以看到11 ==》 10 == 》5==》 0
這條路徑最短,即爲所求。
現在進一步地,體會一下局部最優即爲整體最優:
- 初始問題:amount=11,從coins中選出小於amount的最大面額硬幣(5元),amount變爲6元。
- 問題變爲:amount=6。同樣的,從coins中選出小於amount的最大面額硬幣(5元),amount變爲1元。
- 問題變爲:amount=1。於是再選一個1元硬幣即可。
我們每一次操作都選定一個硬幣,這是我們的局部最優,當解完後,我們發現每一次的局部最優也就構成了我們的整體最優解(11=5+5+1)。
進一步的,貪心思想這樣想,我們一開始就用最大的面額的最多張數去構成amount,比如11 就用兩張5元的。
下面是代碼,其中包含了dfs式寫法和迭代寫法,和前講述的遞歸設計取得了形式上的統一:
class Solution_g01{
int[] values = new int[] {1,2,5};
//迭代版
public int findMinCom1(int amount) {
int count = 0;
for(int i = values.length-1 ; i>=0 ; i -- ) {
int use = amount / values[i]; //最多能取多少個,當前最大面額的硬幣
count += use;
amount -= use *values[i]; //減去當前最大面額的硬幣數,進入下一輪迭代
}
return count;
}
//遞歸版
public int findMinCom2(int amount) {
int count = 0;
return dfs(amount , values.length -1);
}
private int dfs(int amount, int i) {
if(amount == 0 )
return 0;
int use = amount / values[i];
return use+dfs(amount - use * values[i], i-1);
}
}
2.柃檬水找零LeetCode860
在檸檬水攤上,每一杯檸檬水的售價爲 5 美元。
顧客排隊購買你的產品,(按賬單 bills 支付的順序)一次購買一杯。
每位顧客只買一杯檸檬水,然後向你付 5 美元、10 美元或 20 美元。你必須給每個顧客正確找零,也就是說淨交易是每位顧客向你支付 5 美元。
注意,一開始你手頭沒有任何零錢。
如果你能給每位顧客正確找零,返回 true ,否則返回 false 。
示例 1:
輸入:[5,5,5,10,20]
輸出:true
解釋:
前 3 位顧客那裏,我們按順序收取 3 張 5 美元的鈔票。
第 4 位顧客那裏,我們收取一張 10 美元的鈔票,並返還 5 美元。
第 5 位顧客那裏,我們找還一張 10 美元的鈔票和一張 5 美元的鈔票。
由於所有客戶都得到了正確的找零,所以我們輸出 true。示例 2:
輸入:[5,5,10]
輸出:true示例 3:
輸入:[10,10]
輸出:false示例 4:
輸入:[5,5,10,10,20]
輸出:false
解釋:
前 2 位顧客那裏,我們按順序收取 2 張 5 美元的鈔票。
對於接下來的 2 位顧客,我們收取一張 10 美元的鈔票,然後返還 5 美元。
對於最後一位顧客,我們無法退回 15 美元,因爲我們現在只有兩張 10 美元的鈔票。
由於不是每位顧客都得到了正確的找零,所以答案是 false。提示:
0 <= bills.length <= 10000
bills[i] 不是 5 就是 10 或是 20
思路:當給別人進行找零時,儘量先用大面額的找個他,比如20找15時,先用10元,再用5元。當所擁有的錢數無法滿足找零時返回false。
具體而言:
-
用兩個變量模擬5元和10各有多少張。
-
依次模擬買東西,根據不同的面額進行相應的處理
-
5元:fives++
-
10元:判斷fives > 0 , five --, tens++
-
20元: if tens > 0 : t -= 10;
t>0 且 fives >0 : t-=5, fives –
-
-
循環結束返回true
public boolean lemonadeChange(int[] bills) {
int fives = 0,tens = 0 ;
for(int b : bills) {
if(b == 5 ) fives ++;
else if(b == 10) {
if(fives > 0) fives --;
else return false;
tens++;
}else { //拿20過來買
int t = 15;
if(tens > 0) {
t -= 10;
tens --;
}
while(t >0 && fives > 0) {
t -= 5;
fives --;
}
if(t > 0) return false;
}
}
return true;
}
3.分發餅乾 LeetCode 455
假設你是一位很棒的家長,想要給你的孩子們一些小餅乾。但是,每個孩子最多隻能給一塊餅乾。對每個孩子 i ,都有一個胃口值 gi ,這是能讓孩子們滿足胃口的餅乾的最小尺寸;並且每塊餅乾 j ,都有一個尺寸 sj 。如果 sj >= gi ,我們可以將這個餅乾 j 分配給孩子 i ,這個孩子會得到滿足。你的目標是儘可能滿足越多數量的孩子,並輸出這個最大數值。
注意:
你可以假設胃口值爲正。
一個小朋友最多隻能擁有一塊餅乾。示例 1:
輸入: [1,2,3], [1,1]
輸出: 1
解釋:
你有三個孩子和兩塊小餅乾,3個孩子的胃口值分別是:1,2,3。
雖然你有兩塊小餅乾,由於他們的尺寸都是1,你只能讓胃口值是1的孩子滿足。
所以你應該輸出1。示例 2:
輸入: [1,2], [1,2,3]
輸出: 2
思路:
- 假設輸出答案爲k的話,表示一定能滿足從小到大排序中前k個小孩的胃口。 因此我們反過來想,假設給定的胃口序列不是單調遞增的,我們可以將其轉換爲單增,然後用餅乾序列去滿足。
- 如何滿足呢?對於每一個小孩胃口,我們儘量用所滿足的最小的餅乾去分配。
public int findContentChildren(int[] g, int[] s) {
Arrays.sort(g);
Arrays.sort(s);
int i = 0 , j = 0 , res = 0;
for(i = 0 ; i < g.length ; i++) {
while(j < s.length && s[j] < g[i] )j++; //當前餅乾小於當前小孩胃口,跳過,尋找下一塊餅乾
if(j < s.length) { //將當前餅乾分給第i個
res ++;
j++;
}
}
return res;
}
4.搖擺序列 LeetCode376
如果連續數字之間的差嚴格地在正數和負數之間交替,則數字序列稱爲擺動序列。第一個差(如果存在的話)可能是正數或負數。少於兩個元素的序列也是擺動序列。
例如, [1,7,4,9,2,5] 是一個擺動序列,因爲差值 (6,-3,5,-7,3) 是正負交替出現的。相反, [1,4,7,2,5] 和 [1,7,4,5,5] 不是擺動序列,第一個序列是因爲它的前兩個差值都是正數,第二個序列是因爲它的最後一個差值爲零。
給定一個整數序列,返回作爲擺動序列的最長子序列的長度。 通過從原始序列中刪除一些(也可以不刪除)元素來獲得子序列,剩下的元素保持其原始順序。
示例 1:
輸入: [1,7,4,9,2,5]
輸出: 6
解釋: 整個序列均爲擺動序列。示例 2:
輸入: [1,17,5,10,13,15,10,5,16,8]
輸出: 7
解釋: 這個序列包含幾個長度爲 7 擺動序列,其中一個可爲[1,17,10,13,10,16,8]。示例 3:
輸入: [1,2,3,4,5,6,7,8,9]
輸出: 2
進階:
你能否用 O(n) 時間複雜度完成此題?
- 思路: 先去重,然後找出序列中的所有極大值和極小值
public int wiggleMaxLength(int[] nums) {
//去重
int j = 0;
if(nums.length == 0) return 0;
for(int i = 1; i <nums.length ; i ++) {
if(nums[i] != nums[j])
nums[++j] = nums[i];
}
// int nums_new[] = Arrays.copyOf(nums, ++j);
j++;
int res = 2;
if(j <= 2) return j;
for(int i = 1; i +1 < j ; i++) {
int a= nums[i -1],b = nums[i],c = nums[i+1];
//如果是局部最小和局部最大
if(a<b && b >c) res++; //極大值
else if (a>b && b<c) res++; //極小值
}
return res;
}
5.根據身高重建隊列LeetCode 406
假設有打亂順序的一羣人站成一個隊列。 每個人由一個整數對(h, k)表示,其中h是這個人的身高,k是排在這個人前面且身高大於或等於h的人數。 編寫一個算法來重建這個隊列。
注意:
總人數少於1100人。示例
輸入:
[[7,0], [4,4], [7,1], [5,0], [6,1], [5,2]]輸出:
[[5,0], [7,0], [5,2], [6,1], [4,4], [7,1]]
思考:前面放個高的人,根據題意,個高的人是看不到個子比自己低的人,因此可以先考慮身高高的人。
-
將所有人按照身高從高到低排序。若遇到兩個身高相同的,則按照k值從小到大排序(k小的要在前面)
[7,0][7,1][6,1][5,0][5,1][4,4]
-
接下來就比較容易了,不過剛開始可能不太好想:
- 在上面按身高和k排好序的序列中,k值表示的是其所應該處在的位置下標,比如
[7,0]
應該放在第0個位置上,[5,1]
應該放在下標爲1的位置上。 - 不過,當k出現相同值時應該怎麼辦呢?很簡單,身高低的放前面,比如下面這個例子:
- [7,0],[5,0]顯然是不合理的,5前有比他高的,k不會爲0.
- 具體如何實習這一細節呢?其實先插入[7,1]到下標爲1的位置,再插[6,1]
- 在上面按身高和k排好序的序列中,k值表示的是其所應該處在的位置下標,比如
public int[][] reconstructQueue(int[][] people) {
//按照身高排
Arrays.sort(people,new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
int res =o2[0] - o1[0];
if(res == 0) return o1[1] - o2[1];
else return res;
}
});
List<int[]> output = new LinkedList<>();
for(int[] p : people)
output.add(p[1], p);
return output.toArray(new int[people.length][2]);
}
6.用最少數量的箭引爆氣球
在二維空間中有許多球形的氣球。對於每個氣球,提供的輸入是水平方向上,氣球直徑的開始和結束座標。由於它是水平的,所以y座標並不重要,因此只要知道開始和結束的x座標就足夠了。開始座標總是小於結束座標。平面內最多存在104個氣球。
一支弓箭可以沿着x軸從不同點完全垂直地射出。在座標x處射出一支箭,若有一個氣球的直徑的開始和結束座標爲 xstart,xend, 且滿足 xstart ≤ x ≤ xend,則該氣球會被引爆。可以射出的弓箭的數量沒有限制。 弓箭一旦被射出之後,可以無限地前進。我們想找到使得所有氣球全部被引爆,所需的弓箭的最小數量。
Example:
輸入:
[[10,16], [2,8], [1,6], [7,12]]輸出:
2解釋:
對於該樣例,我們可以在x = 6(射爆[2,8],[1,6]兩個氣球)和 x = 11(射爆另外兩個氣球)。
-
先將區間按照左端點從小到大 排序,依次向右邊考慮
-
end表示目前能夠到達的最遠位置
-
每添上一段區間,考慮minCount是否需要增加:
- 若新添上來的區間右端點小於end,則將end更新爲小的那個
- 若添上來的區間右端點大於end,即說明:要麼和現在的區間沒重合,要麼有重合但一定要多一筆(總之minCount++):
public int findMinArrowShots(int[][] points) {
if(points.length < 2) return points.length;
//按照區間起點進行排隊
Arrays.sort(points,new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
if(o1[0] != o2[0])
return o1[0] - o2[0];
return o1[1] - o2[1];
}
});
int minCount = 1;
//end表示目前能夠到達的最遠位置
int end = points[0][1];
//貪心過程,每射上一支箭,記錄當前能夠射穿的區間
for(int i = 1 ; i < points.length; i ++) {
if(points[i][0] <= end)
end = Math.min(end, points[i][1]);
else {
minCount++;
end = points[i][1];
}
}
return minCount;
}
7.移掉k位數字 LeetCode402
給定一個以字符串表示的非負整數 num,移除這個數中的 k 位數字,使得剩下的數字最小。
注意:
num 的長度小於 10002 且 ≥ k。
num 不會包含任何前導零。
示例 1 :輸入: num = “1432219”, k = 3
輸出: “1219”
解釋: 移除掉三個數字 4, 3, 和 2 形成一個新的最小的數字 1219。
示例 2 :輸入: num = “10200”, k = 1
輸出: “200”
解釋: 移掉首位的 1 剩下的數字爲 200. 注意輸出不能有任何前導零。
示例 3 :輸入: num = “10”, k = 2
輸出: “0”
解釋: 從原數字移除所有的數字,剩餘爲空就是0。
思路: 在一個數字中移掉固定k位數字,使得剩下最小。進行如下思考:剩下的的數字長度是一定的,因此我們希望的是高位數字越小越好。
- 假設:該數字重高位到地位逐漸遞增,如 “ 123456”,若k=3,則很顯然去掉後三個數字得到的“123” 爲最小值。這是容易發現的
- 那麼,並非單調遞增,像“123452”,k=3呢,首先得刪掉這個5. why?
- 假設刪掉5,數字變爲“12345**** ”
- 假設不刪5,數字變爲“ 12342**** ” 可以看到第二個一定比第一個小
- 因此在“123452中” 應當刪掉5, 變爲“12342” 。
- 同樣的道理,下一次我們應當刪除4, 變爲“1232”。
- 當刪掉的數字個數等於k時停止刪除,也就得到了“122”,此爲最小、
算法:
- 上述的規則使得我們通過一個接一個的刪除數字,逐步的接近最優解。
- 這個問題可以用貪心算法來解決。上述規則闡明瞭我們如何接近最終答案的基本邏輯。一旦我們從序列中刪除一個數字,剩下的數字就形成了一個新的問題,我們可以繼續使用這個規則。
- 注意,在某些情況下,規則對任意數字都不適用,即單調遞增序列。在這種情況下,我們只需要刪除末尾的數字來獲得最小數。
- 我們可以利用棧來實現上述算法,存儲當前迭代數字之前的數字。
public String removeKdigits(String num, int k) {
//用一個鏈表模擬棧
LinkedList<Character> stack = new LinkedList<Character>();
for(char c : num.toCharArray()) {
while(stack.size() > 0 && k > 0 && stack.peekLast() >c) {
stack.removeLast();
k--;
}
stack.add(c);
}
//當K還沒用完,序列已經變爲順序了,那麼從棧後面直接刪除即可
while(k-- > 0)
stack.removeLast();
//將這個棧(鏈表)轉換爲最終的string,另外若前面是0,注意刪除
StringBuilder res = new StringBuilder();
boolean prezero = true;
for(char c : stack) {
if(prezero && c == '0') continue;
prezero = false;
res.append(c);
}
if(res.length() == 0) return "0";
return res.toString();
}
8.加油站 LeetCode134
在一條環路上有 N 個加油站,其中第 i 個加油站有汽油 gas[i] 升。
你有一輛油箱容量無限的的汽車,從第 i 個加油站開往第 i+1 個加油站需要消耗汽油 cost[i] 升。你從其中的一個加油站出發,開始時油箱爲空。
如果你可以繞環路行駛一週,則返回出發時加油站的編號,否則返回 -1。
說明:
如果題目有解,該答案即爲唯一答案。
輸入數組均爲非空數組,且長度相同。
輸入數組中的元素均爲非負數。
示例 1:輸入:
gas = [1,2,3,4,5]
cost = [3,4,5,1,2]輸出: 3
解釋:
從 3 號加油站(索引爲 3 處)出發,可獲得 4 升汽油。此時油箱有 = 0 + 4 = 4 升汽油
開往 4 號加油站,此時油箱有 4 - 1 + 5 = 8 升汽油
開往 0 號加油站,此時油箱有 8 - 2 + 1 = 7 升汽油
開往 1 號加油站,此時油箱有 7 - 3 + 2 = 6 升汽油
開往 2 號加油站,此時油箱有 6 - 4 + 3 = 5 升汽油
開往 3 號加油站,你需要消耗 5 升汽油,正好足夠你返回到 3 號加油站。
因此,3 可爲起始索引。
示例 2:輸入:
gas = [2,3,4]
cost = [3,4,3]輸出: -1
解釋:
你不能從 0 號或 1 號加油站出發,因爲沒有足夠的汽油可以讓你行駛到下一個加油站。
我們從 2 號加油站出發,可以獲得 4 升汽油。 此時油箱有 = 0 + 4 = 4 升汽油
開往 0 號加油站,此時油箱有 4 - 3 + 2 = 3 升汽油
開往 1 號加油站,此時油箱有 3 - 3 + 3 = 3 升汽油
你無法返回 2 號加油站,因爲返程需要消耗 4 升汽油,但是你的油箱只有 3 升汽油。
因此,無論怎樣,你都不可能繞環路行駛一週。
思路:該題一拿到肯定想到的是暴力枚舉。即枚舉每一個位置出發,看其加的油和消耗的油能否滿足(gas_left > 0)…若走到了一圈,則返回該處下標i,否則進行下一個位置的檢測。 最後如若所有位置不成,返回-1。
//暴力
public int canCompleteCircuit(int[] gas, int[] cost) {
for(int i = 0 ,j = 0; i < gas.length ; i ++) {
int gas_left = 0;
for(j = 0 ; j < cost.length; j ++) {
int k = (i + j) % cost.length;
gas_left += gas[k] - cost[k];
if(gas_left < 0)
break;
}
if( j >= gas.length) return i; //走了一圈
}
return - 1;
}
其實上述代碼可以進行優化,將O(n^2)降爲O(n)級別。
- 仔細看上述代碼,從i處出發,當我們走到 j 處時,自己當前前所剩的油加上該處可以加的油小於cost,則無法繼續走了。
-
那麼也就是說,i 出發是可以到達i 和 j 中間的任一位置。 並且,到達中間某一位置k時,其剩餘的油量是大於等於0的,(可能有結餘,也可能沒有嘛2)
-
那麼現在假設從k出發(出發時是沒有結餘的),則也是無論如何到不了 j +1 的,最終和 i 出發類似,死在了j 與j+1之間。不知道有沒有體會到。。
- 而我們外層遍歷的 i, 就是當i 到 j+1 失敗後, 繼續從i +1 出發,遍歷。這明顯多餘了。以爲i 與j 中間任何點都到不了j + 1嘛。
因此對外層循環進行小改動:
public int canCompleteCircuit(int[] gas, int[] cost) {
for(int i = 0 ,j = 0; i < gas.length ; i +=j+1) { //直接邁過 i 和j 中間的所有點
int gas_left = 0;
for(j = 0 ; j < cost.length; j ++) {
int k = (i + j) % cost.length;
gas_left += gas[k] - cost[k];
if(gas_left < 0)
break;
}
if( j >= gas.length) return i; //走了一圈
}
return - 1;
}
9.分糖果 LeetCode135
老師想給孩子們分發糖果,有 N 個孩子站成了一條直線,老師會根據每個孩子的表現,預先給他們評分。
你需要按照以下要求,幫助老師給這些孩子分發糖果:
每個孩子至少分配到 1 個糖果。
相鄰的孩子中,評分高的孩子必須獲得更多的糖果。
那麼這樣下來,老師至少需要準備多少顆糖果呢?示例 1:
輸入: [1,0,2]
輸出: 5
解釋: 你可以分別給這三個孩子分發 2、1、2 顆糖果。
示例 2:輸入: [1,2,2]
輸出: 4
解釋: 你可以分別給這三個孩子分發 1、2、1 顆糖果。
第三個孩子只得到 1 顆糖果,這已滿足上述兩個條件。
思路:兩個數組left和right,先後從左和從右遍歷。
- 從左向右遍歷
left[]
時,若left[i] > left[i-1]
,則left[i] = left[i-1] +1
- 從右向左遍歷
right[]
時,若right[i] > right[i+1]
,則right[i] = righr[i+!] +1
- 最終,計算數量res 取
left[i]
和right[i]
中較大的那個
public int candy(int[] ratings) {
int[] left = new int [ratings.length];
Arrays.fill(left,1);
for(int i = 1 ; i < ratings.length ; i ++)
if(ratings[i] > ratings[i - 1])
left[i] = left[i-1] +1;
int res = left[ratings.length -1];
int[] right = new int[ratings.length];
Arrays.fill(right, 1);
for(int i = ratings.length - 2 ; i >=0 ;i --) {
if(ratings[i+1] < ratings[i])
right[i] = right[i +1] +1;
res += Math.max(left[i] ,right[i] );
}
return res;
}
9.去除重複字母 LeetCode316
給你一個僅包含小寫字母的字符串,請你去除字符串中重複的字母,使得每個字母只出現一次。需保證返回結果的字典序最小(要求不能打亂其他字符的相對位置)。
示例 1:
輸入: “bcabc”
輸出: “abc”
示例 2:輸入: “cbacdcbc”
輸出: “acdb”
思路:
- 遍歷字符串裏的字符,如果讀到的字符的 ASCII 值是升序,依次存到一個棧中;
- 如果讀到的字符在棧中已經存在,這個字符我們不需要;(這裏用一個flag[] 來存放每個元素是否已經在棧中)
- 如果讀到的 ASCII 值比棧頂元素嚴格小,看看棧頂元素在後面是否還會出現,如果還會出現,則捨棄棧頂元素,而選擇後出現的那個字符,這樣得到的字典序更小。
- !stack.empty()
- ASCII 值比棧頂元素嚴格小:
curChar < stack.peek()
- 當前位置不是最後出現的位置,
charLastIndex[stack.peek() - 'a'] >= i
(這裏charLastIndex數組保存每個元素在string中最後出現的位置)
- 最後的結果即爲
stack
中的。
public String removeDuplicateLetters(String s) {
int len = s.length();
// 特判
if (len < 2) {
return s;
}
// 記錄是否在已經得到的字符串中
boolean[] set = new boolean[26];
// 記錄每個字符出現的最後一個位置
int[] lastAppearIndex = new int[26];
for (int i = 0; i < len; i++) {
lastAppearIndex[s.charAt(i) - 'a'] = i;
}
//使用棧來模擬
Stack<Character> stack = new Stack<>();
for (int i = 0; i < len; i++) {
char currentChar = s.charAt(i);
//如果棧中已經存在了,跳過即可
if (set[currentChar - 'a'])
continue;
//棧非空,並且當前字符小於棧頂,同時棧頂元素在後面的字符串中還會出現
while (!stack.empty() && stack.peek() > currentChar &&
lastAppearIndex[stack.peek() - 'a'] >= i) {
char top = stack.pop();
set[top - 'a'] = false;
}
//入棧
stack.push(currentChar);
set[currentChar - 'a'] = true;
}
StringBuilder stringBuilder = new StringBuilder();
while (!stack.empty())
stringBuilder.insert(0, stack.pop());
return stringBuilder.toString();
}
10.無重複區間 LeetCode435
給定一個區間的集合,找到需要移除區間的最小數量,使剩餘區間互不重疊。
注意:
可以認爲區間的終點總是大於它的起點。
區間 [1,2] 和 [2,3] 的邊界相互“接觸”,但沒有相互重疊。示例 1:
輸入: [ [1,2], [2,3], [3,4], [1,3] ]
輸出: 1
解釋: 移除 [1,3] 後,剩下的區間沒有重疊。示例 2:
輸入: [ [1,2], [1,2], [1,2] ]
輸出: 2
解釋: 你需要移除兩個 [1,2] 來使剩下的區間沒有重疊。示例 3:
輸入: [ [1,2], [2,3] ]
輸出: 0
解釋: 你不需要移除任何區間,因爲它們已經是無重疊的了。
思路: 以每個區間的右端點從小到大排序,然後找出不重複的區間,最後用總區間個數去減。
- count計算不重複的區間個數,初始爲1
- end 保存當前的右端點,初始爲第一個元素的右端點,
- 依次遍歷intervals,當interval的start大於等於end時,count++
- 最後用總區間個數減去不重複的區間數,得到即爲需要刪除的區間數
public int eraseOverlapIntervals(int[][] intervals) {
if(intervals.length <= 1) return 0;
Arrays.sort(intervals,new Comparator<int[]>() {
public int compare(int[] o1, int[] o2) {
return o1 [1] - o2[1];
}
});
int count = 1;
//end 表示當前的末尾端點,初始爲第一元素
int end = intervals[0][1];
for(int[] interval : intervals) {
int start = interval[0];
if(start >= end) { //表示找到了一個不重合的
count++;
end = interval[1]; //end更新爲當前最右端點
}
}
return intervals.length - count;
}
現在可以再看看第6題射區間問題。其和該題的不同之處在於兩點:
- 在區間端點處重合也算重合,而該題不算
- 求得是不重合區間(集)個數,而該題求的是重合多餘的。
因此只需要小改動,相比原題的以左端點排序可能還是要簡單點
... ...
int count = 1;
//end 表示當前的末尾端點,初始爲第一元素
int end = intervals[0][1];
for(int[] interval : intervals) {
int start = interval[0];
if(start > end) { //表示找到了一個不重合的(這裏是嚴格大於)
count++;
end = interval[1]; //end更新爲當前最右端點
}
}
return count;
}
貪心小結
- 最優子結構:對比DFS,不是進行各種支路的試探,而是當下就可用某種策略確定選擇,無需考慮未來。(未來的情況即使演變也不會影響當下的選擇)
- 只要一直這麼選下去,就能得出最終的解,每一步都是當下(子問題)的最優解,結果是原問題的最優解,這叫做最優子結構。
- 更書面的說法:如果問題的一個最優解中包含了子問題的最優解,則該問題具有最優子結構。
- 具備這類結構的問題,可以用局部最優解來推導全局最優解,可以認爲其是一種剪枝法,每層剪去那些不是最佳的子樹,所以其本質是對“dfs遍歷法”的優化,
- 由上一步的最優解推導下一步的最優解,而上一步之前的(歷史)最優解則不作保留。
貪心算法設計
- 首先要確定是否能夠用貪心,即需要證明當前決定的策略是否包含在了最優解當中,並且後續的子問題與現在規模問題的決策(路徑)不相互依賴。比如找零錢中,每次在[1,2,5]中選取最大面額的,總能使得構成的數量最少,以14元爲例,你總會先用5元的進行找零得到9元,再用5元的進行找零得到4元,然後用兩張2元的。但是這是如何證明的呢?
另外,思考一波,如果面額數組爲[1,2,5,7,10],同樣構成14元,這時候如果按照最大的取,14=10+2+2,需要3張,而顯然最優解爲14=7+7,需要兩張,這不是貪心,那是怎麼想的呢? - 其次,貪心其實是一種算法設計的策略,而並非一種實實在在的算法,因此相比比較有規律的dfs,貪心的表現形式有不少(如上面例題),其解答模式也不盡相同,有一種具體問題具體分析的感覺,那麼在貪心問題的思考中,有沒有一些不變的方式和套路呢?
從上面的題解中可以發現,貪心往往會和排序、遍歷聯繫在一起,我們可能會用“最大或最小,最多或最少”的去逐個滿足,具體看題目要求,但是其根本問題是以一種相同的方式去縮小問題規模(這一點同遞歸一樣),同時這個過程就是我們所需要的最優過程,結果即是最優解。 - 找零錢、零錢兌換;分餅乾糖果;區間重合問題;排隊問題;加油站問題
之後也會繼續擴充本篇博文,記錄一些貪心例題與解答。
往期回顧: