455. 分发饼干
自己写的AC了,主要思路与代码随想录思路类似,就是尽量把大的饼干给胃口大的孩子;代码随想录建议遍历孩子,遍历孩子确实逻辑清楚一些。
遍历饼干:
给饼干数组和孩子数组按从大到小排序,初始化孩子索引child=0,初始化满足孩子数量count=0,对于每一个饼干,查找能满足的孩子,直到查找完所有的孩子,退出循环;如果找到了可以满足的孩子,计数器加一,移动到下一个孩子准备在下一轮循环中用稍小的饼干去尝试。
class Solution:
def findContentChildren(self, g: List[int], s: List[int]) -> int:
g.sort(reverse = True)
s.sort(reverse = True)
child = 0
count = 0
for biscuit in s:
while child < len(g) and biscuit < g[child]:
child += 1
if child >= len(g): break
count += 1
child += 1
return count
遍历孩子:
相比较而言,还是遍历孩子比较直观。对于每一个孩子,如果当前没用过的最大的饼干能满足胃口,饼干移动到下一个,计数器+1。
这样的方法可以减少迭代次数,效率稍高,但总体复杂度相似。
class Solution:
def findContentChildren(self, g: List[int], s: List[int]) -> int:
g.sort(reverse = True)
s.sort(reverse = True)
biscuit = 0
count = 0
for child in g:
if biscuit <= len(s) - 1 and s[biscuit] >= child:
biscuit += 1
count += 1
return count
最优子结构:
虽然这是一道简单题,这里尝试说明(并不会严格证明)分发饼干问题具有最优子结构。
将问题抽象一下,有整数数组g=<g1,g2,g3,...,gm>,s=<s1,s2,s3,...,sn>,目标是找到最大长度的z=<(gi1, sj1), (gi2,sj2), ... , (gik,sjk)>,使得每一对gi<=sj并且每一个gi和sj只能在z中出现一次。
在一次决策中,我们为gi分配了sj,这就产生了一个子问题:在不包括gi和sj的g'和s'中,找到最长的z';通过从原问题中选择一个最优的分配(如果可能),然后求解剩下的子问题,我们可以递归地构建整个问题的最优解。每一步的最优决策都基于一个局部的最优选择,这个选择使得我们能够得到z。
在这个问题中,适合使用贪心算法是因为:在一次决策后,只剩下一个子问题需要求解;我们设计贪心选择,尽量把最大的饼干分给胃口最大的孩子,这样去除最大的饼干和最大的孩子(可能是孩子们)之后,又只剩下和原问题相同只是规模不同的子问题,且每一步的贪心选择都是在确保当前步骤下最多的孩子被满足,这样的局部最优选择累积起来,就形成了全局最优解。
376. 摆动序列
这题自己的思路感觉要比代码随想录的思路直观。详细记录一下:
考虑异号差值:
首先明确一点,一个序的最大摆动子序列一定可以包含序列的末尾元素。这可以通过归纳法证明。
res
用于记录摆动序列的最大长度,初始值为 1,因为至少有一个元素时,它自身就构成一个摆动序列。diff
用于记录前一对元素之间的差值,初始值为 None
,表示还未设置。
循环从第二个元素开始,一般而言,需要考虑当前差值与上一差值的符号关系,但是序列有可能一直是相等的,这里通过初始化diff为None来解决特例问题;如果前一差值存在,也就是已有摆动,且前i个元素构成的序列的最大摆动子序列一定会包含末尾元素;按照摆动定义,只有严格的异号该位置才能多出一次摆动。
class Solution:
def wiggleMaxLength(self, nums: List[int]) -> int:
res = 1
diff = None
for i in range(1, len(nums)):
cur_diff = nums[i] - nums[i - 1]
if diff is None:
if cur_diff == 0:
continue
else:
res += 1
diff = cur_diff
else:
if ((diff < 0 and cur_diff > 0) or (diff > 0 and cur_diff < 0)):
res += 1
diff = cur_diff
return res
代码随想录中考虑当前位置删还是不删感觉会有点绕了,最后写出代码跟我自己的异曲同工了吧,也就跳一下。
最优子结构:
摆动序列问题是具有最优子结构的,这也很直观,对于以元素i结尾的序列,我们需要对i+1做决策,添加的末尾元素是否可以增加摆动次数。这就是规模不同但是独立的子问题。
对于摆动序列的问题,由于我们可以只考虑增量是否异号,即可以通过差值的符号差异简单地做出贪心选择,每迭代一次都尽量增加摆动次数,最终一定可以得到最大摆动次数的子序列。
动态规划:
本题是可以通过一次贪心选择使得只剩一个子问题的,因此适用贪心算法。但是本题也有动态规划的解决方案:
对于一个以元素i结尾的序列,当考察第i+1个元素时,他可能可以接在以元素i结尾的最大摆动子序列的后面,形成一个len+1的最大摆动子序列,也可能接不进最大子序列。这时就要定义两个dp数组了,因为摆动有两种可能,子序列末尾可能时上升的,也可能时下降的,第i+1个元素接进子序列的时候,如果接进末尾上升的,就形成了末尾下降的子序列。
在填充dp数组的时候,每次加入一个元素,要遍历从0到i的序列中所有的以j结尾的子序列,在num[i]接进这个子序列后长度发生变化
def wiggleMaxLength(nums):
if len(nums) < 2:
return len(nums)
up = [1] * len(nums)
down = [1] * len(nums)
for i in range(1, len(nums)):
for j in range(i):
if nums[i] > nums[j]:
up[i] = max(up[i], down[j] + 1)
elif nums[i] < nums[j]:
down[i] = max(down[i], up[j] + 1)
return max(max(up), max(down))
53. 最大子数组和
这题春节放假期间做了,是算法导论分治法的一道例题。
贪心算法:
就是Kadane算法,贪心选择为如果本轮发现连续子数组和为负数,这一段数组对求最值是负收益的,最终结果不可能包含这个数,直接舍去,从下一元素开始重开连续子数组;如果本轮为正,则继续添加下一元素希望可以得到更大的(用全局最值来与这个值作比较)。
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
cur_sum = max_sum = nums[0]
for num in nums[1:]:
if cur_sum < 0:
cur_sum = num
else:
cur_sum += num
max_sum = max(max_sum, cur_sum)
return max_sum
动态规划:
迭代写法:
因为是连续子数组的和,如果z = [z1,z2,...,zk]是nums=[n1,n2,...,ni]的最大连续子数组,对于nums = [n1,n2,...,ni,n(i + 1)],最大连续子数组要么是z,要么是一个以n(i + 1)结尾的子数组。
按照这个思路,
定义dp数组dp[i]表示以i结尾的最大连续子数组的和。
递推式可以是dp[i + 1] = nums[i + 1] if dp[i] < 0 else dp[i] + nums[i + 1]
最终填充完的dp数组要取max(dp)
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
dp = [0] * len(nums)
dp[0] = nums[0]
for i in range(1,len(nums)):
dp[i] = nums[i] if dp[i - 1] < 0 else dp[i - 1] + nums[i]
return max(dp)
带备忘的递归写法:
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
res = float('-inf')
memo = [None] * len(nums)
def helper(i):
if i == 0:
return nums[0]
if memo[i] is not None:
return memo[i]
last = helper(i - 1)
memo[i] = max(last + nums[i], nums[i])
return memo[i]
for j in range(len(nums)):
res = max(res, helper(j))
return res
主要复习理解一下,带备忘的递归写法在性能上不是最优的。
分治法:
书上主要就是介绍分治法,
一个和最大的连续子数组一共就三种情况:
1、完全在左半边
2、完全在右半边
3、跨越中点
实际上情况1和2是可以递归实现的,就是一个规模为一半的子问题。而跨越中点的最大连续子数组是可以直接求解的,因为多了限定条件。
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
# 辅助函数,用于找出跨越中点的最大子数组和
def findMaxCrossingSubarray(nums, left, mid, right):
# 左半边的最大子数组和
left_sum = float('-inf')
sum = 0
for i in range(mid, left-1, -1):
sum += nums[i]
if sum > left_sum:
left_sum = sum
# 右半边的最大子数组和
right_sum = float('-inf')
sum = 0
for i in range(mid + 1, right + 1):
sum += nums[i]
if sum > right_sum:
right_sum = sum
# 返回跨越中点的最大子数组和
return left_sum + right_sum
# 主递归函数
def maxSubArrayRec(nums, left, right):
# 基本情况
if left == right:
return nums[left]
mid = (left + right) // 2
# 分别找到左半边、右半边和跨越两边的最大子数组和
left_sum = maxSubArrayRec(nums, left, mid)
right_sum = maxSubArrayRec(nums, mid + 1, right)
cross_sum = findMaxCrossingSubarray(nums, left, mid, right)
# 返回这三个中的最大值
return max(left_sum, right_sum, cross_sum)
return maxSubArrayRec(nums, 0, len(nums) - 1)
今日总结:
题已经刷过了,今天通过理解问题的最优子结构性质以及尝试通过动态规划和贪心写法来学习两者的不同之处,加深了理解。
今天另外又学了个词叫无后效性,之后遇到继续学。