算法题总结(十五)——贪心算法(下)

server/2024/10/17 21:42:55/

1005、K 次取反后最大化的数组和

给你一个整数数组 nums 和一个整数 k ,按以下方法修改该数组:

  • 选择某个下标 i 并将 nums[i] 替换为 -nums[i] 。

重复这个过程恰好 k 次。可以多次选择同一个下标 i 。

以这种方式修改数组后,返回数组 可能的最大和

示例 1:

输入:nums = [4,2,3], k = 1
输出:5
解释:选择下标 1 ,nums 变为 [4,-2,3] 。

示例 2:

输入:nums = [3,-1,0,2], k = 3
输出:6
解释:选择下标 (1, 2, 2) ,nums 变为 [3,1,0,2] 。
class Solution {public int largestSumAfterKNegations(int[] nums, int k) {// 排序,把可能有的负数排到前面Arrays.sort(nums);int sum = 0;for (int i = 0; i < nums.length; i++) {// 贪心:如果是负数,而k还有盈余,就把负数反过来if (nums[i] < 0 && k > 0) {nums[i] = -1 * nums[i];k--;}sum += nums[i];}Arrays.sort(nums);// 如果k没剩,那说明能转的负数都转正了,已经是最大和,返回sum,此时k等于0// 如果k有剩,说明负数已经全部转正,所以如果k还剩偶数个就自己抵消掉,不用删减,如果k还剩奇数个就减掉2倍最小正数。return sum - (k % 2 == 0 ? 0 : 2 * nums[0]); }
}

134、加油站

在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。

你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。

给定两个整数数组 gas 和 cost ,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回 -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 可为起始索引。

示例 2:

输入: 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 升汽油。
因此,无论怎样,你都不可能绕环路行驶一周。

首先如果总油量减去总消耗大于等于零那么一定可以跑完一圈,说明 各个站点的加油站 剩油量rest[i]相加一定是大于等于零的。

i从0开始累加rest[i],和记为curSum,一旦curSum小于零,说明[0, i]区间都不能作为起始位置,因为这个区间选择任何一个位置作为起点,到i这里都会断油,那么起始位置从i+1算起,再从0计算curSum。

有没有可能从[0, i]区间的某一位置开始,也可以有起始位置?,即curSum大于0

如图:

如果 curSum<0 说明 区间和1 + 区间和2 < 0, 那么 假设从上图中的位置开始计数curSum不会小于0的话,就是 区间和2>0。

区间和1 + 区间和2 < 0 同时 区间和2>0,只能说明区间和1 < 0, 那么就会从假设的箭头初就开始从新选择其实位置了。即如果可以作为起始位置,我们在i之前就会把它作为起始位置了

那么局部最优:当前累加rest[i]的和curSum一旦小于0,起始位置至少要是i+1,因为从i之前开始一定不行。全局最优:找到可以跑一圈的起始位置

class Solution {public int canCompleteCircuit(int[] gas, int[] cost) {int curSum=0;int totalSum=0;int start=0;for(int i=0;i<gas.length;i++)totalSum+=gas[i]-cost[i];if(totalSum<0)return -1;for(int i=0;i<gas.length;i++){curSum+=gas[i]-cost[i];if(curSum<0){start=i+1;curSum=0;}}return start;}
}

135、分发糖果

n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。

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

  • 每个孩子至少分配到 1 个糖果。
  • 相邻两个孩子评分更高的孩子会获得更多的糖果。

请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目

示例 1:

输入:ratings = [1,0,2]
输出:5
解释:你可以分别给第一个、第二个、第三个孩子分发 2、1、2 颗糖果。

示例 2:

输入:ratings = [1,2,2]
输出:4
解释:你可以分别给第一个、第二个、第三个孩子分发 1、2、1 颗糖果。第三个孩子只得到 1 颗糖果,这满足题面中的两个条件。

先确定右边评分大于左边的情况(也就是从前向后遍历)

此时局部最优:只要右边评分比左边大,右边的孩子就多一个糖果,全局最优:相邻的孩子中,评分高的右孩子获得比左边孩子更多的糖果

再确定左孩子大于右孩子的情况(从后向前遍历)

如果 ratings[i] > ratings[i + 1],此时candyVec[i](第i个小孩的糖果数量)就有两个选择了,一个是candyVec[i + 1] + 1(从右边这个加1得到的糖果数量),一个是candyVec[i](之前比较右孩子大于左孩子得到的糖果数量)。

class Solution {public int candy(int[] ratings) {int len =ratings.length;int[] candynum =new int[len];candynum[0]=1;//从左向右比较for(int i=1;i<len;i++){if(ratings[i]>ratings[i-1]){candynum[i]=candynum[i-1]+1;}else{candynum[i]=1;}}//从右向左比较:for(int i=len-2;i>=0;i--){if(ratings[i]>ratings[i+1]){candynum[i]=Math.max(candynum[i],candynum[i+1]+1);}}//统计:int sum=0;for(int i=0;i<candynum.length;i++){sum+=candynum[i];}return sum;}
}

860、柠檬水找零

在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。

每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。

注意,一开始你手头没有任何零钱。

给你一个整数数组 bills ,其中 bills[i] 是第 i 位顾客付的账。如果你能给每位顾客正确找零,返回 true ,否则返回 false 。

示例 1:

输入:bills = [5,5,5,10,20]
输出:true
解释:
前 3 位顾客那里,我们按顺序收取 3 张 5 美元的钞票。
第 4 位顾客那里,我们收取一张 10 美元的钞票,并返还 5 美元。
第 5 位顾客那里,我们找还一张 10 美元的钞票和一张 5 美元的钞票。
由于所有客户都得到了正确的找零,所以我们输出 true。

逻辑是非常固定的,唯一贪心的点就是尽可能的多留下五元的。

class Solution {public boolean lemonadeChange(int[] bills) {int[] money=new int[2]; //存储剩余的5块和10块的数量for(int i=0;i<bills.length;i++){if(bills[i]==5){money[0]++;}else if(bills[i]==10){if(money[0]>0){money[0]--;money[1]++;} else return false;}else if(bills[i]==20){if(money[0]>0 && money[1]>0){money[0]--;money[1]--;}else if(money[0]>=3){money[0]=money[0]-3;}else{return false;}}}return true;}
}

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]] 是重新构造后的队列

本题好人分发糖果一样,都是有两个维度!所以要一个一个的来确定!

如果按照k来从小到大排序,排完之后,会发现k的排列并不符合条件,身高也不符合条件,两个维度哪一个都没确定下来。

那么按照身高h来排序呢,身高一定是从大到小排(身高相同的话则k小的站前面),让高个子在前面。

此时我们可以确定一个维度了,就是身高,前面的节点一定都比本节点高!

那么只需要按照k为下标重新插入队列就可以了

例如:

局部最优:优先按身高高的people的k来插入。插入操作过后的people满足队列属性

全局最优:最后都做完插入操作,整个队列满足题目队列属性

people[i] = [hi, ki]表示前面 正好 有 ki 个身高大于或等于 hi 的人。

所以,先按照身高进行排序,然后k来进行插入。

class Solution {public int[][] reconstructQueue(int[][] people) {int[][] result =new int[people.length][];Arrays.sort(people,(a,b)->{if(a[0]==b[0]) return a[1]-b[1]; //身高相同的,让k小的在前面return b[0]-a[0];  //身高不同,按身高降序});LinkedList<int []> list =new LinkedList<>();for(int[] p:people){list.add(p[1],p);  //使用LinkedList 来根据位置进行插入}return list.toArray(result);}
}

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

有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ,其中points[i] = [xstart, xend] 表示水平直径在 xstart 和 xend之间的气球。你不知道气球的确切 y 坐标。

一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart</font><font style="color:rgb(51, 51, 51);">,</font><font style="color:rgb(51, 51, 51);background-color:rgb(243, 244, 244);">xend, 且满足 xstart ≤ x ≤ x``end,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。

给你一个数组 points ,返回引爆所有气球所必须射出的 最小 弓箭数

示例 1:

输入:points = [[10,16],[2,8],[1,6],[7,12]]
输出:2
解释:气球可以用2支箭来爆破:
-在x = 6处射出箭,击破气球[2,8]和[1,6]。
-在x = 11处发射箭,击破气球[10,16]和[7,12]。

示例 2:

输入:points = [[1,2],[3,4],[5,6],[7,8]]
输出:4
解释:每个气球需要射出一支箭,总共需要4支箭。

局部最优:当气球出现重叠,一起射,所用弓箭最少。全局最优:把所有气球射爆所用弓箭最少。

为了让气球尽可能的重叠,需要对数组进行排序

使用重叠气球最小右边界来 判断是否重叠,只要后面的气球的左边界小于前面重叠气球的最小右边界,就可以使用同一只箭来引爆,另外要注意要收缩后面气球的右边界。

如果气球重叠了,重叠气球中右边边界的最小值 之前的区间一定需要一个弓箭

可以看出首先第一组重叠气球,一定是需要一个箭,气球3,的左边界大于了 第一组重叠气球的最小右边界,所以再需要一支箭来射气球3了。

class Solution {public int findMinArrowShots(int[][] points) {// 根据气球直径的开始坐标从小到大排序// 使用Integer内置比较方法,不会溢出Arrays.sort(points,(a,b)->Integer.compare(a[0],b[0]));//Arrays.sort(points,(a,b)->{ return a[0]-b[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][1],points[i][1]);}}return count;}
}

超时: 即最小的int减去最大的int,结果溢出。

435、无重叠区间

给定一个区间的集合 intervals ,其中 intervals[i] = [starti, endi] 。返回 需要移除区间的最小数量,使剩余区间互不重叠

示例 1:

输入: intervals = [[1,2],[2,3],[3,4],[1,3]]
输出: 1
解释: 移除 [1,3] 后,剩下的区间没有重叠。

示例 2:

输入: intervals = [ [1,2], [1,2], [1,2] ]
输出: 2
解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。

本题和前一题思路相同,先排序然后看是否重叠,如果重叠的话count++,并且把intervals[i]的右边界取最小,相当于把右边界大的给移除了

class Solution {public int eraseOverlapIntervals(int[][] intervals) {Arrays.sort(intervals,(a,b)->{return Integer.compare(a[0],b[0]);});int count=0; //用来计算需要移除的区间for(int i=1;i<intervals.length;i++){if(intervals[i][0]>=intervals[i-1][1]) //不重叠{continue;}else{  //重叠count++;//移除,并且右边界取最小的,即右边界大的会被移除intervals[i][1]=Math.min(intervals[i-1][1],intervals[i][1]);}}return count;}
}

763、划分字母区间

给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。

注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s 。

返回一个表示每个字符串片段的长度的列表。

示例 1:

输入:s = "ababcbacadefegdehijhklij"
输出:[9,7,8]
解释:
划分结果为 "ababcbaca"、"defegde"、"hijhklij" 。
每个字母最多出现在一个片段中。
像 "ababcbacadefegde", "hijhklij" 这样的划分是错误的,因为划分的片段数较少。

在遍历的过程中相当于是要找每一个字母的边界,如果找到之前遍历过的所有字母的最远边界,说明这个边界就是分割点了。此时前面出现过所有字母,最远也就到这个边界了。

可以分为如下两步:

  • 统计每一个字符最后出现的位置
  • 从头遍历字符,并更新字符的最远出现下标,如果找到字符最远出现位置下标和当前下标相等了,则找到了分割点

如图:

即找到最大的那个字符边界,就是这一段的分割点

class Solution {public List<Integer> partitionLabels(String s) {List<Integer> result =new LinkedList<>();int [] edge =new int [26];   //记录每个字母最后出现的位置char[] chars =s.toCharArray();for(int i=0;i<chars.length;i++){edge[chars[i]-'a']=i;   //记录每个字母最后出现的位置}int index=0;int last=0;for(int i=0;i<chars.length;i++){//找到最后出现的最大值,以防止字母在后面还会出现index=Math.max(index,edge[chars[i]-'a']);  if(i==index)   //如果i就等于最后出现的位置,就可以是分界点了{result.add(index-last+1);last=index+1;}}return result;}
}

56、合并区间

以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间

示例 1:

输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].

示例 2:

输入:intervals = [[1,4],[4,5]]
输出:[[1,5]]
解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。
class Solution {public int[][] merge(int[][] intervals) {List<int[]> result =new LinkedList<>();Arrays.sort(intervals,(a,b)->{return a[0]-b[0];});result.add(intervals[0]);for(int i=1;i<intervals.length;i++){if(intervals[i][0]>intervals[i-1][1])  //没有重叠{result.add(intervals[i]);}else //有重叠的话,要移除前一个{intervals[i][1]=Math.max(intervals[i][1],intervals[i-1][1]);intervals[i][0]=intervals[i-1][0];result.removeLast();result.add(intervals[i]);}}return result.toArray(new int[result.size()][]);}
}

738、单调递增的数字

当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。

给定一个整数 n ,返回 小于或等于 n 的最大数字,且数字呈 __单调递增

示例 1:

输入: n = 10
输出: 9

示例 2:

输入: n = 1234
输出: 1234

示例 3:

输入: n = 332
输出: 299

例如:98,一旦出现strNum[i - 1] > strNum[i]的情况(非单调递增),首先想让strNum[i - 1]–,然后strNum[i]给为9,这样这个整数就是89,即小于98的最大的单调递增整数。

从前向后遍历的话,遇到strNum[i - 1] > strNum[i]的情况,让strNum[i - 1]减一,但此时如果strNum[i - 1]减一了,可能又小于strNum[i - 2]。

所以要从后向前遍历,就可以重复利用上次比较得出的结果了

class Solution {public int monotoneIncreasingDigits(int n) {String s=String.valueOf(n);char[] chars=s.toCharArray();int start=chars.length;for(int i=chars.length-2;i>=0;i--)   //从后向前判断{if(chars[i]>chars[i+1]){chars[i]--;start=i+1;    //从最后一个减1的数之后开始都要变为9}}for(int i=start;i<chars.length;i++){chars[i]='9';}//先把chars变为String,在转为Integerreturn Integer.parseInt(String.valueOf(chars));}
}

968、监控二叉树

给定一个二叉树,我们在树的节点上安装摄像头。

节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。

计算监控树的所有节点所需的最小摄像头数量。

示例 1:

输入:[0,0,null,0,0]
输出:1
解释:如图所示,一台摄像头足以监控所有节点。

示例 2:

输入:[0,0,null,0,null,0,null,null,0]
输出:2

贪心思想:为了充分利用摄像头的覆盖,要让叶子结点的父节点安装摄像头。

所以我们要从下往上看,局部最优:让叶子节点的父节点安摄像头,所用摄像头最少,整体最优:全部摄像头数量所用最少!

从下到上,所以是后序遍历。

jclass Solution {int  res=0;public int minCameraCover(TreeNode root) {// 对根节点的状态做检验,防止根节点是无覆盖状态 .if(minCame(root)==0){res++;}return res;
}
/**节点的状态值:0 表示无覆盖1 表示 有摄像头2 表示有覆盖后序遍历,根据左右节点的情况,来判读 自己的状态*/
public int minCame(TreeNode root){
if(root==null){// 空节点默认为 有覆盖状态,避免在叶子节点上放摄像头return 2;
}
int left=minCame(root.left);
int  right=minCame(root.right);// 如果左右节点都覆盖了的话, 那么本节点的状态就应该是无覆盖,没有摄像头
if(left==2&&right==2){//(2,2)return 0;
}else if(left==0||right==0){// 左右节点都是无覆盖状态,那根节点此时应该放一个摄像头// (0,0) (0,1) (0,2) (1,0) (2,0)// 状态值为 1 摄像头数 ++;res++;return 1;
}else{// 左右节点的 状态为 (1,1) (1,2) (2,1) 也就是左右节点至少存在 1个摄像头// 那么本节点就是处于被覆盖状态return 2;
}
}
}

http://www.ppmy.cn/server/131706.html

相关文章

网络学习第二篇

认识网关和路由器 这里大家先了解一下什么三层设备。 三层设备 三层设备是指在网络架构中能够工作在第三层&#xff08;网络层&#xff09;的设备&#xff0c;通常包括三层交换机和路由器。这些设备可以根据IP地址进行数据包的转发和路由选择&#xff0c;从而在不同的网络之间…

Leecode刷题之路第19天之删除链表的倒数第N个结点

题目出处 19-删除链表的倒数第N个结点-题目出处 题目描述 个人解法 思路&#xff1a; todo 代码示例&#xff1a;&#xff08;Java&#xff09; todo复杂度分析 todo 官方解法 19-删除链表的倒数第N个结点-官方解法 前言 方法1&#xff1a;计算链表长度 思路&#xff1a; …

bat脚本banenr

飞出个未来班得 echo off echo .-. echo ( ) echo - echo J L echo ^| ^| echo J L echo ^| ^| echo J L echo …

【Golang】关于Go语言中的IO操作

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

Spring Cache与Redis实现自动缓存处理:入门指南

Spring Cache与Redis实现自动缓存处理:入门指南 在现代Web应用程序开发中,缓存是提升性能的关键技术之一。本文将介绍如何在Spring Boot应用程序中使用Spring Cache和Redis实现自动缓存处理,帮助你快速入门这项强大的技术组合。 为什么选择Spring Cache和Redis? Spring Cac…

#pragma DATA_ALIGN地址对齐指令

背景描述: 在学习#pragma DATA_ALIGN时看到有句描述"地址的低几位一定为0"&#xff0c;当时有点没明白是什么意思&#xff0c;后面才反应过来&#xff0c;这里记录下。 相关博客 个人理解: 以8字节对齐为例&#xff0c;地址是8的倍数&#xff0c;转换成二进制来看&a…

R语言绘制气泡图

气泡图是一种数据可视化图表。它通常在二维或三维空间中展示数据。两个变量决定气泡在平面或空间中的位置&#xff0c;第三个变量则以气泡大小呈现。能直观反映三个变量间关系&#xff0c;帮助用户快速理解数据特征和趋势&#xff0c;在数据分析和展示中广泛应用。 0x01 使用s…

深度学习:循环神经网络——LSTM

目录 前言 一、LSTM主要组成部分 二、LSTM的工作原理 三、LSTM的工作步骤详解 1.遗忘门 2.输入门 3.输出门 前言 LSTM&#xff08;长短期记忆网络&#xff09;是一种特殊类型的循环神经网络&#xff08;RNN&#xff09;&#xff0c;用于处理和预测序列数据。与传统的RNN…