> 技术文档 > leetcode—贪心算法总结_leetcode 贪心算法

leetcode—贪心算法总结_leetcode 贪心算法


lc455.分发饼干

都排序,小饼干给小胃口,如果不够喂,考虑下一个饼干

class Solution: def findContentChildren(self, g: List[int], s: List[int]) -> int: g.sort() s.sort() g_cur = 0 s_cur = 0 while g_cur < len(g) and s_cur = g[g_cur]: s_cur += 1 g_cur += 1 else: s_cur += 1 return g_cur

lc376 摆动序列

up记录当前最长“最后一个差值为正”的子序列长度

down记录当前最长“最后一个差值为负”的子序列长度

每次都根据当前的差值做出更新

1:单调时,会一直更新值,但并不改变up与down的实际值(因为单调时不计数)

2:平台时,不更新

3:直到遇到“拐点时”,才会实际更新up和down

贪心思想:每次遇到差值,都认为这是一个拐点,进行更新,但是否实际更新了,要看是否是真的拐点,但我们只考虑目前这个差值。

class Solution: def wiggleMaxLength(self, nums: List[int]) -> int: if len(nums) == 1: return 1 up = 1 down = 1 for i in range(1, len(nums)): if (nums[i] - nums[i-1]) > 0: up = down + 1 elif (nums[i] - nums[i-1]) < 0: down = up + 1 return max(up, down) 

lc53 最大的子数组和

res进行“打擂台”记录最大值

如果此时的连续和为负数,那这个连续和只会让后面的子数组和变得更小,不如抛弃,所以此时重新更新连续和为0,下一个num开始累计

class Solution: def wiggleMaxLength(self, nums: List[int]) -> int: if len(nums) == 1: return 1 up = 1 down = 1 for i in range(1, len(nums)): if (nums[i] - nums[i-1]) > 0: up = down + 1 elif (nums[i] - nums[i-1]) < 0: down = up + 1 return max(up, down) 

lc122 买卖股票的最佳时机II

我只买赚钱的股票,加起来就是最大利润

class Solution: def maxProfit(self, prices: List[int]) -> int: pre = prices[0] res = 0 for price in prices: profit = price - pre if profit > 0: res += profit pre = price return res

lc55 跳跃游戏

贪心思想,每次尽可能往前覆盖“可到达的范围”,本文为cover,当cover覆盖了终点时,一定可到达。

class Solution: def canJump(self, nums: List[int]) -> bool: if len(nums) == 1: return True cover = 0 i = 0 while i = len(nums) - 1: return True i += 1 return False 

lc45 跳跃游戏II

每次尽可能扩大cover(选择能够最大化cover的跳跃)

每次进行扩大cover时,就将步数+1

class Solution: def jump(self, nums: List[int]) -> int: res = 0 cur_cover = 0 next_cover = 0 i = 0 while i = len(nums) - 1:  break else:  res += 1  cur_cover = next_cover i += 1 return res

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

按绝对值排序

最后结束for循环有三种情况:

1、k不够用(即k<负数个数),此时直接返回

2、k够用:此时数组已经全正

        (1)k为偶数,直接返回

        (2)k为奇数,对最小的非负数取反

class Solution: def largestSumAfterKNegations(self, nums: List[int], k: int) -> int: nums.sort(key=lambda x: abs(x), reverse=True) for i in range(len(nums)): if k > 0 and nums[i] < 0: nums[i] *= -1 k -= 1 if k % 2 == 1: nums[-1] *= -1 return sum(nums)

lc134 加油站

这题的贪心思想有意思了

用delta来考虑,gas[i] - cost[i],即经过此加油站所补充\\消耗的油量

我不妨用delta数组来存储这n个差值

首先我假设起点为0,然后去按顺序累加delta数组,当区间为【0,i】时,一旦发现累加和为负了,则更新起点为i+1,然后以i+1为起点,重新向后累加

以上过程,事实上是一种贪心构造 + 局部失败剪枝。

代码随想录的一句话:当【i,j】区间和为负时,【i,j】内不可能有可以作为起点的存在。

这其实是不严谨的

如果我们试图用反证法去证明,只会发现:

假设i+k存在于【i,j】之内,且作为起点,则【i+k,j】区间和为正,而因为【i,j】为负,则【i,i+k】为负,则i+k天然为一个起点。这分明就是可以存在一个起点的

因此这无法严格用反证法证明,这个说法本身不严谨

我们应该这样说:

我们在【i,j】失败的时候,直接从j+1作为起点再次开始尝试时,【i,j】内可以作为起点的地方已经被我们都尝试过了,这是一种剪枝手段,或者说,如果【i,j】内存在可以作为起点的地方如(i+k),我们现在应该正在尝试这个i+k,而不是i
也就是说,我们是在进行顺序尝试起点,进行了剪枝~

其次,只要满足sum(delta) >= 0(即总油量大于等于总消耗量),就一定存在一个合理起点可以走完一圈,为什么?

class Solution: def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int: total_sum = 0 for i in range(len(gas)): total_sum += (gas[i] - cost[i]) if total_sum < 0: return -1 i = 0 start = 0 cur_sum = 0 for i in range(len(gas)): #局部最优 cur_sum += (gas[i] - cost[i])  if cur_sum = 0 return start

lc135 分发糖果

class Solution: def candy(self, ratings: List[int]) -> int: n = len(ratings) candy = [1] * n for i in range(1, n): if ratings[i] > ratings[i-1]: candy[i] = candy[i-1] + 1 for i in range(n-2, -1, -1): if ratings[i] > ratings[i+1]: candy[i] = max(candy[i], candy[i+1]+1) return sum(candy) 

lc860 柠檬水找零

遇到20,优先用10+5找零

class Solution: def lemonadeChange(self, bills: List[int]) -> bool: money_list = [0] * 2 if bills[0] != 5: return False for bill in bills: if bill == 5: money_list[0] += 1 elif bill == 10: if money_list[0] == 0:  return False else:  money_list[0] -= 1  money_list[1] += 1 else: if money_list[0] == 0:  return False elif money_list[0] >= 1 and money_list[1] >= 1:  money_list[0] -= 1  money_list[1] -= 1  continue elif money_list[0] >= 3:  money_list[0] -= 3  continue return False return True

lc406 根据身高重建队列

两个维度,先搞定一个维度,再搞定第二个维度(确保第二个维度调整时,不影响第一个维度

class Solution: def reconstructQueue(self, people: List[List[int]]) -> List[List[int]]: queue = [] people.sort(key = lambda x: (-x[0], x[1])) for p in people: queue.insert(p[1], p) return queue

lc452 用最少数量的箭射爆气球

按起点升序排

注意边界更新

class Solution: def findMinArrowShots(self, points: List[List[int]]) -> int: res = 1 points.sort(key = lambda x: x[0]) end = points[0][1] for i in range(1,len(points)): if points[i][0] <= end: end = min(end, points[i][1]) else: res += 1 end = points[i][1] return res

lc435 无重叠区间

换句话讲,尽可能保留多的组数(组内两两重叠(一箭射爆),最后组内只留一个区间(且是end最小的区间,但这不重要,这题无需考虑这么细了)
最后 res = 总区间数 - 组数

所以和射箭是基本一直的,就是把箭数求出来

class Solution: def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int: group = 1 intervals.sort(key = lambda x: x[0]) end = intervals[0][1] for i in range(1, len(intervals)): if intervals[i][0] < end: end = min(end, intervals[i][1]) else: group += 1 end = intervals[i][1] return len(intervals) - group

lc763 划分字母区间

先得到每个字母最晚出现位置,再不断更新边界end,当到达边界end时,代表之前的字母已经出现完毕,后续不再出现,可划分出一个区间。

class Solution: def partitionLabels(self, s: str) -> List[int]: hash_table = [0] * 26 for i in range(len(s)): hash_table[ord(s[i]) - ord(\'a\')] = i pre_end = 0 end = 0 res = [] for i in range(len(s)): end = max(end, hash_table[ord(s[i]) - ord(\'a\')]) if i == end: res.append(end - pre_end + 1) pre_end = end + 1 return res

lc56 合并区间

注意对end的更新,这里取的是max,而非min,意图为合并区间

class Solution: def merge(self, intervals: List[List[int]]) -> List[List[int]]: intervals.sort(key = lambda x: x[0]) res = [] min_left = intervals[0][0] end = intervals[0][1] for i in range(1, len(intervals)): if intervals[i][0] <= end: end = max(end, intervals[i][1]) else: res.append([min_left, end]) min_left = intervals[i][0] end = intervals[i][1] res.append([min_left, end]) return res

lc738 单调递增的数字

从后往前遍历,这样可以利用到已修改的结果

遇到不符合的情况,就将当前及之后全部变为9,前一位-1。

class Solution: def monotoneIncreasingDigits(self, n: int) -> int: n = list(map(int, str(n))) flag = len(n) for i in range(len(n)-1, 0, -1): if n[i-1] > n[i]: flag = i n[i-1] -= 1 for i in range(flag, len(n)): n[i] = 9 return int(\"\".join(map(str, n)))

lc968 监控二叉树

1、尽量在叶子节点的父节点放摄像头,放在叶子节点的话就浪费了,所以从下往上放

2、根据1的原则,采取后序遍历,根据左右孩子的状态来决定父节点的操作

3、

        状态1:有覆盖

        状态2:无覆盖

        状态3:摄像头

4、特殊情况,根节点是没有父节点的,所以遍历完之后根节点可能是无覆盖的状态,此时单独处理

class Solution: def minCameraCover(self, root: Optional[TreeNode]) -> int: self.res = 0 # 1有覆盖 # 2无覆盖 # 3摄像头 def post_traversal(node): if not node: return 1 left_res = post_traversal(node.left) right_res = post_traversal(node.right) if left_res == 1 and right_res == 1: return 2 elif left_res == 2 or right_res == 2: self.res += 1 return 3 elif left_res == 3 or right_res == 3: return 1 if post_traversal(root) == 2: self.res += 1 return self.res