贪心题目总结(录哥题集)
文章目录
1.分饼干
思路
小饼干喂给小胃口。大饼干喂给大胃口。小饼干喂给小胃口的时候一定是第一块不行那么立刻转换第二块而不是第一块一直往下喂,由于从小到大所以是无法喂的,所以饼干需要移动,拓展到刚好能投喂的饼干给当前index的胃口。另外一种思路就是大饼干喂给大胃口。大饼干如果喂不了最大的胃口那么立刻往前遍历更小的胃口指直到可以。这次就是胃口需要移动,胃口比饼干大,那么就要找到更小的胃口对应饼干。
大胃口对大饼干
class Solution {
public int findContentChildren(int[] g, int[] s) {
Arrays.sort(g);
Arrays.sort(s);
int index=s.length-1;
int res=0;
for(int i=g.length-1;i>=0;i--){
if(index>=0&&s[index]>=g[i]){
res++;
index--;
}
}
return res;
}
}
小胃口对小饼干
class Solution {
public int findContentChildren(int[] g, int[] s) {
Arrays.sort(g);
Arrays.sort(s);
int res=0;
int index=0;
for(int i=0;i<s.length;i++){
if(index<g.length&&s[i]>=g[index]){
res++;
index++;
}
}
return res;
}
}
2.摆动序列
思路
贪心算法,只要让每次坡度发生变化的时候给结果+1。去掉坡道里面点,只剩下峰值,那么就能够获取到最大的摆动序列。pre=0的时候,cur>0或者是<0也算是一次坡度发生变化。
class Solution {
public int wiggleMaxLength(int[] nums) {
int pre=0;
int cur=0;
int res=1;
for(int i=1;i<nums.length;i++){
cur=nums[i]-nums[i-1];
if((pre<=0&&cur>0)||(pre>=0&&cur<0)){
res++;
pre=cur;
}
}
return res;
}
}
3.最大子序和
思路
贪心,其实就是当count+nums[i]<=0的时候,更新结果之后,就把count重新设置为0,重新从i+1开始添加。因为如果count是负数,那么加上下一个值一定就是减少了,那么就直接重新开始一个子串,选择局部最大,最后能够得到整体最大。时间复杂度是n,如果是暴力那么就是n^2
class Solution {
public int maxSubArray(int[] nums) {
int count=0;
int res=Integer.MIN_VALUE;
for(int i=0;i<nums.length;i++){
count+=nums[i];
//先更新结果
if(count>res){
res=count;
}
if(count<=0){
count=0;
}
}
return res;
}
}
4.买卖股票的最佳时机2
思路
通常的想法就是最低买入最高卖出,先找出最低然后找出最高接着就是移除和计算。而且不能够连续购买所以这种想法还是难以执行。另外一种就是尝试贪心算法,能够把每天的股票利润变成(prices[i+1]-prices[i])+(prices[i]-prices[i-1]),那么最后得到的是price[i+1]-prices[i-1]这是整体看待i-1到i+1天的情况,还有一种看法就是把prices[i+1]-prices[i]看成是一天的利润,变成每天的利润唯度。那么只需要把那些大于0的天加入到结果就是总利润。
class Solution {
public int maxProfit(int[] prices) {
//把它变成天数
int res=0;
for(int i=1;i<prices.length;i++){
int todayMoney=prices[i]-prices[i-1];
if(todayMoney>0){
res+=todayMoney;
}
}
return res;
}
}
5.跳跃游戏1
思路
贪心,每次争取最大的覆盖范围,只要跳跃覆盖范围大于等于数组大小最后的位置那么就结束。每次遍历一个都可以更新覆盖范围,而且遍历数组只能遍历到覆盖范围之内。只要在范围内,不管怎么跳都能跳过去。
class Solution {
public boolean canJump(int[] nums) {
int cover=nums[0];
for(int i=0;i<=cover;i++){
cover=Math.max(nums[i]+i,cover);
if(cover>=nums.length-1) return true;
}
return false;
}
}
6.跳跃游戏2
思路
同样需要计算覆盖范围,但是不同的就是每次要加一个步数的时候首先遍历到走完第一步最大跳跃范围如果不能到达数组尾巴,那么才走下一步。也就是需要记录下一步的最大覆盖范围。意思就是每次走一步看看能走多大的范围,然后遍历的同时记录下一步的最大覆盖范围,也就是当前遍历到的i能够跳到什么地方去。如果cur也就是上次的当前走到的那一步到达它最远跳到的覆盖范围位置没有到达数组尾,那么就要更新步数,走下一步因为这一步能够跳跃到任意它之前覆盖范围的位置,那么只要更多新范围,跳跃到可以覆盖新范围的位置,就能够用最小步数跳到最新的范围,如果最新范围大于数组尾那么就成功
class Solution {
public int jump(int[] nums) {
int res=0;
int cover=0;
int next=0;
int cur=0;
for(int i=0;i<nums.length;i++){
next=Math.max(nums[i]+i,next);
if(cur==i){
if(cur<nums.length-1){
res++;
cur=next;
if(next>=nums.length-1) break;
}
}
}
return res;
}
}
7.k次取反之后最大化数组和
思路
第一种思路就是排序好绝对值,然后把那些排在前面最大的负数绝对值取反,如果k还存在而且是个奇数那么就要给数组最小的那个绝对值数取反。如果是偶数可以直接抵消掉。这里的贪心思维其实就是把负数去掉,那么就能够获取局部最优,把最大负数转换成正数,最小正数转换成负数。最后实现总体最优
class Solution {
public int largestSumAfterKNegations(int[] nums, int k) {
nums=IntStream.of(nums)
.boxed()
.sorted((o1,o2)->{
return Math.abs(o2)-Math.abs(o1);})
.mapToInt(Integer::intValue).toArray();
for(int i=0;i<nums.length;i++){
if(nums[i]<0&&k>0){
nums[i]=-nums[i];
k--;
}
}
if(k%2==1) nums[nums.length-1]=-nums[nums.length-1];
int res=0;
for(int i=0;i<nums.length;i++){
res+=nums[i];
}
return res;
}
}
这种解法思路是,先排好序,如果是负数才进行反转,不是那么就一直在同一个数来回进行反转。而且当前一个绝对值大于下一个的时候才能取反下一个数。原因是现在已经排好序,如果绝对值不大于后一个,也就是说明后面那个数就是正数。正数通常不取反。其实这里的思路也类似于上面id找到的值肯定是绝对值最小的。但是上面的思路更清晰,先把绝对值排好序,再把负数的反转,最后k还剩而且是奇数那么直接反转绝对值最小的那个。如果还有负数那么k不可能还剩下。
class Solution {
public int largestSumAfterKNegations(int[] nums, int k) {
if(nums.length==1) return k%2==1?-nums[0]:nums[0];
Arrays.sort(nums);
int id=0;
for(int i=0;i<k;i++){
if(i<nums.length-1&&nums[id]<0){
nums[id]=-nums[id];
if(nums[id]>=Math.abs(nums[id+1])) id++;
continue;
}
nums[id]=-nums[id];
}
int res=0;
for(int i:nums){
res+=i;
}
return res;
}
}
8.加油站
思路
①暴力法,直接每个站点都遍历一次,遍历一圈使用while循环加上i%size这样的方式。n^2
②如果总的油量小于总消耗那么就一定是失败的,并且每次局部发现油量不够的时候就跳转到下一个起点继续遍历,而不是遍历一圈,只遍历一次。每次遍历都记录下当前剩下的油量,如果小于0那么就i+1到下一个起点继续,并且记录为下一个起点。如此类推,最后达到全局最优,因为已经确定好能够通行整个路程,那么最后记录的起点就是结果。如果期间剩下油量小于0其实就是油量不足,前面遍历好的0–i无法通过,或者是i–j无法通过,那么就要到下一个节点i+1和j+1继续遍历,只要不出现油量不足那么最后肯定能遍历完,并且是起点。
class Solution {
public int canCompleteCircuit(int[] gas, int[] cost) {
int targetSum=0;
int curSum=0;
int start=0;
for(int i=0;i<cost.length;i++){
targetSum+=(gas[i]-cost[i]);
curSum+=(gas[i]-cost[i]);
if(curSum<0){
start=i+1;
curSum=0;
}
}
if(targetSum<0) return -1;
return start;
}
}
9.分发糖果
思路
先把右边同学比相邻同学大的糖果分发好,然后就是左边的同学评分更高的同学糖果分发。相当于就是先处理右边,然后再处理左边,但是处理右边之后,右边同学已经取得最多的糖果,这个时候还需要使用右边同学得出的糖果来进行计算左边同学能够获取的最多糖果进行对比取出全局最优的糖果分配。实际上就是贪心思路。
class Solution {
public int candy(int[] ratings) {
int[] candy=new int[ratings.length];
Arrays.fill(candy,1);
for(int i=1;i<ratings.length;i++){
if(ratings[i]>ratings[i-1]){
candy[i]=candy[i-1]+1;
}
}
for(int i=ratings.length-2;i>=0;i--){
if(ratings[i]>ratings[i+1]){
candy[i]=Math.max(candy[i],candy[i+1]+1);
}
}
int res=0;
for(int i:candy){
res+=i;
}
return res;
}
}
10.柠檬水找零
思路
这里其实就只有几种情况,第一种就是5直接获取,第二种就是10,需要消耗5,获得10。如果是20可以用3张5或者是一张10一张5.优先处理一张10和一张5,因为5可以处理10的情况。所以这个地方有贪心的性质。这里的局部最优就是bill如果是20的时候优先使用10和5,让5尽可能多,这样才更有机会找零成功。
class Solution {
public boolean lemonadeChange(int[] bills) {
int five=0;
int ten=0;
int twenty=0;
for(int bill:bills){
if(bill==5) five++;
if(bill==10){
if(five>=1){
five--;
ten++;
}else{
return false;
}
}
if(bill==20){
if(ten>0&&five>0){
ten--;
five--;
twenty++;
}else if(five>=3){
five-=3;
twenty++;
}else{
return false;
}
}
}
return true;
}
}
11.根据身高重建队列
思路
思路和分发糖果相似,需要先把某一个东西排序,再处理另外一样。先处理身高,从大到小,如果是从大到小,那么就有一个好处就是直接根据p[1]能够得出人排序的位置,原因是如果前面有两个人比你高那么就可以排在第三个位置,相当就是下标2。那么每次进入的元素根据这个来安放位置就可以了。因为是从大到小,所以进来的时候刚好就是有这样的一个规律。这种需要安排位置的可以使用数据结构LinkedList链表+列表,快速拓展和插入。并且可以插入指定的位置。最后把数组加入之后需要把queue.toArray(new int ‘[ people.length]’[ ] );这里的局部最优其实就是身高排序之后优先处理身高高的,放到p[1]的位置上。
class Solution {
public int[][] reconstructQueue(int[][] people) {
Arrays.sort(people,(o1,o2)->{
if(o1[0]==o2[0]) return o1[1]-o2[1];
return o2[0]-o1[0];
});
LinkedList<int[]> queue=new LinkedList<>();
for(int[] p:people){
queue.add(p[1],p);
}
return queue.toArray(new int[people.length][]);
}
}
12.用最少的数量的箭引爆气球
思路
主要就是一支箭射重叠的气球为主。那么这里的贪心就是尽量射更多的重叠气球,先找到那些气球的左边界比上一个气球的右边界更小的气球,找到之后就更新右边界,取最小的那个右边界,如果不是取最小的右边界那么就没办法射穿所有重叠气球。因为最小的右边界前面包含了好几个重叠气球的左边界。在对比之前需要给气球的第一个x排序,排序之后就按照当前气球的左边界与上一个气球的最小右边界进行对比就可以了,如果左边界比最小右边界更大,那么就需要射多一支箭了。
class Solution {
public int findMinArrowShots(int[][] points) {
if(points.length==1) return 1;
Arrays.sort(points,(o1,o2)->Integer.compare(o1[0],o2[0]));
int count=1;
for(int i=1;i<points.length;i++){
if(points[i][0]>points[i-1][1]){
count++;
}else{
points[i][1]=Math.min(points[i][1],points[i-1][1]);
}
}
return count;
}
}
13.无重叠区间
思路
主要就是找没有重叠的个数,然后总数减去没有重叠的个数就是需要去掉的个数。问题是怎么找?什么时候知道他是没有重叠的?右边界如果是小于等于下一个左边界那么就不是重叠的。问题是怎么排序?排序的目的是什么?排序右边界,目的是为了找到不重叠的,而不是找到重叠的个数。找不重叠,那么就要右边界小给腾出更多空间。接着就是更新右边界对比下一个的左边界,并且计算出不重叠的个数。这里的贪心是先把右边界的最小值进行排序,局部最优,然后慢慢找出不重叠的,最后得出全局最优,能够分出来的最多的不重叠的区间。最后只需要移除总数减去不重叠个数即可
class Solution {
public int eraseOverlapIntervals(int[][] intervals) {
if(intervals.length<2) return 0;
Arrays.sort(intervals,(a,b)->{
if(a[1]!=b[1]){
return a[1]-b[1];
}else{
return a[0]-b[0];
}
});
int end=intervals[0][1];
int count=1;
for(int i=1;i<intervals.length;i++){
if(end<=intervals[i][0]){
end=intervals[i][1];
count++;
}
}
return intervals.length-count;
}
}
14划分字母区间
思路
先遍历整个字符串,然后找到每个字母在字符串中最后的位置。并且与数组或者hash进行一个对应存储。再次遍历整个字符串,并且通过idx记录最远的那个字符的位置,如果遍历到的i和idx相等那么就把这个字符串长度加入结果集,并且更新切分位置last,如果不是那么就继续遍历和找字符最远的位置。为什么找字符最远位置可以进行划分?因为可以确定后面没有这个字符,那么最远这个字符也只会出现在这个分割字符串里面,而字符串里面的字符最远的位置也不比最远的那个多。所以这里的局部最优就是找到某个串最远的字符那么就能够进行划分,这个字符就是串里面排最后的字符了。没有比他排的更后了,那么从下一个开始的字符是不可能出现在前一个字符串,因为前一个字符串最远也就是上一次的last
class Solution {
public List<Integer> partitionLabels(String s) {
int[] array=new int[123];
char[] chars=s.toCharArray();
for(int i=0;i<chars.length;i++){
array[chars[i]-0]=Math.max(array[chars[i]-0],i);
}
int last=-1;
List<Integer> res=new ArrayList<>();
int idx=0;
for(int i=0;i<chars.length;i++){
idx=Math.max(array[chars[i]-0],idx);
if(idx==i){
res.add(i-last);
last=i;
}
}
return res;
}
}
15.合并区间
思路
先排序,排序的目的就是让左边比较小的放到前面,那么优先把左边小的开始合并放入结果集。判断可以根据前面最大的右边界,下一个的左边界进行比较,如果是左边界更大那么就不能够合并,需要把前面的数组放入结果集,如果不是那么就更新最大的右边界继续扩大遍历。
class Solution {
public int[][] merge(int[][] intervals) {
Arrays.sort(intervals,(a,b)->{
return a[0]-b[0];
});
int start=intervals[0][0];
List<int[]> res=new ArrayList<>();
for(int i=1;i<intervals.length;i++){
if(intervals[i][0]>intervals[i-1][1]){
res.add(new int[]{
start,intervals[i-1][1]});
start=intervals[i][0];
}else{
intervals[i][1]=Math.max(intervals[i][1],intervals[i-1][1]);
}
}
res.add(new int[]{
start,intervals[intervals.length-1][1]});
return res.toArray(new int[res.size()][]);
}
}
16.单调递增的数字
思路
如果发现nums[i-1]>nums[i],那么可以把nums[i-1]–,nums[i]设置为9。实际上就是在找可以设置为9的位置。这样改变之后的数值就是最大的单调递增数字。这里使用到的贪心思维就是局部的num[i-1]nums[i]通过上面的方式对比和修改之后得到局部最大,那么只要从后往前遍历就能够得到全局最优。但是为什么不能从前往后呢?原因是前面的已经经过了对比并且进行修改,如果下一个对比-1之后比上一个小,那么就是不合法的。比如332,修改之后就是329。
class Solution {
public int monotoneIncreasingDigits(int n) {
String[] s=new String(n+"").split("");
int flag=s.length;
for(int i=s.length-1;i>0;i--){
if(Integer.parseInt(s[i-1])>Integer.parseInt(s[i])){
flag=i;
s[i-1]=(Integer.parseInt(s[i-1])-1)+"";
}
}
for(int i=flag;i<s.length;i++){
s[i]="9";
}
return Integer.parseInt(String.join("",s));
}
}
17.买卖股票的最佳时机含手续费
思路
这里多了手续费没办法直接利润全部相加。需要考虑到手续费的影响。因为一个数组[3,7,10,5]其中一种策略是10-7-2和7-3-2那么利润是3,但是10-3-2那么利润只是5.影响他们的是其中的手续费问题。那么这个时候可以使用贪心,也就是只要有利润就能够继续加入。但是遇到有连续利润的情况,那么就要每次最低价格都是减去手续费防止下一次再减。如果遇到价格比当前更低的股价,那么直接买入就好了。
class Solution {
public int maxProfit(int[] prices, int fee) {
//实际需要的费用
int minPirce=prices[0];
int res=0;
for(int p:prices){
if(minPirce>p){
minPirce=p;
}
if(p>minPirce+fee){
res+=(p-minPirce-fee);
minPirce=p-fee;
}
}
return res;
}
}
18.监控二叉树
思路
这道题主要是查看每个节点的状态。无覆盖,覆盖,和有这个监控器。这里的贪心思维体现在,只要是叶子节点的父节点上有监控器那么就能够一下子监控两个节点甚至更多。这里可以采用后序。并且每隔两个节点就要加上监控器,那么怎么知道隔两个节点?解决办法就是对每个节点的状态进行管理。空节点的状态是有覆盖,因为不能够在空节点上安装监控器也不能是无覆盖。这个地方分了三种情况,第一种是左右子节点都是有覆盖那么证明父节点一定是无覆盖,因为从下往上。第二种情况是只要左子节点或者是右子节点有一个是没有被覆盖,那么父节点是一定要监控的。最后一种情况是左节点或者是右节点有监控,并且另一个节点被覆盖,那么父节点一定就是有覆盖的,因为肯定被下面的节点监控.
class Solution {
int res=0;
public int minCameraCover(TreeNode root) {
if(root==null) return 0;
if(traversal(root)==0) res++;
return res;
}
public int traversal(TreeNode root){
if(root==null) return 2;
int left=traversal(root.left);
int right=traversal(root.right);
if(left==2&&right==2) return 0;
if(left==0||right==0){
res++;
return 1;
}
if(left==1||right==1) return 2;
return -1;
}
}
CameraCover(TreeNode root) {
if(root==null) return 0;
if(traversal(root)==0) res++;
return res;
}
public int traversal(TreeNode root){
if(root==null) return 2;
int left=traversal(root.left);
int right=traversal(root.right);
if(left==2&&right==2) return 0;
if(left==0||right==0){
res++;
return 1;
}
if(left==1||right==1) return 2;
return -1;
}
}