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

读PDF复习环节1

  • 本博客的内容只是做一个大概的记录,整个PDF看下来,内容上是不如代码随想录网站上的文章全面的,并且PDF中有些地方的描述,是很让我疑惑的,在困扰我很久后,无意间发现,其网站上的讲解完全符合我的思路。这次看完这些PDF后,暂时一段时间内不会再看了,要复习还是依靠,代码随想录网站,视频,和我自己写的博客吧
  • 动态规划章节
    • 动态规划五部曲
    • 有一些情况是,递推公式决定了dp数组要如何初始化
    • 不要盲目追求空间压缩,以现在的水平,先把最能体现动态规划思维过程的代码写熟练了再说
    • 746 使用最少花费爬楼梯
    • 62 不同路径
    • 343. 整数拆分
    • 96 不同的二叉搜索树
    • 背包问题的暴力解法
    • 目前的认知是:原版二维dp数组背包,遍历区间要完全,要有if判断,不满足放入条件就等于上一个值。实用版一维dp数组背包,可以在for循环中的遍历区间稍作修改,不遍历不满足放入条件的情况。
    • 1049.最后一块石头的重量II
    • 494.目标和
    • 题目做到这里,也发现了,背包问题,能用一维数组的,就按照惯性用一维数组过吧,二维数组要处理的细节更多,且初始化部分,可能要根据递推公式确定初始值,有些初始值很难赋予意义。二维数组的坑也更多。
    • 474 一和零
    • 01背包和完全背包的遍历顺序
    • 完全背包问题存在的疑问!在求排列数的相关问题上,为什么用二维DP数组,写不出来?
    • 求装满背包有多少种方法,递推公式一般都是,dp[i]+=dp[i-nums[j]]
    • 求组合数的经典题目:零钱兑换
    • 求排列数的经典题目:组合总和
    • 爬楼梯(进阶版)
    • 后面两题,零钱兑换和完全平方数,是动态规划题目中,求满足条件的最小数目的题目
    • 139 单词拆分
    • 打家劫舍问题,需要学习的就两个点,处理环形数据的方法就是拆分为两段,分别计算,不带头,不带尾。还有就是,如何利用动态规划求解二叉树结构的题目。
    • 买卖股票系列
    • 子序列系列、编辑距离系列、回文子串系列
  • 贪心算法章节
    • 376 摆动序列
    • 53 最大子序和
    • 122 买卖股票的最佳时机II
    • 跳跃游戏问题系列
    • 134 加油站
    • 135 分发糖果
    • 406 根据身高重建队列
    • 引爆气球和无重叠区间
    • 763 划分字母区间
    • 56 合并区间
    • 738 单调递增的数字
    • 968 监控二叉树
      • 对空节点的讨论
      • 总结出一共只有四种情形

本博客的内容只是做一个大概的记录,整个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


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/43160.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

使用rknn-toolkit2把YOLOV5部署到OK3588上

使用rknn-toolkit2把YOLOV5部署到OK3588上 虚拟环境搭建软件包安装在PC机上运行yolov5目标检测 虚拟环境搭建 首先在PC的ubuntu系统安装虚拟环境&#xff1a; 我的服务器是ubuntu18.04版本&#xff0c;所以安装python3.6 conda create -n ok3588 python3.6 需要键盘输入y&…

Python 算法基础篇:插入排序和希尔排序

Python 算法基础篇&#xff1a;插入排序和希尔排序 引言 1. 插入排序算法概述2. 插入排序算法实现实例1&#xff1a;插入排序 3. 希尔排序算法概述4. 希尔排序算法实现实例2&#xff1a;希尔排序 5. 插入排序与希尔排序的对比总结 引言 插入排序和希尔排序是两种常用的排序算法…

Day 61-62 决策树(ID3)

代码&#xff1a; package dl;import java.io.FileReader; import java.util.Arrays; import weka.core.*;/*** The ID3 decision tree inductive algorithm.*/ public class ID3 {/*** The data.*/Instances dataset;/*** Is this dataset pure (only one label)?*/boolean …

结构型模式 - 适配器模式

概述 如果去欧洲国家去旅游的话&#xff0c;他们的插座如下图最左边&#xff0c;是欧洲标准。而我们使用的插头如下图最右边的。因此我们的笔记本电脑&#xff0c;手机在当地不能直接充电。所以就需要一个插座转换器&#xff0c;转换器第1面插入当地的插座&#xff0c;第2面供…

springboot+vue开发后台增删改查

效果图 前端代码【User.vue】 <template><div class"data-container"><!--添加 start--><div class"data-header"><el-button round click"addHander" size"large" type"primary"><el-ic…

区间预测 | MATLAB实现QRBiLSTM双向长短期记忆神经网络分位数回归多输入单输出区间预测

区间预测 | MATLAB实现QRBiLSTM双向长短期记忆神经网络分位数回归多输入单输出区间预测 目录 区间预测 | MATLAB实现QRBiLSTM双向长短期记忆神经网络分位数回归多输入单输出区间预测效果一览基本介绍模型描述程序设计参考资料 效果一览 基本介绍 区间预测 | MATLAB实现QRBiLSTM…

uview中的常用的框

第一步&#xff1a; 先下载 uview UI 框架 详见 项目 引入 uView_vue引入uview_qq_2524963996的博客-CSDN博客【代码】 项目 引入 uView。_vue引入uviewhttps://blog.csdn.net/qq_44161703/article/details/131168976?spm1001.2014.3001.5501 第二步&#xff1a; 可以直接…

解锁潜力,驭数赋能:大数据与云计算的强强联合

随着数字化时代的来临&#xff0c;大数据和云计算已成为信息技术领域的两大热门话题。大数据指的是以海量、高速、多样化的数据为基础&#xff0c;通过分析和挖掘来获得有价值的信息和洞察。而云计算则是一种基于网络的计算模式&#xff0c;通过将数据和应用程序存储在云端服务…

【前端动画】科技感扫描效果 css动画animation

扫描出现动画 参考了网友写的二维码扫描 <template><div><div class"scan-content"><img style"width: 2rem;height: 2rem;" src"../../assets/images/eye.png" alt"" /><div class"line">…

Express 框架的基本操作

目录 1、应用生成器 2、基本路由 2.1、在跟路由下配置 GET请求&#xff0c;返回对应相应内容。 2.2、在跟路由下配置 POST请求&#xff0c;返回对应相应内容。 2.3、在跟路由下配置 PUT请求&#xff0c;返回对应相应内容。 2.4、在根路由下配置DELETE请求&#xff0c;返回对…

<Java物联网> 从主动到被动:Java中的BACnet设备属性查询

目录 BACnet 使用软件 资源 模拟器 使用Java主动查 引入maven 创建网络对象 获取远程设备 获取设备属性 使用DeviceEventAdapter订阅 初始化本地BACnet设备和IP网络配置&#xff1a; 启动本地设备和添加监听器&#xff1a; 搜寻远程设备&#xff1a; 发送订阅COV报…

mybatis事物是如何和spring事物整合的

目录 1、mybatis事物管理器 2、SpringManagedTransactionFactory如何处理事物 3、spring事物如何设置connection连接到threadLocal 1、mybatis事物管理器 mybatis事物抽象接口类&#xff1a;Transaction。该接口定义了事物基本方法和获取数据库连接方法 该类有三个实现类Jd…

基于Java+SpringBoot+Vue前后端分离旅游网站详细设计和实现

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…

pytorch工具——使用pytorch构建一个分类器

目录 分类器任务和数据介绍训练分类器的步骤在GPU上训练模型 分类器任务和数据介绍 训练分类器的步骤 #1 import torch import torchvision import torchvision.transforms as transformstransformtransforms.Compose([transforms.ToTensor(),transforms.Normalize((0.5,0.5,0.…

docker数据网络管理

数据管理 管理 Docker 容器中数据主要有两种方式&#xff1a;数据卷&#xff08;Data Volumes&#xff09;和数据卷容器&#xff08;DataVolumes Containers&#xff09;。 1&#xff0e;数据卷 数据卷是一个供容器使用的特殊目录&#xff0c;位于容器中。可将宿主机的目录挂…

详细解析python视频选择--【思维导图知识范围】

C ,JAVA JAVAWEB ,微信小程序等 都有视频选择的分析。 语言视频选择收录专辑链接C张雪峰推荐选择了计算机专业之后-在大学期间卷起来-【大学生活篇】JAVA黑马B站视频JAVA部分的知识范围、学习步骤详解JAVAWEB黑马B站视频JAVAWEB部分的知识范围、学习步骤详解SpringBootSpringB…

Qt/C++音视频开发48-推流到rtsp服务器

一、前言 之前已经打通了rtmp的推流&#xff0c;理论上按照同样的代码&#xff0c;只要将rtmp推流地址换成rtsp推流地址&#xff0c;然后格式将flv换成rtsp就行&#xff0c;无奈直接遇到协议不支持的错误提示&#xff0c;网上说要换成rtp&#xff0c;换了也没用&#xff0c;而…

斯坦福数据挖掘教程·第三版》读书笔记(英文版)Chapter 13 Neural Nets and Deep Learning

来源&#xff1a;《斯坦福数据挖掘教程第三版》对应的公开英文书和PPT Chapter 13 Neural Nets and Deep Learning In this chapter, we shall consider the design of neural nets, which are collections of perceptrons, or nodes, where the outputs of one rank (or lay…

使用 Docker 快速上手中文版 LLaMA2 开源大模型

本篇文章&#xff0c;我们聊聊如何使用 Docker 容器快速上手朋友团队出品的中文版 LLaMA2 开源大模型&#xff0c;国内第一个真正开源&#xff0c;可以运行、下载、私有部署&#xff0c;并且支持商业使用。 写在前面 感慨于昨天 Meta LLaMA2 模型开放下载之后&#xff0c;Git…