• 贪心算法:寻找最优方案,分配问题、区间覆盖问题、最大子列和问题等


    贪心的本质:选择每一阶段的局部最优,从而达到全局最优。想不到反例,即可尝试贪心

    贪心算法一般分为如下四步:

    • 将问题分解为若干个子问题
    • 找出适合的贪心策略
    • 求解每一个子问题的最优解
    • 将局部最优解堆叠成全局最优解

    目录

    1、分配问题:所需,所得,之间的关系,按序分配,实现最优

    2、最大子列和:该子列前后不能有负数包括和为负数的子列,不断调整区间的起始位置,组内的负数尽可能少。for循环适合模拟从头到尾的遍历,而while循环适合模拟环形遍历

    3、震荡波动数据:取谷峰(左右差值符号不同),或单增区间(正差值),或单减区间(负差值)  的值

    4、跳跃问题:跳跃覆盖范围究竟可不可以覆盖到终点!在每次跳跃的范围内获得下次跳跃可覆盖的最大范围。

    5、分解问题,拆分步骤,对不同量进行贪心算法, 两个维度相互影响的情况依次确定各维度。

    6、重叠区间问题, 对区间数组排序,根据边界值判断重叠区域

    7、根据状态判断调整,注意遍历的方向


    1、分配问题:所需,所得,之间的关系,按序分配,实现最优

    1. class Solution {
    2. //s分配的尺寸,g是所需的尺寸
    3. public int findContentChildren(int[] g, int[] s) {
    4. int count = 0;
    5. //对数组排序,升序
    6. Arrays.sort(g);
    7. Arrays.sort(s);
    8. //根据条件匹配,分配的尺寸 >= 所需的尺寸
    9. //从后向前先分配大的,或者从前向后先分配小的/小饼干优先喂饱小胃口的 这样可以尽量保证最后省下来的是大饼干
    10. // int index = g.length-1;
    11. // for(int i = s.length-1; i>=0 && index>=0; i-- ){//index>=0放在for循环中判断可以实现剪枝
    12. // if(s[i] >= g[index]){
    13. // index--;
    14. // count++;
    15. // }
    16. // }
    17. int index = 0;
    18. for(int i = 0; i < s.length && index < g.length; i++){//index>=0放在for循环中判断可以实现剪枝
    19. if(s[i] >= g[index]){
    20. index++;
    21. count++;
    22. }
    23. }
    24. return count;
    25. }
    26. }

    2、最大子列和:该子列前后不能有负数包括和为负数的子列,不断调整区间的起始位置,组内的负数尽可能少。for循环适合模拟从头到尾的遍历,而while循环适合模拟环形遍历

    53. 最大子数组和(增加负数只会变得更小)=》不断调整最大子序和区间的起始位置,即只有当前子列的和为正数时才会继续增加子列,否则重置子列。

    1005. K 次取反后最大化的数组和:数组排序,对负数部分按升序进行翻转,(即优先反转绝对值大的负数),若反转后K>0,若k为偶数,结果不变,为奇数,选着绝对值最小的数反转。

    134. 加油站:余量+gsa[i]>cost[i],可到达下一站,所得 > 所需。一旦[i,j] 区间和为负数,起始位置就可以是j+1,    (从i到j中间的任何一个位置出发,剩余油量只会更少)

    1. class Solution {
    2. public int maxSubArray(int[] nums) {
    3. int maxsum = Integer.MIN_VALUE;
    4. int sum = 0;
    5. int start = 0;
    6. int end = 0;
    7. for(int i = 0; i< nums.length; i++){
    8. sum += nums[i];
    9. if(sum > maxsum){//需要在前面进行比较,避免当前最大值为负,下面的if影响结果
    10. maxsum = sum;
    11. end = i;
    12. }
    13. if(sum < 0){
    14. sum = 0;
    15. start = i;
    16. }
    17. }
    18. return maxsum;
    19. }
    20. }
    21. //K 次取反后 最大化的数组和
    22. class Solution {
    23. public int largestSumAfterKNegations(int[] nums, int k) {
    24. Arrays.sort(nums);//或者按照绝对值排序,k有余且为奇时减去2*最后一个元素
    25. int index = 0;
    26. int sum = 0;
    27. for(int i = 0; i < nums.length; i++){
    28. if(nums[i] < 0){
    29. if(k > 0) {
    30. nums[i] = 0 - nums[i];
    31. k--;
    32. }
    33. index = i;
    34. }
    35. sum += nums[i];
    36. }
    37. if(k >= 0 && k%2 == 0) return sum;
    38. //sum加了该值的正值,需要减去,所以2*
    39. if(index == nums.length-1) return sum-2*nums[index];//最后一个元素最小
    40. return sum-2*Math.min(nums[index],nums[index+1]);//正负交界处选较小值
    41. }
    42. }
    43. //余量
    44. class Solution {
    45. //第 i 个加油站有汽油 gas[i] 升
    46. //第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升
    47. //必须:余量+gsa[i]>cost[i],可到达下一站
    48. //可绕环路行驶一周则返回出发时加油站的编号,也是终点编号,否则返回 -1
    49. // 一旦[i,j] 区间和为负数,起始位置就可以是j+1, 从i到j中间的任何一个位置出发,油量只会更少
    50. public int canCompleteCircuit(int[] gas, int[] cost) {
    51. int currest = 0;
    52. int sumrest = 0;
    53. int start = 0;//记录起始点
    54. for(int i = 0; i< gas.length; i++){
    55. currest += gas[i] - cost[i];
    56. sumrest += gas[i] - cost[i];
    57. if(currest < 0){
    58. currest = 0;//重设起点
    59. start = (i+1)%gas.length;
    60. }
    61. }
    62. if(sumrest < 0) return -1;//总油量不足,否则一定能到达
    63. return start;
    64. }
    65. }

    3、震荡波动数据:取谷峰(左右差值符号不同),或单增区间(正差值),或单减区间(负差值)  的值

    376. 摆动序列: 最长子序列的长度 。只保留一谷一峰,不记录单调区间内的元素

    122. 买卖股票的最佳时机 II:最大差值和,只在谷峰处买入卖出(尽可能少交易),只在上升区间(差值为正)处每次交易都买入卖出(尽可能的多交易或者是不限制交易次数)

    714. 买卖股票的最佳时机含手续费:每笔交易都需要付手续费,相当于买入时,价格高了fee元,在此基础去判断升序降序,累加收入。求最大差值和

    1. //摆动序列 的 最长子序列的长度 。
    2. class Solution {
    3. //摆动序列:连续数字之间的差严格地在正数和负数之间,即 小大小大 或大小大小
    4. //从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。
    5. //局部最优:删除单调坡度上的节点(不包括单调坡度两端的节点),那么这个坡度就可以有两个局部峰值。
    6. //整体最优:整个序列有最多的局部峰值,从而达到最长摆动序列。
    7. public int wiggleMaxLength(int[] nums) {
    8. int presign = 0;
    9. int cursign;
    10. int count = 1;//默认记录最左侧的元素为峰值
    11. for(int i=0; i< nums.length-1; i++){
    12. cursign = nums[i+1]- nums[i];
    13. if((cursign < 0 && presign >=0) || (cursign > 0 && presign <= 0)){
    14. count++;
    15. presign = cursign;
    16. }
    17. }
    18. return count;
    19. }
    20. }
    21. //122. 买卖股票获取最大利润
    22. class Solution {
    23. public int maxProfit(int[] prices) {
    24. int sum = 0;
    25. if(prices.length <= 1) return 0;
    26. for(int i = 1; i < prices.length; i++){
    27. sum += Math.max(0,prices[i] - prices[i-1]);//只加正利润
    28. }
    29. return sum;
    30. }
    31. }
    32. class Solution {
    33. //卖出价-买入价-手续费>0 且子列和最大,即升序区间不反复交易,最低买入,最高卖出
    34. //相当于买入时,价格高了fee元,在此基础去判断升序降序,累加收入
    35. public int maxProfit(int[] prices, int fee) {
    36. int interest = 0;
    37. int buyprice = prices[0]+fee;
    38. for(int i = 1; i < prices.length; i++){
    39. if(prices[i] + fee < buyprice){
    40. buyprice = prices[i] + fee;
    41. }
    42. if(prices[i] > buyprice){
    43. interest += prices[i]-buyprice;
    44. buyprice = prices[i];
    45. }
    46. }
    47. return interest;
    48. }
    49. }

    4、跳跃问题:跳跃覆盖范围究竟可不可以覆盖到终点!在每次跳跃的范围内获得下次跳跃可覆盖的最大范围。

    若要求:最少的跳跃次数,则获取当前步的下一步可达到的最大范围,覆盖终点则当前步数+1,不覆盖则当前步数+1,继续按照新的最大可到达范围查找。

    1. //能否到达终点
    2. class Solution {
    3. //选择跳跃的位置+所跳的位置上的数 应>= 数组长度(距离
    4. //不用拘泥于每次究竟跳跳几步,而是看覆盖范围,覆盖范围内一定是可以跳过来的
    5. public boolean canJump(int[] nums) {
    6. int cover = 0;
    7. for(int i = 0; i <= cover; i++){//在覆盖范围内移动
    8. cover = Math.max(cover,i+nums[i]);
    9. if(cover >= nums.length-1) return true;
    10. }
    11. return false;
    12. }
    13. }
    14. //到达终点的最少步数
    15. class Solution {
    16. public int jump(int[] nums) {
    17. int cover = 0;
    18. int curcover = 0;//当前步的覆盖范围
    19. int jump = 0;
    20. if(nums.length == 1) return 0;
    21. for(int i = 0; i< nums.length; i++){
    22. cover = Math.max(cover,i+nums[i]);
    23. //下一步可以到达终点
    24. if(cover >= nums.length-1) return jump+1;
    25. //下一步不能到达终点、已获得了下一步可覆盖的最大位置
    26. if(i == curcover){
    27. curcover = cover;
    28. jump++;
    29. }
    30. }
    31. return -1;
    32. }
    33. }

    5、分解问题,拆分步骤,对不同量进行贪心算法

    135. 分发糖果:将两侧同时比较(复杂),转化为:先依次比较左侧再依次比较右侧,即两次贪心,分数高的糖果数+1(求最少的数),取两种情况中的较大值。

    406. 根据身高重建队列:一般这种数对,还涉及排序的,根据第一个元素正向排序,根据第二个元素反向排序,或者根据第一个元素反向排序,根据第二个元素正向排序,往往能够简化解题过程。

    1. class Solution {
    2. public int candy(int[] ratings) {
    3. int[] candy = new int[ratings.length];
    4. int sumCandy = 0;
    5. //每个孩子至少分配到 1 个糖果。
    6. Arrays.fill(candy,1);
    7. //保证 相邻两个孩子评分更高的孩子会获得更多的糖果,相邻=》左右两侧
    8. //先从左向右看
    9. for(int i = 1; i < ratings.length; i++){
    10. if(ratings[i] > ratings[i-1]){
    11. candy[i] = candy[i-1]+1;
    12. }
    13. }
    14. //先从左向右看
    15. for(int i = ratings.length-2; i >= 0 ; i--){
    16. if(ratings[i] > ratings[i+1]){
    17. candy[i] = Math.max(candy[i],candy[i+1]+1);//取两种情况下的较大值
    18. }
    19. }
    20. for(int i = 0; i < candy.length; i++){
    21. sumCandy += candy[i];
    22. }
    23. return sumCandy;
    24. }
    25. }
    26. class Solution {
    27. public int[][] reconstructQueue(int[][] people) {
    28. LinkedList<int[]> res = new LinkedList<>();
    29. //每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。
    30. //身高从大到小排(身高相同k小的站前面), 即第项前面的每一项都会当前项造成影响,即:ki = i-1;
    31. //则可以通过ki确定插入的位置
    32. //sort(List,Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
    33. Arrays.sort(people, (a, b) -> {
    34. if (a[0] == b[0]) return a[1] - b[1];
    35. return b[0] - a[0];
    36. });//Lambda表达式的基本语法:相当于重写方法并调用,默认从小到大排序,用来判断两者ab需要交换位置吗,false不交换,true交换
    37. //表达式为a-b 返回false:从大到小排序
    38. for(int[] p:people){
    39. res.add(p[1],p);//插:add(int index, Object ele)
    40. }
    41. return res.toArray(new int[people.length][]);
    42. }
    43. }

    6、重叠区间问题, 对区间数组排序,根据边界值判断重叠区域

    452. 用最少数量的箭引爆气球:为了让气球尽可能的重叠,需要对数组进行排序。判断下一起始点与当前结束点区域是否重合,不重合则需要重置当前区间并箭数+1;

    435. 无重叠区间: 先按照右边界排序,由小到大  相同情况下按左边界排序,由大到小,则前面的区间覆盖范围一定比后面小.在从左向右依次判断区间是否重叠。

    763. 划分字母区间:在遍历的过程中,找每一个字母int[26]的边界,即最后出现的位置。再找当前字符串字母可到达的最远位置即覆盖范围,在最远范围处分割(第二步类似跳跃问题)。

    56. 合并区间:合并所有重叠的区间,左边界排序,排序之后局部最优:每次合并都取最大的右边界

    1. //452. 用最少数量的箭引爆气球
    2. class Solution {
    3. //points中存放,气球的直径的开始和结束坐标为 xstart,xend
    4. public int findMinArrowShots(int[][] points) {
    5. if(points == null || points.length == 0) return 0;
    6. if(points.length == 1) return 1;
    7. // Arrays.sort(points,(a,b)->{
    8. // return a[0]-b[0];
    9. // });//返回true,对起始坐标从小到大排序;会因为差值过大而产生溢出
    10. Arrays.sort(points, (x, y) -> Integer.compare(x[0], y[0]));
    11. int count = 0;//弓箭数
    12. int xstart = Integer.MIN_VALUE, xend = Integer.MIN_VALUE;//记录当前可射箭区域
    13. for(int[] point:points){
    14. if(point[0] <= xend){//有重叠区域
    15. xstart = point[0];
    16. xend = Math.min(xend,point[1]);//以重叠区域作为射击范围
    17. }else{
    18. count++;
    19. xstart = point[0];
    20. xend = point[1];
    21. }
    22. }
    23. return count;
    24. }
    25. }
    26. //合并所有重叠的区间
    27. class Solution {
    28. //合并所有重叠的区间,并返回 一个不重叠的区间数组
    29. public int[][] merge(int[][] intervals) {
    30. List<int[]> mergeinterval = new LinkedList<>();
    31. //区间排序,左边界由小到大
    32. Arrays.sort(intervals,(a,b) -> Integer.compare(a[0],b[0]));
    33. //下一区间左边界<= 当前右边界,重叠
    34. //[1,2] [4,5] [2,5]排序后不存在这种情况
    35. for(int i = 0; i < intervals.length;i++){
    36. int start = intervals[i][0], end = intervals[i][1];
    37. while(i < intervals.length && intervals[i][0] <= end){
    38. end = Math.max(end,intervals[i][1]);
    39. i++;
    40. }
    41. i--;
    42. mergeinterval.add(new int[]{start, end});
    43. }
    44. return mergeinterval.toArray(new int[mergeinterval.size()][]);
    45. }
    46. }
    47. //763. 划分字母区间
    48. class Solution {
    49. //尽可能多的片段,
    50. //同一字母最多出现在一个片段中
    51. //返回一个表示 每个字符串片段的长度 的列表。
    52. public List partitionLabels(String s) {
    53. List res = new LinkedList<>();
    54. int[] last = new int[26];//记录26个字符最后的出现位置
    55. int end = 0;//覆盖的最远范围
    56. int start = 0;
    57. for(int i = 0; i < s.length(); i++){
    58. last[s.charAt(i)-'a'] = i;
    59. }
    60. for(int i = 0; i < s.length(); i++){
    61. if(last[s.charAt(i)-'a'] > end)
    62. end = last[s.charAt(i)-'a'];//更新覆盖范围
    63. if(i == end){
    64. res.add(end-start+1);
    65. start = end+1;
    66. }
    67. }
    68. return res;
    69. }
    70. }
    71. //435. 无重叠区间
    72. class Solution {
    73. //需要移除区间的最小数量,则区间覆盖范围越大的应被删除
    74. public int eraseOverlapIntervals(int[][] intervals) {
    75. // 先按照右边界排序,由小到大 相同情况下按左边界排序,由大到小,则前面的区间覆盖范围一定比后面小
    76. Arrays.sort(intervals,(a,b) -> {
    77. if(a[1] == b[1]) return b[0] - a[0];//大到小,可以不比较左区间,同样正确
    78. return a[1] - b[1];
    79. });
    80. int count = 0;
    81. int end = Integer.MIN_VALUE;
    82. for(int[] interval : intervals){
    83. if(interval[0] >= end) {
    84. end = interval[1];
    85. continue;//不重叠
    86. }
    87. count++;
    88. }
    89. return count;
    90. }
    91. }

    7、根据状态判断调整,注意遍历的方向

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

     968. 监控二叉树:给定一个二叉树,我们在树的节点上安装摄像头。求最小数。从下至上安装,从叶子结点开始,因为叶子结点多,

    1. class Solution {
    2. //返回 小于或等于 n 的最大数字,且数字呈 单调递增
    3. //最大数字:高位上数值尽可能的大
    4. //单调递增:当前位上的值 < 后一位
    5. //则从低位开始取,取最高可取值,如果某高位减一,其后低位元素均可为9
    6. public int monotoneIncreasingDigits(int n) {
    7. if(n == 0) return 0;
    8. String num = ""+n;
    9. char[] nums = num.toCharArray();
    10. //98,一旦出现strNum[i - 1] > strNum[i]的情况(非单调递增),首先想让strNum[i - 1]减一,strNum[i]赋值9,这样这个整数就是89
    11. int start = nums.length;
    12. for(int i = nums.length-2; i >= 0; i--){
    13. if(nums[i] > nums[i+1]){//非单调递增
    14. nums[i]--;
    15. start = i;//记录减一的位置,则其后的数值可为9
    16. }
    17. }
    18. for(int i = start+1; i< nums.length; i++){
    19. nums[i] = '9';
    20. }
    21. return Integer.parseInt(new String(nums));
    22. }
    23. }
    24. class Solution {
    25. //节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。
    26. //局部最优:让叶子节点的父节点安摄像头,所用摄像头最少
    27. //从下向上,后序遍历:左右中
    28. //状态标识
    29. // 0:该节点无覆盖
    30. // 1:本节点有摄像头
    31. // 2:本节点有覆盖
    32. int res = 0;
    33. public int minCameraCover(TreeNode root) {
    34. if(root == null) return 0;
    35. if(traversal(root) == 0){//根节点未被覆盖
    36. res++;
    37. }
    38. return res;
    39. }
    40. public int traversal(TreeNode root){
    41. if(root == null) return 2;
    42. int left = traversal(root.left);
    43. int right = traversal(root.right);
    44. //确定当前根节点状态
    45. if(left == 0 || right == 0){//左右有一个没覆盖,根节点处就需要一个摄像头
    46. res++;
    47. return 1;
    48. }
    49. if(left == 1 || right == 1){//左右有一个摄像头,根节点已经处于覆盖范围
    50. return 2;
    51. }a
    52. // if(left == 2 && right == 2){//左右均被覆盖,根节点不可能在被覆盖
    53. // return 0;
    54. // }
    55. return 0;
    56. }
    57. }

  • 相关阅读:
    SpringBoot整合RabbitMQ学习笔记
    Mysql日志
    性能测试 —— JMeter分布式测试及其详细步骤
    许啸宇:从内部研发到开源开发之路|OneFlow U
    MySQL之MHA集群
    2022/7/2做题总结
    1196: 数星星(二)(结构体专题)
    flink学习知识点小结
    【光学】基于matlab GUI单缝夫琅禾费衍射【含Matlab源码 2120期】
    软件测试行业35岁职场魔咒,你准备怎么应对?
  • 原文地址:https://blog.csdn.net/habe33/article/details/126249363