目录
134.加油站
文档讲解:代码随想录
视频讲解:
状态:没做出来。
我写的半截代码:
class Solution:
def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int:
for i in range(len(gas)):
if gas[i]-cost[i] >= 0:
restore = gas[i] - cost[i] + gas[i+1]
if restore >= 0:
return -1
输入: gas = [1,2,3,4,5], cost = [3,4,5,1,2]
输出: 3
解释:
从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油
开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油
开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油
开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油
开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油
开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。
因此,3 可为起始索引。
先对着测试用例模拟这个过程:遍历数组,当遇到gas[i]-cost[i] >= 0时,说明i可以作为一个起点,从i出发,接下来还是重复这个过程,restore的油量要大于cost,如果一直没有遇到可以出发的情况就返回-1。
关于过程的重复的实现,我想把if改成while,但是for循环里面套while,变量i到底在哪里++?当for循环遍历到数组最后了,如何控制其回到数组开头,构成环形遍历呢?如果可以环游一圈,怎么将起点i 记录下来呢?
正确思路:
暴力方法:
暴力的方法很明显就是O(n^2)的,关键是要模拟跑一圈的过程。for循环适合模拟从头到尾的遍历,而while循环适合模拟环形遍历!
class Solution:
def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int:
# 遍历每个加油站作为起点
for i in range(len(gas)):
rest = gas[i]-cost[i] # 剩余油量(到达下一站index后所剩余的油量)
index = (i+1) % len(cost) # 设置下一站的索引(使用取模实现环形)
# 模拟以i为起点行驶一圈
while rest > 0 and index != i: #当油量足够到达下一站且未绕完一圈时继续行驶
rest += gas[index] - cost[index] # 注意是+=, 是累加的
index = (index + 1)% len(cost) # 继续移动到下一站(环形处理)
# 如果以i为起点跑一圈,剩余油量非负,并且回到起始位置
if rest >= 0 and index == i:
return i # 返回成功起点
# 无解情况
return -1
代码的逻辑和上面示例的模拟过程不太一样,代码是:离开某站时先消耗,到达下一站时才加油,结果都是一样的。
while rest > 0 and index != i: # 模拟以i为起点行驶一圈,如果有rest==0,那么答案就不唯一了。因为如果某次移动后 rest == 0
,说明车辆刚好耗尽油量到达某个加油站 j
,此时,如果从 j
出发也能完成环路(因为 rest == 0
时油箱为空,相当于从 j
重新开始)。
力扣中暴力解法超时了。
贪心算法:
观察示例解释和上面暴力方法的代码,我也发现了:整体计算过程是 全部的gas - 全部的cost,如果总油量减去总消耗大于等于零那么一定可以跑完一圈,即各个站点的 剩油量rest[i]=gas[i]-cost[i] 相加是>=0才可以。
i从0开始累加rest[i],和记为curSum,一旦curSum小于零,说明[0, i]区间都不能作为起始位置,因为这个区间选择任何一个位置作为起点,到i这里都会断油,那么起始位置从i+1算起,curSum从0开始计算.....
下标: 0 1 2 3 4
gas: [ 2 5 2 3 5 ]
cost: [ 1 2 8 2 4 ]
rest: 1 3 -6 1 1
curSum: 1 4 -2(0) 1 2 (从i=0开始累加rest)
totalSum: 1 4 -2 -1 0 (最后totalSum>=0,说明是可以绕一圈的,只要找到那个起始位置就可以)
curSum: 3 6 0 1 2 (从i=3开始累加rest)
totalSum:3 6 0 1 2
怎么确保[0,i]区间 任何一个位置作为起点 累加到 i 这里都会小于0呢,会不会有大于0的情况?如果>0,那么该起点的前半段和一定小于0,该起点正是相当于新的起始位置i+1啊,逻辑是自洽的!
局部最优:当前累加rest[i]的和curSum一旦小于0,起始位置至少要是i+1,因为从i之前开始一定不行。全局最优:找到可以跑一圈的起始位置。
class Solution:
def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int:
curSum = 0 # 当前累计的剩余油量
totalSum = 0 # 总剩余油量
start = 0 # 起始位置
for i in range(len(gas)):
rest = gas[i]-cost[i] # 剩余油量
curSum += rest
totalSum += rest # curSum在下面可能会被清0,所以totalSum一定要自己去记录
if curSum < 0:
start = i+1 # # 起始位置更新为i+1
curSum = 0 # curSum清0,重新开始累加
if totalSum < 0:
return -1 # 总剩余油量totalSum小于0,说明一定无法环绕一圈
else:
return start # 返回成功起点
-
时间复杂度:O(n),仅需一次遍历。
-
空间复杂度:O(1),仅用常数空间。
这个方法没有 也不需要在找到起始位置后 模拟环绕一圈,直接通过totalSum判断是否可以环绕一圈,通过数学性质确定起点start是否合法。
135. 分发糖果
文档讲解:代码随想录
视频讲解:
状态:没做对。
我的错误代码:
class Solution:
def candy(self, ratings: List[int]) -> int:
numbers = len(ratings) # 每人先有一颗糖 ,numbers是糖的总数
for i in range(len(ratings)):
# 首尾情况
if i == 0:
if ratings[i] > ratings[i+1]:
numbers += 1
elif i == len(ratings)-1:
if ratings[i-1] < ratings[i]:
numbers += 1
# 中间的情况
else:
if ratings[i-1] < ratings[i] or ratings[i] > ratings[i+1]:
numbers += 1
return numbers
对于输入 [1,2,87,87,87,2,1],
分配结果是[1,2, 2, 1, 2, 2, 1]。87比2大,单纯的number+1是不对的,应该有3颗糖,正确的分配应该是[1,2, 3, 1, 3, 2, 1],总共需要13颗糖。
错误原因:只进行了一次正向遍历,无法正确处理所有相邻关系。
正确解法:
这道题目一定是要确定一边之后,再确定另一边,如果两边一起考虑一定会顾此失彼。
采用两次贪心的策略:
-
一次是从前到后遍历,只比较右边孩子评分比左边大的情况。
-
一次是从后到前遍历,只比较左边孩子评分比右边大的情况。
这样从局部最优推出了全局最优,即:相邻的孩子中,评分高的孩子获得更多的糖果。
正确的解法中,不是直接用糖果总数+1,而是用candy数组记录给每个孩子分配的糖果数量,最后再sum求和。所以在第二次遍历中,要从后向前,因为,举个栗子, rating[4]与rating[5]的比较 要利用上 rating[5]与rating[6]的比较结果,即在candy[5]的基础上+1或者不变。
class Solution:
def candy(self, ratings: List[int]) -> int:
n = len(ratings)
candies = [1] * n # 糖果分配数组,每人先有一颗糖
# 正向遍历
for i in range(1,n):
if ratings[i-1] < ratings[i]: # 右边比左边大
candies[i] = candies[i-1] + 1
# 反向遍历
for i in range(n-2,-1,-1):
if ratings[i] > ratings[i+1]: # 左边比右边大
# 两个选择中取最大才能保证糖果数量既大于左边的也大于右边的
# candyVec[i + 1] + 1(比右边大,在右边的基础上加1),
# candyVec[i](之前比较右孩子大于左孩子得到的糖果数量)
candies[i] = max(candies[i],candies[i+1]+1)
return sum(candies)
860.柠檬水找零
文档讲解:
视频讲解:
状态:做出来了。
直接模拟收钱找零的过程。
用字典:
class Solution:
def lemonadeChange(self, bills: List[int]) -> bool:
if bills[0] != 5:
return False
chargebill = defaultdict(int)
for i in range(len(bills)):
if bills[i] == 5:
chargebill[5] += 1
elif bills[i] == 10:
if chargebill[5] == 0:
return False
chargebill[5] -= 1
chargebill[10] += 1
else:
if chargebill[10] != 0 and chargebill[5] != 0:
chargebill[10] -= 1
chargebill[5] -=1
elif chargebill[10] == 0 and chargebill[5] >= 3:
chargebill[5] -= 3
else:
return False
return True
直接用两个变量记录五元和十元的数量:
class Solution:
def lemonadeChange(self, bills: List[int]) -> bool:
if bills[0] != 5:
return False
a5 = 0
b10 = 0
for i in range(len(bills)):
if bills[i] == 5:
a5 += 1
elif bills[i] == 10:
if a5 == 0:
return False
a5 -= 1
b10 += 1
else:
if b10 != 0 and a5 != 0: # 先尝试用10元的找,因为5元的留着还能后续给10元的找零
b10 -= 1
a5 -=1
elif b10 == 0 and a5 >= 3:
a5 -= 3
else:
return False
return True
406.根据身高重建队列
文档讲解:代码随想录
视频讲解:
状态:不会做,理解题意,但是没找到程序的切入点,手动在纸上模拟还行。因为身高h并不是升序或降序排列,k也不是升序或降序排列
提示说,做好一边再处理另一边。
按身高h来排序,从大到小排序,身高相同的话则k小的站前面。
按照身高排序之后,优先按身高高的people的k来插入,后序插入节点也不会影响前面已经插入的节点,最终按照k的规则完成队列的重建。
所以在按照身高从大到小排序后,再按照这个顺序依次进行 构造队列。
局部最优:优先按身高高的people的k来插入。插入操作过后的people满足队列属性
全局最优:最后都做完插入操作,整个队列满足题目队列属性
举个栗子:
people = [[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]]
排序完的people: [[7,0], [7,1], [6,1], [5,0], [5,2], [4,4]]
插入的过程:
- 插入[7,0]:[[7,0]]
- 插入 [7,1]:[[7,0],[7,1]]
- 插入[6,1]:[[7,0],[6,1],[7,1]]
- 插入[5,0]:[[5,0],[7,0],[6,1],[7,1]]
- 插入[5,2]:[[5,0],[7,0],[5,2],[6,1],[7,1]]
- 插入 [4,4]:[[5,0],[7,0],[5,2],[6,1], [4,4],[7,1]] 完成排列
class Solution:
def reconstructQueue(self, people: List[List[int]]) -> List[List[int]]:
people.sort(key = lambda x:(-x[0],x[1])) # sort默认是升序排序,先按身高h降序排序,再按k升序排序
# lambda返回的是一个元组:当-x[0](维度h)相同时,再根据x[1](维度k)从小到大排序
# 根据每个元素的第二个维度k,贪心算法,进行插入
queue = []
for p in people:
queue.insert(p[1],p) # p[1]是当前人的k值,表示他前面应有k个身高≥他的人。
# 因为是按排序后的身高从高到低插入的,之前插入的人都比当前人高或相等,所以当前人插入到位置p[1],前面正好有p[1]个 ≥ 他的人
return queue
-
时间复杂度:O(nlog n + n^2) (排序:O(n log n) ;插入:每次
insert
是 O(n),要进行n次,所以是 O(n²) ) -
空间复杂度:O(n)
在 Python 中,
list.insert()
是一个用于在列表指定位置插入元素的方法。list.insert(index, element)
index是
要插入元素的索引位置;element是
要插入的元素。该操作直接修改原列表,不返回新列表
时间复杂度:O(n),因为需要移动插入位置后的所有元素
如果
index > len(list)
,元素会插入到末尾;如果index < -len(list)
,元素会插入到开头,索引越界不会报错。与 append() 区别:
append()
只能在末尾添加,insert()
可以在任意位置添加
感想
学习用时:晚上1.2h+中午2h+晚上3h+中午2.5h