青训营X豆包MarsCode 技术训练营刷题技巧(动态规划专题篇一) | 豆包MarsCode AI 刷题

139 阅读6分钟

打卡青训营开营第三天刷题记录,先放个当前记录图吧:

image.png

下面是一些AI辅助刷题小心得(陆续更新中): 1.追求数量快速通关类型:已完成的90道题中,我主要是按照算法类型完成的,其中位运算和和哈希表是较为简单的类型,AI辅助刷题主要提供的是函数的辅助。 2.借助AI辅助理清算法思路:动态规划和贪心算法。 为了系统一点,我想先在12月更新完动态规划的,希望将拿到一道动态规划的题,快速锁定类型,分析题意绘制状态图,得到状态转移方程。听起来是通俗的话,中间需要先系统熟悉动规题型的类型,然后对各类型典型例题进行分析(力扣热题100+AI刷题题库500道),直到熟练绘制动态规划状态图,得到状态转移方程,边界条件不遗漏。

动态规划题型主要为:

动态规划题型分类.png

1. 线性DP

线性DP问题是最常见的动态规划问题,通常涉及一个序列或数组,状态转移只依赖于前一个或几个状态。

  • 经典问题

    • 斐波那契数列:状态dp[i]表示第i个斐波那契数。
    • 最长递增子序列(LIS)dp[i]表示以第i个元素结尾的最长递增子序列的长度。
    • 爬楼梯问题:每次可以爬1步或2步,状态dp[i]表示到达第i级台阶的方法数。

下面先拿力扣热题100作为例题分析:

该类主要以斐波那契数列为主,当前状态数组可以由前两个状态数组之和得到,即满足斐波那契数列的定义:F(n)=F(n-1)+F(n-2)

爬楼梯

image.png

问题分析:(以下按照较为规范的形式书写,实际写题,我习惯按照自己舒服的方式来)

  • 状态定义dp[i] 表示到达第 i 阶楼梯的不同方法数。

  • 状态转移方程: 如果爬到第 i 阶,可以从第 i-1 阶爬 1 阶,或者从第 i-2 阶爬 2 阶。因此:

    dp[i]=dp[i−1]+dp[i−2]

  • 边界条件

    dp[0] = 1

    dp[1] = 1

杨辉三角

image.png

问题分析:这道题也是斐波那契数列的变体,不过存储结构从一维数组,变为了二维数组,考虑使用下三角存储,而转化为数组后,我们将题目条件中的每个数是左上方和右上方数之和,转为是对角线上一个元素和正上方元素的和(丑丑的手绘了一个,不过我一般是这么梳理思路的,划重点,经常动规写状态转移方程晕晕的可拿去学!):

8cfa59dd5b330ae71b970bda2709dd0.jpg

  • 状态定义

    • triangle[i] 表示第 i 行的元素。
    • triangle[i][j] 表示第 i 行中第 j 个元素。
  • 状态转移方程

    • 首尾元素:triangle[i][0] = 1triangle[i][i] = 1
    • 中间元素:triangle[i][j] = triangle[i - 1][j - 1] + triangle[i - 1][j]
  • 边界条件

    • triangle[0][0] = 1,这是杨辉三角的起始条件。

打家劫舍

image.png

问题分析: 这道题也是斐波那契的变体,不过在当前状态由前两种状态结果决定时加入了选择

  • 状态定义

    • dp[i] 为偷到第 i 房屋时的最大金额。
  • 状态转移方程

    • 对于每一栋房屋,有两种选择:

      • 偷第 i 房屋:则不能偷第 i-1 房屋,因此金额为 nums[i] + dp[i-2]
      • 不偷第 i 房屋:则金额为 dp[i-1]
    • 综合这两种选择,我们得到状态转移方程: dp[i]max(dp[i−1],nums[i]+dp[i−2])

  • 边界条件

    • dp[0]=nums[0](偷第一个房屋的金额)

    • dp[1]=max(nums[0],nums[1])(比较前两个房屋的偷窃金额)

单词拆分

image.png

  • 初始化

    • 创建一个布尔数组 dp,长度为 len(s)+1len(s) + 1len(s)+1,其中 dp[0] 表示空字符串,初始化为 true
  • 状态转移

    • 对于每个位置 i1len(s),检查所有可能的前缀 s[j:i],其中 j0i-1
    • 如果 dp[j]true(表示前 j 个字符可以拼接),并且 s[j:i] 在字典中,设置 dp[i]true
  • 结果返回

    • 最终返回 dp[len(s)],它表示整个字符串是否可以由字典中的单词拼接。
  • 代码

image.png

解法二:记忆化搜索
  • 初始化

    • 定义一个辅助函数 canBreak(start) 表示从 start 开始的子字符串 s[start:] 是否可以被拆分成字典中的单词。
  • 状态转移方程

  • start 开始,尝试所有可能的分割位置 end,检查 s[start:end] 是否在 wordDict 中:

    • 如果 s[start:end]wordDict 中,并且 canBreak(end)True,则 canBreak(start)True
  • 如果尝试了所有 end 都不能成功拆分,则返回 False

  • 边界条件

    • 当索引到达字符串末尾时,表示已经成功拆分。
  • 代码

image.png

最长递增子序列

image.png

动态规划解法一:(时间复杂度 O(n2)O(n^2)O(n2)

初始化:每个 dp[i] 初始化为1,因为每个元素自身可以形成一个长度为1的递增子序列。 状态转移:对于每个元素 nums[i],遍历其前面的所有元素 nums[j](其中 j < i),如果 nums[i] > nums[j],说明 nums[i] 可以接在 nums[j] 之后形成一个更长的递增子序列,则更新 dp[i] = max(dp[i], dp[j] + 1)

image.png

代码image.png

动态规划解法二: + 二分查找(时间复杂度 O(nlog⁡n))

状态转移方程

通过二分查找,确定每个 nums[i]tails 中的插入位置 pos,分情况更新 tails

  • 如果 nums[i]tails 中所有元素都大,则将其添加到 tails 的末尾,表示我们找到了一条更长的递增子序列。
  • 否则,将 nums[i] 替换 tails[pos],以保持当前长度 pos+1 的子序列的最小末尾值。

边界条件

  • 空数组情况:如果 nums 为空,直接返回 0,因为没有递增子序列。

  • 长度为 1 的情况:如果 nums 只有一个元素,那么最长递增子序列长度就是 1。tails 会在第一个元素遍历时直接添加此元素。

代码

image.png

Python 的 bisect 模块的 bisect_left 函数,用于在一个已排序的列表中找到某个值的插入位置,使得插入后列表仍然有序。

解释 bisect_left

  • 语法bisect_left(list, value)
  • 功能:返回 value 应插入 list 的位置 pos,使得插入后 list 仍然是有序的。如果 value 已经存在于 list 中,那么 pos 会指向 value 第一次出现的位置的索引(即插在已有元素的左边)。
  • 适用条件bisect_left 只能在有序的列表上使用(在无序列表上使用结果不正确)。