代码随想录算法训练营第二十一天 | 读PDF复习环节1

读PDF复习环节1

本博客的内容只是做一个大概的记录,整个PDF看下来,内容上是不如代码随想录网站上的文章全面的,并且PDF中有些地方的描述,是很让我疑惑的,在困扰我很久后,无意间发现,其网站上的讲解完全符合我的思路。这次看完这些PDF后,暂时一段时间内不会再看了,要复习还是依靠,代码随想录网站,视频,和我自己写的博客吧

动态规划章节

动态规划五部曲

1、确定dp数组以及下标的含义。
2、确定递推公式。
3、dp数组如何初始化。
4、确定遍历顺序。
5、举例推导dp数组。

有一些情况是,递推公式决定了dp数组要如何初始化

不要盲目追求空间压缩,以现在的水平,先把最能体现动态规划思维过程的代码写熟练了再说

746 使用最少花费爬楼梯

本题PDF中的描述,让人迷惑!我不赞同第一步要花费体力的想法,在没走之前肯定就不花费啊。我认为本题的理解应该是这样的,在开始时,人是站在层底的,而最后,要求站在最后一层层顶,即:* 0 1 2 3 * 4,假如选择从0开始,那么人就站在第一个星花的位置,而要求爬上台阶3,就是要到第二个星花的位置,那么我们可以往后想一步,不就是台阶4的层底吗?

所以 dp[i] 自然就定义为:站在第 i 层的层底的最小花费,dp[i] = min( dp[i-1] + cost[i-1] , dp[i-2] + cost[i-2] )

非常合理且自然!初始化也很好做,前两个状态设置为0,也很符合直觉!

网站上的示例代码也是按照上述方式编写的!

62 不同路径

这道题动态规划的思路已经完全掌握了,数论的代码写法可以看看,熟悉熟悉,尤其是,提前除以分母的写法。

class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        numerator = 1  # 分子
        denominator = m - 1  # 分母
        count = m - 1  # 计数器,表示剩余需要计算的乘积项个数
        t = m + n - 2  # 初始乘积项
        while count > 0:
            numerator *= t  # 计算乘积项的分子部分
            t -= 1  # 递减乘积项
            while denominator != 0 and numerator % denominator == 0:
                numerator //= denominator  # 约简分子
                denominator -= 1  # 递减分母
            count -= 1  # 计数器减1,继续下一项的计算
        return numerator  # 返回最终的唯一路径数

带障碍的路径问题,我和代码随想录的看法一致,难以将二维DP数组压缩至一维。

343. 整数拆分

这道题,代码随想录给出的代码是,剪枝减的最狠的一版:dp[i] 只比较 (i - j) * j, dp[i - j] * j , 且 j 只遍历到 i 的一半。

for i in range(3, n + 1):
            # 遍历所有可能的切割点
            for j in range(1, i // 2 + 1):
                # 计算切割点j和剩余部分(i-j)的乘积,并与之前的结果进行比较取较大值                
                dp[i] = max(dp[i], (i - j) * j, dp[i - j] * j)

我觉得,最让我容易理解的应该是下面这版:

for i in range(3, n + 1):
            # 遍历所有可能的切割点
            for j in range(1, i // 2 + 1):
                # 计算切割点j和剩余部分(i-j)的乘积,并与之前的结果进行比较取较大值                
                dp[i] = max(dp[i], (i - j) * j, dp[i - j] * j, dp[i - j] * dp[j], (i - j) * dp[j])

dp[i] 是由四个值决定的。当然这种,四个值的情况,很自然的就可以取一半,而不用遍历全部的值。

我觉得之所以能够这样剪枝,根本原因在于这道题自身的特性,当一个数大于4时,分解他一定比不分解他的值大,所以最大值基本上来自于 dp[i - j] * j , 且 j 是一个小值。

如果题意变化,如果要延续,只用两个状态放在递推公式里的话,就不能只遍历一半:

for i in range(3, n + 1):
            # 遍历所有可能的切割点
            for j in range(1, i):
                # 计算切割点j和剩余部分(i-j)的乘积,并与之前的结果进行比较取较大值                
                dp[i] = max(dp[i], (i - j) * j, dp[i - j] * j)

这样也是可以说明,是考虑到所有情况的,即:所有分解的情况中,只要包含因子 j 的情况,我都已经考虑到了。因为分解数 i ,那么其中一个加数一定是【1,i-1】。

最让我舒服的还是,max里面取四个值,遍历只需遍历一半。

96 不同的二叉搜索树

这道题的思路真的巧妙,要想到因为所给的数组是排好序的,假如是【1,n】,那么我们选择其中一个数,i , 那么很自然的就有:【1,i-1】为左子树,【i+1,n】为右子树(这里,经典重现:有序数组和二叉搜索树的对应关系),那么选择 i 为头结点,所能组成的二叉搜索树的数量,就是【1,i-1】(左子树)是二叉搜索树的数量,乘上,【i+1,n】(右子树)是二叉搜索树的数量。

只要想到了这个思路,递推公式就呼之欲出,代码也基本在脑海中成型了。

class Solution:
    def numTrees(self, n: int) -> int:
        dp = [0] * (n + 1)  # 创建一个长度为n+1的数组,初始化为0
        dp[0] = 1  # 当n为0时,只有一种情况,即空树,所以dp[0] = 1
        for i in range(1, n + 1):  # 遍历从1到n的每个数字
            for j in range(1, i + 1):  # 对于每个数字i,计算以i为根节点的二叉搜索树的数量
                dp[i] += dp[j - 1] * dp[i - j]  # 利用动态规划的思想,累加左子树和右子树的组合数量
        return dp[n]  # 返回以1到n为节点的二叉搜索树的总数量


背包问题的暴力解法

每一个物品都有两个状态,取或者不取,所以可以使用回溯法搜索出所有的情况。

目前的认知是:原版二维dp数组背包,遍历区间要完全,要有if判断,不满足放入条件就等于上一个值。实用版一维dp数组背包,可以在for循环中的遍历区间稍作修改,不遍历不满足放入条件的情况。

1049.最后一块石头的重量II

这道题的精髓就是明白:两块质量不相等的石头相撞,质量大的那一个,还会把差值退回去!所以本题还是分两堆,然后用01背包去装。

494.目标和

这道题,是第一次接触求一共有多少种组合的题,后面类似的题目还会再次出现,本质上就是理解递推公式的核心。
二维DP数组:先不管符不符合,把上一个值赋值过来。在判断,如果符合放入条件,在此基础上累加。
一维DP数组:由于滚动数组的特性,自动完成拷贝赋值,所以直接累加就好。

 # 动态规划过程
        for i in range(1, len(nums) + 1):
            for j in range(target_sum + 1):
                dp[i][j] = dp[i - 1][j]  # 不选取当前元素
                if j >= nums[i - 1]:
                    dp[i][j] += dp[i - 1][j - nums[i - 1]]  # 选取当前元素
 for num in nums:
            for j in range(target_sum, num - 1, -1):
                dp[j] += dp[j - num]  # 状态转移方程,累加不同选择方式的数量

题目做到这里,也发现了,背包问题,能用一维数组的,就按照惯性用一维数组过吧,二维数组要处理的细节更多,且初始化部分,可能要根据递推公式确定初始值,有些初始值很难赋予意义。二维数组的坑也更多。

474 一和零

只要把一和零的个数,看做是01背包问题中的物品重量,换句话说,重量信息由一维,变为了两维,其他的保持01背包编写方式就好!

所以本题的最原始方式,是三维DP数组,用滚动数组后,为二维DP数组。

class Solution:
    def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
        dp = [[0] * (n + 1) for _ in range(m + 1)]  # 创建二维动态规划数组,初始化为0
        # 遍历物品
        for s in strs:
            ones = s.count('1')  # 统计字符串中1的个数
            zeros = s.count('0')  # 统计字符串中0的个数
            # 遍历背包容量且从后向前遍历
            for i in range(m, zeros - 1, -1):
                for j in range(n, ones - 1, -1):
                    dp[i][j] = max(dp[i][j], dp[i - zeros][j - ones] + 1)  # 状态转移方程
        return dp[m][n]


01背包和完全背包的遍历顺序

01背包中,二维DP数组的两个for遍历,先后顺序可以颠倒。一维DP数组的两个for循环的顺序,一定是先遍历物品,再遍历背包,另外背包要倒序遍历,因为要保证每个物品只拿一个。而如果,01背包的一维DP数组,倒序遍历背包,但是先遍历背包,再遍历物品,得到的结果是,最后的背包中只有一个物品。(这里因为背包是倒序遍历,上来就把结果位置的输出遍历掉了,此时其他位置的值还都是初始值)

完全背包,不管是一维DP数组还是二维DP数组,遍历顺序都可以颠倒,一维DP数组时,背包的遍历顺序为正序遍历,且必须是正序遍历。

但是完全背包有一个要注意的点,就是完全背包问题的拓展应用题中,会涉及组合数和排列数的问题,而01背包没有类似的问题。求组合数,必须是先遍历物品,再遍历背包;求排列数,是先遍历背包,再遍历物品、

二维DP,区别仅仅在于 max 比较中的部分,01背包是 dp[i-1][j-weight[i]]+value[i],而完全背包是dp[i][j-weight[i]]+value[i]。

01背包
for i in range(n):
	for j in range(m):
		if j >= weight[i] :
			dp[i][j] = max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i])
		else :
			dp[i][j] = dp[i-1][j]
完全背包
for i in range(n):
	for j in range(m):
		if j >= weight[i] :
		# 区别仅仅就在这一句话
			dp[i][j] = max(dp[i-1][j],dp[i][j-weight[i]]+value[i])
		else :
			dp[i][j] = dp[i-1][j]

一维DP数组,01背包的遍历为从后向前,完全背包的遍历为从前向后,01背包需要的是i-1,所以不能用新值去覆盖i-1,所以必须倒序遍历,这样dp[j]的更新公式,用的才是上一个i的值。完全背包需要的是i,所以要用新值去覆盖i-1,需要正序遍历,这样dp[j]的更新公式,用的是当前i的值。

01背包
for i in range(n):
	for j in range(m-1,weight[i]-1,-1):
		dp[j] = max(dp[j],dp[j-weight[i]]+value[i])
		
完全背包
for i in range(n):
	for j in range(weight[i],m):
		dp[j] = max(dp[j],dp[j-weight[i]]+value[i])

完全背包问题存在的疑问!在求排列数的相关问题上,为什么用二维DP数组,写不出来?

求装满背包有多少种方法,递推公式一般都是,dp[i]+=dp[i-nums[j]]

求组合数的经典题目:零钱兑换

class Solution:
    def change(self, amount: int, coins: List[int]) -> int:
        dp = [0]*(amount + 1)
        dp[0] = 1
        # 遍历物品
        for i in range(len(coins)):
            # 遍历背包
            for j in range(coins[i], amount + 1):
                dp[j] += dp[j - coins[i]]
        return dp[amount]

求排列数的经典题目:组合总和

注意本题的代码写法,因为先遍历的背包,后遍历的物品,所以在递推公式那里,要加一个 if 判断。

class Solution:
    def combinationSum4(self, nums: List[int], target: int) -> int:
        dp = [0] * (target + 1)
        dp[0] = 1
        for i in range(1, target + 1):  # 遍历背包
            for j in range(len(nums)):  # 遍历物品
                if i - nums[j] >= 0:
                    dp[i] += dp[i - nums[j]]
        return dp[target]

爬楼梯(进阶版)

本题需要注意的点:本题是一个完全背包问题,另外要明确到,是求排列数,要先遍历背包,再遍历物品。

class Solution:
    def climbStairs(self, n: int) -> int:
        dp = [0]*(n + 1)
        dp[0] = 1
        m = 2
        # 遍历背包
        for j in range(n + 1):
            # 遍历物品
            for step in range(1, m + 1):
            # 这里的 if 判断,是要学习的点
                if j >= step:
                    dp[j] += dp[j - step]
        return dp[n]

后面两题,零钱兑换和完全平方数,是动态规划题目中,求满足条件的最小数目的题目

求解要点:首先是完全背包问题,其次在递推公式上,就是求min的过程,先遍历背包还是物品都没有关系,但是先遍历物品的代码比较好写,因为在遍历背包那里,可以直接在循环中限制范围,用一维DP数组。

求最小值的题目,最需要注意的地方是,初始化,要初始化为最大值。

139 单词拆分

这道题,首先明确是,排列问题,而不是像PDF中介绍的,组合或排列均可。

但这道题,同前面的求排列的题一样,二维DP数组的我写不出来。

这道题算是利用动态规划求解字符串问题的典型题了,重视重视再重视。

class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> bool:
        dp = [False]*(len(s) + 1)
        dp[0] = True
        # 遍历背包
        for j in range(1, len(s) + 1):
            # 遍历单词
            for word in wordDict:
                if j >= len(word):
                    dp[j] = dp[j] or (dp[j - len(word)] and word == s[j - len(word):j])
        return dp[len(s)]

打家劫舍问题,需要学习的就两个点,处理环形数据的方法就是拆分为两段,分别计算,不带头,不带尾。还有就是,如何利用动态规划求解二叉树结构的题目。

题目:337 打家劫舍III

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def rob(self, root: Optional[TreeNode]) -> int:
        # dp数组(dp table)以及下标的含义:
        # 1. 下标为 0 记录 **不偷该节点** 所得到的的最大金钱
        # 2. 下标为 1 记录 **偷该节点** 所得到的的最大金钱
        dp = self.traversal(root)
        return max(dp)

    # 要用后序遍历, 因为要通过递归函数的返回值来做下一步计算
    def traversal(self, node):
        
        # 递归终止条件,就是遇到了空节点,那肯定是不偷的
        if not node:
            return (0, 0)

        left = self.traversal(node.left)
        right = self.traversal(node.right)

        # 不偷当前节点, 偷子节点
        val_0 = max(left[0], left[1]) + max(right[0], right[1])

        # 偷当前节点, 不偷子节点
        val_1 = node.val + left[0] + right[0]

        return (val_0, val_1)

买卖股票系列

买卖股票系列,最重要的就是学会这种DP数组定义方式,dp[i][0]表示第i天持有股票的最大现金,dp[i][1]表示第i天不持有股票的最大现金。

关于股票问题,想想清楚,对于限制,只可买卖一次,和不限制买卖次数的题目来说,dp数组的定义可以是相同的,就定义两个状态,dp[i][0]:持有股票,dp[i][1]:不持有股票,都对第0天做单独初始化,唯一区别在于,在循环中推导 dp[i][0]时,需不需要前一天的状态。

对于限制最多交易次数的题,就按照顺序,去定义每一天的状态。

含冷冻期的题,注意定义的四种状态。持有股票的状态,不持有股票但是不在冷冻期的状态,今天卖出股票的状态,昨天卖出股票所以今天在冷冻期的状态。

子序列系列、编辑距离系列、回文子串系列

不想写了,直接看之前自己写的博客吧。

子序列系列、编辑距离系列、回文子串系列

贪心算法章节

没什么方法论,见得多了自然就有了感觉。

376 摆动序列

本题贪心的核心在于:让峰值尽可能地保持峰值,然后删除单一坡度上的节点。

本题有很多要注意的点,首先在思路上,要意识到,可以通过两个变量,prediff 和 curdiff 来判断当前是不是坡度变换的时候。

主要考虑三种情况:
上下坡中有平坡,数组首尾两端,单调坡中有平坡。

每种情况都对应着不同的判断条件,在分析时,要通过举例,来分析判断条件的细节。

不过本题的难点也在于怎样想到这三种情况吧。

本题的动态规划方法,也值得学习。

在这里插入图片描述

贪心法:

class Solution:
    def wiggleMaxLength(self, nums):
        if len(nums) <= 1:
            return len(nums)  # 如果数组长度为0或1,则返回数组长度
        curDiff = 0  # 当前一对元素的差值
        preDiff = 0  # 前一对元素的差值
        result = 1  # 记录峰值的个数,初始为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  # 峰值个数加1
                preDiff = curDiff  # 注意这里,只在摆动变化的时候更新preDiff
        return result  # 返回最长摆动子序列的长度

动态规划方法:

class Solution:
    def wiggleMaxLength(self, nums):
        dp = [[0, 0] for _ in range(len(nums))]  # 创建二维dp数组,用于记录摆动序列的最大长度
        dp[0][0] = dp[0][1] = 1  # 初始条件,序列中的第一个元素默认为峰值,最小长度为1
        for i in range(1, len(nums)):
            dp[i][0] = dp[i][1] = 1  # 初始化当前位置的dp值为1
            for j in range(i):
                if nums[j] > nums[i]:
                    dp[i][1] = max(dp[i][1], dp[j][0] + 1)  # 如果前一个数比当前数大,可以形成一个上升峰值,更新dp[i][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]
        return max(dp[-1][0], dp[-1][1])  # 返回最大的摆动序列长度

up-down 方法:(这个思路真的很巧妙,但是很难想到)

class Solution:
    def wiggleMaxLength(self, nums):
        if len(nums) <= 1:
            return len(nums)  # 如果数组长度为0或1,则返回数组长度
        
        up = down = 1  # 记录上升和下降摆动序列的最大长度
        for i in range(1, len(nums)):
            if nums[i] > nums[i-1]:
                up = down + 1  # 如果当前数比前一个数大,则可以形成一个上升峰值
            elif nums[i] < nums[i-1]:
                down = up + 1  # 如果当前数比前一个数小,则可以形成一个下降峰值
        
        return max(up, down)  # 返回上升和下降摆动序列的最大长度

53 最大子序和

当,连续子数组的和,为负数时,立即放弃。

122 买卖股票的最佳时机II

要意识到,利润是可以拆分到每一天的。
p3 - p1 = p3 - p2 + p2 - p1

取每一天的正利润即可。

跳跃游戏问题系列

跳跃问题的关键点在于,去计算可跳的覆盖范围。跳跃问题,不要去纠结每次究竟跳几步,而是看覆盖范围。

第一个跳跃问题,问是否可以到达终点,问题就可以转化为跳跃覆盖范围究竟可不可以覆盖到终点,每移动一个单位,就更新最大覆盖范围。

第二个跳跃问题,思路是,在当前的可移动范围内(即当前最大覆盖范围),尽可能多走,如果还没到终点,步数就加一。这里就需要统计两个覆盖范围,当前的最大覆盖和到目前为止的最大覆盖,如果移动下标到了当前的最大覆盖范围,但是没到终点,步数加一,当前最大覆盖更新为之前统计的最大覆盖。

第一题代码:

class Solution:
    def canJump(self, nums: List[int]) -> bool:
        cover = 0
        if len(nums) == 1: return True
        i = 0
        # python不支持动态修改for循环中变量,使用while循环代替
        while i <= cover:
            cover = max(i + nums[i], cover)
            if cover >= len(nums) - 1: return True
            i += 1
        return False

第二题代码:

class Solution:
    def jump(self, nums):
        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:  # 当前覆盖最远距离达到数组末尾,不用再做ans++操作,直接结束
                    break
        
        return ans

134 加油站

这道题的关键在于作差。主要学习代码随想录的方法二,方法一难以理解。

i从0开始累加rest[i],和记为curSum,一旦curSum小于零,说明[0, i]区间都不能作为起始位置,因为这个区间选择任何一个位置作为起点,到i这里都会断油,那么起始位置从i+1算起,再从0计算curSum。

那么为什么一旦[0,i] 区间和为负数,起始位置就可以是i+1呢,i+1后面就不会出现更大的负数?如果出现更大的负数,就是更新i,那么起始位置又变成新的i+1了。

那有没有可能 [0,i] 区间 选某一个作为起点,累加到 i这里 curSum是不会小于零呢?依然不可能,这种情况如果会出现,在之前也一定被考虑了。

那么局部最优:当前累加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)):
            curSum += gas[i] - cost[i]
            totalSum += gas[i] - cost[i]
            
            if curSum < 0:  # 当前累计剩余油量curSum小于0
                start = i + 1  # 起始位置更新为i+1
                curSum = 0  # curSum重新从0开始累计
        
        if totalSum < 0:
            return -1  # 总剩余油量totalSum小于0,说明无法环绕一圈
        return start

135 分发糖果

这是第一道,需要用两次遍历的题目,两边同时考虑一定会顾此失彼。

同时,本题的遍历顺序也很重要,不过我觉得这个点倒是不容易犯错,确定右边大于左边的情况,用正序遍历。确定左边大于右边的情况,用倒序遍历。

本题中,最关键的贪心思想体现在,初始化后(初始化的数组为全1数组),假如我们先进行正序遍历,得到了一个candy数组,那么在接下来的,进行倒序遍历的过程中,我们要对当前的candy[i]和candy[i+1]+1的值,二者取max,因为只有取max才能同时满足两边的情况。

class Solution:
    def candy(self, ratings: List[int]) -> int:
        candyVec = [1] * len(ratings)
        
        # 从前向后遍历,处理右侧比左侧评分高的情况
        for i in range(1, len(ratings)):
            if ratings[i] > ratings[i - 1]:
                candyVec[i] = candyVec[i - 1] + 1
        
        # 从后向前遍历,处理左侧比右侧评分高的情况
        for i in range(len(ratings) - 2, -1, -1):
            if ratings[i] > ratings[i + 1]:
                candyVec[i] = max(candyVec[i], candyVec[i + 1] + 1)
        
        # 统计结果
        result = sum(candyVec)
        return result

406 根据身高重建队列

本题很新颖,只要理解了题意就好做一些,后面要多看,总体思路就是,按身高排序,按Index插入。

本题同样是有两个维度的题,和前一题有某种程度的相似,看到这种题目,一定要想如何确定一个维度,然后再按照另一个维度重新排列。

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

引爆气球和无重叠区间

二者是同一类型的题目,其求解思路,都是一定要先排序,将最有可能重叠的区间,放在一起。然后要注意区间端点的判断细节,当A的右边界=B的左边界,题目到底是如何定义的。在最后,就是这类题相同的核心:当区间发生重叠后,注意更新重叠后的最小右边界,取min。

用箭引爆气球:

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]: # 气球i和气球i-1不挨着,注意这里不是>=
                result += 1     
            else:
                points[i][1] = min(points[i - 1][1], points[i][1]) # 更新重叠气球最小右边界
        return result

无重叠区间:
直接统计重叠区间的版本

class Solution:
    def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
        if not intervals:
            return 0
        
        intervals.sort(key=lambda x: x[0])  # 按照左边界升序排序
        count = 0  # 记录重叠区间数量
        
        for i in range(1, len(intervals)):
            if intervals[i][0] < intervals[i - 1][1]:  # 存在重叠区间
                intervals[i][1] = min(intervals[i - 1][1], intervals[i][1])  # 更新重叠区间的右边界
                count += 1
        
        return count

统计不重叠区间数量:(不重叠数量即为所需弓箭数)

class Solution:
    def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
        if not intervals:
            return 0
        
        intervals.sort(key=lambda x: x[0])  # 按照左边界升序排序
        
        result = 1  # 不重叠区间数量,初始化为1,因为至少有一个不重叠的区间
        
        for i in range(1, len(intervals)):
            if intervals[i][0] >= intervals[i - 1][1]:  # 没有重叠
                result += 1
            else:  # 重叠情况
                intervals[i][1] = min(intervals[i - 1][1], intervals[i][1])  # 更新重叠区间的右边界
        
        return len(intervals) - result


763 划分字母区间

本题的核心在于,先遍历字符串,统计每个字符的最后一次出现的index,然后遍历字符串,当 遍历下标==当前统计的字符中出现的最远下标 时,这就是分割出来的一个字符串了。

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

代码随想录中给出的另一种思路,我觉得很牵强。

56 合并区间

和前面,最小弓箭射气球的题目很像,只不过前一题是求min,本题是求max,同样,做这类题目时,不要忘记先排序,将最有可能合并的区间,放在一起。

不要忘记,每次符合合并条件时,区间右端点要取max,之前是取min,总之,合并区间的题,在符合合并条件后,一定要对当前右端点进行操作的,不管是min还是max,什么都不做一定是不对的。

class Solution:
    def merge(self, intervals):
        result = []
        if len(intervals) == 0:
            return result  # 区间集合为空直接返回

        intervals.sort(key=lambda x: x[0])  # 按照区间的左边界进行排序

        result.append(intervals[0])  # 第一个区间可以直接放入结果集中

        for i in range(1, len(intervals)):
            if result[-1][1] >= intervals[i][0]:  # 发现重叠区间
                # 合并区间,只需要更新结果集最后一个区间的右边界,因为根据排序,左边界已经是最小的
                result[-1][1] = max(result[-1][1], intervals[i][1])
            else:
                result.append(intervals[i])  # 区间不重叠

        return result

738 单调递增的数字

这道题,基本上就是学习代码随想录的思路了,当时会了之后,基本就记住了,这道题需要注意的坑,还是比较多的,比如:要倒序遍历(遍历顺序很关键,正序遍历是不行的),要用flag记录末尾9的个数(这只是一个小技巧)。

代码随想录的解析文章链接如下:
738 单调递增的数字

在这里插入代码片`class Solution:
    def monotoneIncreasingDigits(self, N: int) -> int:
        # 将整数转换为字符串
        strNum = str(N)
        # flag用来标记赋值9从哪里开始
        # 设置为字符串长度,为了防止第二个for循环在flag没有被赋值的情况下执行
        flag = len(strNum)
        
        # 从右往左遍历字符串
        for i in range(len(strNum) - 1, 0, -1):
            # 如果当前字符比前一个字符小,说明需要修改前一个字符
            if strNum[i - 1] > strNum[i]:
                flag = i  # 更新flag的值,记录需要修改的位置
                # 将前一个字符减1,以保证递增性质
                strNum = strNum[:i - 1] + str(int(strNum[i - 1]) - 1) + strNum[i:]
        
        # 将flag位置及之后的字符都修改为9,以保证最大的递增数字
        for i in range(flag, len(strNum)):
            strNum = strNum[:i] + '9' + strNum[i + 1:]
        
        # 将最终的字符串转换回整数并返回
        return int(strNum)`

968 监控二叉树

不多说了,这道题太牛逼了。

自己要明确两点:一定是用后序遍历,因为要利用左右孩子的信息。然后就是状态的定义,一共定义三个状态,有覆盖,无覆盖,有摄像头。

对空节点的讨论

因为在遍历树的过程中,就会遇到空节点,那么问题来了,空节点究竟是哪一种状态呢? 空节点表示无覆盖? 表示有摄像头?还是有覆盖呢?

回归本质,为了让摄像头数量最少,我们要尽量让叶子节点的父节点安装摄像头,这样才能摄像头的数量最少。

那么空节点不能是无覆盖的状态,这样叶子节点就要放摄像头了,空节点也不能是有摄像头的状态,这样叶子节点的父节点就没有必要放摄像头了,而是可以把摄像头放在叶子节点的爷爷节点上。

所以空节点的状态只能是有覆盖,这样就可以在叶子节点的父节点放摄像头了

代码随想录–监控二叉树

总结出一共只有四种情形

情况1:左右节点都有覆盖
情况2:左右节点至少有一个无覆盖的情况
情况3:左右节点至少有一个有摄像头
情况4:头结点没有覆盖

class Solution:
         # Greedy Algo:
        # 从下往上安装摄像头:跳过leaves这样安装数量最少,局部最优 -> 全局最优
        # 先给leaves的父节点安装,然后每隔两层节点安装一个摄像头,直到Head
        # 0: 该节点未覆盖
        # 1: 该节点有摄像头
        # 2: 该节点有覆盖
    def minCameraCover(self, root: 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)

        # 情况1: 左右节点都有覆盖
        if left == 2 and right == 2:
            return 0

        # 情况2:
        # left == 0 && right == 0 左右节点无覆盖
        # left == 1 && right == 0 左节点有摄像头,右节点无覆盖
        # left == 0 && right == 1 左节点无覆盖,右节点有摄像头
        # left == 0 && right == 2 左节点无覆盖,右节点覆盖
        # left == 2 && right == 0 左节点覆盖,右节点无覆盖
        if left == 0 or right == 0:
            result[0] += 1
            return 1

        # 情况3:
        # left == 1 && right == 2 左节点有摄像头,右节点有覆盖
        # left == 2 && right == 1 左节点有覆盖,右节点有摄像头
        # left == 1 && right == 1 左右节点都有摄像头
        if left == 1 or right == 1:
            return 2


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
第二十二算法训练营主要涵盖了Leetcode题目中的三道题目,分别是Leetcode 28 "Find the Index of the First Occurrence in a String",Leetcode 977 "有序数组的平方",和Leetcode 209 "长度最小的子数组"。 首先是Leetcode 28题,题目要求在给定的字符串中找到第一个出现的字符的索引。思路是使用双指针来遍历字符串,一个指向字符串的开头,另一个指向字符串的结尾。通过比较两个指针所指向的字符是否相等来判断是否找到了第一个出现的字符。具体实现的代码如下: ```python def findIndex(self, s: str) -> int: left = 0 right = len(s) - 1 while left <= right: if s[left == s[right]: return left left += 1 right -= 1 return -1 ``` 接下来是Leetcode 977题,题目要求对给定的有序数组中的元素进行平方,并按照非递减的顺序返回结果。这里由于数组已经是有序的,所以可以使用双指针的方法来解决问题。一个指针指向数组的开头,另一个指针指向数组的末尾。通过比较两个指针所指向的元素的绝对值的大小来确定哪个元素的平方应该放在结果数组的末尾。具体实现的代码如下: ```python def sortedSquares(self, nums: List[int]) -> List[int]: left = 0 right = len(nums) - 1 ans = [] while left <= right: if abs(nums[left]) >= abs(nums[right]): ans.append(nums[left ** 2) left += 1 else: ans.append(nums[right ** 2) right -= 1 return ans[::-1] ``` 最后是Leetcode 209题,题目要求在给定的数组中找到长度最小的子数组,

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值