leetcode 经典贪心算法题目(思路、方法、code)

用于回顾数据结构与算法时刷题的一些经验记录

(提出对应的贪心算法时最好自己举例子试试能否可行)

文章目录

        • [455. 分发饼干](https://leetcode-cn.com/problems/assign-cookies/)
        • [376. 摆动序列](https://leetcode-cn.com/problems/wiggle-subsequence/)
        • [402. 移掉K位数字](https://leetcode-cn.com/problems/remove-k-digits/)
        • [55. 跳跃游戏](https://leetcode-cn.com/problems/jump-game/)
        • [45. 跳跃游戏 II](https://leetcode-cn.com/problems/jump-game-ii/)
        • [134. 加油站](https://leetcode-cn.com/problems/gas-station/)
        • [452. 用最少数量的箭引爆气球](https://leetcode-cn.com/problems/minimum-number-of-arrows-to-burst-balloons/)
        • [135. 分发糖果](https://leetcode-cn.com/problems/candy/)
        • [921. 使括号有效的最少添加](https://leetcode-cn.com/problems/minimum-add-to-make-parentheses-valid/)
        • [1326. 灌溉花园的最少水龙头数目](https://leetcode-cn.com/problems/minimum-number-of-taps-to-open-to-water-a-garden/)

455. 分发饼干

假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。对每个孩子 i i i ,都有一个胃口值 g i gi gi ,这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j j j,都有一个尺寸 s j sj sj 。如果 s j > = g i sj >= gi sj>=gi,我们可以将这个饼干 j j j分配给孩子 i i i,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。

注意:

你可以假设胃口值为正,且一个小朋友最多只能拥有一块饼干。

示例 1:
输入: [1,2,3], [1,1]
输出: 1
解释: 
你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。所以你应该输出1

分析:由于每个孩子最多只需要一个饼干,并且我们需要的是满足尽可能多的孩子,因此我们有如下策略

  • 如果一个孩子能被更小的饼干满足,则就应该采用更小的饼干,尽量保留大的饼干给胃口更大的孩子
  • 如果一个饼干不能满足胃口最小的孩子,故它将不能满足每个孩子

因此,我们可以对 饼干尺寸和孩子胃口进行排序,然后遍历饼干尺寸。

  • 如果当前饼干可以满足当前孩子,就满足该孩子,向后遍历饼干和孩子胃口
  • 如果当前饼干不可以满足当前孩子,说明该饼干不会再被利用,向后遍历饼干
  • 如果孩子或者饼干遍历完了,则返回结果即可
class Solution {
public:
    int findContentChildren(vector<int>& g, vector<int>& s) 
    {
        sort(g.begin(),g.end());
        sort(s.begin(),s.end());
        int cookie=0;  //表示cookie遍历到第几个了 
        int child=0;
        while(child<g.size()&&cookie<s.size())
        {
        	if(g[child]<=s[cookie]) //该饼干可以满足孩子,使用即可 
        		child++;  //孩子向后遍历 
        	cookie++;  //饼干向后遍历 
		}
		return child;
    }
};

376. 摆动序列

如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。

例如, [1,7,4,9,2,5] 是一个摆动序列,因为差值 (6,-3,5,-7,3) 是正负交替出现的。相反, [1,4,7,2,5] 和 [1,7,4,5,5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。

给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。

分析:

可视化数字发现贪心规律

分析题目可知,实际上摇摆序列就是画到坐标轴上连线后,上下波动的线条的每个顶点。
leetcode 经典贪心算法题目(思路、方法、code)_第1张图片

因此我们可以将相邻数字之间差计算出来,以次表示两个数字间是上升还是下降关系。 需要注意的是,我们贪心思想表现在,如果出现连续的上升或者下降,则应当取最后一个(即上升或下降最后的端点)作为子序列节点

  • 将相邻数字差表示出来
  • 用一个变量表示当前是处于上升还是下降,发生变化才+1
  • 需要考虑头部是平缓的情况,将其度过
class Solution {
public:
    int wiggleMaxLength(vector<int>& nums)
	{
		int length=nums.size();
		int result=0;
		if(length<=1) return length;
		for(int i=0;i<length-1;i++) //将差值存入到nums中,共length-1个 
		{
			nums[i]=nums[i+1]-nums[i];
		}
		int up_or_down=0;   //0表示当前平,1表示当前为up,2表示当前为down 
		int i=0;
		for(i=0;i<length-1;i++) //将头部的平缓区度过,并初始化up_or_down 
		{
				if(nums[i]>0)
				{
					up_or_down=1;
					result++;
					break;
				}
				else if(nums[i]<0)
				{
					up_or_down=2;
					result++;
					break;
				}
		}
		for(i;i<length-1;i++)
		{
			if(nums[i]>0&&up_or_down==2)  //如果当前为下降并且该值为上升,则result+1 
			{
				up_or_down=1;
				result++;
				continue;
			}
			else if(nums[i]<0&&up_or_down==1)//如果当前为上升并且该值为下降,则result+1 
			{
				up_or_down=2;
				result++;
				continue;
			}
		}
		return result+1;
    }
};

402. 移掉K位数字

给定一个以字符串表示的非负整数 num,移除这个数中的 k 位数字,使得剩下的数字最小。

注意: num 的长度小于 10002 且 ≥ k。 num 不会包含任何前导零。

示例 1 :
输入: num = "1432219", k = 3
输出: "1219"
解释: 移除掉三个数字 4, 3,2 形成一个新的最小的数字 1219

分析:如果给定 “1432219” ,去掉一个数字令其最大,如何去?去掉后肯定减少一位,因此应该尽可能地使高位数最小,分析该数字,由于1<4,故不应该去掉1,否则高位数将增大,4>3,故去掉4,会使得第二位变为3,从而达到尽可能小。

因此去掉数字的原则:从高位向地位遍历,如果对应的数字大于下一位数字,则把该位数字去掉,得到的数字最小

可以用存储结果,这样从高位向地位遍历时如果有错位则将其不加入栈,最终栈中存储内容应该是每一个数字不大于下一位,如果还需要删除,那么我们就将栈顶pop出,直至停止

还需要考虑的是,如果数字中有0出现如何处理(可以考虑将其不放入栈),还要注意的是如何将栈中内容返回所需字符串。

class Solution {
public:
    string removeKdigits(string num, int k) 
	{
		vector<int> s;  //用vector来表达栈即可,方便遍历元素
		string result="";
		for(int i=0;i<num.length();i++) //循环遍历 
		{
			int number=num[i]-'0';
			while(s.size()!=0&&s[s.size()-1]>number&&k>0) //如果当前遍历的数字比前面的数字小,则将前面的数字pop 
			{
				s.pop_back();
				k--;
			}
			if(number!=0||s.size()!=0) //0就当作没有,不加入即可 
			{
				s.push_back(number);
			} 
		}
		while(s.size()!=0&&k>0) //如果已经遍历完但是还需要删,从尾部删即可 
		{
			s.pop_back();
			k--;
		}
		for(int i=0;i<s.size();i++) //将结果转换为字符串 
			result.append(1,'0'+s[i]);
		if(result=="") result="0";
		return result;
    }	
};

55. 跳跃游戏

给定一个非负整数数组,你最初位于数组的第一个位置。

数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个位置。

示例 1:
输入: [2,3,1,1,4]
输出: true
解释: 我们可以先跳 1 步,从位置 0 到达 位置 1, 然后再从位置 13 步到达最后一个位置。

分析:在第 i i i 个位置,最远可以跳到第 i + n u m [ i ] i+num[i] i+num[i] 个位置,这意味着从 i i i i + n u m [ i ] i+num[i] i+num[i] 之间的位置都可以到达。因此,我们可以用一个 m a x _ j u m p max\_jump max_jump 存取目前能到达的最远位置,以次遍历所能到达的位置,如果 m a x _ j u m p max\_jump max_jump 小于目标地址,则说明不能到达。每次到达一个地方,都再次计算该位置所能到达的最远位置,刷新 m a x _ j u m p max\_jump max_jump

class Solution {
public:
    bool canJump(vector<int>& nums)
	{
		int length=nums.size();
		int max_jump=0;
		for(int i=0;i<length-1;i++)  //
		{
			if(max_jump<i) //说明不能再向前
				return false;
			if(nums[i]+i>max_jump) //说明可以达到更远,刷新max_jump
				max_jump=nums[i]+i;
		}
		if(max_jump>=length-1) //说明可以跳到目标位置
			return true;
		return false;		
    }
};

45. 跳跃游戏 II

给定一个非负整数数组,你最初位于数组的第一个位置。数组中的每个元素代表你在该位置可以跳跃的最大长度。

你的目标是使用最少的跳跃次数到达数组的最后一个位置。

示例:
输入: [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
     从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。

分析:

  • 如果现在在某一个起跳点,且该起跳点的距离为 d ,则之后的 $d $ 个点是可以从目前位置一步跳到的。即如果当前是第一次跳跃,则对于后面d个点来说,都是第二次跳跃,故后面的d个点的跳跃最大距离,就是目前来说第二次跳跃的最远距离
  • 因此在实现时,用 m a x _ j u m p max\_jump max_jump 标记最远距离,用 e n d end end 表示当前步数所能到达的最远距离,因此每次到达 e n d end end处,就需要更新 t i m e s times times e n d end end ,而 m a x _ j u m p max\_jump max_jump是在每一个位置都要更新的。基于该思路,从第一个位置开始进行贪心,首先令 e n d end end 为当前位置能到达的最远,则说明从第一个位置到 e n d end end 都是一步可达的,然后遍历这些位置,再次计算这些位置所能到达的最远处的最大值,作为新的 e n d end end .
class Solution {
public:
    int jump(vector<int>& nums) {
        int length=nums.size();
		int times=0;
		int max_jump=0;
		int end=0; 
		for(int i=0;i<length-1;i++)
		{
			max_jump=max(max_jump,nums[i]+i);  //刷新max_jump 
			if(i==end) //到达times步所能到达的最远距离了,之后需要times+1步 
			{
				times++;
				end=max_jump;  //end更新为下一步能到达的最远距离	
			}	
		}
		return times;
    }
};

134. 加油站

在一条环路上有 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 可为起始索引。

分析:将gas和cost联合起来考虑,当前的问题可以简化为 从某一点出发,在其他地方会有一个油量,该油量可正可负,实际上就是到该地方获得的 gas 减去到达该地方所需的 cost 。因此,该题就类似于最大连续数列和了 。

  • 该题特殊在是环行路,且如果有解则解唯一
  • 因此设置一个sum变量,sum为 ∑ g a s i − c o s t i \sum{gas_i}-cost_i gasicosti ,最终如果sum>=0,则说明存在解,否则无解
  • 从第一个节点开始,设置其为起始点,如果从起始点到某个点的和是正数,则继续遍历,如果的到达某一点为负数,说明从该点出发不能遍历(不但这个起始点出发不行,而且说明了从这个起始点到该点间的所有点都不可以作为起始点)。因此将起始点设置为当前点的下一个节点(因为当前节点一定是耗油而不是加油),重置space,继续遍历
class Solution {
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) 
	{
		int start=0;// 从start出发 
		int spare=0; //从start出发的话到当前位置的油量
		int sum=0;  //记录总和
		for(int i=0;i<gas.size();i++)
		{
			spare+=gas[i]-cost[i]; //
			sum+=gas[i]-cost[i];
			if(spare<0)  //spare<0说明从start开始不满足,将start更新为当前位置的下一个位置
			{
				start=i+1;	
				spare=0;
			}	
		}
		return (sum<0)?-1:(start); 
    }
};

452. 用最少数量的箭引爆气球

在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以y坐标并不重要,因此只要知道开始和结束的x坐标就足够了。开始坐标总是小于结束坐标。平面内最多存在104个气球。

一支弓箭可以沿着x轴从不同点完全垂直地射出。在坐标x处射出一支箭,若有一个气球的直径的开始和结束坐标为 x s t a r t x_{start} xstart x e n d x_{end} xend, 且满足 x s t a r t x_{start} xstart ≤ x ≤ x e n d x_{end} xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量.

Example:
输入:
[[10,16], [2,8], [1,6], [7,12]]
输出:
2
解释:
对于该样例,我们可以在x = 6(射爆[2,8],[1,6]两个气球)和 x = 11(射爆另外两个气球)。

分析:将气球按照开始坐标进行排序,然后维护一个射击区间,以次考虑气球,如果可以通过调整射击区间使该气球能够被一起引爆,则调整射击区间即可。如果一个气球在射击区间之外,则说明需要增加弓箭,再次射击。

bool cmp(vector<int> &a,vector<int> &b) //按照begin排序 
{
	return a[0]<b[0]; 
} 
class Solution 
{
public:
    int findMinArrowShots(vector< vector<int> >& points)
     {
		if(points.size()<=1) return points.size();
		sort(points.begin(),points.end(),cmp); //按照左端点从小到大排序
		int result=1; //弓箭数量
		int shoot_left=points[0][0];  //维护一个射击区间 
		int shoot_right=points[0][1]; 
		for(int i=1;i<points.size();i++)
		{
			if(points[i][0]<=shoot_right)  //说明可以一并射击 
			{
				shoot_left=points[i][0]; //射击区间左端点向右移动
				if(points[i][1]<shoot_right)
					shoot_right=points[i][1]; 
			}
			else  //不可以一并射击,
			{
				result++;
				shoot_left=points[i][0];
				shoot_right=points[i][1];
			}
		}
		return result;	 
    }
};

135. 分发糖果

老师想给孩子们分发糖果,有 N 个孩子站成了一条直线,老师会根据每个孩子的表现,预先给他们评分。

你需要按照以下要求,帮助老师给这些孩子分发糖果:

每个孩子至少分配到 1 个糖果。 相邻的孩子中,评分高的孩子必须获得更多的糖果。
那么这样下来,老师至少需要准备多少颗糖果呢?

示例 1:
输入: [1,0,2]
输出: 5
解释: 你可以分别给这三个孩子分发 212 颗糖果。

分析:相邻的孩子中,评分高的孩子必须获得更多的糖果,因此假设A与B相邻(A在B左侧)

  • 如果 r a t i n g s A < r a t i n g s B ratings_A ratingsA<ratingsB ,则B应当比A糖果多,左规则
  • 如果 r a t i n g s A > r a t i n g s B ratings_A >ratings_B ratingsA>ratingsB , 则A应当比B糖果多,右规则

因此,令每一个相邻的孩子都满足以上两个规则即可。

  • 设置两个变量数组,left和right,分别用来存储单独满足左规则和右规则所需的最小糖数
  • 从左向右遍历,如果 r a t i n g s B > r a t i n g s A ratings_B>ratings_A ratingsB>ratingsA ,则令 l e f t [ B ] = l e f t [ A ] + 1 left[B]=left[A]+1 left[B]=left[A]+1 ,否则保持不变即可
  • 从右向左遍历,如果 r a t i n g s A > r a t i n g s B ratings_A >ratings_B ratingsA>ratingsB ,则令 $right[A]=right[B]+1 $,否则保持不变即可
  • 因此对于任意一个学生, m a x ( l e f t [ i ] , r i g h t [ i ] ) max(left[i],right[i]) max(left[i],right[i]) 必定满足要求
  • 因此对 m a x ( l e f t [ i ] , r i g h t [ i ] ) max(left[i],right[i]) max(left[i],right[i]) 求和即可
class Solution {
public:
    int candy(vector<int>& ratings) 
    {
        int length=ratings.size();
        if(length<=1) return length;
        int left[length];
		int right[length]; 
        for(int i=0;i<length;i++)
        {
        	left[i]=right[i]=1;
		}
		for(int i=1;i<length;i++) //从左向右遍历 
		{
			if(ratings[i]>ratings[i-1])
				left[i]=left[i-1]+1;
		}
		for(int i=length-2;i>=0;i--)
		{
			if(ratings[i]>ratings[i+1])
				right[i]=right[i+1]+1;
		}
		int sum=0;
		for(int i=0;i<length;i++)
		{
			sum+=max(right[i],left[i]);
		}
		return sum;
		
    }
};

921. 使括号有效的最少添加

给定一个由 ′ ( ′ '(' ( ′ ) ′ ')' )括号组成的字符串 S,我们需要添加最少的括号( ′ ( ′ '(' ( 或是 ′ ) ′ ')' ),可以在任何位置),以使得到的括号字符串有效。

从形式上讲,只有满足下面几点之一,括号字符串才是有效的:

它是一个空字符串,或者它可以被写成 AB (A 与 B 连接), 其中 A 和 B 都是有效字符串,或者它可以被写作 (A),其中 A 是有效字符串。
给定一个括号字符串,返回为使结果字符串有效而必须添加的最少括号数。

分析:因为字符串中只存在左括号或者右括号,因此将能成立的完整括号消除,则最后剩下的就是需要添加括号进行消除的。 在括号匹配中,常用栈作为数据结构,如果添加的和栈顶的匹配可消除,则将栈顶弹出,否则将其入栈

class Solution {
public:
    int minAddToMakeValid(string S) 
    {
		stack<char> sta;
		for(int i=0;i<S.size();i++)
		{
			if(!sta.empty()&&sta.top()=='('&&S[i]==')') //可消除 
				sta.pop();
			else
				sta.push(S[i]);	 //不可消除则push进 
		}
		return sta.size();		 
    }
};

1326. 灌溉花园的最少水龙头数目

在 x 轴上有一个一维的花园。花园长度为 n n n,从点 0 开始,到点 n n n 结束。

花园里总共有 n + 1 n + 1 n+1 个水龙头,分别位于 [0, 1, …, n] 。

给你一个整数 n n n 和一个长度为 n + 1 n + 1 n+1 的整数数组 r a n g e s ranges ranges ,其中 r a n g e s [ i ] ranges[i] ranges[i] (下标从 0 开始)表示:如果打开点 i 处的水龙头,可以灌溉的区域为 [ i − r a n g e s [ i ] , i + r a n g e s [ i ] ] [i - ranges[i], i + ranges[i]] [iranges[i],i+ranges[i]]

请你返回可以灌溉整个花园的 最少水龙头数目 。如果花园始终存在无法灌溉到的地方,请你返回 -1 。
leetcode 经典贪心算法题目(思路、方法、code)_第2张图片

分析:感觉该题目有点 45. 跳跃游戏 II 和452. 用最少数量的箭引爆气球 的结合版

因此大致思路为:首先要确定出每一个水龙头的灌溉区间,然后对其左排序,贪心思想为从一个起点出发,应该尽可能地选取右届最大的区间,即相当于确定出第一个水龙头灌溉的区间后,在该区间内找到第二个水龙头,尽可能使得该水龙头的右届最远,依次继续贪心

class Solution {
public:
    //题解:贪心法
    //1:首先遍历rangs,建立跳跃游戏Ⅱ中的跳跃数组,left表示起始点,right-left表示最大跳跃距离
    //2:使用跳跃游戏Ⅱ中的代码即可,不过每次到达边界end,需判断furthest是否超过end
    int minTaps(int n, vector<int>& ranges) {
        //1、建立跳跃数组
        vector<int> jumps(n+1);
        for(int i=0;i<n+1;++i){
            int left=max(i-ranges[i],0);
            int right=min(i+ranges[i],n);
            if(jumps[left]<right-left){
                jumps[left]=right-left;
            }
        }
        //2、贪心法跳跃
        int furthest=0,end=0,count=0;
        for(int i=0;i<n;++i){//注意最后一个点不能遍历,因为在i==end==0时,count多统计了一次
            furthest=max(jumps[i]+i,furthest);
            if(furthest>=n){
                count++;
                break;
            }
            if(i==end){
                //若最远距离没有超过边界,直接返回-1
                if(furthest<=end)return -1;
                count++;
                end = furthest;
            }
        }
        return count;
    }
};

你可能感兴趣的:(数据结构与算法)