什麼是貪心算法?
貪婪算法(貪心算法)是指在對問題進行求解時,在每一步選擇中都採取最好或者最優(即最有利)的選擇,從而希望能夠導致結果是最好或者最優的算法。
貪婪算法所得到的結果往往不是最優的結果(有時候會是最優解),但是都是相對近似(接近)最優解的結果。
貪心解決問題的基本思路?
1.建立數學模型來描述問題
2.把求解的問題分成若干個子問題
3.對每一子問題求解,得到子問題的局部最優解
4.把子問題對應的局部最優解合成原來整個問題的一個近似最優解
例題
1. 分配餅乾
題目描述:每個孩子都有一個滿足度,每個餅乾都有一個大小,只有餅乾的大小大於等於一個孩子的滿足度,該孩子纔會獲得滿足。求解最多可以獲得滿足的孩子數量。
給一個孩子的餅乾應當儘量小又能滿足該孩子,這樣大餅乾就能拿來給滿足度比較大的孩子。因爲最小的孩子最容易得到滿足,所以先滿足最小的孩子。
/*
*每個孩子都有一個滿足度,每個餅乾都有一個大小,只有餅乾的大小大於等於一個孩子的滿足度,該孩子纔會獲得滿足。求解最多可以獲得滿足的孩子數量。
* */
public int findContentChildren(int[] g, int[] s) {
/*
* 思路:將所有 餅乾,孩子的願望 分別從小到大排序。
* 從小到大遍歷每一個孩子的願望, 按照餅乾從小到大的順序來滿足從小到大的孩子的願望。
* 判斷當前塊的餅乾是否可以滿足孩子的願望。如果不能(那麼後面有更大願望的孩子就更不能滿足了),所以刨除此塊,繼續用下一塊來判斷;如果能,則累計count。
* 直到餅乾遍歷結束或者孩子的願望遍歷結束。
* */
Arrays.sort(g);
Arrays.sort(s);
int gi = 0;
int si = 0;
int count = 0;
while (gi < s.length && si < s.length) {
if (s[si] >= g[gi]) {
count++;
gi++;
}
si++;
}
return count;
}
2. 不重疊的區間個數
題目描述:計算讓一組區間不重疊所需要移除的區間個數
/*
* 計算讓一組區間不重疊所需要移除的區間個數
* */
public int eraseOverlapIntervals(int[][] intervals) {
/*
* 思路:讓一組區間不重疊,優先選擇區間末尾值小的,這樣才能讓後面的區間最可能不重疊。
* 換句話說 優先選擇區間末尾值小的區間,才能拼接更多的區間,移除更少的區間
* 做法:
* a.首先根據區間末尾值由小到大的順序 將區間排序
* b.維持一個區間末尾值end
* c.按照排序後的順序遍歷所有區間,如果當前區間不會與上一個區間重疊(判斷方式:當前區間的首值是否大於等於end),就更新end
* */
Arrays.sort(intervals, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
return o1[1] - o2[1];
}
});
int end = Integer.MIN_VALUE;
int removeCount = 0;
for (int i = 0; i < intervals.length; i++) {
if (intervals[i][0] >= end) {
end = intervals[i][1];
} else {
removeCount++;
}
}
return removeCount;
}
相同思路的題目:
a.假設有如下課程,希望儘可能多的將課程安排在一間教室裏:
解題思路:選擇結束最早的課
b.氣球在一個水平數軸上擺放,可以重疊,飛鏢垂直投向座標軸,使得路徑上的氣球都被刺破。求解最小的投飛鏢次數使所有氣球都被刺破。也是計算不重疊的區間個數,不過和Non-overlapping Intervals 的區別在於,[1, 2] 和 [2, 3] 在本題中算是重疊區間。
3.根據身高和序號重組隊列
題目描述:一個學生用兩個分量 (h, k) 描述,h 表示身高,k 表示排在前面的有 k 個學生的身高比他高或者和他一樣高。
/*
* 根據身高和序號重組隊列
* 題目描述:一個學生用兩個分量 (h, k) 描述,h 表示身高,k 表示排在前面的有 k 個學生的身高比他高或者和他一樣高。
* */
public int[][] recombination(int[][] people) {
/*
* 解題思路:優先向隊伍中插入最高的。原因是,確保了最高的學生的組隊條件,後面再向隊伍中插入比他矮的學生時,都不會影響他的條件。
* 因爲他的條件是:排在他前面的k個學生 身高比他高或者和他一樣高。即使比他矮的學生插入到了他的前面也不會影響他,當然插到他後面更不會影響他了
* 做法:
* a.將所有的學生按照身高由大到小的順序排序,身高相同的情況下,將k小的排在前面。
* b.按照排序後的順序 不斷將學生插入到隊列中
* */
Arrays.sort(people, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
return o2[0] == o1[0] ? o2[1] - o2[1] : o2[0] - o1[0];
}
});
LinkedList<int[]> list = new LinkedList<>();
list.add(people[0]);
for (int i = 1; i < people.length; i++) {
list.add(people[i][1], people[i]);
}
return list.toArray(new int[list.size()][]);
}
4.買賣股票最大的收益
/*
* 題目描述:一次股票交易包含買入和賣出,只進行一次交易,求最大收益。
* */
public int maxProfit(int[] prices) {
int minBuy = Integer.MAX_VALUE;
int maxProfit = 0;
for (int i = 0; i < prices.length; i++) {
if (prices[i] < minBuy) {
minBuy = prices[i];
continue;
}
maxProfit = Math.max(maxProfit, prices[i] - minBuy);
}
return maxProfit;
}
/*
* 題目描述:可以進行多次交易,多次交易之間不能交叉進行,可以進行多次交易。
* 做法:當遇到a[i]-a[i-1]>0時,就把這個差值累計算的利潤中
* 思路:
* 1.會不會出現a[i]前面有比a[i-1]還小的情況,不應該累計最多的利潤嗎?
* 例如:2~5~9。 (5-2)+(9-5)=9-5 這種情況時,已經將5-2的利潤累計了,所以不必擔心累計不到9-2這種更大利潤的情況。寬泛的說:當a<b<c<d時,(b-a)+(c-b)+(d-c)=d-a
* 2. 萬一出現2~5~10~9這種情況呢?
* 9之前出現比它更大的元素,這種情況出現時,這段區間的最大利潤已經被10截斷了,跟9沒關係了。 最大利潤就是(5-2)+(10-5)=10-2.
* 3.那麼9還有可能被累計嗎?
* 當2~5~10~9~11時會被累計(5-2)+(10-5)+(11-9)。當2~5~10~9~8時,就直接捨棄9了。
* */
public int maxProfit2(int[] prices) {
int maxProfit = 0;
for (int i = 1; i < prices.length; i++) {
if (prices[i] - prices[i - 1] > 0) {
maxProfit += prices[i] - prices[i - 1];
}
}
return maxProfit;
}
5.種植花朵
題目描述:flowerbed 數組中 1 表示已經種下了花朵。花朵之間至少需要一個單位的間隔,求解是否能種下 n 朵花。
/*
*種植花朵
* 題目描述:flowerbed 數組中 1 表示已經種下了花朵。花朵之間至少需要一個單位的間隔,求解是否能種下 n 朵花。
* */
public boolean canPlaceFlower(int[] flowerbed, int n) {
/*
* 解題思路:如果當前元素爲0,判斷當前元素左右是否也爲0
* 注意邊界值就好
* */
if (flowerbed.length == 1) {
if (flowerbed[0] == 0) n--;
} else {
for (int i = 0; i < flowerbed.length; i++) {
if (flowerbed[i] == 0) {
if (i == 0) {
if (flowerbed[i + 1] == 0) {
n--;
flowerbed[i] = 1;
}
} else if (i == flowerbed.length - 1) {
if (flowerbed[i - 1] == 0) {
n--;
flowerbed[i] = 1;
}
} else if (flowerbed[i - 1] == 0 && flowerbed[i + 1] == 0) {
n--;
flowerbed[i] = 1;
}
}
}
}
if (n <= 0) return true;
return false;
}
6.判斷是否爲子序列
/*
*判斷是否爲子序列
* */
public boolean isSubsequence(String s, String t) {
/*做法一*/
char []strT=t.toCharArray();
int index=0;
for(int i=0;i<strT.length;i++){
if(index==s.length()) return true;
if(s.charAt(index)==strT[i]){
index++;
}
}
if(index==s.length()){
return true;
}
return false;
}
public boolean isSubsequence2(String s, String t) {
/*做法二*/
int index=-1;
for(char c:s.toCharArray()){
index=t.indexOf(c,index+1);
if(index==-1) return false;
}
return true;
}
7.修改一個數成爲非遞減數組
題目描述: 修改一個數,能否成爲非遞減數組
/*
* 修改一個數成爲非遞減數組
* 思路:當nums[i]>nums[i+1]時,有兩種修改方案,修改nums[i]或nums[i+1]。但要判斷修改後,非遞減數組這個條件能否成立,成立纔可
* 想要修改nums[i],要確保nums[i-1]<=nums[i+1]
* 想要修改nums[i+1],要確保nums[i]<=nums[i+2]
* */
public boolean checkPossibility(int[] nums) {
boolean isModify=false;
for(int i=0;i<nums.length-1;i++){
if(nums[i]>nums[i+1]){
if(isModify) return false;
if(i+1==nums.length-1) return true;
if(nums[i]<=nums[i+2]) {
nums[i+1]=nums[i];
isModify=true;
}else if(i==0||nums[i-1]<=nums[i+1]){
nums[i]=nums[i+1];
isModify=true;
}else {
return false;
}
}
}
return true;
}
8.子數組最大的和
/*
* 子數組最大的和
* 思路:
* a.max作爲最大的和,隨時更新
* b.用curSum累計,考慮某元素nums[i]時,如果curSum<0,一定會curSum+nums[i]<nums[i]。所以如果curSum<0,就將curSum置爲0
* */
public int maxSubArray(int[] nums) {
int max=Integer.MIN_VALUE;
int curSum=0;
for (int i=0;i<nums.length;i++){
curSum+=nums[i];
max=Math.max(max,curSum);
if(curSum<0) curSum=0;
}
return max;
}
9. 分隔字符串使同種字符出現在一起
/*
分隔字符串使同種字符出現在一起
Input: S = "ababcbacadefegdehijhklij"
Output: [9,7,8]
Explanation:
The partition is "ababcbaca", "defegde", "hijhklij".
This is a partition so that each letter appears in at most one part.
A partition like "ababcbacadefegde", "hijhklij" is incorrect, because it splits S into less parts.
* */
public List<Integer> partitionLabels(String S) {
/*
* 思路:每次遍歷到一個元素時,就要考慮後面還有沒有相同的元素,如果有,就要分到一組。
* 做法:遍歷整個字符串,遍歷到元素curElem時,就從後向前查找,找到最後一個與之相同的元素,此時,這兩個區間爲一組,還要考慮在這個區間的元素
* */
char str[]=S.toCharArray();
char label[]=new char[30];
for(int i=0;i<str.length;i++){
label[str[i]-'a']++;
}
List<Integer> list=new LinkedList<>();
int firstIndex=0;//區間首元素索引
int lastIndex=0;//區間尾元素索引
int curIndex=0;//當前元素
while (firstIndex<str.length){
lastIndex=Math.max(lastIndex,S.lastIndexOf(S.charAt(curIndex)));//每次遍歷一個元素,就要找到字符串中最後一個與之相同的元素。並更新區間尾元素索引
if(curIndex==lastIndex){//直到當前元素是區間尾元素(與當前元素相同的最後一個元素是自己,說明同種字符都在一個區間內了),此時首尾元素之間的所有可以分爲一組。
list.add(lastIndex-firstIndex+1);
firstIndex=lastIndex+1;//更新下一個區間的首元素索引
curIndex=lastIndex+1;//更新下一個區間的尾元素索引
}else {
curIndex++;
}
}
return list;
}
10.集合覆蓋問題:
假設存在如下表的需要付費的廣播臺,以及廣播臺信號可以覆蓋的地區。 如何選擇最少的廣播臺,讓所有的地區都可以接收到信號。
解題思路:
1.將每個地區可以接收到信號的廣播臺 的數量 放入到map裏。
ID–>2
NV–>3
…
2.遍歷每個廣播,如果該廣播臺能覆蓋的地區,都可以由其他 廣播臺覆蓋到(map中的value都大於1),那麼拋棄該廣播臺,同時將map中 該廣播臺能覆蓋的地區的value值減1
每次都要選擇一個 能覆蓋到其他廣播臺覆蓋不到的地區的 廣播臺。