LeetCode解題記錄(貪心演算法)(一)

進擊的汪sir發表於2021-07-08

1. 前言

目前得到一本不錯的演算法書籍,頁數不多,挺符合我的需要,於是正好借這個機會來好好的系統的刷一下演算法題,一來呢,是可以給部分同學提供解題思路,和一些自己的思考,二來呢,我也可以在需要複習的時候,通過部落格來回顧自己,廢話不多說,開始!
目前的規劃
在這裡插入圖片描述

2. 演算法解釋

顧名思義,貪心演算法或貪心思想採用貪心的策略,保證每次操作都是區域性最優的,從而使最後得到的結果是全域性最優的。

舉一個最簡單的例子:小明和小王喜歡吃蘋果,小明可以吃五個,小王可以吃三個。已知蘋果園裡有吃不完的蘋果,求小明和小王一共最多吃多少個蘋果。在這個例子中,我們可以選用的貪心策略為,每個人吃自己能吃的最多數量的蘋果,這在每個人身上都是區域性最優的。又因為全域性結果是區域性結果的簡單求和,且區域性結果互不相干,因此區域性最優的策略也同樣是全域性最優的策略。

如果還是不明白的同學可以自己去百度哈,我相信貪心演算法是不難懂的

455 Assign Cookies (Easy)

題目地址
在這裡插入圖片描述
這題的思路就是,優先滿足胃口小的孩子,而且是優先選擇尺寸接近孩子的餅乾給他,那麼這就是一個貪心的思想,即希望每個孩子給的餅乾儘可能的接近他的胃口
演算法思路就是,先將兩個陣列排序,然後遍歷,如果發現餅乾大於等於孩子的胃口值,則給這個孩子,兩邊的索引同時+1,同時count+1,如果小於,則餅乾的vector的索引+1

下面是我的程式碼,可能不夠優雅,大家覺得哪些地方需要改進,可以在下面提出來

class Solution {
public:
    int findContentChildren(vector<int>& g, vector<int>& s) {
        sort(g.begin(),g.end());
        sort(s.begin(),s.end());
        int gsize = g.size();
        int ssize = s.size();
        int i=0,j=0;
        int count=0;
        while(i<gsize && j<ssize ){
            // 能被滿足
            if(s[j]>=g[i]){
                i++;
                j++;
                count++;
            }else{
                j++;
            }
        }

        return count;
    }
};
  1. 買賣股票的最佳時機 II

題目地址
給定一個陣列 prices ,其中 prices[i] 是一支給定股票第 i 天的價格。

設計一個演算法來計算你所能獲取的最大利潤。你可以儘可能地完成更多的交易(多次買賣一支股票)。

注意:你不能同時參與多筆交易(你必須在再次購買前出售掉之前的股票)。

示例 1:

輸入: prices = [7,1,5,3,6,4]
輸出: 7
解釋: 在第 2 天(股票價格 = 1)的時候買入,在第 3 天(股票價格 = 5)的時候賣出, 這筆交易所能獲得利潤 = 5-1 = 4 。
隨後,在第 4 天(股票價格 = 3)的時候買入,在第 5 天(股票價格 = 6)的時候賣出, 這筆交易所能獲得利潤 = 6-3 = 3 。

示例 2:

輸入: prices = [1,2,3,4,5]
輸出: 4
解釋: 在第 1 天(股票價格 = 1)的時候買入,在第 5 天 (股票價格 = 5)的時候賣出, 這筆交易所能獲得利潤 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接連購買股票,之後再將它們賣出。因為這樣屬於同時參與了多筆交易,你必須在再次購買前出售掉之前的股票。

示例 3:

輸入: prices = [7,6,4,3,1]
輸出: 0
解釋: 在這種情況下, 沒有交易完成, 所以最大利潤為 0。

提示:

1 <= prices.length <= 3 * 104
0 <= prices[i] <= 104

這題的思路還是貪心演算法,雖然動態規劃演算法也可以寫,但是這是貪心專題,就不擴充套件了

貪心就是指根據目前的情況來判斷,獲得每一步的最大值,而不需要考慮其他,在這題裡面就是考慮我怎麼獲得錢的最大值,很明顯,只要第二天的股票比今天貴,我就賣掉,這樣就一定能賺錢,有人可能會想,我第一天買,第二天賣,但是第二天不能買啊,按照題目意思,確實是這樣的,但是可以自己推導一下

比如,[1,2,3,4],這是4天的股票價格,那麼按照貪心演算法是,(2-1)+(3-2)+(4-3) = 4-1
所以等價於我們第一天買,最後一天賣

程式碼如下

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        
        int res = 0;
        if(prices.size() == 1){
            return 0;
        }
        for (int i = 1; i < prices.size(); i++) {
            int diff = prices[i] - prices[i - 1];
            if (diff > 0) {
                res += diff;
            }
        }
        return res;
    }
};

力扣提交結果
在這裡插入圖片描述

  1. 分發糖果

題目連結

老師想給孩子們分發糖果,有 N 個孩子站成了一條直線,老師會根據每個孩子的表現,預先給他們評分。

你需要按照以下要求,幫助老師給這些孩子分發糖果:

每個孩子至少分配到 1 個糖果。
評分更高的孩子必須比他兩側的鄰位孩子獲得更多的糖果。
那麼這樣下來,老師至少需要準備多少顆糖果呢?

示例 1:

輸入:[1,0,2]
輸出:5
解釋:你可以分別給這三個孩子分發 2、1、2 顆糖果。
示例 2:

輸入:[1,2,2]
輸出:4
解釋:你可以分別給這三個孩子分發 1、2、1 顆糖果。
第三個孩子只得到 1 顆糖果,這已滿足上述兩個條件。

把所有孩子的糖果數初始化為 1;
先從左往右遍歷一遍,如果右邊孩子的評分比左邊的高,則右邊孩子的糖果數更新為左邊孩子的
糖果數加 1;再從右往左遍歷一遍,如果左邊孩子的評分比右邊的高,且左邊孩子當前的糖果數
不大於右邊孩子的糖果數
,則左邊孩子的糖果數更新為右邊孩子的糖果數加 1。通過這兩次遍歷,
分配的糖果就可以滿足題目要求了。這裡的貪心策略即為,在每次遍歷中,只考慮並更新相鄰一
側的大小關係。

不理解的,可以在紙上自己畫一畫,就容易理解啦

程式碼如下

#include <iostream>
#include <vector>
#include <numeric>
using namespace std;

int candy(vector<int>& ratings) {
	int len = ratings.size();
	if (len < 2) {
		return len;
	}
	vector<int>  cd(len, 1);

	for (int i = 1; i < len; i++) {
		if (ratings[i] > ratings[i - 1]) {
			cd[i] = cd[i - 1] + 1;
		}
	}

	for (int i = len - 2; i >= 0; i--) {
		if (ratings[i] > ratings[i + 1]) {
			if (cd[i] <= cd[i + 1]) {
				cd[i] = cd[i + 1] + 1;
			}
		}
	}

	//for (auto a : cd) {
	//	cout << a << " ";
	//}

	// 0這個引數代表整數求和,0. 代表浮點數求和
	return accumulate(cd.begin(), cd.end(), 0);
}
int main() {
	vector<int> a = { 1,3,4,5,2 };
	cout << candy(a);
	return 0;
}
  1. 無重疊區間

題目連結

給定一個區間的集合,找到需要移除區間的最小數量,使剩餘區間互不重疊。

注意:

可以認為區間的終點總是大於它的起點。
區間 [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

解釋: 你不需要移除任何區間,因為它們已經是無重疊的了。

在選擇要保留區間時,區間的結尾十分重要:選擇的區間結尾越小,餘留給其它區間的空間
就越大

(這裡用到了貪心的思想,我們希望每一個區間的結尾最小且不會重疊,這樣得到的區間就一定是最大的)
就越能保留更多的區間。因此,我們採取的貪心策略為,優先保留結尾小且不相交的區
間。
具體實現方法為,先把區間按照結尾的大小進行增序排序,每次選擇結尾最小且和前一個選
擇的區間不重疊的區間。我們這裡使用 C++ 的 Lambda,結合 std::sort() 函式進行自定義排

實現程式碼為

class Solution {
public:
    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        if(intervals.empty()){
            return 0;
        }
        int n= intervals.size();
        sort(intervals.begin(),intervals.end(),[](vector<int> a,vector<int> b){
            return a[1] < b[1];
        });

        int total =0,prev = intervals[0][1];
        for(int i=1;i<n;++i){
            if(intervals[i][0] < prev){
                ++total;
            }else{
                prev = intervals[i][1];
            }
        }
        return total;
    }
};
  1. 種花問題

題目連結

假設有一個很長的花壇,一部分地塊種植了花,另一部分卻沒有。可是,花不能種植在相鄰的地塊上,它們會爭奪水源,兩者都會死去。

給你一個整數陣列 flowerbed 表示花壇,由若干 0 和 1 組成,其中 0 表示沒種植花,1 表示種植了花。另有一個數 n ,能否在不打破種植規則的情況下種入 n 朵花?能則返回 true ,不能則返回 false。

示例 1:

輸入:flowerbed = [1,0,0,0,1], n = 1
輸出:true
示例 2:

輸入:flowerbed = [1,0,0,0,1], n = 2
輸出:false

提示:

1 <= flowerbed.length <= 2 * 104
flowerbed[i] 為 0 或 1
flowerbed 中不存在相鄰的兩朵花
0 <= n <= flowerbed.length

解題思路

貪心演算法:從左向右遍歷花壇,在可以種花的位置就種一朵,能種就種(因為在任一種花時候,不種都不會得到更優解),是一種貪心的思想。

對於當前遍歷的位置,這個位置是否能種上花,必須滿足以下條件:

當前位置 flowerbed[i] = 0flowerbed[i]=0 + 當前這個位置是陣列末尾 i == flowerbed.length - 1i==flowerbed.length−1 + 前一個位置上沒有種花 flowerbed[i - 1] = 0flowerbed[i−1]=0,此時可以在該位置種花。

當前位置 flowerbed[i] = 0flowerbed[i]=0 + 當前這個位置後一個位置沒有種花 flowerbed[i + 1] = 0flowerbed[i+1]=0 + 當前這個位置是陣列的開頭 i == 0i==0,此時可以在該位置種花。

當前位置 flowerbed[i] = 0flowerbed[i]=0 + 當前這個位置後一個位置沒有種花 flowerbed[i + 1] = 0flowerbed[i+1]=0 + 前一個位置上沒有種花 flowerbed[i - 1] = 0flowerbed[i−1]=0,此時可以在該位置種花。

當前位置 flowerbed[i] = 0flowerbed[i]=0 並且只有這一個位置,即 i == flowerbed.length - 1iflowerbed.length−1 並且 i == 0i0,此時可以在該位置種花。

程式碼如下

class Solution {

    public boolean canPlaceFlowers(int[] flowerbed, int n) {

        int m = flowerbed.length;
        
        int count = 0;
        for (int i=0;i<m;i++) {

            if (flowerbed[i] == 0 && (i == m - 1 || flowerbed[i + 1] == 0) && (i == 0 || flowerbed[i - 1] == 0)) {

                flowerbed[i] = 1;
                count++;
            }
        }
        return count >= n;
    }
}

但其實還有更加簡便的方法

在 flowerbed 陣列兩端各增加一個 0, 這樣處理的好處在於不用考慮邊界條件,任意位置處只要連續出現三個 0 就可以栽上一棵花。

下面是我寫的程式碼

class Solution {
public:
    bool canPlaceFlowers(vector<int>& flowerbed, int n) {
       
        flowerbed.insert(flowerbed.begin(),0);
        flowerbed.push_back(0);
        int len = flowerbed.size();
        for(int i=1;i<len-1;i++){
            if(flowerbed[i-1]==0&&flowerbed[i]==0&&flowerbed[i+1]==0){
                flowerbed[i] =1;
                n -= 1;
            }
        }
        return n<=0;
    }
};

在這裡插入圖片描述

  1. 用最少數量的箭引爆氣球

題目連結

思路
如何使用最少的弓箭呢?

直覺上來看,貌似只射重疊最多的氣球,用的弓箭一定最少,那麼有沒有當前重疊了三個氣球,我射兩個,留下一個和後面的一起射這樣弓箭用的更少的情況呢?

嘗試一下舉反例,發現沒有這種情況。因為都需要兩支箭

那麼就試一試貪心吧!區域性最優:當氣球出現重疊,一起射,所用弓箭最少。全域性最優:把所有氣球射爆所用弓箭最少。

演算法確定下來了,那麼如何模擬氣球射爆的過程呢?是在陣列中移除元素還是做標記呢?

如果真實的模擬射氣球的過程,應該射一個,氣球陣列就remove一個元素,這樣最直觀,畢竟氣球被射了。

但仔細思考一下就發現:如果把氣球排序之後,從前到後遍歷氣球,被射過的氣球僅僅跳過就行了,沒有必要讓氣球陣列remote氣球,只要記錄一下箭的數量就可以了。

以上為思考過程,已經確定下來使用貪心了,那麼開始解題。

為了讓氣球儘可能的重疊,需要對陣列進行排序。

那麼按照氣球起始位置排序,還是按照氣球終止位置排序呢?

其實都可以!只不過對應的遍歷順序不同,我就按照氣球的起始位置排序了。

既然按照起始位置排序,那麼就從前向後遍歷氣球陣列,靠左儘可能讓氣球重複。

從前向後遍歷遇到重疊的氣球了怎麼辦?

如果氣球重疊了,重疊氣球中右邊邊界的最小值 之前的區間一定需要一個弓箭。
在這裡插入圖片描述
如圖,氣球三雖然與氣球2重疊了,但是因為它的左邊界大於前面一組的最小右邊界,因此不能一起射穿
因此在演算法實現的過程中,如果發現有氣球重疊,就需要更新這一組氣球的最小右邊界

參考:https://leetcode-cn.com/problems/minimum-number-of-arrows-to-burst-balloons/solution/dai-ma-sui-xiang-lu-dai-ni-xue-tou-tan-x-5wfl/

程式碼實現

class Solution {
public:
	
	int findMinArrowShots(vector<vector<int>>& points) {
		if (points.size() == 0) {
			return 0;
		}

		sort(points.begin(), points.end(), [](vector<int> a, vector<int> b) {
			return a[0] < b[0];
			});

		int len = points.size();
		
		int res = 1;// 因為氣球不為空,所以至少需要一支箭

		for (int i = 1; i < len; i++) {
			// 如果不重合
			if (points[i][0] > points[i - 1][1]) {
				res++;
			}
			else {
				// 如果重和,更新最小右邊界
				points[i][1] = min(points[i - 1][1], points[i][1]);
			}
		}
		return res;
	}
};

總結

先發布一版,後續還有一期,這期的題目難度,簡單,中等,困難都有,可以感受到,有時候貪心演算法思路很簡單,但是實現起來,是需要很多小技巧的,這些技巧,只有通過不斷的練習,才能掌握!

相關文章