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

news2025/1/12 1:35:39

读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


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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JavaEE——Spring中存取Bean的注解

目录 一、存储Bean对象 1、定义 2、存储方式 &#xff08;1&#xff09;、类注解 【1】、Controller&#xff08;控制器存储&#xff09; 【2】、Service&#xff08;服务存储&#xff09; 【3】、Repository&#xff08;仓库存储&#xff09; 【4】、Component&#xf…

创造型模式-原型模式(场景体验-》方案解决===代码图解)

创造型模式-原型模式 创建重复对象-场景体验解决方案&#xff08;原型模式&#xff09;原型模式定义 创建重复对象-场景体验 今天来一个大客户&#xff0c;他要求帮他下100个订单。每个订单除了用户ID&#xff0c;和用户名不同之外&#xff0c;其他个人信息完全相同。 订单类 …

DASCTF 2023 0X401七月暑期挑战赛RE题解

比赛期间没有什么时间&#xff0c;赛后做的题。 TCP 这题最难&#xff0c;耗时最久&#xff0c;好像做出来的人不多。 程序开始有个初始化随机数的过程&#xff0c;数据写入qword_5060开始的48个字节。 这里是主函数&#xff0c;连接到服务器以后&#xff0c;先接收32个字节…

c函数学习

函数的概念 函数是c语言的功能单位&#xff0c;实现一个功能可以封装一个函数来实现。定义函数的时候一切以功能为目的&#xff0c;根据功能去定义函数的参数和返回值 函数的分类 从定义角度分类&#xff1a;库函数&#xff08;c库实现的&#xff09;&#xff0c;自定义函数&…

springboot集成

maven配置 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency><groupId>org.apache.commons</groupId><artifactId>…

小程序中vant-weapp时间选择使用方法

一、选择单个时间点&#xff1a; wxml&#xff1a; <van-celltitle"选择预约时间"value"{{ time }}"bind:click"onDisplay"/><van-calendarshow"{{ show }}"bind:close"onClose"bind:confirm"onConfirm"…

数学建模学习(3):综合评价类问题整体解析及分析步骤

一、评价类算法的简介 对物体进行评价&#xff0c;用具体的分值评价它们的优劣 选这两人其中之一当男朋友&#xff0c;你会选谁&#xff1f; 不同维度的权重会产生不同的结果 所以找到每个维度的权重是最核心的问题 0.25 二、评价前的数据处理 供应商ID 可靠性 指标2 指…

Redis应用(2)——Redis的项目应用(一):验证码 ---> UUID到雪花ID JMeter高并发测试 下载安装使用

目录 引出Redis的项目应用&#xff08;一&#xff09;&#xff1a;验证码1.整体流程2.雪花ID1&#xff09;UUID&#xff08;Universally Unique Identifier&#xff0c;通用唯一识别码&#xff09;2&#xff09;Twitter 的雪花算法&#xff08;SnowFlake&#xff09; 雪花ID优缺…

Jenkins常用管理功能配置 - 插件管理

Jenkins插件介绍 Jenkins是一个流行的开源持续集成/持续交付(CI/CD)工具&#xff0c;它有大量的插件来扩展其功能。这些插件可以用于构建、测试、部署和监控软件项目。下面是一些常用的Jenkins插件及其简单介绍和使用方法&#xff1a; 1. Git插件&#xff1a;允许Jenkins从Gi…

网络概念,《TCP/IP五层网络模型》与《数据的网络传输---“封装”与“分用”过程》

文章目录 概念协议协议分层TCP/IP五层网络模型数据的网络传输---“封装”与“分用”“封装”与“分用” 的过程 接收过程 概念 局域网&#xff1a;把一些设备通过交换机/路由器连接起来。 广域网&#xff1a;把更多的局域网也相互连接称为广域网。 交换机&#xff1a;交换机是…

采用桥接模式使虚拟机\笔记本\linux台式机互通

目录 一、环境&#xff1a;二、连接模式1. 桥接模式2. 主机共享模式3. NAT模式 三、配置1. 笔记本WIFI网络配置2. VM配置3.虚拟机配置3.1. 先看网络信息&#xff0c;确定修改ens333.2. 修改ens333.3. 重启网络 四、测试五、错误解决5.1 现象5.2 解决办法5.3 结果 一、环境&…

Spring中Bean的作用域和Spring生命周期

从前面的文章中我们看出Spring是用来存储和读取Bean的&#xff0c;因此Spring中Bean是最核心的资源&#xff0c;所以我们将对Bean进行深入的理解。 Bean的作用域 现在有一个公共的Bean&#xff0c;提供给了两个用户去使用&#xff0c;但是在使用过程中&#xff0c;用户一修改…

Android Studio 修改AVD模拟器文件默认存储路径

AndroidStudio默认的模拟器文件路径为&#xff1a;C:\Users\用户名\.android\avd路径&#xff0c;通常windows系统上&#xff0c;C盘不是太大&#xff0c;而avd文件却不小&#xff0c;通常几个GB&#xff0c;所以有必要将avd路径换到一个非系统盘。 更换方法如下&#xff1a;H…

LeetCode:6. N 字形变换

&#x1f34e;道阻且长&#xff0c;行则将至。&#x1f353; &#x1f33b;算法&#xff0c;不如说它是一种思考方式&#x1f340; 算法专栏&#xff1a; &#x1f449;&#x1f3fb;123 题解目录 一、&#x1f331;[6. N 字形变换](https://leetcode.cn/problems/zigzag-conv…

5.3 Bootstrap 模态框(Modal)插件

文章目录 Bootstrap 模态框&#xff08;Modal&#xff09;插件用法选项方法事件 Bootstrap 模态框&#xff08;Modal&#xff09;插件 模态框&#xff08;Modal&#xff09;是覆盖在父窗体上的子窗体。通常&#xff0c;目的是显示来自一个单独的源的内容&#xff0c;可以在不离…

C语言:杨氏矩阵中查找某数(时间复杂度小于O(N))

题目&#xff1a; 有一个数字矩阵&#xff08;二维数组&#xff09;&#xff0c; 矩阵的每行从左到右是递增的&#xff0c;矩阵从上到下是递增的&#xff0c; 请编写程序在这样的矩阵中查找某个数字是否存在&#xff0c; 要求&#xff1a;时间复杂度小于O(N)。 思路&#xff1…

Linux中docker的基本操作

文章目录 一、docker概述1.1 什么是docker1.2 Docker与虚拟机的特性区别1.3 容器在内核中支持2种重要技术1.4 docker的核心概念 二、安装docker三、Docker 镜像操作四、Docker 容器操作 一、docker概述 1.1 什么是docker 是一个开源的应用容器引擎&#xff0c;基于go语言开发…

Spring Web MVC 详解(1)

目录 一、介绍 MVC 二、Spring MVC 的三个基本功能 1.1 连接功能 1.2 Spring MVC 的创建和使用 1.3 RequestMappig 介绍 1.4 Spring MVC 实现用户和 Spring 程序的连接 1.5 GetMapping 和 PostMaping 注解 1.6 Get 和 Post请求注解的多种写法 2.1 获取请求中参数的功…

Redis的内存回收与内存淘汰策略

对于redis这样的内存型数据库而言&#xff0c;如何删除已过期的数据以及如何在内存满时回收内存是一项很重要的工作。 常见的redis内存回收的工作主要分为两个方面&#xff1a; 清理过期的key在内存不足时回收到足够的内存用以存储新的key 清理过期的key 我们很少在redis中…

直接插入排序、希尔排序、直接选择排序、堆排序、冒泡排序——“数据结构与算法”

各位CSDN的uu们你们好呀&#xff0c;今天小雅兰的内容是数据结构与算法啦&#xff0c;是排序&#xff01;&#xff01;&#xff01;下面&#xff0c;让我们进入七大排序的世界吧&#xff01;&#xff01;&#xff01; 排序的概念及其运用 排序的概念 排序&#xff1a;所谓排序…