贪心算法是遵循在每个阶段做出局部最优选择从而解决启发式(近似最优解)问题的任何算法。
因为贪心策略在很多情况下不会产生最优解,可能大部分是近似最优解,也有小部分可能是最糟糕的结果。但对某些特殊问题,采用贪心可以取到最好的效果,即可以从局部最优可以推导到全局最优。
我们分别来看看两个例子:
例子1 (局部最优到全局最优):
某货币有1, 5, 10 ,20的面值大小,现在要找给客户36面值大小的货币,要求找给的货币数量最少。
这里使用贪心算法,如下图,从最大的币值20开始 (因为20可以代替2张10,4张5或20张1),不断地找回20,直到不够该币值时,往小的币值10,5,1依次遍历。
但局部最优一定能推导到全局最优吗?看下一个例子:
例子2 (局部最优不能到全局最优):
为了达到最大和,在每一步,贪心算法都会选择看起来是最优的直接选择,所以它会在第二步选择 12 而不是 3,并且不会达到包含 99 的最佳解决方案。
我们可以做出目前看起来最好的任何选择,即局部最优选择,然后解决以后出现的子问题。贪心算法做出的选择可能取决于到目前为止所做的选择,但不取决于未来的选择或子问题的所有解决方案。它迭代地做出一个又一个贪婪的选择,将每个给定的问题减少为一个较小的问题。
换句话说,贪心算法永远不会重新考虑它的选择,它是不可更改的,即我们无法在执行过程中的任何后续点更改决定。这是与动态规划的主要区别,动态规划是详尽的并且保证能找到解决方案。在每个阶段之后,动态规划都会根据前一阶段做出的所有决策做出下一步决策,并且可能会重新考虑前一阶段解决方案的算法路径。
贪心算法是独立每步最大,而动态规划是每一步之间状态关联最大,是个重叠子问题,这会在下一篇文章中讲解。
当遇到一道题,如何判别是否适合使用贪心算法?如何看出能从局部最优推导到全局最优?
贪心无套路,也没有框架之类的,需要多看多练培养感觉才能想到贪心的思路。
一般而言,就靠自己去手动模拟,即单纯的过程模拟,如果模拟可行,就试一试贪心策略,如果找出局部最优并可以推出全局最优,就是贪心,如果局部最优都没找出来,就不是贪心,可能是单纯的模拟;如果连模拟都不行,可能需要动态规划。
刷题或者面试的时候,手动模拟一下感觉可以局部最优推出整体最优,而且想不到反例,那么就试一试贪心。
如果使用贪心算法,对于推导时,举例子得出的结论靠谱与否不确定,想要严格的数学证明。一般数学证明有如下两种方法:1. 数学归纳法,2. 反证法
但面试中基本不会让面试者现场证明贪心的合理性,代码写出来跑过测试用例即可,或者自己能自圆其说理由就行了。并且,贪心有时候就是常识性的推导,所以自然而然会认为本应该就这么做。
贪心算法一般分为如下四步:
这是一个细分,真正做题的时候很难分出这么详细的解题步骤,可能就是因为贪心的题目往往还和其他方面的知识混在一起。
力扣题目链接
这里的局部最优就是大饼干喂给胃口大的,充分利用饼干尺寸喂饱一个,全局最优就是喂饱尽可能多的小孩。
贪心算法1(以小孩的胃口为主体):
先满足胃口大的孩子,需要排序后逆序
class Solution:
def findContentChildren(self, g: List[int], s: List[int]) -> int:
# 两个指针分别遍历两个list
i, j = 0, 0
# 存储结果
count = 0
# 排序,从胃口大的孩子开始满足
g.sort(reverse=True)
s.sort(reverse=True)
# 从大遍历孩子和饼干
# 孩子胃口过大,则换下一个胃口小的孩子
while i<len(g) and j<len(s):
# 匹配成功,都移到下一个匹配项
if g[i]<=s[j]:
count+=1
i+=1
j+=1
# 匹配不成功,原因只有孩子胃口过大,因为饼干大于或等于胃口都满足匹配
# 那么换下一个孩子
else:
i+=1
return count
贪心算法2(以饼干为主体):
排序后,饼干从小的开始分发,不满足则往后换大的
class Solution:
def findContentChildren(self, g: List[int], s: List[int]) -> int:
# 两个指针分别遍历两个list
i, j = 0, 0
# 存储结果
count = 0
# 排序,从小饼干开始分发
g.sort()
s.sort()
# 从小遍历孩子和饼干
# 饼干过小,换一个大一些的饼干
while i<len(g) and j<len(s):
# 匹配成功,都移到下一个匹配项
if g[i]<=s[j]:
count+=1
i+=1
j+=1
# 匹配不成功,原因只有饼干太小
# 那么换下一个饼干
else:
j+=1
return count
力扣题目链接
贪心的思路,局部最优:让绝对值大的负数变为正数,当前数值达到最大,整体最优:整个数组和达到最大。局部最优可以推出全局最优。
那么如果将负数都转变为正数了,K依然大于0,此时的问题是一个有序正整数序列,如何转变K次正负,让 数组和 达到最大。
那么又是一个贪心:局部最优:只找数值最小的正整数进行反转,当前数值可以达到最大(例如正整数数组{5, 3, 1},反转1 得到-1 比 反转5得到的-5 大多了),全局最优:整个 数组和 达到最大。
这里注意有两次贪婪算法。
贪心算法1(一次排序,绝对值排序):
解题步骤为:
第一步:将数组按照绝对值大小从大到小排序,注意要按照绝对值的大小
第二步:从前向后遍历,遇到负数将其变为正数,同时K–
第三步:如果K还大于0,那么反复转变数值最小的元素,将K用完
第四步:求和
class Solution:
def largestSumAfterKNegations(self, A: List[int], K: int) -> int:
A = sorted(A, key=abs, reverse=True) # 将A按绝对值从大到小排列
for i in range(len(A)):
if K > 0 and A[i] < 0:
A[i] *= -1
K -= 1
if K > 0:
A[-1] *= (-1)**K #取A最后一个数只需要写-1
return sum(A)
贪心算法2(两次排序,直接排序):
class Solution:
def largestSumAfterKNegations(self, nums: List[int], k: int) -> int:
nums.sort()
count=0
# 第一次贪婪,将负数变为正数,遍历列表一次
while count<k and count<len(nums):
# 负的变为正的
if nums[count]<0:
nums[count] = -nums[count]
# 改变的次数计数
count+=1
# 排序后,遇到正的,退出,整个列表都为正的
else:
break
# 第二次贪婪,偶数改变次数,则不变
# 奇数改变次数,只变化最小值
if (k-count)%2!=0:
nums.sort()
nums[0] = -nums[0]
return sum(nums)
力扣题目链接
贪心算法:
仔细分析可知,这题只需要维护三种金额的数量,5,10和20就行,并且这题的判断条件非常少,有如下三种情况:
情况一:账单是5,直接收下。
情况二:账单是10,消耗一个5,增加一个10
情况三:账单是20,优先消耗一个10和一个5,如果不够,再消耗三个5
情况一,情况二,都是固定策略,不用具体分析了,而唯一不确定的就是情况三。情况三这里使用了贪心算法,优先使用大的币值10,因为5更加万能,如果对于交付20过多使用5找零,那么对于10的找零5就会相应的减少,同时存了很多10而用不掉。
class Solution:
def lemonadeChange(self, bills: List[int]) -> bool:
res = [0,0,0]
for i in bills:
# 情况一
if i == 5:
# 存钱
res[0]+=1
# 情况二
elif i ==10:
# 存钱
res[1]+=1
# 判断钱是否够找零
if res[0]<0:
return False
# 找钱
res[0]-=1
# 情况三
else:
res[2]+=1
# 先找大零钱,判断1
if res[1]>=1 and res[0]>=1:
res[0]-=1
res[1]-=1
# 大零钱不够,用小零钱,判断2
elif res[0]>=3:
res[0]-=3
# 都不够,无法找零
else:
return False
return True
注意: 这里其实可以去掉20的存取,因为不会用到。
力扣题目链接
改题使用贪心算法,相邻元素相减,一旦符号与前一个不同,则为摆动子序列
贪心算法:
class Solution:
def wiggleMaxLength(self, nums: List[int]) -> int:
# 单个元素也是摇摆序列,提前+1
res= 1
# 定义符号
pre_sign = 0
# 这里从1开始,每次跟前面的元素比较
for i in range(1, len(nums)):
# 这里符号0直接被考虑进去,相当于起始点,即一个元素
# [0,0,2,1]第二个零当做起始点
# 正向摆动,但前一个摆动符号不为正
if nums[i]-nums[i-1]>0 and pre_sign<=0:
pre_sign = 1
res+=1
# 负向摆动,但前一个摆动符号不为负
elif nums[i]-nums[i-1]<0 and pre_sign>=0:
pre_sign=-1
res+=1
return res
这道题,如果用贪心算法,有些找规律的意味在里面。
我们可以从上图看到,理论上,原本应该是1-17-5-15-5-16-8,即取低谷与峰值为判断条件才是合理的,然而对于像5-10-13-15-10-5这个从低谷到峰值中间都有值,从贪心的角度,我们直接判断,取第二个10当做峰值,同样达到效果,编程也更加方便。
动态规划:
子序列相关题目理论上可以使用动态规划算法,但是理解有些复杂,并且算法复杂度较高。目前先不做展开,后续补充。
力扣题目链接
贪心算法:
class Solution:
def monotoneIncreasingDigits(self, n: int) -> int:
tmp = list(str(n))
for i in range(len(tmp)-1,0,-1):
if int(tmp[i])<int(tmp[i-1]):
# 前一个值退一位
tmp[i-1] = str(int(tmp[i-1])-1)
# 后面的值全部变成9
tmp[i:] = '9'*(len(tmp)-i)
return int(''.join(tmp))
时间复杂度: O ( n ) O(n) O(n),n 为数字长度
空间复杂度: O ( n ) O(n) O(n)
力扣题目链接
普通解法:
不去深入理解题目含义,照着题目步骤做
class Solution:
def maxProfit(self, prices: List[int]) -> int:
# 最终值
res = 0
# 记录涨的股票的起始和终止位置
start, end = 0,0
# 遍历完成退出
while end<len(prices):
# 遍历到结尾,或者下一个为跌则赶紧提前卖出,求和
if end == len(prices)-1 or prices[end]>prices[end+1]:
# start==end时未买入,这里求买入区间的和
if start!=end:
res += prices[end]-prices[start]
# 买区间求完,换下一股
end+=1
start = end
# 当涨的时候,继续等待,直到跌
else:
end += 1
return res
贪心算法:
贪心算法又开始找规律,明白利润是可以分解的,就相对简单一些。如何理解?
假如第0天买入,第3天卖出,那么利润为:prices[3] - prices[0]。
相当于(prices[3] - prices[2]) + (prices[2] - prices[1]) + (prices[1] - prices[0])。
此时就是把利润分解为每天为单位的维度,而不是从0天到第3天整体去考虑!
那么根据prices可以得到每天的利润序列:(prices[i] - prices[i - 1])…(prices[1] - prices[0])。
这里注意,第一天是没有利润的,利润是两天相比较而产生的的。
从图中可知,只需收集每天的正利润就可以,收集正利润的区间,就是股票买卖的区间,而我们只需要关注最终利润,不需要记录区间。那么只收集正利润就是贪心算法所贪的地方。
局部最优:收集每天的正利润,全局最优:求得最大利润。
既然如此,写起来就非常简单了,
class Solution:
def maxProfit(self, prices: List[int]) -> int:
# 最终值
res = 0
for i in range(1, len(prices)):
# 利润值为正的累加起来
if prices[i]>prices[i-1]:
res+=prices[i]-prices[i-1]
return res
时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( 1 ) O(1) O(1)
动态规划:
class Solution:
def maxProfit(self, prices: List[int]) -> int:
# 创建dp table
# 含义第N天的最大利润
dp = [0] * (len(prices)+1)
# 初始化,第0天和第一天没有利润,为0,当然这里多余
dp[0] = dp[1] = 0
# 从第二天开始产生正负利润
for i in range(2, len(prices)+1):
# 当前第N天的利润等于,max(之前的利润,今天的利润+之前的利润)
# 这里取最大值,若今天产生负利润,则不买,只取之前的利润
dp[i] = max(dp[i-1], prices[i-1]-prices[i-2]+dp[i-1])
return dp[-1]
"""
结果样例:
1 2 3 4 5 6 7 (days)
[ 7,1,5,3,6,4]
[0,0,0,4,4,7,7]
"""
时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( n ) O(n) O(n)
力扣题目链接
贪心算法:
此题无非就是要找到两个点,买入日期,和卖出日期。
买入日期:其实很好想,遇到更低点就记录一下。
卖出日期:这个就不好算了,但也没有必要算出准确的卖出日期,只要当前价格大于(最低价格+手续费),就可以收获利润,至于准确的卖出日期,就是连续收获利润区间里的最后一天(并不需要计算是具体哪一天)。
有三种情况:
情况一:收获利润的这一天并不是收获利润区间里的最后一天(不是真正的卖出,相当于持有股票),所以后面要继续收获利润。
情况二:前一天是收获利润区间里的最后一天(相当于真正的卖出了),今天要重新记录最小价格了。
情况三:不作操作,保持原有状态(买入,卖出,不买不卖)
class Solution:
def maxProfit(self, prices: List[int], fee: int) -> int:
res = 0
minPrice = prices[0]
for i in range(1, len(prices)):
# 情况二:相当于买入
if prices[i]<minPrice:
minPrice = prices[i]
# 情况三:保持原有状态(因为此时买则不便宜,卖则亏本)
elif prices[i]>=minPrice and prices[i]<=minPrice+fee:
continue
else:
# 计算利润,可能有多次计算利润,最后一次计算利润才是真正意义的卖出
res += prices[i]-minPrice-fee
# 让minPrice = prices[i] - fee
# 这样在明天收获利润的时候,不会多减一次手续费
# 通过降低minPrice,使得隐式的加上了fee,如果连续获取利润的话
minPrice = prices[i]-fee
return res
时间复杂度:O(n)
空间复杂度:O(1)
动态规划:
可见上一篇动态规划系列文章
力扣题目链接
这题的关键是,遇到连续的重复得分,应该如何去赋予糖果呢?这里看几个例子,试着自己手推,找找规律,注意,相邻元素相同时,糖果数是可以任意的。
因为,每个小孩至少一个糖果,则初始化,每个小孩糖果数量都为1
# 1
# 得分
1, 2, 7, 3, 2
# 糖果
1, 2, 3, 2, 1
每个元素与前面相比,大就+1,小就-1
# 2
# 得分
1, 2, 7, 7, 7, 3, 2
# 糖果
1, 2, 3, 1, 3, 2, 1
中间重复的只需要发1个糖果,也就是不变就行,初始化就是1
# 3
# 得分
1, 2, 7, 7, 7, 3
# 糖果
1, 2, 3, 1, 2, 1
注意,重复的7后面少了一个元素,7分发糖果从3变为2
# 得分
1, 2, 7, 7, 7, 4, 3, 2
# 糖果
1, 2, 3, 1, 4, 3, 2, 1
这里,可以总结,7后面有多少个不同元素,值就为多少
如果编程的话,需要统计7之后的元素集合的个数,赋予给7,复杂度明显很高,不是个很好的选择
那么,可以这样想,最后一个7之后的所有元素,倒着[2,3,4,7]进行上面的操作
或者从list右侧开始与后一个元素比较,大则+1,小则-1,相等则不变
从上面的规律可以看出,这是个两个维度的问题,需要两次贪心算法,正向和反向。这里举个例子:
得分:
[1,2,7,7,7,3]
正向糖果:从第二个小孩开始,比左边大,则+1,其他情况不做任何处理
(因为相当于只做递增部分,后续反向会处理递减部分)
[1,2,3,1,1,1]
反向糖果:要么把得分list和糖果list都翻转进行处理,这样很麻烦。
这里可以倒着,从倒数第二个开始,和右边的比,大则+1,其余情况不不做任何和处理
[1,2,3,1,2,1]
可以看到,两个方向互不干扰,只会处理自己的那一条序列。
结果也是正确的,试试其他例子,结果也同样准确,这就是编程思路。
贪心算法:
class Solution:
def candy(self, ratings: List[int]) -> int:
# 初始化,每个小孩一个糖果
tmp = [1]*len(ratings)
# 正向分发糖果,与左边小孩比较,大则+1,否则无操作
for i in range(1, len(ratings)):
if ratings[i]>ratings[i-1]:
tmp[i]=tmp[i-1]+1
# 反向分发糖果,反向与右侧孩子比较,大则取,自身与+1的最大值,否则无操作
# 取最大值,因为同一个孩子,多的糖果可以兼容少的糖果,相反则不成立
for i in range(len(ratings)-2,-1,-1):
if ratings[i]>ratings[i+1]:
tmp[i]=max(tmp[i], tmp[i+1]+1)
return sum(tmp)
正向:
反向:
这题如果在考虑局部的时候想两边兼顾,就会顾此失彼。
力扣题目链接
贪心算法:
局部最优:优先按身高高的people的k来插入。插入操作过后的people满足队列属性
全局最优:最后都做完插入操作,整个队列满足题目队列属性
class Solution:
def reconstructQueue(self, people: List[List[int]]) -> List[List[int]]:
# 先按照h维度的身高顺序从高到低排序。确定第一个维度
# lambda返回的是一个元组:当-x[0](维度h)相同时,再根据x[1](维度k)从小到大排序
people.sort(key=lambda x:(-x[0],x[1]))
que = []
# 根据每个元素的第二个维度k,贪心算法,进行插入
# people已经排序过了:同一高度时k值小的排前面。
for p in people:
que.insert(p[1], p)
return que
力扣题目链接
贪心算法:
class Solution:
def canJump(self, nums: List[int]) -> bool:
cover = 0
# 长度为1则直接返回True
if len(nums)==1:
return True
# 从0开始遍历,保证i在cover的范围内
i = 0
while i<=cover:
# 不断更新范围,只要有只值就往前跳,扩大cover
# 如果cover不再扩大,i会逐渐逼近cover临界值
cover = max(i+nums[i], cover)
# 范围扩张到列表最后一个值的位置,或者大于它
if cover>=len(nums)-1:
return True
i+=1
return False
对于这道题,最开始的想法是,假设对于3,每次到底跳几步?一步?两步?三步?每一步貌似影响的下一步也不同,将每一步分开,遍历起来会很复杂。
那么,将思路从近拉远,如果跳几步是无所谓的,那么关键是可跳的范围,将范围内的步数打包到一起看,能使范围扩大的是有效的跳跃。
那么这个问题就转化为跳跃覆盖范围究竟可不可以覆盖到终点!
每次移动取最大跳跃步数(得到最大的覆盖范围),每移动一个单位,就更新最大覆盖范围。
贪心算法局部最优解:每次取最大跳跃步数(取最大覆盖范围),整体最优解:最后得到整体最大覆盖范围,看是否能到终点。
力扣题目链接
贪心算法:
class Solution:
def jump(self, nums: List[int]) -> int:
if len(nums) == 1: return 0
ans = 0
curDistance = 0
nextDistance = 0
for i in range(len(nums)):
nextDistance = max(i + nums[i], nextDistance)
if i == curDistance:
if curDistance != len(nums) - 1:
ans += 1
curDistance = nextDistance
if nextDistance >= len(nums) - 1: break
return ans
力扣题目链接
贪心算法:
class Solution:
def findMinArrowShots(self, points: List[List[int]]) -> int:
# points.sort(key=lambda x:x[1]) 两个都行,
# 这时points[i][1] = min(points[i][1], points[i-1][1])可变更
points.sort(key=lambda x:x[0])
result = 1
for i in range(1, len(points)):
# 本气球的左边界若大于上一个气球的右边界
# 气球i和气球i-1不挨着,注意这里不是>=
if points[i][0]>points[i-1][1]:
result+=1
else:
# 更新重叠气球最小右边界
points[i][1] = min(points[i][1], points[i-1][1])
return result
时间复杂度:O(nlog n)
空间复杂度:O(1)
注意: 满足 xstart ≤ x ≤ xend,则该气球会被引爆。那么说明两个气球挨在一起不重叠也可以一起射爆,所以代码中 if points[i][0] > points[i - 1][1] 不能是>=
力扣题目链接
贪心算法:
class Solution:
def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
# 排序,比较右边界,这里右边界排序
intervals.sort(key=lambda x:x[1])
remove = 0
for i in range(1, len(intervals)):
if intervals[i][0]<intervals[i-1][1]:
# 重叠,则删除+1
remove+=1
# 因为相邻相比较,那么,被删除的元素直接变为上一个不重叠的元素
# [1,2]肯定在[1,3]的前面,保留小的区间,重叠概率小,删除次数减小
intervals[i]=intervals[i-1]
return remove
或者
class Solution:
def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
# 注意这里的变化,左边界排序,右边界的改变变化了
intervals.sort(key=lambda x:x[0])
remove = 0
for i in range(1, len(intervals)):
if intervals[i][0]<intervals[i-1][1]:
remove+=1
# 这里需要更新右边界,因为右边界不是从小到大排序
intervals[i][1]=min(intervals[i-1][1],intervals[i][1])
return remove
力扣题目链接
贪心算法:
本题本可以将每个字母的区间画出来,但是这样做对解题很麻烦,因为每次迭代节点都要更新前面所有区间,即便能解出来,复杂度相应偏高。
class Solution:
def partitionLabels(self, s: str) -> List[int]:
# 使用字典统计每个字母的最大索引位置
l = {}
for i in range(len(s)):
l[s[i]]=i
# print(l)
# 寻找包含区间,要设置结果存储,左和右变量
res = []
left = 0
right = 0
# 遍历list,进行区间选取
for i in range(len(s)):
# 包含区间有两种情况:
# 1.某个单词的起始区间包含多个其他字母的区间
# 2.某两个单词,起始最小,结尾最大,共同包含多个其他字母的区间
right = max(l[s[i]], right)
# 遍历到最大结尾索引,无任何更大的扩展新区间了
# 那么,该区间为包含区间,输出,再重新移动起始节点为下一位
if right==i:
res.append(right-left+1)
left=i+1
return res
力扣题目链接
贪心算法:
class Solution:
def merge(self, intervals: List[List[int]]) -> List[List[int]]:
intervals.sort(key=lambda x:x[0])
# 初始化一个区间进行区间扩展
result = [intervals[0]]
for i in range(1, len(intervals)):
last = result[-1]
# 如果交叉,改变last的右区间
if last[1]>=intervals[i][0]:
result[-1][1] = max(intervals[i][1], result[-1][1])
# 如果不交叉,换下一个区间,输入一个区间进行初始化
else:
result.append(intervals[i])
return result
或者:在原表的基础上修改
class Solution:
def merge(self, intervals: List[List[int]]) -> List[List[int]]:
# 排序,将相邻区间排到一起
intervals.sort(key=lambda x:x[0])
for i in range(1, len(intervals)):
# 重复的往后更改,扩展区间,之前的变为空
if intervals[i][0]<=intervals[i-1][1]:
intervals[i][1] = max(intervals[i][1],intervals[i-1][1])
intervals[i][0] = min(intervals[i][0],intervals[i-1][0])
intervals[i-1] = []
# 将空的去掉
while [] in intervals:
intervals.remove([])
return intervals
力扣题目链接
暴力解法(超时):
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
res = nums[0]
for i in range(len(nums)):
tmp = 0
for j in range(i, len(nums)):
tmp+=nums[j]
if tmp>res:
res=tmp
return res
时间复杂度:O(n^2)
空间复杂度:O(1)
贪心算法:
如果 -2 1 在一起,计算起点的时候,一定是从1开始计算,因为负数只会拉低总和,这就是贪心贪的地方!
局部最优:当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素 “连续和”只会越来越小。
全局最优:选取最大“连续和”
局部最优的情况下,并记录最大的“连续和”,可以推出全局最优。
从代码角度上来讲:遍历nums,从头开始用count累积,如果count一旦加上nums[i]变为负数,那么就应该从nums[i+1]开始从0累积count了,因为已经变为负数的count,只会拖累总和。
这相当于是暴力解法中的不断调整最大子序和区间的起始位置。
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
res = -float(inf)
count = 0
# 不断调整左区间
for i in range(len(nums)):
count += nums[i]
# 取累积的最大值,
if res<count:
res = count
# 重置起始位置的值
if count <0:
count=0
return res
时间复杂度:O(n)
空间复杂度:O(1)
动态规划:
力扣题目链接
暴力法(循环列表,超时):
class Solution:
def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int:
# 计算公式为:
# 上一轮剩余的汽油+(这一轮获得的汽油-这一轮消耗的汽油)
# 这里括号括起来的部分,可以融合到一起
new = list(map(lambda x,y: x-y, gas, cost))
for i in range(len(gas)):
# 索引往后一位,即i+1
tmp = new[i]
index = (i+1)%(len(gas))
while tmp>=0 and index!=i:
tmp += new[index]
index = (index+1)%(len(gas))
if tmp >=0:
return i
return -1
时间复杂度:O(n^2)
空间复杂度:O(1)
贪心算法:
本题的规律为:
情况一:当gas的总和小于cost总和,那么无论从哪里出发都跑不了一圈;反之大于等于0,则总有一个起点可以跑完一圈。
情况二: rest[i] = gas[i]-cost[i]为一天剩下的油,i从0开始计算累加到最后一站,如果累加没有出现负数,说明从0出发,油就没有断过,那么0就是起点。
情况三: 如果累加的最小值是负数,汽车就要从非0节点为起点重新出发,继续走,看哪个节点能这个负数填平,能把这个负数填平的节点就是出发节点。
(注意这里,非0开始时,不需要走完循环一圈,因为满足了情况一的话,从非0走完最后一个索引,剩余下来的油量必然能跑完前面没走过的加油站)
class Solution:
def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int:
# 计算公式为:
# 上一轮剩余的汽油+(这一轮获得的汽油-这一轮消耗的汽油)
# 这里括号括起来的部分,可以融合到一起
tmp, index = 0, 0
su_m = 0
for i in range(len(gas)):
# 索引往后一位,即i+1
tmp += gas[i]-cost[i]
su_m += gas[i]-cost[i]
if tmp <0:
tmp = 0
index=i+1
if su_m<0:
return -1
return index
力扣题目链接
贪心算法:
class Solution:
def minCameraCover(self, root: TreeNode) -> int:
# Greedy Algo:
# 从下往上安装摄像头:跳过leaves这样安装数量最少,局部最优 -> 全局最优
# 先给leaves的父节点安装,然后每隔两层节点安装一个摄像头,直到Head
# 0: 该节点未覆盖
# 1: 该节点有摄像头
# 2: 该节点有覆盖
result = 0
# 从下往上遍历:后序(左右中)
def traversal(curr: TreeNode) -> int:
nonlocal result
if not curr: return 2
left = traversal(curr.left)
right = traversal(curr.right)
# Case 1:
# 左右节点都有覆盖
if left == 2 and right == 2:
return 0
# Case 2:
# left == 0 && right == 0 左右节点无覆盖
# left == 1 && right == 0 左节点有摄像头,右节点无覆盖
# left == 0 && right == 1 左节点有无覆盖,右节点摄像头
# left == 0 && right == 2 左节点无覆盖,右节点覆盖
# left == 2 && right == 0 左节点覆盖,右节点无覆盖
elif left == 0 or right == 0:
result += 1
return 1
# Case 3:
# left == 1 && right == 2 左节点有摄像头,右节点有覆盖
# left == 2 && right == 1 左节点有覆盖,右节点有摄像头
# left == 1 && right == 1 左右节点都有摄像头
elif left == 1 or right == 1:
return 2
# 其他情况前段代码均已覆盖
if traversal(root) == 0:
result += 1
return result
475. 供暖器
双层循环,遍历每个heaters,取距离所有houses最小的值作为每一次整体半径的最大值。
class Solution:
def findRadius(self, houses: List[int], heaters: List[int]) -> int:
max_dis = 0
for i in range(len(houses)):
min_dis = float('inf')
for j in range(len(heaters)):
min_dis = min(min_dis, abs(houses[i]-heaters[j]))
max_dis = max(max_dis, min_dis)
return max_dis
思路没问题,但是结果超时,需要进行优化。可以很轻易的看出,第二层循环是寻找最优,最接近的点,那么二分法可以优化这个问题。
bisect,是实现 二分 (bisection) 算法 的模块,能够保持序列顺序不变的情况下对其进行 二分查找和插入,适合用于降低对冗长序列查找的时间成本。当然也可以通过 “以空间换时间” 的方式,例如用于构造 hashmap 的 Counter 类 。
import bisect
class Solution:
def findRadius(self, houses: List[int], heaters: List[int]) -> int:
max_dis = 0
# 进行排序,便于正确采用二分法
# 二分法空间换时间
heaters.sort()
for i in range(len(houses)):
# bisect_right的插入可能在该点也可能在该点右边一个坐标
ind = bisect.bisect_right(heaters, houses[i])
# 所以两个坐标都进行选择与比较,取差值较小的
ind_left = ind-1
dis = heaters[ind]-houses[i] if ind<len(heaters) else float('inf')
left_dis = houses[i]-heaters[ind_left] if ind_left>=0 else float('inf')
max_dis = max(max_dis, min(left_dis, dis))
return max_dis
参考 二分法的使用
554. 砖墙
第 1 行的间隙有 [1,3,5]
第 2 行的间隙有 [3,4]
第 3 行的间隙有 [1,4]
第 4 行的间隙有 [2]
第 5 行的间隙有 [3,4]
第 6 行的间隙有 [1,4,5]
from collections import defaultdict
class Solution:
def leastBricks(self, wall: List[List[int]]) -> int:
ans = len(wall)
ha = defaultdict(int)
# 找缝隙
for bricks in wall:
wall_width = 0
for j in range(len(bricks)-1):
wall_width+=bricks[j]
ha[wall_width]+=1
print(ha)
# 有缝隙则减去墙壁大小,没则直接输出墙壁大小
return ans-max(ha.values()) if ha else ans
621. 任务调度器
from collections import Counter
class Solution:
def leastInterval(self, tasks: List[str], n: int) -> int:
freq = Counter(tasks)
# 同种类的数量最多的任务
maxExec = max(freq.values())
# 与数量最多的任务相同的次数
maxCount = sum([1 for i in freq.values() if i==maxExec])
return max((maxExec-1)*(n+1)+maxCount, len(tasks))
官方参考 构造方法
class Solution:
def maxArea(self, height: List[int]) -> int:
# 从左右两侧边缘开始向内计算最大面积
l, r = 0, len(height)-1
area = 0
while l<r:
# 面积的计算,取较短的一个边界,长方形计算公式
area = max((r-l)*min(height[r], height[l]),area)
# 移动较短的边界,试图去寻找较高的边界
# 边界向内移动会造成宽度减少,每次移动-1,面积相对减小
# 但是同时也能找到较大的高度去弥补缺失的宽度,甚至提升总面积
if height[l]>=height[r]:
r-=1
else:
l+=1
return area
贪心算法 (维基百科)
启发式 (维基百科)
贪心算法(blog)