贪心算法理论基础:
① 贪心的本质是选择每一阶段的局部最优,从而达到全局最优。
② 一般能够手动模拟感觉到局部最优可以推出全局最优,没有找到反例,就可以用贪心算法。
③ 比如,一堆东西中可以选择4个东西,每次选择当时这堆东西中最贵的,那么最后得到的就是最贵的。
④ 而背包问题:背包容积固定,一堆东西向背包中装,求最后得到的背包所装东西的价值最大,那么不应该采用贪心,而是动态规划
LeetCode 455.分发饼干:
思路:
贪心算法,要么从大饼干开始,大饼干先满足胃口大的孩子;要么从小饼干开始,小饼干先满足胃口小的孩子
"""
大饼干先满足胃口大的孩子
先遍历胃口,再遍历饼干
如果g[i] > s[-1],也就是g[i] > max(s),所以胃口g[i]一定满足不了
因此只需要if判断即可
实际上是对于每个饼干,能不能找到一个合适的胃口与之对应
"""
class Solution:
def findContentChildren(self, g: List[int], s: List[int]) -> int:
if s == []:
return 0
g.sort(reverse=True)
s.sort()
count = 0
for i in range(len(g)):
if s!=[] and g[i] <= s[-1]:
count += 1
s.pop()
else:
continue
return count
"""
大饼干先满足大胃口的孩子
先遍历饼干,再遍历胃口。
因为先遍历的饼干,如果sj < g[-1],但是可能在g[:-1]中找到饼干sj对应的胃口
所以需要嵌套for循环找到饼干对应的胃口
"""
class Solution:
def findContentChildren(self, g: List[int], s: List[int]) -> int:
if s == []:
return 0
g.sort()
s.sort(reverse=True)
count = 0
for sj in s:
while g != [] and sj < g[-1]:
g.pop()
if g !=[]:
count += 1
g.pop()
return count
"""
小饼干先满足小胃口。
先遍历饼干,再遍历胃口
如果s[i] < g[index],表明这个饼干确实没有对应满足的胃口
所以可以直接使用if判断
"""
class Solution:
def findContentChildren(self, g: List[int], s: List[int]) -> int:
if s == []:
return 0
g.sort()
s.sort()
index = 0
for sj in s: # 遍历饼干
if index < len(g) and g[index] <= sj:
index += 1
return index
感悟:
大饼干先满足大胃口与小饼干先满足小胃口,其中的遍历顺序:
大饼干对应的遍历顺序是先胃口后饼干(因为如果大胃口比大饼干要大,那么该大胃口一定不会满足)
小饼干对应的遍历顺序为先饼干后胃口(因为如果小饼干比小胃口要小,那么该小饼干找不到对应满足的胃口)
LeetCode 376.摆动序列:
思路:
-
贪心
可以先把原数组的摆动情况画出来,以[1, 17, 5, 10, 13, 15, 10, 5, 16, 8]为例
从上图可以看出,原数组中的元素,有些为波峰和波谷,有些不是波峰波谷。
题目要求从原数组中找到作为摆动序列的最长子序列,而摆动序列画出来的图中的点都是波峰或者波谷,所以我们可以只去掉原序列中非波峰波谷的点,从而得到摆动序列。
再进一步,只要求最长子序列长度,那么我们可以实际上可以只记录波峰波谷的数量。-
怎么判断为波峰波谷:
使用preval和curval,分别记录nums[i] - nums[i - 1]和nums[i + 1] - nums[i]的值 ,只要preval > 0 && curval < 0 或preval < 0 && curval > 0 ,那么就表明为波谷或波峰
-
题目中没有说数组元素不能重复,因此会有重复的元素,也就是平坡出现。根据平坡右边和左边的上下情况,有如下几种情形
-
左右相反。左边上右边下(即平坡是最高的),左边下右边上(即平坡是最低的)。以第一种为例
上图中,我们既可以去掉最右边两个10,也可以去掉最左边两个10,我们采用去掉最左边两个10的方式,那么最后一个10也是一个波峰,此处pre = = 0 && cur < 0 \\\\(2)。
同理可得,左边下右边上同样保留最右边的情况为pre == 0 && cur > 0 \\\\(3) -
左右相同,即都为上或都为下,此时平坡为整个上坡或下坡的一部分。
在上图中,平坡中的元素都不应该作为波峰或波谷存在。那么可以看出最后一个10的preval和curval应当为preval > 0 and curval > 0才能不将10作为波峰和波谷,之前的最后一个10的preval和curval为,preval = 0 and curval > 0。那么办法是,只在出现摆动时更新preval
-
-
数组最左/右边的元素
前面使用preval和curval都是当前元素的左右都存在元素时进行判断,那么我们还需要对数组的边界元素进行判断。
怎么对边界元素进行判断呢:- 最右边的元素:
如果左边为平坡过来的,那么这个实际上是按照平坡的左右不等来计算的,因此最右边应当作为波峰/波谷
如果左边不是平坡,那么最右边元素也是波峰/波谷。
因此不管怎么样,最右边元素都会作为最长摆动子序列的一个部分。
- 最左边元素:
如果右边为平坡,那么最左边元素不作为波峰/波谷。
如果右边不为平坡,那么最左边元素为波峰/波谷。
那么这个判断与正常具有左右元素的判断相同,因此我们可以采用在最左边元素前添加一个相同的元素作为nums[i - 1],从而判断最左边元素与正常元素相同
- 最右边的元素:
-
class Solution:
def wiggleMaxLength(self, nums: List[int]) -> int:
lenNums = len(nums)
if lenNums <= 1:
return lenNums
preval = 0 # nums[i]与nums[i-1]之间的差值,对于第一个元素,那么就是在前面加上了一个相同的元素,从而preval=0
curval = 0 # nums[i]与nums[i+1]之间的差值
result = 1 # 默认数组最右边是一个波峰
for i in range(lenNums - 1):
curval = nums[i + 1] - nums[i]
if (preval <= 0 and curval > 0) or (preval >= 0 and curval <0):
result += 1
preval = curval # 摆动时才更新preval
return result
- 动态规划:
可以将nums[i]作为波峰或波谷,接到前面的最长摆动序列中
将nums[i]作为波峰接到前面的摆动序列中,摆动序列前一个元素应当为波谷
dp[i][0] = max(dp[i][0], dp[j][1] + 1)
将nums[i]作为波谷接到前面的摆动序列中,摆动序列前一个元素应当为波峰
class Solution:
def wiggleMaxLength(self, nums: List[int]) -> int:
lenNums = len(nums)
if lenNums <= 1:
return lenNums
# 对于第i行元素, 0列表示将nums[i]作为波峰的最大摆动序列长度,1列表示将nums[i]作为波谷的最大摆动序列长度
dp = [[0,0] for _ in range(lenNums)]
dp[0] = [1,1]
# 每次确定一行的元素
for i in range(1, lenNums):
dp[i][0], dp[i][1] = 1, 1 # 自己
for j in range(i):
# 波峰
if nums[j] < nums[i]:
dp[i][0] = max(dp[i][0], dp[j][1] + 1) #dp[i][0]实际上是找最大值,nums[i]作为波峰,那么前一个元素应当为波谷
# 波谷
if nums[j] > nums[i]:
dp[i][1] = max(dp[i][1], dp[j][0] + 1) # nums[i]作为波谷,前面一个元素作为波峰
return max(dp[-1][0], dp[-1][1])
时间复杂度O(n^2)
空间复杂度O(n)
感悟:
摆动序列使用贪心算法的各种情况及解决办法
LeetCode 53.最大子序和:
思路:
- 动态规划
如果采用dp[i]为nums[:i + 1]的最大连续子数组的和,那么dp[i + 1]与dp[i]衔接非常麻烦,因为要求的子数组为连续子数组。
因此采用dp[i]为nums[: i + 1]的包括nums[i]的最大连续子数组的和,从而dp[i + 1] = max(nums[i + 1], dp[i] + nums[i + 1])。最后再遍历一遍dp得到最大值即可
以[-2, 1, -3, 4, -1, 2, 1, -5, 4]数组为例,构造dp如下图所示:
"""
可以再优化一下,求dp数组最大值可以在构建dp时处理
"""
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
lenNums = len(nums)
if lenNums == 1:
return nums[0]
dp = [0] * lenNums # dp[i]为nums[:i + 1]中且包含nums[i]的最大连续子数组的和
dp[0] = nums[0]
for i in range(1, lenNums):
dp[i] = nums[i]
if nums[i] < dp[i - 1] + nums[i]:
dp[i] = dp[i - 1] + nums[i]
# 遍历dp数组,找到其中的最大值即为所求
maxIndex = 0
for i in range(1, lenNums):
if dp[i] > dp[maxIndex]:
maxIndex = i
return dp[maxIndex]
- 贪心
由上面的动态规划可以发展出贪心,使用count记录nums[:i]的包括nums[i - 1]的连续和。
若count < 0: count = nums[i] # 从nums[i]开始
否则:count += nums[i] # 增加nums[i]的值
(PS:不是nums[i] < 0就不加,而是count < 0就重新开始,因为要求的是连续子数组,count记录的是包含nums[i]的最大连续子数组,所以nums[i]一定要带上)
同时使用result记录count的最大值
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
lenNums = len(nums)
if lenNums == 1:
return nums[0]
result = nums[0] # 记录最大值
count = nums[0] # 连续和
for i in range(1, lenNums):
if count < 0: # 为负数的话,count重新开始
count = nums[i]
else:
count += nums[i] # 否则加上nums[i]
if count > result:
result = count
return result
感悟:
注意到题目中的最大连续子数组
学习收获:
贪心主要是不知道在哪里贪心。
分发饼干可以明确知道,摆动序列的贪心算法浅浅过了一遍,最大子序和先想到的用动态规划,再把动态规划转成贪心。