大家好,我是Simon 郎,一个每天想要博学一点点的小青年!
明天为大家分享的是 贪婪算法。
贪婪算法:贪婪算法(又称贪心算法)是指,在对问题求解时,总是做出在以后看来是最好的抉择。也就是说,不从整体最优上加以思考,算法失去的是在某种意义上的部分最优解,从而使得失去的最终后果是全局最优或者靠近于全局最优。
本文选取 LeetCode 比拟有代表性的题目来学习贪婪算法,选取的题目如下:
[toc]
每道题目都有五局部组成:
- 题目形容
- 示例
- 解题思路
- 代码实现
- 执行后果
1、散发饼干(455)
题目形容:
假如你是一位很棒的家长,想要给你的孩子们一些小饼干。然而,每个孩子最多只能给一块饼干。
对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j]。如果 s[j] >= g[i],咱们能够将这个饼干 j 调配给孩子 i,这个孩子会失去满足。你的指标是尽可能满足越多数量的孩子,并输入这个最大数值。
示例:
示例 1:
输出: g = [1,2,3], s = [1,1]
输入: 1
解释:
你有三个孩子和两块小饼干,3 个孩子的胃口值别离是:1,2,3。
尽管你有两块小饼干,因为他们的尺寸都是 1,你只能让胃口值是 1 的孩子满足。
所以你应该输入 1示例 2:
输出: g = [1,2], s = [1,2,3]
输入: 2
解释:
你有两个孩子和三块小饼干,2 个孩子的胃口值别离是 1,2。
你领有的饼干数量和尺寸都足以让所有孩子满足。
所以你应该输入 2.
思路:
采纳贪婪算法:大尺寸的饼干可能满足胃口小的孩子,也可能满足胃口大的孩子,所以对于大尺寸的饼干要优先满足胃口大的孩子,而胃口小的孩子用小尺寸的饼干来喂,尽量减少节约。
部分最优 :充分利用饼干尺寸满足一个; 全局最优:就是满足尽可能多的孩子
首先将饼干数组和孩子数组进行排序,而后从前往后顺次进行比拟。
胃口比饼干尺寸小,孩子被满足,持续遍历下一个孩子和下一块饼干
胃口比饼干尺寸大,遍历下一块饼干
返回已被满足的孩子数量
class Solution {public int findContentChildren(int[] g, int[] s) {Arrays.sort(g);
Arrays.sort(s);
int child=0, cookie=0;
while(child<g.length && cookie<s.length){if(g[child]<=s[cookie]) child++;
cookie++;
}
return child;
}
}
执行后果:
2、散发糖果(135)
题目形容:
老师想给孩子们散发糖果,有 N 个孩子站成了一条直线,老师会依据每个孩子的体现,事后给他们评分。
你须要依照以下要求,帮忙老师给这些孩子散发糖果:
每个孩子至多调配到 1 个糖果。
评分更高的孩子必须比他两侧的邻位孩子取得更多的糖果。
那么这样下来,老师至多须要筹备多少颗糖果呢?
示例:
示例 1:
输出:[1,0,2]
输入:5
解释:你能够别离给这三个孩子散发 2、1、2 颗糖果。示例 2:
输出:[1,2,2]
输入:4
解释:你能够别离给这三个孩子散发 1、2、1 颗糖果。第三个孩子只失去 1 颗糖果,这已满足上述两个条件。
解题思路:
将相邻孩子,评分高的孩子必须取得更多的糖果 拆分成两个规定:
- 规定定义:设学生 A 和学生 B 左右相邻,A 在 B 的右边
左规定:当 B >A 时,B 的糖果比 A 的数量多
右规定:当 A >B 时,则 A 的糖果比 B 的糖果数量多
相邻的学生中,评分高的学生必须取得更多的糖果 等价于 所有学生满足左规定且满足右规定。
- 算法流程
- 把所有孩子的糖果数初始化为 1。
- 先从左往右遍历一遍,如果左边孩子的评分比右边的高,则左边孩子的糖果数更新为右边孩子的 糖果数加 1。
- 再从右往左遍历一遍,如果右边孩子的评分比左边的高,且右边孩子以后的糖果数 不大于左边孩子的糖果数,则右边孩子的糖果数更新为左边孩子的糖果数加 1。
代码:
class Solution {public int candy(int[] ratings) {
// 循环遍历两次
int[] candy=new int[ratings.length];
int count=candy.length;
for(int i=1;i<ratings.length;i++){if(ratings[i-1]<ratings[i]){candy[i]=candy[i-1]+1;
}
}
for(int j=ratings.length-1;j>0;j--){if(ratings[j]<ratings[j-1]){candy[j-1]=Math.max(candy[j-1],candy[j]+1);
}
}
for(int k=0;k<candy.length;k++){count+=candy[k];
}
return count;
}
}
执行后果:
3、无重叠区间(435)
题目形容:
给定一个区间的汇合,找到须要移除区间的最小数量,使残余区间互不重叠。
留神:能够认为区间的起点总是大于它的终点;区间 [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
解释: 你不须要移除任何区间,因为它们曾经是无重叠的了。
解题思路:
首先要对区间进行排序,本题采纳的是对区间的的头元素进行排序,而后在遍历空间
- 如果前面区间的头小于以后区间的尾,比方以后区间是 [3,6],前面区间是[4,5] 或者是[5,9], 阐明这两个区间有反复,必须要移除一个,那么要移除哪个呢,为了避免在下一个区间和现有区间有重叠,咱们应该让现有区间越短越好,所以应该移除尾部比拟大的,保留尾部比拟小的。
2,如果前面区间的头不小于以后区间的尾,阐明他们没有重合,不须要移除
代码:
class Solution {public int eraseOverlapIntervals(int[][] intervals) {
// 对 2 * 2 数组依照左边界排序
Arrays.sort(intervals,(a,b)->a[0]-b[0]);
int end=intervals[0][1];
int count=0;
for(int i=1;i<intervals.length;i++){if(intervals[i][0]<end){
// 区间有重叠, 选取右边界最小的数组
end=Math.min(end,intervals[i][1]);
count++;
}else{
// 区间无重叠,间接更新就好
end=intervals[i][1];
}
}
return count;
}
}
执行后果:
4、种花问题(605)
题目形容:
假如有一个很长的花坛,一部分地块种植了花,另一部分却没有。可是,花不能种植在相邻的地块上,它们会抢夺水源,两者都会死去。
给你一个整数数组 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
解题思路:
题目要求是否能在不突破规定的状况下插入 n 朵花,与间接计算不同,采纳“跳格子”的解法只需遍历不到一遍数组,解决以下两种不同的状况即可:
- 当遍历到 index 遇到 1 时,阐明这个地位有花,那必然从 index+ 2 的地位才有可能种花,因而当碰到 1 时间接跳过下一格。
- 当遍历到 index 遇到 0 时,因为每次碰到 1 都是跳两格,因而前一格必然是 0,此时只须要判断下一格是不是 1 即可得出 index 这一格能不能种花,如果能种则令 n 减一,而后这个地位就依照遇到 1 时解决,即跳两格;如果 index 的后一格是 1,阐明这个地位不能种花且之后两格也不可能种花, 间接跳过 3 格。
当 n 减为 0 时,阐明能够种入 n 朵花,则能够间接退出遍历返回 true;如果遍历完结 n 没有减到 0,阐明最多种入的花的数量小于 n,则返回 false。
代码:
class Solution {public boolean canPlaceFlowers(int[] flowerbed, int n) {
// 采纳跳格子的思维解决
//1,index 为 1,跳两格
//2 index 为 0,证实前一个格子是 0,只须要判断下一个格子是否为 0
// 若为 0,则 n -1,
// 若为 1,则跳三格
for(int i=0;i<flowerbed.length && n>0;){if(flowerbed[i]==1){
i=i+2;
// 肯定要留神边界问题
} else if(i==flowerbed.length-1 || flowerbed[i+1]==0){
n--;
i=i+2;
}else{i=i+3;}
}
return n<=0;
}
}
执行后果:
5、用起码数量的箭引爆气球(452)
题目形容:
在二维空间中有许多球形的气球。对于每个气球,提供的输出是程度方向上,气球直径的开始和完结坐标。因为它是程度的,所以纵坐标并不重要,因而只有晓得开始和完结的横坐标就足够了。开始坐标总是小于完结坐标。
一支弓箭能够沿着 x 轴从不同点齐全垂直地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和完结坐标为 xstart,xend,且满足 xstart ≤ x ≤ xend,则该气球会被引爆。能够射出的弓箭的数量没有限度。弓箭一旦被射出之后,能够有限地后退。咱们想找到使得所有气球全副被引爆,所需的弓箭的最小数量。
给你一个数组 points,其中 points [i] = [xstart,xend],返回引爆所有气球所必须射出的最小弓箭数。
示例:
示例 1:
输出:points = [[10,16],[2,8],[1,6],[7,12]]
输入:2
解释:对于该样例,x = 6 能够射爆 [2,8],[1,6] 两个气球,以及 x = 11 射爆另外两个气球示例 2:
输出:points = [[1,2],[3,4],[5,6],[7,8]]
输入:4示例 3:
输出:points = [[1,2],[2,3],[3,4],[4,5]]
输入:2示例 4:
输出:points = [[1,2]]
输入:1示例 5:
输出:points = [[2,3],[2,3]]
输入:1
解题思路:
首先想到的思路是对数组中的区间值按左端点进行排序
- 如果新气球的左端点大于射击区间的右边界,那么咱们就须要从新开拓一个区间;
- 如果新气球的左端点小于射击区间的右边界,这里又要分为两种状况:如果右端点大于射击区间的右边界,那么咱们的射击区间的右边界无需变动;如果右端点小于射击区间的右边界,那么咱们射击区间的右边界就要向左挪动,以新气球的右端点为准,确立新的边界。
代码:
class Solution {public int findMinArrowShots(int[][] points) {
// 贪婪算法,找出一支箭引爆最多的气球的
if(points.length==0) return 0;
// 对二维数组依照左边界从小到大排休排序
Arrays.sort(points,(a,b)->a[0]<b[0]?-1:1);
int right=points[0][1];
int count=1;
for(int i=1;i<points.length;i++){if(points[i][0]>right){
count++;
right=points[i][1];
}
else{if(points[i][1]<right){right=points[i][1];
}
}
}
return count;
}
}
执行后果:
6、划分字母区间(763)
题目形容:
字符串 S
由小写字母组成。咱们要把这个字符串划分为尽可能多的片段,同一字母最多呈现在一个片段中。返回一个示意每个字符串片段的长度的列表。
示例:
输出:S = “ababcbacadefegdehijhklij”
输入:[9,7,8]
解释:
划分后果为 “ababcbaca”, “defegde”, “hijhklij”。
每个字母最多呈现在一个片段中。
像 “ababcbacadefegde”, “hijhklij” 的划分是谬误的,因为划分的片段数较少。
解题思路:
因为同一个字母只能呈现在同一个片段,显然同一个字母的第一次呈现的下标地位和最初一次呈现的下标地位必须呈现在同一个片段。因而须要遍历字符串,失去每个字母最初一次呈现的下标地位。
在失去每个字母最初一次呈现的下标地位之后,能够应用贪婪的办法将字符串划分为尽可能多的片段,具体做法如下。
- 从左到右遍历字符串,遍历的同时保护以后片段的开始下标 start 和完结下标 end,初始化 start=end=0。
- 对于每个拜访到的字母,失去以后字母的最初一次呈现的下标地位 end1,则以后片段的完结下标肯定不会小于 end1,因而,令 end=max(end,end1)
- 当拜访到下标 end 时,以后片段拜访完结,以后片段的下标范畴是 [start,end],长度为 end−start+1,将以后片段的长度增加到返回值,而后令 start=end+1,持续寻找下一个片段
- 反复上述过程,直至遍历完字符串
代码:
class Solution {public List<Integer> partitionLabels(String s) {
// 学生成一个长度为 26 的数组,用于寄存每个的不同字符在序列中的最终地位
int[] alphabet=new int[26];
List list=new ArrayList<>();
int start=0, end=0;
for(int i=0;i<s.length();i++){alphabet[s.charAt(i)-'a']=i;
}
for(int i=0;i<s.length();i++){end=Math.max(end,alphabet[s.charAt(i)-'a']);
if(end==i){list.add(end-start+1);
start=end+1;
}
}
return list;
}
}
执行后果:
7、交易股票的最佳时机(122)
题目形容:
给定一个数组 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。
解题思路:
- 因为该题能够交易有限次,只有第二天的价格大于前一天的价格咱们就买前一天的股票,而后在第二天卖掉,这样咱们就能从中获取利润;
- 反之,如果第二天的价格小于前一天的价格咱们就不进行交易。
代码:
class Solution {public int maxProfit(int[] prices) {
//
int sum=0;
int start=0, end=0;
for(int i=1;i<prices.length;i++){if(prices[i]-prices[i-1]>0){sum+=prices[i]-prices[i-1];
}
}
return sum;
}
}
执行后果
8、依据身高重建队列(406)
题目形容:
假如有打乱程序的一群人站成一个队列,数组 people 示意队列中一些人的属性(不肯定按程序)。每个 people[i] = [hi, ki] 示意第 i 集体的身高为 hi,后面 正好 有 ki 个身高大于或等于 hi 的人。
请你从新结构并返回输出数组 people 所示意的队列。返回的队列应该格式化为数组 queue,其中 queue[j] = [hj, kj] 是队列中第 j 集体的属性(queue[0] 是排在队列后面的人)。
示例:
示例 1:
输出:people = [[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]]
输入:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]]
解释:
编号为 0 的人身高为 5,没有身高更高或者雷同的人排在他后面。
编号为 1 的人身高为 7,没有身高更高或者雷同的人排在他后面。
编号为 2 的人身高为 5,有 2 个身高更高或者雷同的人排在他后面,即编号为 0 和 1 的人。
编号为 3 的人身高为 6,有 1 个身高更高或者雷同的人排在他后面,即编号为 1 的人。
编号为 4 的人身高为 4,有 4 个身高更高或者雷同的人排在他后面,即编号为 0、1、2、3 的人。
编号为 5 的人身高为 7,有 1 个身高更高或者雷同的人排在他后面,即编号为 1 的人。
因而 [[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] 是从新结构后的队列。示例 2:
输出:people = [[6,0],[5,0],[4,0],[3,2],[2,2],[1,4]]
输入:[[4,0],[5,0],[2,2],[3,2],[1,4],[6,0]]
解题思路:
个别这种数对,还波及排序的,依据第一个元素正向排序,依据第二个元素反向排序,或者依据第一个元素反向排序,依据第二个元素正向排序,往往可能简化解题过程。
在本题目中,首先对数对进行排序,依照数对的元素 1 降序排序,依照数对的元素 2 升序排序。
起因是,依照元素 1 进行降序排序,对于每个元素,在其之前的元素的个数,就是大于等于他的元素的数量,而依照第二个元素正向排序,咱们心愿 k 大的尽量在前面,缩小插入操作的次数。
代码:
class Solution {public int[][] reconstructQueue(int[][] people) {// [7,0], [7,1], [6,1], [5,0], [5,2], [4,4]
// 再一个一个插入。// [7,0]
// [7,0], [7,1]
// [7,0], [6,1], [7,1]
// [5,0], [7,0], [6,1], [7,1]
// [5,0], [7,0], [5,2], [6,1], [7,1]
// [5,0], [7,0], [5,2], [6,1], [4,4], [7,1]
Arrays.sort(people, (o1, o2) -> o1[0] == o2[0] ? o1[1] - o2[1] : o2[0] - o1[0]);
LinkedList<int[]> list = new LinkedList<>();
for (int[] i : people) {list.add(i[1], i);
}
return list.toArray(new int[list.size()][2]);
}
}
执行后果
9、非递加数列(665)
题目形容:
给你一个长度为 n 的整数数组,请你判断在 最多 扭转 1 个元素的状况下,该数组是否变成一个非递加数列。
咱们是这样定义一个非递加数列的:对于数组中任意的 i (0 <= i <= n-2),总满足 nums[i] <= nums[i + 1]。
示例:
示例 1:
输出: nums = [4,2,3]
输入: true
解释: 你能够通过把第一个 4 变成 1 来使得它成为一个非递加数列。示例 2:
输出: nums = [4,2,1]
输入: false
解释: 你不能在只扭转一个元素的状况下将其变为非递加数列。
解题思路:
本题是要维持一个非递加的数列,所以遇到递加的状况时(nums[i] > nums[i + 1]),要么将后面的元素放大,要么将前面的元素放大。
然而本题惟一的易错点就在这,
如果将 nums[i]放大,可能会导致其无奈融入后面曾经遍历过的非递加子数列;
如果将 nums[i + 1]放大,可能会导致其后续的持续呈现递加;
所以要采取贪婪的策略,在遍历时,每次须要看间断的三个元素,也就是前怕狼; 后怕虎,遵循以下两个准则:须要尽可能不放大 nums[i + 1],这样会让后续非递加更艰难;如果放大 nums[i],但不毁坏后面的子序列的非递减性;
算法步骤:遍历数组,如果遇到递加:
还能批改:
批改计划 1:将 nums[i]放大至 nums[i + 1];
批改计划 2:将 nums[i + 1]放大至 nums[i];
不能批改了:间接返回 false;
代码:
class Solution {public boolean checkPossibility(int[] nums) {if(nums.length==1) return true;
boolean flag=nums[0]<=nums[1]?true:false;
for(int i=1;i<nums.length-1;i++){if(nums[i+1]<nums[i]){if(flag){if(nums[i+1]>=nums[i-1])
nums[i]=nums[i+1];
else
nums[i+1]=nums[i];
flag=false;
}
else
return false;
}
}
return true;
}
}
执行后果