【贪心】435. 无重叠区间
知识点:贪心;
题目描述
给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。
注意:
可以认为区间的终点总是大于它的起点。
区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。
示例
输入: [ [1,2], [2,3], [3,4], [1,3] ]
输出: 1
解释: 移除 [1,3] 后,剩下的区间没有重叠。
输入: [ [1,2], [1,2], [1,2] ]
输出: 2
解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。
输入: [ [1,2], [2,3] ]
输出: 0
解释: 你不需要移除任何区间,因为它们已经是无重叠的了。
解法一:贪心
贪心其实是动态规划的一个特例,贪心算法需要满足更多的条件,但是只要满足条件,贪心算法要更快;
贪心是什么呢,简单来说就是每一步我们都选择在此时的最优解,也就是当前的局部最优解,最终的结果就是全局最优了。它所做的只是当前来看最好的选择,而并不管全局。比如说如果想从一堆人民币里拿10张怎么拿能拿的最多,很显然每次都从剩下的钱里拿最大的就可以了,那最后一定是最多的。
贪心问题的关键就是要选择贪心策略,对于一个问题可以有很多贪心策略,但是很多策略都不能够得到正确答案。如何能得到一个正确的贪心策略没有固定的模板,需要的是经验。
我们把这道题目转化一下思路:移除最小数量,然后区间互不重叠,那反过来,其实就是在求这个区间最多有几个互不相交的空间,得到这个结果之后,拿总个数减去就是需要移除的最小个数了。
像这个问题还有很多,可以把其统一称为:区间调度问题。基本上就是给很多个区间,找出这个区间内最多有多少个不重叠的。比如此题,再比如说在一天内有很多活动。每个活动有自己的时间段,问一天最多能参加几个活动呢。这也是求区间内最多有几个不相交的空间。关键就是要去找一种调度策略。
我们怎么来计算一个区间内最多有几个不相交的空间呢,怎么选择贪心策略呢,比如我们可以选择区间开始最早的那个,但是一想又不行,万一这个是开始的早,但是它维持时间长呢;那我们选择区间时间最短的? 其实也不行,想一下比如有个区间很短,但是呢它正好和前后两个长的都有交集,所以一旦选择这个短的,那我们长的就不能选了,很显然也错误。
正确的贪心策略:
我们从所有区间里选择结束时间最早的,也就是右区间最小的,然后把与其相交的就不要了,接着选择这个区间走完之后下一个的。(就是想让一个区间早点结束开始下一个)
这道题目贪心贪的就是谁先结束;
流程:
1.按结束时间排序;
2.如果此区间与上一区间不重叠,那可以安排;
3.直到遍历完所有区间;
其实是一种最自然朴素的贪心法,我们看哪件事情能最早结束,就先去做它,也就是把结束早的都依次做了,那最后就是做的最多的。数学上也是可以证明的,但是在做题时不必纠结这个问题。重要的是一种经验的积累。
class Solution {
//按end的升序排序;
//初始化结尾;
//区间的开始;
//找到了互补重叠的区间;
//更新end;
}
体会
- 要学会重写Comparator接口,可以按照自己的意愿就行排序;
Collections.sort(list, new Comparator<Node>() {
/**o1-o2为升序序排列,o2-o1为降序排列,若具体到某一字段,则根据该字段进行排列*/
@Override
public int compare(Node o1, Node o2) {
if (o1.x==o2.x) //若x属性相等,根据y来升序
return o1.y-o2.y;
return o1.x-o2.x;//x属性不相等,根据x来升序排列
}
});
- 涉及到数对的,一般都会对数对中的某一个维度进行排序,或者两个维度都排序,比如说区间调度按照结尾排序,比如说重建队列按照身高降序,按照k升序;所以一定要会重写比较器接口;
738. 单调递增的数字
知识点:字符串;贪心
题目描述
给定一个非负整数 N,找出小于或等于 N 的最大的整数,同时这个整数需要满足其各个位数上的数字是单调递增。
(当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。)
示例
输入: N = 10
输出: 9
输入: N = 1234
输出: 1234
输入: N = 332
输出: 299
解法一:贪心
想一下这个过程,如果比前一位大的话那就可以不用动直接返回;如果比前一位小的话例如98,应该怎么做呢,我们取小于等于8的都没用,因为都比前一位要小,不满足递增,所以只能让前一位变小一位了,9变成8,那我们最后一位自然要取到9,高位数和这个二位数一个道理,只要有比前一位小的了,那自然要把前一位-1,后面所有位变为9;
但是还有一点要注意,比如332,我们找到2小了,那把前一位减1了,但是减完之后自己又比更前一位的小了,所有更前一位也得减1;只要自己前面减1了,那自己就不用管了,赋成9就行,所以还要找到减完之后仍然满足递增的那个位置;
所以这道题是在找两个位置
- 往后找:第一个比前一位小的位置;
- 往前找:往前开始-1后第一个仍然满足递增的位置;
class Solution {
//转换成字符数组方便操作,注意这个方法;
//往后找:找到第一个比前一位小的元素;
//往前找:前一位-1后可能又比更前一位小了,所以循环往前找到满足递增的;
//后面全是9就可以了;
//字符数组转成字符串,再转成数组;
}
体会
要学会整形转字符串,转字符数组:Integer.toString(n).toCharArray();
要学会字符数组转字符串,转整形:Integer.parseInt(new String(str));
134. 加油站
知识点:贪心;KMP
题目描述
在一条环路上有 N 个加油站,其中第 i 个加油站有汽油 gas[i] 升。
你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。
如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -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 = [2,3,4]
cost = [3,4,3]
输出: -1
解释:
你不能从 0 号或 1 号加油站出发,因为没有足够的汽油可以让你行驶到下一个加油站。
我们从 2 号加油站出发,可以获得 4 升汽油。 此时油箱有 = 0 + 4 = 4 升汽油
开往 0 号加油站,此时油箱有 4 - 3 + 2 = 3 升汽油
开往 1 号加油站,此时油箱有 3 - 3 + 3 = 3 升汽油
你无法返回 2 号加油站,因为返程需要消耗 4 升汽油,但是你的油箱只有 3 升汽油。
因此,无论怎样,你都不可能绕环路行驶一周。
解法一:贪心
这道题目也是一种贪心,仔细看一下这个题,我们怎么就能转一圈呢,我们可以用gas[i]-cost[i]得到我们经过第i个加油站能得到的汽油,当然这个汽油有可能是负数,那证明入不敷出了,肯定走不下去了。所以可以把所有的这个值加起来,如果最后<0, 那说明这道题谁做起点也走不了一圈,如果>0, 说明是有解的。
想一下暴力的解法:我们从第一个加油站开始,假设走到第i个位置没法走了,接着我们从第二个加油站再开始,直到最后。
其实暴力解法这个过程中有很多重复的判断和计算,想一下KMP算法:充分利用之前的信息。比如第一个匹配不上后我们不用再从第二个开始,直接跳到最大公共前后缀的位置上就可以,这就是断定了这中间的那些值不可能;这道题目也是,比如说我们从i开始,走到k走不下去了,其实从i到k这之间的所有值都已经不能作为起点了。 为什么,想一下,假设从i到k的gas[i]-cost[i]的总和,这个和肯定<0;因为到k走不下去了嘛,那在i和k之间的j呢,j到k肯定也小于0,因为i肯定能到j(i走到k才走不动),所以i到j是大于0的,所以j到k肯定小于0,所以i到k之间的都不能用了,直接从k+1开始,这就是这道题目的贪心所在。
贪心选择策略:从左到右遍历,如果当前油量大于消耗,那就能接着走,走不动的时候直接从走不动位置的下一个开始;
class Solution {
//从start开始是不可能;
//经过此站剩余多少油;
//判断环;
//贪心所在:中间的就都不用看了;
}
122. 买卖股票的最佳时机 II
知识点:贪心;
题目描述
给定一个数组 prices ,其中 prices[i] 是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例
输入: prices = [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
输入: prices = [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
输入: prices = [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
解法一:贪心
这道题也是一道贪心的题目,因为可以重复买卖,所以贪心的选择策略就是 如果后一天的价格比前一天高,那就卖了。
这个贪心比较常识类的。
class Solution {
public int maxProfit(int[] prices) {
int profit = 0;
if(prices == null || prices.length < 2) return 0;
int index = 0;
while(index < prices.length-1){
if(prices[index] < prices[index+1]){
profit += prices[index+1]-prices[index];
}
index++;
}
return profit;
}
}
860. 柠檬水找零
知识点:贪心
题目描述
在柠檬水摊上,每一杯柠檬水的售价为 5 美元。
顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。
每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。
注意,一开始你手头没有任何零钱。
如果你能给每位顾客正确找零,返回 true ,否则返回 false 。
示例
输入:[5,5,5,10,20]
输出:true
解释:
前 3 位顾客那里,我们按顺序收取 3 张 5 美元的钞票。
第 4 位顾客那里,我们收取一张 10 美元的钞票,并返还 5 美元。
第 5 位顾客那里,我们找还一张 10 美元的钞票和一张 5 美元的钞票。
由于所有客户都得到了正确的找零,所以我们输出 true。
输入:[5,5,10]
输出:true
输入:[10,10]
输出:false
输入:[5,5,10,10,20]
输出:false
解释:
前 2 位顾客那里,我们按顺序收取 2 张 5 美元的钞票。
对于接下来的 2 位顾客,我们收取一张 10 美元的钞票,然后返还 5 美元。
对于最后一位顾客,我们无法退回 15 美元,因为我们现在只有两张 10 美元的钞票。
由于不是每位顾客都得到了正确的找零,所以答案是 false。
解法一:贪心
这道题看起来好像复杂,但是仔细一分析其实发现情况只有3种,那就是我们只能收到5,10,20这三种钞票。那情况就很有限了
- 收到5的直接收下;
- 收到10的找5;
- 收到20的优先找10和5,没有10了再去找3个5.
我们前两种情况没得选,只有第三种,第三种也就是贪心选择:策略就是优先去找10,因为10只有在收到20的时候有用,而5的用处就比它要大多了,5更万能!
在这个过程中只要10和5发现 < 0的时候就返回false。没得找了。
class Solution {
//要记录自己5和10的钱数,20没用,又不能找钱;
//优先找10块的;
//没钱找了,已经透支了;
}
53. 最大子序和(剑指 Offer 42)
知识点:数组;前缀和;哨兵;动态规划;贪心;分治;
题目描述
输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。
要求时间复杂度为O(n)。
示例
输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
解法一:贪心
这道题贪心怎么解?贪什么呢?想一下在这个过程中,比如-2 1,我们需要-2吗?不需要!因为负数只会拉低我们最后的和,只起副作用的索性不如不要了。直接从1开始就行了; 贪的就是负数和一定会拉低结果。
所以我们的贪心选择策略就是:只选择和>0的,对于和<=0的都可以舍弃了。
class Solution {
//对于<=0的前缀和,已经没要意义了,从下一位置开始;
}