376. 摆动序列 如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。 例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。 相反,[1, 4, 7, 2, 5] 和 [1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。 子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。 给你一个整数数组 nums ,返回 nums 中作为 摆动序列 的 最长子序列的长度 。
1 2 3 输入:nums = [1,7,4,9,2,5] 输出:6 解释:整个序列均为摆动序列,各元素之间的差值为 (6, -3, 5, -7, 3) 。
考虑用动态规划的思想来解决这个问题。
很容易可以发现,对于我们当前考虑的这个数,要么是作为山峰(即 nums[i] > nums[i-1]),要么是作为山谷(即 nums[i] < nums[i - 1])。
设 dp 状态dp[i][0],表示考虑前 i 个数,第 i 个数作为山峰的摆动子序列的最长长度 设 dp 状态dp[i][1],表示考虑前 i 个数,第 i 个数作为山谷的摆动子序列的最长长度 则转移方程为:
dp[i][0] = max(dp[i][0], dp[j][1] + 1),其中0 < j < i且nums[j] < nums[i],表示将 nums[i]接到前面某个山谷后面,作为山峰。 dp[i][1] = max(dp[i][1], dp[j][0] + 1),其中0 < j < i且nums[j] > nums[i],表示将 nums[i]接到前面某个山峰后面,作为山谷。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class Solution : def wiggleMaxLength (self, nums: List [int ] ) -> int : if len (nums) <= 1 : return len (nums) curDiff = 0 preDiff = 0 result = 1 for i in range (len (nums) - 1 ): curDiff = nums[i+1 ] - nums[i] if (preDiff <= 0 and curDiff > 0 ) or (preDiff >= 0 and curDiff < 0 ): result += 1 preDiff = curDiff return result class Solution : def wiggleMaxLength (self, nums: List [int ] ) -> int : dp = [[0 ,0 ] for _ in range (len (nums))] dp[0 ][0 ] = dp[0 ][1 ] = 1 for i in range (1 , len (nums)): dp[i][0 ] = dp[i][1 ] = 1 for j in range (i): if nums[j] > nums[i]: dp[i][1 ] = max (dp[i][1 ], dp[j][0 ]+1 ) for j in range (i): if nums[j] < nums[i]: dp[i][0 ] = max (dp[i][0 ], dp[j][1 ]+1 ) return max (dp[-1 ][0 ], dp[-1 ][1 ])
53. 最大子数组和 给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。 子数组是数组中的一个连续部分。
1 2 3 4 5 6 7 8 9 10 11 class Solution : def maxSubArray (self, nums: List [int ] ) -> int : result = float ('-inf' ) count = 0 for i in range (len (nums)): count += nums[i] if count > result: result = count if count <= 0 : count = 0 return result
122.买卖股票的最佳时机 II 给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。 在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。 返回 你能获得的 最大 利润 。
1 2 3 4 5 输入:prices = [7,1,5,3,6,4] 输出:7 解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4。 随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3。 最大总利润为 4 + 3 = 7 。
如果想到其实最终利润是可以分解的,那么本题就很容易了!如何分解呢? 假如第 0 天买入,第 3 天卖出,那么利润为:prices[3] - prices[0]。 相当于(prices[3] - prices[2]) + (prices[2] - prices[1]) + (prices[1] - prices[0])。 此时就是把利润分解为每天为单位的维度,而不是从 0 天到第 3 天整体去考虑!
1 2 3 4 5 6 class Solution : def maxProfit (self, prices: List [int ] ) -> int : result = 0 for i in range (1 , len (prices)): result += max (prices[i] - prices[i-1 ], 0 ) return result
55. 跳跃游戏 给定一个非负整数数组,你最初位于数组的第一个位置。 数组中的每个元素代表你在该位置可以跳跃的最大长度。 判断你是否能够到达最后一个位置。
1 2 3 4 示例 1: 输入: [2,3,1,1,4] 输出: true 解释: 我们可以先跳 1 步,从位置 0 到达 位置 1, 然后再从位置 1 跳 3 步到达最后一个位置。
1 2 3 4 5 6 7 8 9 10 11 class Solution : def canJump (self, nums: List [int ] ) -> bool : cover = 0 if len (nums) == 1 : return True i = 0 while i <= cover: cover = max (i + nums[i], cover) if cover >= len (nums) - 1 : return True i += 1 return False
45.跳跃游戏 II 给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。 每个元素 nums[i] 表示从索引 i 向后跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处: 0 <= j <= nums[i] i + j < n 返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]。
1 2 3 4 5 示例 1: 输入: nums = [2,3,1,1,4] 输出: 2 解释: 跳到最后一个位置的最小跳跃数是 2。 从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
本题要计算最少步数,那么就要想清楚什么时候步数才一定要加一呢? 贪心的思路,局部最优:当前可移动距离尽可能多走,如果还没到终点,步数再加一。整体最优:一步尽可能多走,从而达到最少步数。 思路虽然是这样,但在写代码的时候还不能真的能跳多远就跳多远,那样就不知道下一步最远能跳到哪里了。 所以真正解题的时候,要从覆盖范围出发,不管怎么跳,覆盖范围内一定是可以跳到的,以最小的步数增加覆盖范围,覆盖范围一旦覆盖了终点,得到的就是最少步数! 这里需要统计两个覆盖范围,当前这一步的最大覆盖和下一步最大覆盖。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution : def jump (self, nums: List [int ] ) -> int : if len (nums) == 1 : return 0 cur_distance = 0 ans = 0 next_distance = 0 for i in range (len (nums)): next_distance = max (nums[i]+i, next_distance) if i == cur_distance: ans += 1 cur_distance = next_distance if next_distance >= len (nums) - 1 : break return ans
134. 加油站 在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。 你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。 给定两个整数数组 gas 和 cost ,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的。
1 2 3 4 5 6 7 8 9 10 11 12 示例 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 可为起始索引。
如果总油量减去总消耗大于等于零那么一定可以跑完一圈,说明 各个站点的加油站 剩油量rest[i]相加一定是大于等于零的。每个加油站的剩余量rest[i]为gas[i] - cost[i]。i从0开始累加rest[i],和记为curSum,一旦curSum小于零,说明[0, i]区间都不能作为起始位置,因为这个区间选择任何一个位置作为起点,到i这里都会断油,那么起始位置从i+1算起,再从0计算curSum。那么局部最优:当前累加rest[i]的和curSum一旦小于0,起始位置至少要是i+1,因为从i之前开始一定不行。全局最优:找到可以跑一圈的起始位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution : def canCompleteCircuit (self, gas: List [int ], cost: List [int ] ) -> int : curSum = 0 totalSum = 0 start = 0 for i in range (len (gas)): curSum += gas[i] - cost[i] totalSum += gas[i] - cost[i] if curSum < 0 : start = i+1 curSum = 0 if totalSum < 0 : return -1 return start
直接从全局进行贪心选择,情况如下: 情况一:如果gas的总和小于cost总和,那么无论从哪里出发,一定是跑不了一圈的 情况二:rest[i] = gas[i]-cost[i]为一天剩下的油,i从0开始计算累加到最后一站,如果累加没有出现负数,说明从0出发,油就没有断过,那么0就是起点。 情况三:如果累加的最小值是负数,汽车就要从非0节点出发,从后向前,看哪个节点能把这个负数填平,能把这个负数填平的节点就是出发节点。
示例 输入 1 2 gas = [1 , 2 , 3 , 4 , 5 ] cost = [3 , 4 , 5 , 1 , 2 ]
计算 rest[i] = gas[i] - cost[i]
:
1 rest = [-2, -2, -2, 3, 3]
累积 curSum
:
1 2 3 4 5 i=0, curSum = -2 (curSum < 0,说明0不能作为起点) i=1, curSum = -4 (curSum < 0,说明1不能作为起点) i=2, curSum = -6 (curSum < 0,说明2不能作为起点) i=3, curSum = -3 (curSum 变大) i=4, curSum = 0 (curSum >= 0,所有油量累加后变为非负)
🔹 最小 curSum = -6
(在 i=2
处),所以需要填平 -6
。
如何找到能填平 -6
的站点
从后往前检查 rest[i]
,找出从哪开始可以让 curSum
回正。
rest = [-2, -2, -2, 3, 3]
**从 i=4
开始向前累加,直到 curSum >= 0
**:1 2 i=4, sum = 3 (填平 -6 还差 3) i=3, sum = 6 (已经填平了 -6)
说明 从 i=3
开始出发,可以填平之前的亏损 -6
,最终回到 0
或以上 。
🔹 **最终答案:起点是 i=3
**。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Solution : def canCompleteCircuit (self, gas: List [int ], cost: List [int ] ) -> int : curSum = 0 minFuel = float ('inf' ) for i in range (len (gas)): rest = gas[i] - cost[i] curSum += rest if curSum < minFuel: minFuel = curSum if curSum < 0 : return -1 if minFuel >= 0 : return 0 for i in range (len (gas) - 1 , -1 , -1 ): rest = gas[i] - cost[i] minFuel += rest if minFuel >= 0 : return i return -1
135. 分发糖果 n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。 你需要按照以下要求,给这些孩子分发糖果: 每个孩子至少分配到 1 个糖果。 相邻两个孩子评分更高的孩子会获得更多的糖果。 请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。
1 2 3 输入:ratings = [1,0,2] 输出:5 解释:你可以分别给第一个、第二个、第三个孩子分发 2、1、2 颗糖果。
这道题目一定是要确定一边之后,再确定另一边,例如比较每一个孩子的左边,然后再比较右边,如果两边一起考虑一定会顾此失彼 先确定右边评分大于左边的情况(也就是从前向后遍历) 此时局部最优:只要右边评分比左边大,右边的孩子就多一个糖果,全局最优:相邻的孩子中,评分高的右孩子获得比左边孩子更多的糖果 局部最优可以推出全局最优。
再确定左孩子大于右孩子的情况(从后向前遍历) 遍历顺序这里有同学可能会有疑问,为什么不能从前向后遍历呢? 因为 rating[5]与rating[4]的比较 要利用上 rating[5]与rating[6]的比较结果,所以 要从后向前遍历。 如果从前向后遍历,rating[5]与rating[4]的比较 就不能用上 rating[5]与rating[6]的比较结果了 。如图:
所以确定左孩子大于右孩子的情况一定要从后向前遍历! 如果 ratings[i] > ratings[i + 1],此时candyVec[i](第i个小孩的糖果数量)就有两个选择了,一个是candyVec[i + 1] + 1(从右边这个加1得到的糖果数量),一个是candyVec[i](之前比较右孩子大于左孩子得到的糖果数量)。 那么又要贪心了,局部最优:取candyVec[i + 1] + 1 和 candyVec[i] 最大的糖果数量,保证第i个小孩的糖果数量既大于左边的也大于右边的。全局最优:相邻的孩子中,评分高的孩子获得更多的糖果。
为什么要取 max(candyVec[i], candyVec[i + 1] + 1)? 如果我们不取最大值,而只是直接让 candyVec[i] = candyVec[i + 1] + 1,可能会破坏第一次遍历的结果,导致最终答案不正确。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution : def candy (self, ratings: List [int ] ) -> int : n = len (ratings) candies = [1 ] * n for i in range (1 , n): if ratings[i] > ratings[i-1 ]: candies[i] = candies[i-1 ] + 1 for i in range (n-2 , -1 , -1 ): if ratings[i] > ratings[i+1 ]: candies[i] = max (candies[i], candies[i+1 ]+1 ) return sum (candies)
406. 根据身高重建队列 假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。 请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。
1 2 3 4 5 6 7 8 9 10 输入:people = [[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]] 输出:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] 解释: 编号为 0 的人身高为 5 ,没有身高更高或者相同的人排在他前面。 编号为 1 的人身高为 7 ,没有身高更高或者相同的人排在他前面。 编号为 2 的人身高为 5 ,有 2 个身高更高或者相同的人排在他前面,即编号为 0 和 1 的人。 编号为 3 的人身高为 6 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。 编号为 4 的人身高为 4 ,有 4 个身高更高或者相同的人排在他前面,即编号为 0、1、2、3 的人。 编号为 5 的人身高为 7 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。 因此 [[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] 是重新构造后的队列。
在按照身高从大到小排序后: 局部最优:优先按身高高的people的k来插入。插入操作过后的people满足队列属性 全局最优:最后都做完插入操作,整个队列满足题目队列属性 局部最优可推出全局最优,找不出反例,那就试试贪心。
1 2 3 4 5 6 7 8 9 10 class Solution : def reconstructQueue (self, people: List [List [int ]] ) -> List [List [int ]]: people.sort(key=lambda x:(-x[0 ], x[1 ])) que = [] for p in people: que.insert(p[1 ], p) return que
452. 用最少数量的箭引爆气球 有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ,其中points[i] = [xstart, xend] 表示水平直径在 xstart 和 xend之间的气球。你不知道气球的确切 y 坐标。 一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。 给你一个数组 points ,返回引爆所有气球所必须射出的 最小 弓箭数 。
直觉上来看,貌似只射重叠最多的气球,用的弓箭一定最少,那么有没有当前重叠了三个气球,我射两个,留下一个和后面的一起射这样弓箭用的更少的情况呢?尝试一下举反例,发现没有这种情况。 那么就试一试贪心吧!局部最优:当气球出现重叠,一起射,所用弓箭最少。全局最优:把所有气球射爆所用弓箭最少。 算法确定下来了,那么如何模拟气球射爆的过程呢?是在数组中移除元素还是做标记呢?
如果真实的模拟射气球的过程,应该射一个,气球数组就remove一个元素,这样最直观,毕竟气球被射了。 但仔细思考一下就发现:如果把气球排序之后,从前到后遍历气球,被射过的气球仅仅跳过就行了,没有必要让气球数组remove气球,只要记录一下箭的数量就可以了。 以上为思考过程,已经确定下来使用贪心了,那么开始解题。
为了让气球尽可能的重叠,需要对数组进行排序。 那么按照气球起始位置排序,还是按照气球终止位置排序呢? 其实都可以!只不过对应的遍历顺序不同,我就按照气球的起始位置排序了。 既然按照起始位置排序,那么就从前向后遍历气球数组,靠左尽可能让气球重复。 从前向后遍历遇到重叠的气球了怎么办? 如果气球重叠了,重叠气球中右边边界的最小值 之前的区间一定需要一个弓箭。 以题目示例: [[10,16],[2,8],[1,6],[7,12]]为例,如图:(方便起见,已经排序)
1 2 3 4 5 6 7 8 9 10 11 class Solution : def findMinArrowShots (self, points: List [List [int ]] ) -> int : if len (points) == 0 : return 0 points.sort(key = lambda x : x[0 ]) result = 1 for i in range (1 , len (points)): if points[i][0 ] > points[i-1 ][1 ]: result += 1 else : points[i][1 ] = min (points[i-1 ][1 ], points[i][1 ]) return result
763. 划分字母区间 给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。例如,字符串 “ababcc” 能够被分为 [“abab”, “cc”],但类似 [“aba”, “bcc”] 或 [“ab”, “ab”, “cc”] 的划分是非法的。 注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s 。 返回一个表示每个字符串片段的长度的列表。
1 2 3 4 5 6 输入:s = "ababcbacadefegdehijhklij" 输出:[9,7,8] 解释: 划分结果为 "ababcbaca"、"defegde"、"hijhklij" 。 每个字母最多出现在一个片段中。 像 "ababcbacadefegde", "hijhklij" 这样的划分是错误的,因为划分的片段数较少。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 class Solution : def partitionLabels (self, s: str ) -> List [int ]: last_occurrence = {} for i, ch in enumerate (s): last_occurrence[ch] = i result = [] start = 0 end = 0 for i,ch in enumerate (s): end = max (end, last_occurrence[ch]) if i == end: result.append(end-start+1 ) start = i+1 return result class Solution : def countLabels (self, s ): hash = [[float ('-inf' ), float ('-inf' )] for _ in range (26 )] hash_filter = [] for i in range (len (s)): if hash [ord (s[i]) - ord ('a' )][0 ] == float ('-inf' ): hash [ord (s[i]) - ord ('a' )][0 ] = i hash [ord (s[i]) - ord ('a' )][1 ] = i for i in range (len (hash )): if hash [i][0 ] != float ('-inf' ): hash_filter.append(hash [i]) return hash_filter def partitionLabels (self, s: str ) -> List [int ]: res = [] hash = self .countLabels(s) hash .sort(key=lambda x:x[0 ]) rightBoard = hash [0 ][1 ] leftBoard = 0 for i in range (1 , len (hash )): if hash [i][0 ] > rightBoard: res.append(rightBoard - leftBoard + 1 ) leftBoard = hash [i][0 ] rightBoard = max (rightBoard, hash [i][1 ]) res.append(rightBoard - leftBoard + 1 ) return res
end 记录的是当前片段中所有字符的最远出现位置。
738. 单调递增的数字 当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。 给定一个整数 n ,返回 小于或等于 n 的最大数字,且数字呈 单调递增 。
为什么从右向左遍历? 贪心策略:如果 strNum[i-1] > strNum[i],一定要让 strNum[i-1] 变小,并把 flag 位置之后的所有数字变 9,才能保证最大的单调递增数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution : def monotoneIncreasingDigits (self, n: int ) -> int : strNum = str (n) flag = len (strNum) for i in range (len (strNum)-1 , 0 , -1 ): if strNum[i-1 ] > strNum[i]: flag = i strNum = strNum[:i-1 ] + str (int (strNum[i-1 ]) - 1 ) + strNum[i:] for i in range (flag, len (strNum)): strNum = strNum[:i] + '9' + strNum[i+1 :] return int (strNum)
968. 监控二叉树 重要! 给定一个二叉树,我们在树的节点上安装摄像头。 节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。 计算监控树的所有节点所需的最小摄像头数量。
从题目中示例,其实可以得到启发,我们发现题目示例中的摄像头都没有放在叶子节点上! 这是很重要的一个线索,摄像头可以覆盖上中下三层,如果把摄像头放在叶子节点上,就浪费的一层的覆盖。 所以把摄像头放在叶子节点的父节点位置,才能充分利用摄像头的覆盖面积。 那么有同学可能问了,为什么不从头结点开始看起呢,为啥要从叶子节点看呢? 因为头结点放不放摄像头也就省下一个摄像头, 叶子节点放不放摄像头省下了的摄像头数量是指数阶别的。 所以我们要从下往上看,局部最优:让叶子节点的父节点安摄像头,所用摄像头最少,整体最优:全部摄像头数量所用最少!
在二叉树中如何从低向上推导呢? 可以使用后序遍历也就是左右中的顺序,这样就可以在回溯的过程中从下到上进行推导了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class Solution : def minCameraCover (self, root: Optional [TreeNode] ) -> int : result = [0 ] if self .traversal(root, result) == 0 : result[0 ] += 1 return result[0 ] def traversal (self, cur: TreeNode, result: List [int ] ) -> int : if not cur: return 2 left = self .traversal(cur.left, result) right = self .traversal(cur.right, result) if left == 2 and right == 2 : return 0 if left == 0 or right == 0 : result[0 ] += 1 return 1 if left == 1 or right == 1 : return 2