贪心算法学习
基本理论
贪心算法的基本理论是“从局部最优推出全局最优”。
每一次小操作都是在选择局部最优,最终得到的结果就是一个全局最优。
当一个题目能满足这个思路的时候,就可以尝试使用贪心了。
题目
455 分发饼干
https://leetcode.cn/problems/assign-cookies/
题目和思路
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
对每个孩子 i,都有一个胃口值 g[i]
,这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j]
。如果 s[j] >= g[i]
,我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 示例 1: 输入: g = [1,2,3], s = [1,1] 输出: 1 解释: 你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。 虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。 所以你应该输出1。
示例 2: 输入: g = [1,2], s = [1,2,3] 输出: 2 解释: 你有两个孩子和三块小饼干,2个孩子的胃口值分别是1,2。 你拥有的饼干数量和尺寸都足以让所有孩子满足。 所以你应该输出2.
|
这道题的思路其实很简单,即我们每次都应该选择大饼干满足大胃口的人,这样才能让最终可以吃到饼干的小孩最多。
直接排序两个数组,从右往左遍历,判断当前饼干是否能满足当前胃口的人,如果可以,则消耗一个饼干,并让计数器加一。不可以,则继续往左遍历胃口。
在这个例子中,局部最优是用大饼干满足大胃口的,全局最优是最终能吃上饼干的孩子最多。
完整代码
代码中需要注意对index的越界检查,特别是数组s为空的情况。当index已经越界的时候,就可以跳出循环了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class Solution { public: int findContentChildren(vector<int>& g, vector<int>& s) { sort(g.begin(), g.end()); sort(s.begin(), s.end()); int count = 0; int index = s.size() - 1; for (int i = g.size() - 1; i >= 0; i--) { if (index < 0) { break; } if (s[index] >= g[i]) { index--; count++; } } return count; } };
|
376 摆动序列
https://leetcode.cn/problems/wiggle-subsequence/description/
题目和思路
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。
例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。
相反,[1, 4, 7, 2, 5] 和 [1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。
给你一个整数数组 nums ,返回 nums 中作为 摆动序列 的 最长子序列的长度 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 示例 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
|
首先需要拆分情况,除了正常的一上一下的折线图,还可能会出现下面的情况,这些情况都必须特别来处理。
- 连续的上坡或连续的下坡
- 上坡下坡之间有平地
- 上坡和上坡之间有平地(下坡和下坡之间有平地)
先来看第一个,连续的上坡或者连续的下坡的情况:这需要我们将连续的上坡只看作一个上坡,比如上坡有3个节点(包括坡底和坡顶),那么最终可以计算的只有一个(坡顶,坡底已经计算过了)。
上坡下坡之间有平地的情况:将平地其他节点删除,只计算一个节点。
上坡和上坡之间有平地:将平地删除成一个节点,此时就变成了情况1的连续上坡/下坡。
具体示意图的可以参考代码随想录:https://www.programmercarl.com/0376.%E6%91%86%E5%8A%A8%E5%BA%8F%E5%88%97.html#%E6%80%9D%E8%B7%AF
错误代码
最开始的时候,我写出了这样一版代码,它的问题在于,只能处理子数组的情况(子数组是连续的),而不能处理子序列(中间可以不连续)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| class Solution { public: int wiggleMaxLength(vector<int>& nums) { int lastDiff = 0; int maxLength = -1; int curLength = 1; for (int i = 1; i < nums.size(); i++) { int diff = nums[i] - nums[i - 1]; cout << nums[i] << " - " << nums[i - 1] << " = " << diff << " : " << curLength << " f:" << lastDiff << endl; if (diff > 0) { if (lastDiff == 1) { curLength = 1; continue; } curLength++; maxLength = max(curLength, maxLength); lastDiff = 1; } else { if (lastDiff == -1) { curLength = 1; continue; } curLength++; maxLength = max(curLength, maxLength); lastDiff = -1; } } return maxLength == -1 ? 0 : maxLength; } };
|
正确代码
这里面,用curDiff记录当前的差值,preDiff记录上一个有变动的差值,result是返回值。
result初始化为1是因为判断是在nums.size() - 2
就结束的(最后一个节点并没有被循环计入其中),而且当数组长度为2的时候,判断只会有一次,如果不置1就会漏掉一个值。
以下图为例,这是一个上坡和下坡之中有平坡的情况,如下的代码在循环中判断的是i和i+1,最终会在nums.size() - 2
位置(图中已标注)跳出循环。此时result++
是只加入了nums.size() - 2
的这个点,而没有加入nums.size() - 1
这个同样需要被加入的点,所以需要将result初始化为1来覆盖这种漏掉的情况。
下面是详细的判断逻辑:
preDiff < 0 && curDiff > 0
和 preDiff >0 && curDiff < 0
是最基本的上坡和下坡情况,当前差值和之前的并非同正同负;preDiff == 0 && curDiff > 0
和 preDiff == 0 && curDiff < 0
的情况满足上文所述“上坡下坡之间有平地的情况,此时也需要记录摆动的变化。- 只在摆动变化的时候记录preDiff就可以忽略“上坡和上坡之间有平地”的情况(如果不这么做,就会多记录一次节点)
最终可以写出下面的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| class Solution { public: int wiggleMaxLength(vector<int>& nums) { if (nums.size() <= 1) { return nums.size(); } int curDiff = 0; int preDiff = 0; int result = 1; for (int i = 0; i < nums.size() - 1; i++) { curDiff = nums[i + 1] - nums[i]; if ((preDiff <= 0 && curDiff > 0) || (preDiff >= 0 && curDiff < 0)) { result++; preDiff = curDiff; } } return result; } };
|
122 买卖股票的最佳时机2
题目和思路
https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/description/
给你一个整数数组 prices ,其中 prices[i]
表示某支股票第 i 天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 示例 1: 输入: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 。 示例 2: 输入:prices = [1,2,3,4,5] 输出:4 解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。 总利润为 4 。 示例 3: 输入:prices = [7,6,4,3,1] 输出:0 解释:在这种情况下, 交易无法获得正利润,所以不参与交易可以获得最大利润,最大利润为 0 。
|
首先需要想明白,这道题的利润是可以被分解的。第0天买,第3天卖的利润是 prices[3]-prices[0]
,等价于 (p[3]-p[2]) + (p[2]-p[1]) + (p[1]-p[0])
。想明白这一点后,这道题的解法就很明确了。
与其是动态计算咋样才能获得最高利润,还不如分解成一小块一小块的,只要昨天买入,今天卖出可以赚钱,那么就加入到最终的利润里面。因为你0天买3天卖,和每天都卖出昨日买入今日得到的钱是完全一致的。那么我们排除掉昨天买入今天卖出会亏钱的情况,就能计算出最终可以达到的最大利润。
题目代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class Solution { public: int maxProfit(vector<int>& prices) { int count = 0; for (int i = 1; i < prices.size(); i++) { count += max(prices[i] - prices[i - 1], 0); } return count; } };
|
55 跳跃游戏
https://leetcode.cn/problems/jump-game/description/
题目和思路
给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false 。
1 2 3 4 5 6 7 8 9
| 示例 1: 输入:nums = [2,3,1,1,4] 输出:true 解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。
示例 2: 输入:nums = [3,2,1,0,4] 输出:false 解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。
|
这道题的思路主要在于判断当前下标加上可以跳的步数判断是否到了最后一位,到了就肯定能跳到。
超时代码
最开始我写出了这样的递归思路,本质是暴力求解。不出意外的超时了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| class Solution { bool _canJump(vector<int>& nums, int index) { if (index >= nums.size() - 1) { return true; } if (index + nums[index] >= nums.size() - 1) { return true; } int range = min(index + nums[index], (int)(nums.size() - 1)); for (int i = index + 1; i <= range; i++) { if (_canJump(nums, i)) { return true; } } return false; }
public: bool canJump(vector<int>& nums) { return _canJump(nums, 0); } };
|
正确代码
代码随想录上提供了一个更好的办法,思路其实也是类似的,但是可以压缩在一个循环中。动态的改变当前for循环i的边界cover,直到cover大于等于最后一位下标的时候,就是可以跳到,返回true。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class Solution { public: bool canJump(vector<int>& nums) { if (nums.size() == 1) { return true; } int cover = 0; for (int i = 0; i <= cover; i++) { cover = max(i + nums[i], cover); if (cover >= nums.size() - 1) { return true; } } return false; } };
|
45 跳跃游戏2
题目和思路
https://leetcode.cn/problems/jump-game-ii/description/
给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。
每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处:
1 2
| 0 <= j <= nums[i] i + j < n
|
返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]。
1 2 3 4 5 6 7 8 9
| 示例 1: 输入: nums = [2,3,1,1,4] 输出: 2 解释: 跳到最后一个位置的最小跳跃数是 2。 从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。 示例 2: 输入: nums = [2,3,0,1,4] 输出: 2
|
这道题和上一题就不同了,上一题只是判断能不能跳到最后一个位置,这一题是在已经可以跳到最后一个位置的基础上,计算最小的跳跃次数。
不过基本思路还是一样的,就是判断cover的范围。
- 在当前范围中,找到下一步能抵达的最大范围
- i抵达当前范围的边界了,必须跳一步了
- 步数加一
- 更新当前范围为下一步的最大范围
- 判断最大范围是否已经到了
nums.size()-1
,如果到了直接返回步数(只需要再跳一步就可以抵达末尾了)
不管当前怎么跳,当前范围都是可以跳到的,那么我们只需要找到当前范围中,下一步能抵达的最大位置,那么就是最好的选择。直到下一步抵达终点,返回步数。
完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| class Solution { public: int jump(vector<int>& nums) { if (nums.size() == 1) { return 0; } int curJump = 0; int maxJump = 0; int count = 0; for (int i = 0; i < nums.size(); i++) { maxJump = max(i + nums[i], maxJump); if (i == curJump) { count++; curJump = maxJump; if (maxJump >= nums.size() - 1) { return count; } } } return count; } };
|
1005 k次取反后的最大和
https://leetcode.cn/problems/maximize-sum-of-array-after-k-negations/
题目和思路
给你一个整数数组 nums 和一个整数 k ,按以下方法修改该数组:
选择某个下标 i 并将 nums[i] 替换为 -nums[i] 。
重复这个过程恰好 k 次。可以多次选择同一个下标 i 。
以这种方式修改数组后,返回数组 可能的最大和 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 示例 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] 。
示例 3: 输入:nums = [2,-3,-1,5,-4], k = 2 输出:13 解释:选择下标 (1, 4) ,nums 变为 [2,3,-1,5,4]
|
这道题的思路很简单,首先我们肯定是先把负数反转成正的,都是正数了之后,再考虑反转最小的数。
本来我的想法是,反转完毕负数后,重排序,从最小的数开始一直往后反转,但是这样思考就忽略了题目中的一个要求。题目是允许在某个下标处重复反转的。
那么我们就没有必要从最小的数开始把它们都反转成负数了,而是一直操作最小的那个数,直到k用完。上面的示例2就是这样的,先把负数反转成正数,然后重复操作0。
完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| class Solution { static bool cmp(int a, int b) { return abs(a) > abs(b); }
public: int largestSumAfterKNegations(vector<int>& nums, int k) { sort(nums.begin(), nums.end(), cmp); for (int i = 0; i < nums.size(); i++) { if (nums[i] < 0 && k > 0) { nums[i] *= -1; k--; } } if (k % 2 == 1) { nums[nums.size() - 1] *= -1; } int sum = 0; for (auto& e : nums) { sum += e; } return sum; } };
|
134 加油站
题目和思路
https://leetcode.cn/problems/gas-station/
在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。
你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。
给定两个整数数组 gas 和 cost ,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 示例 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 升汽油。 因此,无论怎样,你都不可能绕环路行驶一周。
|
这道题使用贪心的方式,找到局部最优:假设从start出发每次走一步,计算gas[i]-cost[i]
得到剩余的油,如果剩余的油为负数,说明从[start,i]
之中的任意位置出发,走到i的时候都会没油。那么就需要从i+1开始走(更新start)。
最终遍历完毕整个数组的时候,得到的start肯定是能走完整个区间的起始下标。因为其他下标开始走,都没有办法走完某一个区间。
完整代码
这里还有关于start=i+1
是否会出现越界情况的说明,参考代码注释。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| class Solution { public: int canCompleteCircuit(vector<int>& gas, vector<int>& cost) { int start = 0; int totalSum = 0; int leftGas = 0; for (int i = 0; i < gas.size(); i++) { leftGas += gas[i] - cost[i]; totalSum += gas[i] - cost[i]; if (leftGas < 0) { start = i + 1; leftGas = 0; } } if (totalSum < 0) { return -1; } return start; } };
|
135 分发糖果
https://leetcode.cn/problems/candy/description/
题目和思路
n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。
你需要按照以下要求,给这些孩子分发糖果:
每个孩子至少分配到 1 个糖果。
相邻两个孩子评分更高的孩子会获得更多的糖果。
请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。
1 2 3 4 5 6 7 8 9 10
| 示例 1: 输入:ratings = [1,0,2] 输出:5 解释:你可以分别给第一个、第二个、第三个孩子分发 2、1、2 颗糖果。
示例 2: 输入:ratings = [1,2,2] 输出:4 解释:你可以分别给第一个、第二个、第三个孩子分发 1、2、1 颗糖果。 第三个孩子只得到 1 颗糖果,这满足题面中的两个条件。
|
这道题的思路其实不难,但是我们不要一次性考虑某个小孩的两边,而是应该一次只考虑一边。
- 初始化数组,每人一个糖果。
- 先从左往右遍历,如果当前孩子的分值高于左边,则当前孩子的糖果数量是左边孩子+1;
- 然后从右边往左边遍历,如果当前孩子的分值高于右边,则当前孩子的糖果数量是
max(右边孩子糖果数量+1,当前孩子糖果数量)
为什么从右往左的时候需要进行max判断呢?因为题目需要的是最小的糖果数量,同时也需要满足条件,max判断能保证当前的孩子糖果大于右边,同时也满足大于左边的条件(如果直接等于右边的孩子糖果数量+1,可能会导致它不再满足高于左边孩子的糖果的情况)
完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| class Solution { public: int candy(vector<int>& ratings) { vector<int> candyArray(ratings.size(), 1); for (int i = 1; i < ratings.size(); i++) { if (ratings[i] > ratings[i - 1]) { candyArray[i] = candyArray[i - 1] + 1; } } for (int i = ratings.size() - 2; i >= 0; i--) { if (ratings[i] > ratings[i + 1]) { candyArray[i] = max(candyArray[i + 1] + 1, candyArray[i]); } } int count = 0; for (auto& c : candyArray) { count += c; } return count; } };
|
860 柠檬水找零
题目和思路
https://leetcode.cn/problems/lemonade-change/description/
在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。
每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。
注意,一开始你手头没有任何零钱。
给你一个整数数组 bills ,其中 bills[i] 是第 i 位顾客付的账。如果你能给每位顾客正确找零,返回 true ,否则返回 false 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 示例 1: 输入:bills = [5,5,5,10,20] 输出:true 解释: 前 3 位顾客那里,我们按顺序收取 3 张 5 美元的钞票。 第 4 位顾客那里,我们收取一张 10 美元的钞票,并返还 5 美元。 第 5 位顾客那里,我们找还一张 10 美元的钞票和一张 5 美元的钞票。 由于所有客户都得到了正确的找零,所以我们输出 true。
示例 2: 输入:bills = [5,5,10,10,20] 输出:false 解释: 前 2 位顾客那里,我们按顺序收取 2 张 5 美元的钞票。 对于接下来的 2 位顾客,我们收取一张 10 美元的钞票,然后返还 5 美元。 对于最后一位顾客,我们无法退回 15 美元,因为我们现在只有两张 10 美元的钞票。 由于不是每位顾客都得到了正确的找零,所以答案是 false。
|
这道题其实是一个最最基本的数学问题。用到贪心的地方就是有10+5
和3*5
的时候应该先用哪一个进行找零。很明显我们需要用10+5
来找零,因为10元只有这个地方能用上。如果优先用5元找零,后序有人用10元来买柠檬水的时候,可能零钱就不够了。
完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| class Solution { public: bool lemonadeChange(vector<int>& bills) { unordered_map<int, int> momenyMap; for (auto i : bills) { momenyMap[i]++; i -= 5; if (i == 5) { momenyMap[5]--; if (momenyMap[5] < 0) { return false; } } else if (i == 15) { if (momenyMap[10] > 0 && momenyMap[5] > 0) { momenyMap[5]--; momenyMap[10]--; continue; } if (momenyMap[5] < 3) { return false; } momenyMap[5] -= 3; continue; } } return true; } };
|
452 用最少的箭引爆气球
题目和思路
https://leetcode.cn/problems/minimum-number-of-arrows-to-burst-balloons/
有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ,其中points[i] = [xstart, xend]
表示水平直径在 xstart 和 xend之间的气球。你不知道气球的确切 y 坐标。
一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为xstart,xend
, 且满足 xstart ≤ x ≤ xend
,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。
给你一个数组 points ,返回引爆所有气球所必须射出的 最小 弓箭数 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 示例 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: 输入:points = [[1,2],[2,3],[3,4],[4,5]] 输出:2 解释:气球可以用2支箭来爆破: - 在x = 2处发射箭,击破气球[1,2]和[2,3]。 - 在x = 4处射出箭,击破气球[3,4]和[4,5]。
|
首先我们需要对边界进行排序,这里选用左边界升序。
然后从左往右遍历,根据当前的左边界和上一个值的右边界,确定两个气球是否挨着。
- 如果挨着,说明两个气球可以被一起射爆(注意边界值相等也视为重叠),此时需要更新当前值的右边界为
min(当前右边界,上一个气球的右边界)
,因为只有这样才能保证下一次遍历判断的时候得到的结果,能把之前的两个气球射爆 - 如果不挨着,说明前一个气球需要单独一箭,计数器加一。
这里的贪心思想就是不断的更新边界值,来确保找到更多符合同一边界情况的气球,减少箭矢的使用。
完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| class Solution { static bool cmp(vector<int>& a, vector<int>& b) { return a[0] < b[0]; }
public: int findMinArrowShots(vector<vector<int>>& points) { if (points.size() == 1) { return 1; } sort(points.begin(), points.end(), cmp); int arrow = 1; int end = points[0][1]; for (int i = 1; i < points.size(); i++) { if (points[i][0] <= end) { end = min(points[i][1], end); } else { end = points[i][1]; arrow++; } } return arrow; } };
|
406 根据身高重建队列
https://leetcode.cn/problems/queue-reconstruction-by-height/description/
题目和思路
假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。
请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 示例 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]] 是重新构造后的队列。
示例 2: 输入:people = [[6,0],[5,0],[4,0],[3,2],[2,2],[1,4]] 输出:[[4,0],[5,0],[2,2],[3,2],[1,4],[6,0]]
|
这道题目和135分发糖果
有一定相似,都需要从两个维度来考虑。但为了避免出错,我们一次只能考虑一个维度。
首先要做的就是对people进行排序,那么是根据身高排序,还是根据前面有几个人排序?
1 2 3
| 用例 [[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]] 身高 [[7,0],[7,1],[6,1],[5,0],[5,2],[4,4]] 人数 [[7,0],[5,0],[7,1],[6,1],[5,2],[4,4]]
|
会发现,如果按前面有几个人排序,最终得到的序列好像并没有用,也不是题目需要的序列。
但根据身高降序排序,会让这里的序列满足一个特性,即元素i之前的都比i高。
这时候我们就能直接根据元素i之前有几个比他高的人,将这个值视作下标,来构建出新的队列!前面有几个比他高的人,就往第几位之后插入。
完整代码
注意这里使用链表来进行插入操作,效率会更高。因为vector每次的insert都需要对元素进行移动,甚至会涉及到空间扩容的消耗(所以我提前reserve了),而list不会有这个问题。
我们可以先用list来进行插入操作,最后再用迭代器构造vector返回。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| class Solution { static bool cmp(vector<int>& a, vector<int>& b) { if (a[0] > b[0]) { return true; } if (a[0] == b[0]) { return a[1] < b[1]; } return false; }
public: vector<vector<int>> reconstructQueue(vector<vector<int>>& people) { sort(people.begin(), people.end(), cmp); for (auto& v : people) { cout << v[0] << " " << v[1] << "\n"; } vector<vector<int>> que; que.reserve(people.size()); for (int i = 0; i < people.size(); i++) { int offset = people[i][1]; que.insert(que.begin() + offset, people[i]); } return que; } };
|
链表版本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| class Solution { static bool cmp(vector<int>& a, vector<int>& b) { if (a[0] > b[0]) { return true; } if (a[0] == b[0]) { return a[1] < b[1]; } return false; }
public: vector<vector<int>> reconstructQueue(vector<vector<int>>& people) { sort(people.begin(), people.end(), cmp); for (auto& v : people) { cout << v[0] << " " << v[1] << "\n"; } list<vector<int>> que;
for (int i = 0; i < people.size(); i++) { int offset = people[i][1]; auto itr = que.begin(); while(offset--){ itr++; } que.insert(itr, people[i]); } return vector<vector<int>>(que.begin(),que.end()); } };
|
435 无重叠区间
题目和思路
https://leetcode.cn/problems/non-overlapping-intervals/
给定一个区间的集合 intervals ,其中 intervals[i] = [starti, endi]
。返回 需要移除区间的最小数量,使剩余区间互不重叠 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 示例 1: 输入: intervals = [[1,2],[2,3],[3,4],[1,3]] 输出: 1 解释: 移除 [1,3] 后,剩下的区间没有重叠。
示例 2: 输入: intervals = [ [1,2], [1,2], [1,2] ] 输出: 2 解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。
示例 3: 输入: intervals = [ [1,2], [2,3] ] 输出: 0 解释: 你不需要移除任何区间,因为它们已经是无重叠的了。
|
这道题和前文射爆气球的题目很相似,但是需要注意,在射爆气球那道题中,边界值相同视作重叠。而本题边界值相同不视作重叠区间。
还是用左边界升序排序,记录重叠的区间的数量,即需要删除的区间数量。
判断到重叠区间的时候,也需要将边界值更新为区间最小右边界。
1
| end = min(intervals[i][1],end);
|
因为我们发现两个区间重叠的时候,我们应该删除区间范围(只考虑右边界)最大的那个,才能尽可能的避免当前的这个重叠区间进一步和其他节点重叠。
题目需要求的是最少需要删除的区间数量,贪心的思想也是在此,每次都删除右边界更大的那个区间,才能保证最终删除的区间数量最少。
完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| class Solution { static bool cmp(vector<int>& a, vector<int>& b) { return a[0] < b[0]; }
public: int eraseOverlapIntervals(vector<vector<int>>& intervals) { if (intervals.size() <= 1) { return 0; } sort(intervals.begin(), intervals.end(), cmp); int count = 0; int end = intervals[0][1]; for (int i = 1; i < intervals.size(); i++) { if (intervals[i][0] < end) { count++; end = min(intervals[i][1],end); } else { end = intervals[i][1]; } } return count; } };
|
763 划分字母区间
题目和思路
https://leetcode.cn/problems/partition-labels/description/
给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。
注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s 。
返回一个表示每个字符串片段的长度的列表。
1 2 3 4 5 6 7 8 9 10 11
| 示例 1: 输入:s = "ababcbacadefegdehijhklij" 输出:[9,7,8] 解释: 划分结果为 "ababcbaca"、"defegde"、"hijhklij" 。 每个字母最多出现在一个片段中。 像 "ababcbacadefegde", "hijhklij" 这样的划分是错误的,因为划分的片段数较少。
示例 2: 输入:s = "eccbbbbdec" 输出:[10]
|
这道题的思路是,先遍历一次字符串,维护一个相同长度的字母最远出现位置(下标)的数组。
以题目给出的示例1为例,按照如上思路可以维护出一个这样的数组。
再次遍历字符串,最开始start为0,以[start,i]
为一个区间,维护一个这个区间中字母的最远出现位置,当i等于当前的“最远出现位置”时,说明已经得到了一个可分割的子区间,更新start为i+1,继续遍历下一个区间。
- 为什么i等于当前最远出现位置时就找到了一个可分割的子序列呢?
还是看示例1,观察图中被标红的位置,这些都是可以进行分割的位置。当我们遍历[0,8]
这个区间时,维护的这个区间中字母最远出现位置就是8,当i等于8的时候,对应字符的最远出现位置也是8,说明在这个区间中,没有其他字符最远出现的位置大于8了,即符合题目中给出的“每个字母只在其中一个区间出现”的要求。
这里并没有用到贪心思路,而是根据题意进行的一个模拟。
完整代码
注意这道题干说了字符串只包含因英文小写字母,所以使用一个定长的数组,效率会高于unordered_map
数据结构。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| class Solution { public: vector<int> partitionLabels(string s) { int indexMap[27] = {0}; for (int i = 0; i < s.size(); i++) { indexMap[s[i]-'a'] = i; } vector<int> retV; int start = 0; int end = 0; for (int i = 0; i < s.size(); i++) { end = max(end, indexMap[s[i]-'a']); if (i == end) { retV.push_back(i - start + 1); start = i + 1; } } return retV; } };
|
56 合并区间
https://leetcode.cn/problems/merge-intervals/
题目和思路
以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi]
。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
1 2 3 4 5 6 7 8 9
| 示例 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] 可被视为重叠区间。
|
这道题和前面遇到的几个区间重叠的问题一致,都是需要先对给出数组按左区间进行排序,再判断重叠区间,最终将重叠的区间进行合并。
当intervals[i][0] <= intervals[i-1][1]
的时候,视作两个区间重叠(相同也是重叠的情况),此时需要扩展原有区间为max(intervals[i][1],intervals[i-1][1])
,再继续向后遍历。
完整代码
注意vector是有front/back这两个函数来访问第一个/最后一个元素的,且返回的是引用,写retV.back()
的代码可读性会好于retV[retV.size()-1]
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| class Solution { static bool cmp(vector<int>& a, vector<int>& b) { return a[0] < b[0]; }
public: vector<vector<int>> merge(vector<vector<int>>& intervals) { if (intervals.size() <= 1) { return intervals; } sort(intervals.begin(), intervals.end(), cmp); vector<vector<int>> retV; retV.push_back(intervals[0]); for (int i = 1; i < intervals.size(); i++) { if (intervals[i][0] <= retV.back()[1]) { retV.back()[1] = max(intervals[i][1], retV.back()[1]); } else { retV.push_back(intervals[i]); } } return retV; } };
|
738 单调递增的数字
https://leetcode.cn/problems/monotone-increasing-digits/description/
题目和思路
当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。
给定一个整数 n ,返回 小于或等于 n 的最大数字,且数字呈 单调递增 。
1 2 3 4 5 6 7 8 9 10 11
| 示例 1: 输入: n = 10 输出: 9
示例 2: 输入: n = 1234 输出: 1234
示例 3: 输入: n = 332 输出: 299
|
这道题需要我们从后往前遍历一个数字,方便处理,我们可以先把数字转成一个字符串(当然遍历一下写入到数组中也是可以的)。
- 当前位大于上一位,不处理
- 当前位小于上一位,将上一位减一,当前位可以设置为9
因为题目中给出的递增数字中,x99999
也算做递增的情况,所以第二种情况,可以先记录一下当前的下标,并使用第二个循环从这个下标位置开始往后全部都设置为9。
完整代码
代码如下,不算难写。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class Solution { public: int monotoneIncreasingDigits(int n) { string s = to_string(n); int start = s.size() + 1; for (int i = s.size() - 1; i >= 1; i--) { if (s[i - 1] > s[i]) { s[i - 1]--; start = i; } } for (int i = start; i < s.size(); i++) { s[i] = '9'; } return atoi(s.c_str()); } };
|
写完这个代码后,我想到了一个问题,有没有可能s[i-1]='0'
,这样减减一下岂不是上一位就不是数字了?
实际上是不可能出现这个情况的,如果s[i-1]='0'
的情况,当前位s[i]
是不可能小于他的,最多是等于的情况(两个都是零),所以不会出现操作0减减,自然不会有问题。
968 监控二叉树
https://leetcode.cn/problems/binary-tree-cameras/description/
题目和代码
给定一个二叉树,我们在树的节点上安装摄像头。
节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。
计算监控树的所有节点所需的最小摄像头数量。
1 2 3 4 5
| 这道题的示例建议看leetcode上给出的图例。
输入:[0,0,null,0,0] 输出:1 解释:如图所示,一台摄像头足以监控所有节点。
|
本题需要我们计算监控所有节点涉及到的最少的摄像头数量,那么一个摄像头最好是能同时监控下层和上层(没有漏掉的层),才是利用率最大化。这也是贪心的思想所在。首先可以确定的是,我们需要用后续遍历,从最底下开始往上遍历整个树。
根据这个思路,叶子节点肯定不能装摄像头。因为叶子节点没有子树,装摄像头相当于白白浪费了摄像头对下一层子树的监看,最终肯定不能达到最大的监控数量。
这个思路能帮我们确定递归的终止条件,即在空节点应该返回什么。不过在编写代码之前,我们应该先定义好三个状态值。
1 2 3
| 0 没有监控也没有被覆盖上 1 装了监控 2 被覆盖上了(但是没有装监控)
|
那么空节点应该返回哪一个状态码呢?先来考虑一下什么时候当前节点需要装摄像头
- 左子树和右子树只要有其中一个装了摄像头(返回值为1),当前节点就能被覆盖上,不需要装摄像头;
- 右子树和左子树只要有一个没有被覆盖上(返回值为0),那么当前节点就必须装摄像头,否则会有子树无法被覆盖;
由此可见,空节点不能返回1,因为这说明叶子节点被覆盖了,反馈到上一层会认为不需要装摄像头,导致最终叶子节点并没有被覆盖上,不符合题目条件。空节点也不能返回0,因为这说明叶子节点需要装摄像头。
所以,空节点只能返回2啦!
1 2 3
| if(root == nullptr){ return 2; }
|
其他情况就是上文提到的,采用后续遍历,用left/right接受左右子树遍历的结果,只要有一个为0,那么就需要装摄像头,摄像头数量加一,函数返回1;
最后是只要有一个为1,就代表不用装摄像头,返回2;
完整代码
依照上述思路可以写出如下代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| class Solution { int _minCameraCoverResult(TreeNode* root, int& result) { if (root == nullptr) { return 2; } int left = _minCameraCoverResult(root->left, result); int right = _minCameraCoverResult(root->right, result); if (left == 2 && right == 2) { return 0; } if (left == 0 || right == 0) { result++; return 1; } if (left == 1 || right == 1) { return 2; } return 0; }
public: int minCameraCover(TreeNode* root) { int result = 0; if (_minCameraCoverResult(root, result) == 0) { result++; } return result; } };
|