• 【代码随想录】贪心算法刷题


    贪心的本质是:选择每一阶段的局部最优解,从而达到全局最优。

    贪心没有固定的套路,比较好的策略是举反例,想不到反例,就试试贪心。

    分发饼干

    题目:455. 分发饼干 - 力扣(LeetCode)

    假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
    对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。

    输入: g = [1,2,3], s = [1,1]
    输出: 1
    解释: 
    你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
    虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。
    所以你应该输出1。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    贪心 + 双指针:

    public int findContentChildren(int[] g, int[] s) {
    	Arrays.sort(g);
    	Arrays.sort(s);
    	int res = 0;
    	for (int i = 0, j = 0; i < s.length && j < g.length; i++) {
    		if (s[i] >= g[j]) { // 饼干能喂饱孩子
    			res++;
    			j++; // 下一个孩子
    		}
    	}
    	return res;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    优化以上代码:可以发现 res 和 j 其实是一样的,只需要一个变量即可

    public int findContentChildren(int[] g, int[] s) {
        Arrays.sort(g);
        Arrays.sort(s);
        int res = 0;
        for (int i = 0; i < s.length && res < g.length; i++) 
            if (g[res] <= s[i]) res++;
        return res;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    摆动序列*

    题目:376. 摆动序列 - 力扣(LeetCode)

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

    • 例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。
    • 相反,[1, 4, 7, 2, 5][1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
      子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。
      给你一个整数数组 nums ,返回 nums 中作为 摆动序列最长子序列的长度
    输入:nums = [1,7,4,9,2,5]
    输出:6
    解释:整个序列均为摆动序列,各元素之间的差值为 (6, -3, 5, -7, 3) 。
    
    • 1
    • 2
    • 3

    模拟:

    public int wiggleMaxLength(int[] nums) {
        if (nums.length == 1) return 1;
        int res = 1;
        Boolean flag = null; // 控制方向
        for (int i = 1; i < nums.length; i++) {
            if (nums[i] == nums[i - 1]) continue; // 0
            if (nums[i] - nums[i - 1] > 0) { // 正值
                if (flag != null && flag) continue; 
                flag = true;
            } else { // 负值
                if (flag != null && !flag) continue; 
                flag = false;
            }
            res++;
        }
        return res;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    动态规划 一维:

    public int wiggleMaxLength(int[] nums) {
    	int n = nums.length;
    	if (n == 1) return 1;
    	
    	int[] up = new int[n]; // 以 i 升序的最长子序列的长度
    	int[] down = new int[n]; // 以 i 降序的最长子序列的长度
    	up[0] = down[0] = 1;
    	
    	for (int i = 1; i < n; i++) {
    		if (nums[i] > nums[i - 1]) { // 升序
    			up[i] = Math.max(up[i - 1], down[i - 1] + 1);
    			down[i] = down[i - 1];
    		} else if (nums[i] < nums[i - 1]) { // 降序
    			up[i] = up[i - 1];
    			down[i] = Math.max(up[i - 1] + 1, down[i - 1]);
    		} else { // 不变
    			up[i] = up[i - 1];
    			down[i] = down[i - 1];
    		}
    	}
    	return Math.max(up[n - 1], down[n - 1]);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    动态规划 二维:

    public int wiggleMaxLength(int[] nums) {
        int n = nums.length;
        if (n <= 1) return n;
        // dp[i][0] 表示 nums[0] 到 nums[i] 的最长摆动序列长度, 且 nums[i] < nums[i - 1], 往下
        // dp[i][1] 表示 nums[0] 到 nums[i] 的最长摆动序列长度, 且 nums[i] > nums[i - 1], 往上
        int[][] dp = new int[n][2];
        dp[0][0] = dp[0][1] = 1;
        for (int i = 1; i < n; i++) {
            if (nums[i] < nums[i - 1]) {
                dp[i][0] = dp[i - 1][1] + 1;
                dp[i][1] = dp[i - 1][1];
            } else if (nums[i] > nums[i - 1]) {
                dp[i][0] = dp[i - 1][0];
                dp[i][1] = dp[i - 1][0] + 1;
            } else {
                dp[i][0] = dp[i - 1][0];
                dp[i][1] = dp[i - 1][1];
            }
        }
        return Math.max(dp[n - 1][0], dp[n - 1][1]);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    最大子序和

    题目:53. 最大子数组和 - 力扣(LeetCode)

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

    输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
    输出:6
    解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
    
    • 1
    • 2
    • 3

    动态规划:

    public int maxSubArray(int[] nums) {
        // dp[i] 以 i 结尾的最大连续子数组和
        int[] dp = new int[nums.length];
        int max = dp[0] = nums[0];
        for (int i = 1; i < nums.length; i++) {
            dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);
            max = Math.max(max, dp[i]);
        }    
        return max;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    贪心:

    public int maxSubArray(int[] nums) {
        int max = nums[0];
        int tmp = 0; // 保存临时值
        for (int num : nums) {
            tmp += num;
            if (tmp > max) max = tmp;
            if (tmp < 0) tmp = 0;
        }
        return max;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    买卖股票的最佳时机

    题目:122. 买卖股票的最佳时机 II - 力扣(LeetCode)

    给你一个整数数组 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 。
         总利润为 4 + 3 = 7 。
    
    • 1
    • 2
    • 3
    • 4
    • 5

    一维 DP * 2:

    public int maxProfit(int[] prices) {
    	int n = prices.length;
    	int[] buy = new int[n]; // buy[i] 第 i 天买能获得的最大利益
    	int[] sell = new int[n]; // sell[i] 第 i 天卖能获得的最大利益
    	buy[0] = -prices[0];
    	int max = 0;
    	for (int i = 1; i < n; i++) {
    		buy[i] = Math.max(buy[i - 1], sell[i - 1] - prices[i]);
    		sell[i] = Math.max(sell[i - 1], buy[i - 1] + prices[i]);
    		max = Math.max(max, sell[i]); // 最大利益只会产生在卖的时候
    	}
    	return max;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    二维 DP:

    public int maxProfit(int[] prices) {
        int n = prices.length;
        // dp[i][0] - 第 i 天[买]能获得的最大利益
        // dp[i][1] - 第 i 天[卖]能获得的最大利益
        int[][] dp = new int[n][2];
        dp[0][0] = -prices[0];
        dp[0][1] = 0;
        int max = 0;
        for (int i = 1; i < n; 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]);
            max = Math.max(max, dp[i][1]);
        }
        return max;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    贪心:今天比昨天大就直接卖出

    public int maxProfit(int[] prices) {
        int res = 0;
        for (int i = 1; i < prices.length; i++) 
            if (prices[i] > prices[i - 1])
                res += prices[i] - prices[i - 1];
        return res;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    跳跃游戏

    题目:55. 跳跃游戏 - 力扣(LeetCode)

    给定一个非负整数数组 nums ,你最初位于数组的 第一个下标
    数组中的每个元素代表你在该位置可以跳跃的最大长度。
    判断你是否能够到达最后一个下标。

    输入:nums = [2,3,1,1,4]
    输出:true
    解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。
    
    • 1
    • 2
    • 3

    动态规划:

    public boolean canJump(int[] nums) {
        int[] dp = new int[nums.length]; // dp[i] i 能跳到的最远下标
        dp[0] = nums[0];
        for (int i = 1; i < nums.length; i++) {
            if (dp[i - 1] < i) return false; // 前一个位置无法到达当前位置, 直接结束
            dp[i] = Math.max(dp[i - 1], i + nums[i]); // 计算当前位置能跳到的最远位置
        }
        return true;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    贪心:

    public boolean canJump(int[] nums) {
        int cover = 0; // 覆盖范围
        for (int i = 0; i <= cover; i++) {
            cover = Math.max(cover, i + nums[i]); // 每一步的最大覆盖范围
            if (cover >= nums.length - 1) return true; // 能覆盖到终点
        }
        return false;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    跳跃游戏 II*

    题目:45. 跳跃游戏 II - 力扣(LeetCode)

    给你一个非负整数数组 nums ,你最初位于数组的第一个位置。
    数组中的每个元素代表你在该位置可以跳跃的最大长度。
    你的目标是使用最少的跳跃次数到达数组的最后一个位置。
    假设你总是可以到达数组的最后一个位置。

    输入: nums = [2,3,1,1,4]
    输出: 2
    解释: 跳到最后一个位置的最小跳跃数是 2。
         从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
    
    • 1
    • 2
    • 3
    • 4

    DP:

    public int jump(int[] nums) {
    	int n = nums.length;
    	int[] dp = new int[n]; // dp[i] 跳到 i 位置使用的最少跳跃次数
    	Arrays.fill(dp, 10001);
    	dp[0] = 0;
    	for (int i = 1; i < n; i++)
    		for (int j = 0; j < i; j++)
    			if (nums[j] + j >= i)
    				dp[i] = Math.min(dp[i], dp[j] + 1);
    	return dp[n - 1];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    贪心:

    public int jump(int[] nums) {
        int res = 0;
        int curDis = 0; // 当前的跳跃次数下最远能达到的距离
        int maxDis = nums[0]; // 当前的跳跃次数下再加一次最远能达到的距离
        for (int i = 1; i < nums.length; i++) {
            // 能在当前跳跃次数下到达就不会多跳
            if (i > curDis) { // 无法在当前跳跃次数下到达
                res++;
                curDis = maxDis;
            }
            // 更新当前次数下再跳一次能到达的最远距离
            maxDis = Math.max(maxDis, i + nums[i]);
        }
        return res;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    K 次取反后最大化的数组*

    题目:1005. K 次取反后最大化的数组和 - 力扣(LeetCode)

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

    • 选择某个下标 i 并将 nums[i] 替换为 -nums[i]
      重复这个过程恰好 k 次。可以多次选择同一个下标 i
      以这种方式修改数组后,返回数组 可能的最大和

    优先级队列:

    public int largestSumAfterKNegations(int[] nums, int k) {
    	PriorityQueue<Integer> pq = new PriorityQueue<>(); // 小根堆
    	for (int n : nums) pq.add(n);
    
    	int res = 0;
    	// 每次只将最小的值取反
    	while (k-- > 0) pq.add(-pq.poll()); 
    	while (!pq.isEmpty()) res += pq.poll();
    	return res;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    模拟 + 贪心:

    public int largestSumAfterKNegations(int[] nums, int k) {
        Arrays.sort(nums);
    
        int idx = 0; // 遍历下标
        while (k-- > 0) { 
            // 当前数 < 0, 取反后和下一位进行比较
            if (idx < nums.length - 1 && nums[idx] < 0) {
                nums[idx] = -nums[idx]; // 取反
                //
                if (nums[idx] >= Math.abs(nums[idx + 1])) idx++;
                continue;
            }
            // 当前数 >= 0
            nums[idx] = -nums[idx]; // 取反
        }
    
        int res = 0;
        for (int i = 0; i < nums.length; i++) res += nums[i];
        return res;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    加油站*

    题目:134. 加油站 - 力扣(LeetCode)

    在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。
    你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。
    给定两个整数数组 gascost ,如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -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 可为起始索引。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    贪心思想:

    • 如果 gas 的总和小于 cost 总和,那么无论从哪里出发,肯定跑不了一圈
    • 当前油量的累加一旦小于 0,起始位置至少是 i + 1
    public int canCompleteCircuit(int[] gas, int[] cost) {
    	int totalNum = 0; // 总油量
    	int curNum = 0; // 当前油量
    	int start = 0; // 开始位置
    	for (int i = 0; i < gas.length; i++) {
    		curNum += gas[i] - cost[i];
    		totalNum += gas[i] - cost[i];
    		// 遇到油量不够的请求, 从当前开始重新计算
    		if (curNum < 0) {
    			start = i + 1; // 更新起始位置为 i + 1
    			curNum = 0;
    		}
    	} 
    	return totalNum < 0 ? -1 : start;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    分发糖果*

    题目:135. 分发糖果 - 力扣(LeetCode)

    n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。
    你需要按照以下要求,给这些孩子分发糖果:

    • 每个孩子至少分配到 1 个糖果。
    • 相邻两个孩子评分更高的孩子会获得更多的糖果。
      请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目
    输入:ratings = [1,0,2]
    输出:5
    解释:你可以分别给第一个、第二个、第三个孩子分发 2、1、2 颗糖果。
    
    • 1
    • 2
    • 3

    每个孩子的评分有 2 次比较,左比较和右比较,对于碰到类似这种左右比较或者相邻比较,都可以考虑两次遍历。

    需要考虑多个维度情况的题目,尝试分别考虑每个维度,不要想着同时考虑两个维度。

    public int candy(int[] ratings) {
    	int[] candies = new int[ratings.length];
    	Arrays.fill(candies, 1);
    	// 从左往右: 只比较右边孩子评分比左边大的情况
    	for (int i = 1; i < candies.length; i++)
    		if (ratings[i] > ratings[i - 1]) 
    			candies[i] = candies[i - 1] + 1;
    	// 从右往左: 只比较左边孩子评分比右边大的情况
    	for (int i = candies.length - 2; i >= 0; i--)
    		if (ratings[i] > ratings[i + 1])
    			// 考虑这个维度要在第一个维度的基础上考虑, 即还得维护满足第一个维度的条件
    			candies[i] = Math.max(candies[i], candies[i + 1] + 1);
    	// 以上两个维度综合 => 相邻的孩子评分高的可以拿更多的糖果
    	int res = 0;
    	for (int i = 0; i < candies.length; i++) res += candies[i];
    	return res;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    柠檬水找零

    题目:860. 柠檬水找零 - 力扣(LeetCode)

    在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。
    每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。
    注意,一开始你手头没有任何零钱。
    给你一个整数数组 bills ,其中 bills[i] 是第 i 位顾客付的账。如果你能给每位顾客正确找零,返回 true ,否则返回 false

    输入:bills = [5,5,5,10,20]
    输出:true
    解释:
    前 3 位顾客那里,我们按顺序收取 3 张 5 美元的钞票。
    第 4 位顾客那里,我们收取一张 10 美元的钞票,并返还 5 美元。
    第 5 位顾客那里,我们找还一张 10 美元的钞票和一张 5 美元的钞票。
    由于所有客户都得到了正确的找零,所以我们输出 true。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    public boolean lemonadeChange(int[] bills) {
        int[] money = new int[5];
        for (int i = 0; i < bills.length; i++) {
            if (bills[i] == 5) money[5]++;
            else if (bills[i] == 10) {
                if (money[5] < 0) return false;
                money[10]++;
                money[5]--;
            } else if (bills[i] == 20) {
                if (money[5] > 0 && money[10] > 0) {
                    money[5]--;
                    money[10]--;
                } else if (money[5] >= 3) {
                    money[5] -= 3;
                } else return false;
            }
        }
        return true;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    根据身高重建队列

    题目:406. 根据身高重建队列 - 力扣(LeetCode)

    假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好ki 个身高大于或等于 hi 的人。
    请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。

    输入: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]] 是重新构造后的队列。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    public int[][] reconstructQueue(int[][] people) {
        // 先按身高从高到低排序, 身高一样再按 k 从低到高排序
        Arrays.sort(people, (a, b) -> {
            if (a[0] == b[0]) return a[1] - b[1];
            return b[0] - a[0];
        });
    
        LinkedList<int[]> q = new LinkedList<>();
        for (int[] p : people) q.add(p[1], p); // 往指定位置插入元素
    
        return q.toArray(new int[people.length][]);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    ----- 区间问题 -----

    用最少数量的箭引爆气球*

    题目:452. 用最少数量的箭引爆气球 - 力扣(LeetCode)

    有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ,其中points[i] = [x_start, x_end] 表示水平直径在 x_startx_end之间的气球。你不知道气球的确切 y 坐标。
    一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 x_startx_end, 且满足 x_start ≤ x ≤ x_end,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。
    给你一个数组 points返回引爆所有气球所必须射出的 最小 弓箭数

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

    贪心:这题本质上是找最大重叠区间

    • 以左边界升序排序
    • 重叠时,上一气球和当前气球视为一个区间,更新该区间的最小右边界
    • 不重叠时,上一气球和当前气球为两个不同的区间,箭的数量 + 1,最小右边界更新为新区间的

    public int findMinArrowShots(int[][] points) {
    	// 以左边界升序排序
    	Arrays.sort(points, (a, b) -> Integer.compare(a[0], b[0]));
    
    	int res = 1; // 至少需要一支箭
    	int end = points[0][1]; // 折叠气球的最小右边界
    	for (int i = 1; i < points.length; i++) {
    		// 当前气球的左边界 > 最小右边界, 说明不重叠, 箭 + 1
    		if (points[i][0] > end) {
    			res++;
    			end = points[i][1];
    		}
    		// 当前气球的左边界 <= 最小右边界, 说明重叠, 更新最小右边界
    		else end = Math.min(end, points[i][1]);
    	}
    	return res;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    无重叠区间*

    题目:435. 无重叠区间 - 力扣(LeetCode)

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

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

    贪心:

    • 以右边界升序排序
    • 计算最多能组成的不重叠的区间个数,然后用区间总个数减去不重叠区间的个数

    public int eraseOverlapIntervals(int[][] intervals) {
    	// 以右边界升序排序
        Arrays.sort(intervals, (a, b) -> Integer.compare(a[1], b[1]));
    
    	int cnt = 1; // 不重叠的区间个数
        int end = intervals[0][1]; // 
        for (int i = 0; i < intervals.length; i++) {
            // 重叠
            if (intervals[i][0] < end) continue;
            // 未重叠
            end = intervals[i][1];
            cnt++;
        }
    
        return intervals.length - cnt;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    划分字母区间

    题目:763. 划分字母区间 - 力扣(LeetCode)

    字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。

    输入:S = "ababcbacadefegdehijhklij"
    输出:[9,7,8]
    解释:
    划分结果为 "ababcbaca", "defegde", "hijhklij"。
    每个字母最多出现在一个片段中。
    像 "ababcbacadefegde", "hijhklij" 的划分是错误的,因为划分的片段数较少。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    哈希 + 贪心:

    • 记录每个小写字母 最后出现的位置
    • 遍历时更新区间能到达的最远位置,如果当前遍历位置即为最远,则找到一个划分

    public List<Integer> partitionLabels(String s) {
        List<Integer> res = new ArrayList<>();
        
        int[] idx = new int[128]; // 记录每个小写字母最后出现的位置
        for (int i = 0; i < s.length(); i++) idx[s.charAt(i)] = i;
    
        int start = 0, end = 0; // 当前区间的开始与结束位置
        for (int i = 0; i < s.length(); i++) {
            end = Math.max(end, idx[s.charAt(i)]); // 更新当前区间的能到的最远距离
            if (end == i) { // 到达当前区间能到的最远距离, 算是找到一个划分
                res.add(end - start + 1);
                start = end + 1; // 当前区间变为一个新的区间
            }
        }
        return res;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    合并区间

    题目:56. 合并区间 - 力扣(LeetCode)

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

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

    遍历时判断当前区间能否和上一个区间合并:

    public int[][] merge(int[][] intervals) {
        Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0])); 
        List<int[]> list = new ArrayList<>();
        // 每个区间的 起点 和 终点
        int start = intervals[0][0], end = intervals[0][1];
        for (int i = 1; i < intervals.length; i++) {
            if (intervals[i][0] <= end) { // 当前区间可以和上一个区间合并
                end = Math.max(end, intervals[i][1]);
            } else { // 当前区间无法和上一个区间合并
                list.add(new int[] { start, end }); // 添加当前区间
                start = intervals[i][0]; // 更新起点
                end = intervals[i][1]; // 更新终点
            }
        }
        list.add(new int[] { start, end }); // 最后一个区间
    
        return list.toArray(new int[list.size()][]);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    ----------

    单调递增的数字

    题目:738. 单调递增的数字 - 力扣(LeetCode)

    思路:从右往左扫描,若当前数字比其左边一位小,则把左边一位数字减1,并将该位及其右边所有位改成9

    public int monotoneIncreasingDigits(int n) {
    	char[] cs = String.valueOf(n).toCharArray();
    	// 从右往左扫描, 若当前数字比其左边一位小
    	// 
    	int start = cs.length; // 标记哪一位开始要变成9
    	for (int i = cs.length - 1; i >= 1; i--) {
    		if (cs[i] < cs[i - 1]) {
    			cs[i - 1]--;
    			start = i;
    		}
    	}
    	for (int i = start; i < cs.length; i++) cs[i] = '9';
    	return Integer.parseInt(new String(cs));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    买卖股票的最佳时机含手续费

    题目:714. 买卖股票的最佳时机含手续费 - 力扣(LeetCode)

    public int maxProfit(int[] prices, int fee) {
    	int n = prices.length;
    	int[] sell = new int[n]; // 第 i 天卖股票的最高利润
    	int[] buy = new int[n]; // 第 i 天买股票的最高利润 
    	buy[0] = -prices[0];
    	for (int i = 1; i < n; i++) {
    		// 今天卖股票的最高利润: [卖] 或 [不卖] 两种行为做比较
    		// [卖]: 之前买的最高利润 + 今天卖的钱 - 手续费
    		// [不卖]: 之前卖过的最高利润
    		sell[i] = Math.max(buy[i - 1] + prices[i] - fee, sell[i - 1]);
    		// 今天买股票的最高利润: [买] 或 [不买] 两种行为做比较
    		// [买]: 之前卖的最高利润 - 今天买的钱
    		// [不买]: 之前买过的最高利润
    		buy[i] = Math.max(sell[i - 1] - prices[i], buy[i - 1]);
    	}
    	return sell[n - 1];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    监控二叉树

    题目:968. 监控二叉树 - 力扣(LeetCode)

    此题先跳过。

  • 相关阅读:
    【JavaWeb】注册页面验证码,用户名已存在,密码格式校验
    使用webpack基础配置并打包typescript
    线阵相机之行触发
    第一行代码 Android 第九章9.1-9.2(WebView的用法,使用HttpURLConnect发送HTTP请求,使用OkHttp发送HTTP请求)
    再谈基因论
    selenium报错:没有打开网页或selenium.common.exceptions.NoSuchDriverException
    PART2 VUE2.x---基础指令
    Day 05 python学习笔记
    google浏览器chrome无法访问localhost等本地虚拟域名的解决方法
    Servlet学习(三):HttpServlet
  • 原文地址:https://blog.csdn.net/weixin_43734095/article/details/126852947