打卡青训营开营第三天刷题记录,先放个当前记录图吧:
下面是一些AI辅助刷题小心得(陆续更新中): 1.追求数量快速通关类型:已完成的90道题中,我主要是按照算法类型完成的,其中位运算和和哈希表是较为简单的类型,AI辅助刷题主要提供的是函数的辅助。 2.借助AI辅助理清算法思路:动态规划和贪心算法。 为了系统一点,我想先在12月更新完动态规划的,希望将拿到一道动态规划的题,快速锁定类型,分析题意绘制状态图,得到状态转移方程。听起来是通俗的话,中间需要先系统熟悉动规题型的类型,然后对各类型典型例题进行分析(力扣热题100+AI刷题题库500道),直到熟练绘制动态规划状态图,得到状态转移方程,边界条件不遗漏。
动态规划题型主要为:
1. 线性DP
线性DP问题是最常见的动态规划问题,通常涉及一个序列或数组,状态转移只依赖于前一个或几个状态。
-
经典问题:
- 斐波那契数列:状态
dp[i]表示第i个斐波那契数。 - 最长递增子序列(LIS) :
dp[i]表示以第i个元素结尾的最长递增子序列的长度。 - 爬楼梯问题:每次可以爬1步或2步,状态
dp[i]表示到达第i级台阶的方法数。
- 斐波那契数列:状态
下面先拿力扣热题100作为例题分析:
该类主要以斐波那契数列为主,当前状态数组可以由前两个状态数组之和得到,即满足斐波那契数列的定义:F(n)=F(n-1)+F(n-2)
爬楼梯
问题分析:(以下按照较为规范的形式书写,实际写题,我习惯按照自己舒服的方式来)
-
状态定义:
dp[i]表示到达第i阶楼梯的不同方法数。 -
状态转移方程: 如果爬到第
i阶,可以从第i-1阶爬 1 阶,或者从第i-2阶爬 2 阶。因此:dp[i]=dp[i−1]+dp[i−2] -
边界条件:
dp[0] = 1dp[1] = 1
杨辉三角
问题分析:这道题也是斐波那契数列的变体,不过存储结构从一维数组,变为了二维数组,考虑使用下三角存储,而转化为数组后,我们将题目条件中的每个数是左上方和右上方数之和,转为是对角线上一个元素和正上方元素的和(丑丑的手绘了一个,不过我一般是这么梳理思路的,划重点,经常动规写状态转移方程晕晕的可拿去学!):
-
状态定义:
triangle[i]表示第i行的元素。triangle[i][j]表示第i行中第j个元素。
-
状态转移方程:
- 首尾元素:
triangle[i][0] = 1和triangle[i][i] = 1。 - 中间元素:
triangle[i][j] = triangle[i - 1][j - 1] + triangle[i - 1][j]。
- 首尾元素:
-
边界条件:
triangle[0][0] = 1,这是杨辉三角的起始条件。
打家劫舍
问题分析: 这道题也是斐波那契的变体,不过在当前状态由前两种状态结果决定时加入了选择
-
状态定义:
- 设
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])(比较前两个房屋的偷窃金额)
-
单词拆分
-
初始化:
- 创建一个布尔数组
dp,长度为 len(s)+1len(s) + 1len(s)+1,其中dp[0]表示空字符串,初始化为true。
- 创建一个布尔数组
-
状态转移:
- 对于每个位置
i从1到len(s),检查所有可能的前缀s[j:i],其中j从0到i-1。 - 如果
dp[j]为true(表示前j个字符可以拼接),并且s[j:i]在字典中,设置dp[i]为true。
- 对于每个位置
-
结果返回:
- 最终返回
dp[len(s)],它表示整个字符串是否可以由字典中的单词拼接。
- 最终返回
-
代码:
解法二:记忆化搜索
-
初始化:
- 定义一个辅助函数
canBreak(start)表示从start开始的子字符串s[start:]是否可以被拆分成字典中的单词。
- 定义一个辅助函数
-
状态转移方程:
-
从
start开始,尝试所有可能的分割位置end,检查s[start:end]是否在wordDict中:- 如果
s[start:end]在wordDict中,并且canBreak(end)为True,则canBreak(start)为True。
- 如果
-
如果尝试了所有
end都不能成功拆分,则返回False。
-
边界条件:
- 当索引到达字符串末尾时,表示已经成功拆分。
-
代码:
最长递增子序列
动态规划解法一:(时间复杂度 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)。
代码:
动态规划解法二: + 二分查找(时间复杂度 O(nlogn))
状态转移方程:
通过二分查找,确定每个 nums[i] 在 tails 中的插入位置 pos,分情况更新 tails。
- 如果
nums[i]比tails中所有元素都大,则将其添加到tails的末尾,表示我们找到了一条更长的递增子序列。 - 否则,将
nums[i]替换tails[pos],以保持当前长度pos+1的子序列的最小末尾值。
边界条件:
-
空数组情况:如果
nums为空,直接返回 0,因为没有递增子序列。 -
长度为 1 的情况:如果
nums只有一个元素,那么最长递增子序列长度就是 1。tails会在第一个元素遍历时直接添加此元素。
代码:
Python 的 bisect 模块的 bisect_left 函数,用于在一个已排序的列表中找到某个值的插入位置,使得插入后列表仍然有序。
解释 bisect_left
- 语法:
bisect_left(list, value)。 - 功能:返回
value应插入list的位置pos,使得插入后list仍然是有序的。如果value已经存在于list中,那么pos会指向value第一次出现的位置的索引(即插在已有元素的左边)。 - 适用条件:
bisect_left只能在有序的列表上使用(在无序列表上使用结果不正确)。