贪心算法理论基础
- 贪心算法并没有固定的套路,唯一的难点就是如何通过局部最优,推出整体最优。
- 如何验证可不可以用贪心算法呢?最好用的策略就是举反例,如果想不到反例,那么就试一试贪心吧。
- 刷题或者面试的时候,手动模拟一下感觉可以局部最优推出整体最优,而且想不到反例,那么就试一试贪心。
贪心算法一般分为如下四步:
- 将问题分解为若干个子问题
- 找出适合的贪心策略
- 求解每一个子问题的最优解
- 将局部最优解堆叠成全局最优解
这个四步其实过于理论化了,我们平时在做贪心类的题目 很难去按照这四步去思考,真是有点"鸡肋"。做题的时候,只要想清楚 局部最优 是什么,如果推导出全局最优,其实就够了。
455. 分发饼干 - 力扣(LeetCode)
这是一个经典的贪心算法问题,策略是优先满足胃口小的孩子。具体操作就是先将孩子的胃口数组和饼干尺寸数组排序,然后从小到大匹配。如果当前饼干可以满足当前孩子,就把饼干给孩子,然后移动到下一个孩子和下一个饼干;如果当前饼干无法满足当前孩子,就放弃当前饼干,移动到下一个饼干。
这种算法的理论支持是,如果当前饼干无法满足当前孩子,那么它也无法满足胃口更大的孩子,因此应该放弃。如果可以满足当前孩子,就应该先满足他,因为下一个饼干不一定能满足下一个孩子,但是下一个饼干可能会满足当前孩子或者胃口更大的孩子。
Python实现代码如下:
python
def findContentChildren(g, s):
g.sort()
s.sort()
child_i = cookie_j = 0
while child_i < len(g) and cookie_j < len(s):
if s[cookie_j] >= g[child_i]:
child_i += 1
cookie_j += 1
return child_i
在这段代码中,g
代表孩子的胃口数组,s
代表饼干的尺寸数组。函数返回的是能够满足的孩子数量。数组g
和s
都先进行了排序,然后用两个指针child_i
和cookie_j
分别遍历孩子和饼干。如果当前饼干能满足当前孩子,就将饼干给孩子,然后考虑下一个孩子和下一个饼干;否则放弃当前饼干,考虑下一个饼干。这样一直进行,直到没有孩子或者饼干为止。最后返回满足的孩子数量child_i
,即为结果。
376. 摆动序列 - 力扣(LeetCode)
贪心算法
在这个问题中,贪心算法的策略是我们始终尽可能地使序列保持摆动。换句话说,我们总是优先考虑改变趋势,即如果当前是上升的,我们就寻找下一个下降的点,反之亦然。
这个问题实际上是找出数组中的所有"转折点"。一个"转折点"是指该点两侧的差值与该点和其前一点的差值异号。也就是说,如果该点比前一点大,那么它应该比后一点小;反之亦然。
这种策略可以通过一次遍历完成,时间复杂度是O(n)。
Python实现代码如下:
python
def wiggleMaxLength(nums):
n = len(nums)
if n < 2:
return n
prevdiff = nums[1] - nums[0]
count = 2 if prevdiff != 0 else 1
for i in range(2, n):
diff = nums[i] - nums[i - 1]
if (diff > 0 and prevdiff <= 0) or (diff < 0 and prevdiff >= 0):
count += 1
prevdiff = diff
return count
在这段代码中,nums
代表给定的整数数组。函数返回的是最长摆动子序列的长度。首先计算前两个数字的差值prevdiff
,然后从第三个数字开始,如果当前数字与前一个数字的差值diff
和prevdiff
异号,说明当前数字是一个转折点,摆动序列长度增加1,然后更新prevdiff
为当前的差值。最后返回摆动序列长度count
,即为结果。
这个算法的时间复杂度是O(n),空间复杂度是O(1),满足题目的要求。
动态规划
这是一道典型的动态规划问题,我们需要找到数组中的最长摆动子序列的长度。
我们可以维护两个动态规划数组 up
和 down
,其中 up[i]
表示在到达数组的第 i
个位置时,最后一次摆动是上升的最长子序列长度,down[i]
表示在到达数组的第 i
个位置时,最后一次摆动是下降的最长子序列长度。
状态转移方程为:
- 如果
nums[i] > nums[i - 1]
,那么我们可以在以i - 1
结尾的下降子序列后面接一个nums[i]
,形成一个更长的摆动序列,所以有up[i] = down[i - 1] + 1
,down[i] = down[i - 1]
; - 如果
nums[i] < nums[i - 1]
,那么我们可以在以i - 1
结尾的上升子序列后面接一个nums[i]
,形成一个更长的摆动序列,所以有down[i] = up[i - 1] + 1
,up[i] = up[i - 1]
; - 如果
nums[i] == nums[i - 1]
,那么我们无法在子序列后面接一个nums[i]
形成一个更长的摆动序列,所以有up[i] = up[i - 1]
,down[i] = down[i - 1]
。
最后的答案就是 up[n - 1]
和 down[n - 1]
的最大值。
Python 代码如下:
python
def wiggleMaxLength(nums):
n = len(nums)
if n < 2:
return n
up = [0] * n
down = [0] * n
up[0] = down[0] = 1
for i in range(1, n):
if nums[i] > nums[i - 1]:
up[i] = max(up[i - 1], down[i - 1] + 1)
down[i] = down[i - 1]
elif nums[i] < nums[i - 1]:
up[i] = up[i - 1]
down[i] = max(up[i - 1] + 1, down[i - 1])
else:
up[i] = up[i - 1]
down[i] = down[i - 1]
return max(up[-1], down[-1])
这段代码中,nums
代表给定的整数数组。函数返回的是最长摆动子序列的长度。数组 up
和 down
存储了以每个位置结尾的最长上升和下降摆动子序列的长度。然后通过状态转移方程更新 up
和 down
,最后返回 up
和 down
的最大值,即为最长摆动子序列的长度。
这个算法的时间复杂度是 O(n),空间复杂度是 O(n),满足题目的要求。
53. 最大子数组和 - 力扣(LeetCode)
贪心算法
贪心算法的思路是:遍历数组,并在每个步骤中维护当前的子数组和以及到目前为止找到的最大子数组和。如果当前子数组和变为负数,则在下一个元素处开始新的子数组。
Python实现代码如下:
python
def maxSubArray(nums):
current_sum = max_sum = nums[0]
for num in nums[1:]:
current_sum = max(num, current_sum + num)
max_sum = max(max_sum, current_sum)
return max_sum
动态规划
动态规划的思路稍微复杂一些。设dp[i]表示以第i个元素结尾的最大子数组和,那么dp[i]可以由dp[i-1]和nums[i]决定。如果dp[i-1]大于0,dp[i]就等于dp[i-1]加上nums[i],否则等于nums[i]。我们要求的结果就是dp数组中的最大值。
Python实现代码如下:
python
def maxSubArray(nums):
dp = [0] * len(nums)
dp[0] = nums[0]
for i in range(1, len(nums)):
dp[i] = max(dp[i-1] + nums[i], nums[i])
return max(dp)
两种算法的时间复杂度都是O(n),空间复杂度上,贪心算法是O(1),动态规划是O(n),如果将动态规划的解法优化一下,可以降低到O(1),因为dp[i]只依赖于dp[i-1]。
优化后的动态规划解法:
python
def maxSubArray(nums):
max_sum = curr_sum = nums[0]
for num in nums[1:]:
curr_sum = max(curr_sum + num, num)
max_sum = max(max_sum, curr_sum)
return max_sum
这个解法与贪心算法看起来非常相似,但它们的思考方式是不同的。贪心算法是在每一步都采取局部最优解,而动态规划则是在每一步都根据前一步的结果做出决策。
总结
贪心算法是在每一步都采取局部最优解,而动态规划则是在每一步都根据前一步的结果做出决策。