导语
leetcode刷题笔记记录,本篇博客是贪心部分的第一期,主要记录题目包括:
- 455.分发饼干
- 376.摆动序列
-
- 最大子序和
知识点
贪心算法
贪心算法是一种在每一步选择中都采取当前状态下最好或最优的选择,从而希望导致结果是全局最好或最优的算法。贪心算法在有最优子结构的问题中尤为有效。最优子结构的意思是局部最优解能决定全局最优解(对有些问题这是明显的,如在排序问题上,贪心算法选择每次都选最小的未处理元素,总会得到最优解)。
基本思路:
- 建立数学模型来描述问题。
- 把求解的问题分成若干个子问题。
- 对每一子问题求解,得到子问题的局部最优解。
- 把子问题的局部最优解合成原来问题的一个解。
贪心算法的特点:
- 贪心算法所求的解通常不是最优解,但是简单。
- 有时,贪心算法可以得到最优解,如最小生成树、霍夫曼编码等。
使用贪心算法的标准模式:
- 从问题的某一初始解出发。
- 采用循环的过程,但每一迭代的过程中都需要做出决策,进而得到可能的一个解。
- 每一次的决策都是在确认了局部最优的前提下做出的。
- 通常,经过k次决策,就可能得到问题的一个可行解了。
- 利用所得的可行解,可以得到问题的一个最优解。
贪心算法的经典问题
- 找零问题:给定面额为d1、d2...dm的硬币和需要支付的钱数,计算出使用这些面额的硬币来达到这个金额所需的最少的硬币数量。如LeetCode 322. Coin Change
- 背包问题:有一个背包,最大的承载重量是Wkg。有一系列的物品,每个物品有重量和价值两个属性。目标是在不超过背包最大重量的前提下,让背包中物品的总价值最大。如LeetCode 416. Partition Equal Subset Sum
- 最小生成树:给定一个带权的无向连通图,如何选取一棵生成树,使树上所有边上权的总和为最小,这叫最小生成树。如LeetCode 1135. Connecting Cities With Minimum Cost
- 单源最短路径:给定图中的一个起始点s,求从s到图中任意点的最短路径。如LeetCode 743. Network Delay Time
当然,贪心算法并不总是得到全局最优解,只有在问题具有特定的结构时(如最优子结构),贪心选择才能保证得到最优解。
Leetcode 455.分发饼干
题目描述
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
示例 1:
输入: g = [1,2,3], s = [1,1]
输出: 1
解释:
你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。
所以你应该输出1。
提示:
1 <= g.length <= 3 * 1040 <= s.length <= 3 * 1041 <= g[i], s[j] <= 2^{31} - 1
解法
这里的局部最优就是让大尺寸的饼干尽量喂给符合要求的胃口最大的,全局最优就是喂饱尽可能多的小孩。
可以尝试使用贪心策略,先将饼干数组和小孩数组排序。然后从后向前遍历小孩数组,用大饼干优先满足胃口大的,并统计满足小孩数量。
class Solution:
def findContentChildren(self, g: List[int], s: List[int]) -> int:
# 先排序胃口和尺寸
g.sort()
s.sort()
# result用于保存满足的数量,index代表饼干的下标
result = 0
index = len(s) - 1
# 首先遍历胃口,从最大的胃口开始,找到第一个可以满足它的饼干后再处理下一个胃口
for i in range(len(g)-1, -1, -1):
if index >= 0 and s[index] >= g[i]:
result += 1
index -= 1
return result
Leetcode 376.摆动序列
题目描述
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。 第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。
- 例如,
[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) 。
提示:
1 <= nums.length <= 10000 <= nums[i] <= 1000
解法
这道题目只需要我们统计数目,实际上并不需要在数组中删除节点,可以画图分析(下图来自代码随想录):
这里的局部和全局最优情况为:
- 局部最优:删除单调坡度上的节点(不包括单调坡度两端的节点),那么这个坡度就可以有两个局部峰值。
- 整体最优:整个序列有最多的局部峰值,从而达到最长摆动序列。
定义两个变量分别表示当前值与前一个值的差值以及后一个值与当前值的差值,只要两个值异号,就代表当前值是一个峰值。
cur_diff = nums[i+1] - nums[i]
pre_diff = nums[i] - nums[i-1]
if (pre_diff>0 and cur_diff<0) or (pre_diff<0 and cur_diff>0):
result += 1
但本题要考虑三种情况:
- 情况一:上下坡中有平坡
- 情况二:数组首尾两端
- 情况三:单调坡中有平坡
对于第一种情况,只需要统一一下规则,都删掉左边或者右边的值就可以,示意图如下:
代码修改为:
if (pre_diff>=0 and cur_diff<0) or (pre_diff<=0 and cur_diff>0):
result += 1
对于第二种情况,添加一个虚拟的数字,值与第一个数字相同,这样由于是单调,会不考虑它的存在,但是方便代码格式的统一;
对于第三种情况,我们只需要在 这个坡度 摆动变化的时候,更新 prediff 就行,这样 prediff 在 单调区间有平坡的时候 就不会发生变化,造成我们的误判。
完整代码如下:
class Solution:
def wiggleMaxLength(self, nums: List[int]) -> int:
if len(nums) == 1:
return 1
pre_diff, cur_diff = 0, 0
result = 1
for i in range(len(nums)-1):
cur_diff = nums[i+1] - nums[i]
if (pre_diff>=0 and cur_diff<0) or (pre_diff<=0 and cur_diff>0):
result += 1
pre_diff = cur_diff
return result
Leetcode 53. 最大子序和
题目描述
给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组 是数组中的一个连续部分。
示例 1:
输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6 。
提示:
1 <= nums.length <= 105-104 <= nums[i] <= 104
解法
这里的局部最优为:连续和为负数的时候,我们从下一个新的元素开始重新计算连续和,因为负的连续和只会给后面带来负的收益。
完整代码如下:
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
result, count = float("-inf"), 0
for num in nums:
count += num
if count > result:
result = count
if count < 0:
count = 0
return result