贪心0

题目分类大纲

一、简单题目

1.分发饼干

题目描述

题目链接
参考题解

解题思路

  • 为了满足更多的小孩,就不要造成饼干尺寸的浪费。

  • 小的饼干先喂胃口小的

代码

class Solution {
    public int findContentChildren(int[] g, int[] s) {
        Arrays.sort(g); //胃口
        Arrays.sort(s); //饼干
        int i = 0;
        for(int j=0; j<s.length && i<g.length; j++) {
            if(s[j] < g[i]) {
                continue;
            }
            i++;
        }
        return i;
    }
}

2.k次取反后的最大化数组和

题目描述

题目连接

代码

class Solution {
    public int largestSumAfterKNegations(int[] nums, int k) {
        Arrays.sort(nums);
        //先利用k将最小的小于0的数字转换为正数
        int i = 0;
        for(; i<nums.length && k>0; i++) {
            if(nums[i] <= 0) {
                nums[i] = -nums[i];
                k--;
            } else {
                break;
            }
        }
        //剩余的k可以抵消
        if(k == 0 || k % 2 == 0) {
            return Arrays.stream(nums).sum();
        }
        //此时nums全是非负数
        Arrays.sort(nums);
        nums[0] = -nums[0];
        return Arrays.stream(nums).sum();
    }
}
class Solution {
    public int largestSumAfterKNegations(int[] nums, int k) {
        //Arrays.sort(nums, (n1, n2) -> Math.abs(n1) - Math.abs(n2));//需要是Integer
        nums = IntStream.of(nums)   //绝对值从大到小排
		     .boxed()
		     .sorted((o1, o2) -> Math.abs(o2) - Math.abs(o1))
		     .mapToInt(Integer::intValue).toArray();
        for(int i=0; i<nums.length && k>0; i++) {
            if(nums[i] < 0) {
                nums[i] = -nums[i];
                k--;
            }
        }
        //此时有两种可能
        //1.k == 0
        //2.k > 0,并且全是非负数
        if(k % 2 == 1) {
            nums[nums.length-1] = -nums[nums.length-1];
        }
        return Arrays.stream(nums).sum();
    }
}

3.柠檬水找零

这题没必要再做了

题目描述

题目链接

解题思路

  • 当用户支付20时,优先找零10

代码

class Solution {
    public boolean lemonadeChange(int[] bills) {
        int n5 = 0;
        int n10 = 0;
        for(int bill : bills) {
            if(bill == 5) {
                n5++;
            } else if(bill == 10) {
                if(n5 > 0) {
                    n5--;
                    n10++;
                } else {
                    return false;
                }
            } else {
                //如果用户支付20,优先找零10
                if(n10 > 0 && n5 > 0) {
                    n10--;
                    n5--;
                } else if(n10 == 0 && n5 >= 3) {
                    n5 -= 3;
                } else {
                    return false;
                }
            }
        }
        return true;
    }
}

二、中等题目

1.摆动序列

题目描述

题目链接
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。

例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。

相反,[1, 4, 7, 2, 5] 和 [1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。

给你一个整数数组 nums ,返回 nums 中作为 摆动序列 的 最长子序列的长度 。

示例 1:

输入:nums = [1,7,4,9,2,5]
输出:6
解释:整个序列均为摆动序列,各元素之间的差值为 (6, -3, 5, -7, 3) 。

示例 2:

输入:nums = [1,17,5,10,13,15,10,5,16,8]
输出:7
解释:这个序列包含几个长度为 7 摆动序列。
其中一个是 [1, 17, 10, 13, 10, 16, 8] ,各元素之间的差值为 (16, -7, 3, -3, 6, -8) 。

示例 3:

输入:nums = [1,2,3,4,5,6,7,8,9]
输出:2

提示:

1 <= nums.length <= 1000
0 <= nums[i] <= 1000

进阶:你能否用 O(n) 时间复杂度完成此题?

解题思路

  • diff = nums[i] - nums[i-1]
  • flag = 0/-1/1 分别表示上一个状态是:初始状态/下坡/上坡
  • 跳过平坡和上坡过程中的节点,只关注可以导致摆动的坡度变化
class Solution {
    /*
    case1:先增后减是摆动
        /\
    case2:先减后增是摆动
        \/
    case3:有平坡
         --
        /  \
    case4:有平坡
           /
         --
        /
    */
    //何时有摆动?
    //先增后减
    //先减后增
    //diff = nums[i] - nums[i-1]
    public int wiggleMaxLength(int[] nums) {
        if(nums.length <= 1) return nums.length;
        int ans = 1;
        int flag = 0;   //0:上一个是初始状态,-1:上一个是下坡,1:上一个是上坡
        for(int i=1; i<nums.length; i++) {
            int diff = nums[i] - nums[i-1];
            if(diff > 0 && (flag == 0 || flag == -1)) {
                ans++;
                flag = 1;
            } else if(diff < 0 && (flag == 0 || flag == 1)) {
                ans++;
                flag = -1;
            }
            // else if(diff == 0) 跳过平坡;
            // else 跳过上/下坡过程中的点
        }
        return ans;
    }
}

2.买卖股票的最佳时机

题目描述

题目链接
给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。

在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。

返回 你能获得的 最大 利润 。

解题思路

法1:贪心

  • 利润可以拆解,只收集正利润

法2:dp

  • 第i天的状态:持有股票/不持有股票
  • dp[i][0] = 第i天不持有股票获得的最大利润
  • dp[i][1] = 第i天持有股票获得的最大利润

代码

class Solution {
    public int maxProfit(int[] prices) {
        int ans = 0;
        for(int i=1; i<prices.length; i++) {
            ans += Math.max(0, prices[i] - prices[i-1]);
        }
        return ans;
    }
}
class Solution {
    //dp[i][0] = 第i天不持有股票获得的最大利润
    //dp[i][1] = 第i天持有股票获得的最大利润
    public int maxProfit(int[] prices) {
        int[][] dp = new int[prices.length][2];
        dp[0][0] = 0;
        dp[0][1] = - prices[0];
        for(int i=1; i<prices.length; i++) {
            dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
            dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] - prices[i]);
        }
        return dp[prices.length-1][0];
    }
}

3.分发糖果

题目描述

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

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

每个孩子至少分配到 1 个糖果。
相邻两个孩子评分更高的孩子会获得更多的糖果。
请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。

解题思路

  • 需要从两个维度权衡
  • 从前往后遍历一次
  • 从后往前遍历一次

代码

class Solution {
    public int candy(int[] ratings) {
        int[] candys = new int[ratings.length];
        //从前往后遍历,保证比自己左边邻居评分高时糖果也更多
        for(int i=1; i<ratings.length; i++) {
            if(ratings[i] > ratings[i-1] && candys[i] <= candys[i-1]) {
                candys[i] = candys[i-1] + 1;
            }
        }
        //从后往前遍历,保证比自己右边邻居评分高时糖果也更多
        for(int i=ratings.length-2; i>=0; i--) {
            if(ratings[i] > ratings[i+1] && candys[i] <= candys[i+1]) {
                candys[i] = candys[i+1] + 1;
            }
        }
        return Arrays.stream(candys).sum() + ratings.length;
    }
}

4.根据身高重建队列

题目描述

题目描述
数组people[i] = [hi, ki],表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。但是这个数组是被打乱的,恢复这个数组的顺序。

解题思路

  • 很明显这道题有两个维度,分别是身高和前面的比自己高(或相等)的人的个数。因此要分别考虑。
  • 前面 正好 有 ki 个身高大于或等于 hi 的人。因此,hi降序,ki升序。所以先按hi排序,再按ki排序。
  • 然后在新的list,在位置k插入。因为比i高的都在i前面,因此一定可以插进去。

代码

class Solution {
    public int[][] reconstructQueue(int[][] people) {
        //先按照h降序排序
        //再按照k升序排序
        Arrays.sort(people, (p1, p2) -> {
            if(p1[0] == p2[0]) {
                return p1[1] - p2[1];
            } else {
                return p2[0] - p1[0];
            }
        });
        List<int[]> queue = new LinkedList<>();
        for(int[] person : people) {
            queue.add(person[1], person);
        }
        return queue.toArray(people);
    }
}

5.单调递增的数字

题目描述

题目链接

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

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

解题思路

  • 本题只要想清楚个例,例如98,一旦出现strNum[i - 1] > strNum[i]的情况(非单调递增),首先想让strNum[i - 1]减一,strNum[i]赋值9,这样这个整数就是89。就可以很自然想到对应的贪心解法了。

  • 想到了贪心,还要考虑遍历顺序,只有从后向前遍历才能重复利用上次比较的结果。

  • 最后代码实现的时候,也需要一些技巧,例如用一个flag来标记从哪里开始赋值9。

代码

class Solution {
    public int monotoneIncreasingDigits(int n) {
        StringBuilder sb = new StringBuilder(Integer.toString(n));
        int flag = sb.length();     //-1的位数后面的位数都要变成9
        for(int i=sb.length()-1; i>0; i--) {
            if(sb.charAt(i-1) > sb.charAt(i)) {
                sb.setCharAt(i-1, (char)(sb.charAt(i-1) - 1));
                flag = i;
            }
        }
        for(int i=flag; i<sb.length(); i++) {
            sb.setCharAt(i, '9');
        }
        return Integer.parseInt(sb.toString()); //会自动去掉前导0
    }
}
//示例(过程):
// 332
// 329
// 299

// 4321
// 4319
// 4299
// 3999

三、有点难度

1. 最大子数组和

题目描述

题目链接
给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组 是数组中的一个连续部分。

解题思路

法1:贪心

  • 一旦当前子数组的和已经小于0了,那么就没必要继续追加当前子数组了,因为它已经为负数了,只会拖累后面的。就可以重新开始记录新的子数组了
  • 用ans及时和curSum比较,记录最大值

法2:动态规划

  • dp[i] = 以i结尾的连续子数组的最大和

代码

class Solution {
    public int maxSubArray(int[] nums) {
        int ans = -10001;
        int curSum = 0; //当前子数组的累计和
        for(int num : nums) {
            curSum += num;
            if(curSum > ans) {
                ans = curSum;   //及时记录过程中的最大值
            }
            if(curSum < 0) {//如果当前子数组的累计和<0了,它只会拖累后面的,所以果断抛弃重新开始
                curSum = 0;
            }
        }
        return ans;
    }
}
class Solution {
    //dp[i] = 以i结尾的连续子数组的最大和
    public int maxSubArray(int[] nums) {
        int dp0 = nums[0];
        int ans = nums[0];
        for(int i=1; i<nums.length; i++) {
            dp0 = Math.max(dp0 + nums[i], nums[i]);
            ans = Math.max(ans, dp0);
        }
        return ans;
    }
}

2.跳跃游戏

题目描述

题目链接
给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false 。

解题思路

  • 关键在于最大覆盖范围能否覆盖终点,不必关心怎么走过去的
  • 只有被cover到的地方才有资格被判断,cover是不断变化的

代码

class Solution {
    public boolean canJump(int[] nums) {
        int cover = 0;      //
        for(int i=0; i<=cover; i++) {   //只能走到被cover住的位置,cover是不断更新的
            cover = Math.max(cover, i + nums[i]);
            if(cover >= nums.length-1) {
                return true;
            }
        }
        return false;
    }
}

3.跳跃游戏II

题目描述

题目链接
和上一题的区别在于,只能跳规定的距离,而不是最多跳规定的距离

解题思路

  • 关键在于使用最小的步数达到最大的覆盖距离
  • 在走到当前覆盖范围的尽头时,需要再走一步才能到达更大的覆盖范围,这一步要尽可能的去最远的地方,因此要记录下当前覆盖范围的起点之后的最大覆盖范围next

代码

class Solution {
    public int jump(int[] nums) {
        int ans = 0;
        int cover = 0;  //当前覆盖范围
        //比当前覆盖范围更大的最大覆盖范围,
        //是在当前覆盖范围后得到的,所以走到这个位置需要比当前多一步,
        //所以只有在走到当前最大覆盖范围的尽头时,如果还没到终点,就需要走这一步
        int next = 0;
        for(int i=0; i<nums.length-1; i++) {
            next = Math.max(next, i + nums[i]); //更新下一步可以走到的最大范围
            if(i == cover) {    //走到当前覆盖范围的尽头,需要再走一步才能到达下一个最大覆盖范围
                cover = next;
                ans++;
            }
            if(cover >= nums.length-1) {
                break;
            }
        }
        return ans;
    }
}

4.用最少数量的箭引爆气球

题目描述

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

一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。

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

解题思路

  • 先按照每个区间的左边界升序排序,然后遍历右边界。
  • 注意考虑(s1, s2, e1, e2)这种情况会导致当前覆盖范围缩小。
  • 注意-2^31 <= xstart < xend <= 2^31 - 1Arrays.sort(points, (p1, p2) -> p1[0] - p2[0]);会溢出,使用Integer.compare(p1[0], p2[0])防止溢出。

代码

class Solution {
    public int findMinArrowShots(int[][] points) {
        Arrays.sort(points, (p1, p2) -> Integer.compare(p1[0], p2[0]));     //按照start升序排序
        int ans = 1;
        int curEnd = points[0][1];
        for(int i=1; i<points.length; i++) {
            if(points[i][0] <= curEnd) {
                curEnd = Math.min(curEnd, points[i][1]);    // (s1, s2, e2, e1)这种情况会导致覆盖范围缩小
            } else {
                ans++;
                curEnd = points[i][1];
            }
        }
        return ans;
    }
}

5.无重叠区间

题目描述

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

解题思路

  • 先按照左边界排序
  • 若两个排序后的相邻区间有重叠,则删除往后涉及范围更大的那个区间(也就是忽略这个被删除的区间,将没被删除的区间的右边界视作新的边界

代码

class Solution {
    //先按照左边界排序
    //若两个排序后的相邻区间有重叠,则删除往后涉及范围更大的那个区间(也就是忽略这个被删除的区间,将没被删除的区间的右边界视作新的边界
    public int eraseOverlapIntervals(int[][] intervals) {
        Arrays.sort(intervals, (i1, i2) -> Integer.compare(i1[0], i2[0]));
        int ans = 0;
        int curEnd = intervals[0][1];
        for(int i=1; i<intervals.length; i++) {
            if(intervals[i][0] < curEnd) {  //有重叠
                ans++;
                curEnd = Math.min(curEnd, intervals[i][1]);
            } else {
                curEnd = intervals[i][1];
            }
        }
        return ans;
    }
}

6.划分字母区间

题目描述

题目链接

解题思路

  • 先找到每种字符的最远边界。
  • 设置一个变量curEnd更新当前片段的所有字符的最远边界。
  • 当前字符的下标 = 当前片段的所有字符的最远边界 时(后面不会再出现当前片段的字符了),得到一段最小划分。

代码

class Solution {
    public List<Integer> partitionLabels(String s) {
        int[] endForChar = new int[26];     //记录每种字符出现的最后位置
        for(int i=0; i<s.length(); i++) {
            endForChar[s.charAt(i)-'a'] = i;
        }
        List<Integer> ans = new ArrayList<>();
        int curStart = 0;
        int curEnd = 0;
        for(int i=0; i<s.length(); i++) {
            curEnd = Math.max(curEnd, endForChar[s.charAt(i)-'a']); //更新当前片段的所有字符的最远边界
            if(i == curEnd) {
                ans.add(curEnd - curStart + 1);
                curStart = curEnd + 1;
            }
        }
        return ans;
    }
}

7.合并区间

题目描述

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

解题思路

  • 喵喵喵

代码

class Solution {
    public int[][] merge(int[][] intervals) {
        Arrays.sort(intervals, (i1, i2) -> Integer.compare(i1[0], i2[0]));
        List<int[]> ans = new ArrayList<>();
        int curStart = intervals[0][0];
        int curEnd = intervals[0][1];
        for(int i=1; i<intervals.length; i++) {
            if(intervals[i][0] <= curEnd) {
                curEnd = Math.max(curEnd, intervals[i][1]);
            } else {
                ans.add(new int[]{curStart, curEnd});
                curStart = intervals[i][0];
                curEnd = intervals[i][1];
            }
        }
        ans.add(new int[]{curStart, curEnd});
        return ans.toArray(new int[ans.size()][2]);
    }
}

8.监控二叉树

题目描述

题目链接

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

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

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

解题思路

  • 尽量把摄像头安装在父节点上

  • 如何判断父节点是否需要安装?-> 根据子节点的状态 -> 后序遍历,且需要返回子节点的状态

  • 递归函数返回子节点状态:有摄像头/被覆盖/无覆盖

  • null节点返回有覆盖状态,才能让叶节点的父结点放摄像头

代码

class Solution {
    //0 有摄像头
    //1 有覆盖
    //-1 无覆盖
    int ans;
    public int minCameraCover(TreeNode root) {
        ans = 0;
        if(backtrack(root) == -1) {
            ans++;
        }
        return ans;
    }
    int backtrack(TreeNode root) {
        if(root == null) return 1;  //空节点设置为有覆盖状态!
        int left = backtrack(root.left);
        int right = backtrack(root.right);
        //root的子树中有没有被覆盖的 -> 必须在root放摄像头
        if(left == -1 || right == -1) {
            ans++;
            return 0;
        }
        //此时子树都不是未覆盖状态(上下代码不能颠倒)
        //root的子树有摄像头 -> 不用在root放摄像头
        if(left == 0 || right == 0) {
            return 1;
        }
        //root需要被覆盖
        return -1;
    }
}

9.加油站

题目描述

题目链接

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

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

给定两个整数数组 gas 和 cost ,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的。

解题思路

  • rest[i] = gas[i] - cost[i]
  • 如果 总油量 - 总消耗 >= 0 一定可以跑完一圈,说明rest[i]相加一定是大于等于0的
  • 从 i = 0 开始累加rest[i],记为curSum。一旦curSum < 0,说明区间[0, i]都不能作为起始位置,因为在这个区间中选择任何一个位置作为起点,到i这里都会断油(因为前面的累计和preSum只会让curSum更大,因为是正数,如果是正数早就提前断油了)。那么只能从i+1位置开始,重新从0累计curSum。

代码

class Solution {
    public int canCompleteCircuit(int[] gas, int[] cost) {
        int totalSum = 0;
        int curSum = 0;
        int start = 0;
        for(int i=0; i<gas.length; i++) {
            curSum += (gas[i] - cost[i]);
            totalSum += (gas[i] - cost[i]);
            if(curSum < 0) {
                curSum = 0;
                start = i + 1;
            }
        }
        if(totalSum < 0) return -1;
        return start;
    }
}
posted @ 2023-08-12 19:38  shimmer_ghq  阅读(8)  评论(0编辑  收藏  举报