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;
}
};
- 買賣股票的最佳時機 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;
}
};
力扣提交結果
- 分發糖果
老師想給孩子們分發糖果,有 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,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;
}
};
- 種花問題
假設有一個很長的花壇,一部分地塊種植了花,另一部分卻沒有。可是,花不能種植在相鄰的地塊上,它們會爭奪水源,兩者都會死去。
給你一個整數陣列 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;
}
};
- 用最少數量的箭引爆氣球
思路
如何使用最少的弓箭呢?
直覺上來看,貌似只射重疊最多的氣球,用的弓箭一定最少,那麼有沒有當前重疊了三個氣球,我射兩個,留下一個和後面的一起射這樣弓箭用的更少的情況呢?
嘗試一下舉反例,發現沒有這種情況。因為都需要兩支箭
那麼就試一試貪心吧!區域性最優:當氣球出現重疊,一起射,所用弓箭最少。全域性最優:把所有氣球射爆所用弓箭最少。
演算法確定下來了,那麼如何模擬氣球射爆的過程呢?是在陣列中移除元素還是做標記呢?
如果真實的模擬射氣球的過程,應該射一個,氣球陣列就remove一個元素,這樣最直觀,畢竟氣球被射了。
但仔細思考一下就發現:如果把氣球排序之後,從前到後遍歷氣球,被射過的氣球僅僅跳過就行了,沒有必要讓氣球陣列remote氣球,只要記錄一下箭的數量就可以了。
以上為思考過程,已經確定下來使用貪心了,那麼開始解題。
為了讓氣球儘可能的重疊,需要對陣列進行排序。
那麼按照氣球起始位置排序,還是按照氣球終止位置排序呢?
其實都可以!只不過對應的遍歷順序不同,我就按照氣球的起始位置排序了。
既然按照起始位置排序,那麼就從前向後遍歷氣球陣列,靠左儘可能讓氣球重複。
從前向後遍歷遇到重疊的氣球了怎麼辦?
如果氣球重疊了,重疊氣球中右邊邊界的最小值 之前的區間一定需要一個弓箭。
如圖,氣球三雖然與氣球2重疊了,但是因為它的左邊界大於前面一組的最小右邊界,因此不能一起射穿
因此在演算法實現的過程中,如果發現有氣球重疊,就需要更新這一組氣球的最小右邊界
程式碼實現
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;
}
};
總結
先發布一版,後續還有一期,這期的題目難度,簡單,中等,困難都有,可以感受到,有時候貪心演算法思路很簡單,但是實現起來,是需要很多小技巧的,這些技巧,只有通過不斷的練習,才能掌握!