解题步骤
1. 确定dp数组及其下标含义
2. 确定递归顺序
3. dp数组如何初始化
4. 确定遍历顺序
5. 举例推导dp数组
经典题目
类型一 爬楼梯、不同路径
1. 使用最小花费爬楼梯
给你一个整数数组 cost
,其中 cost[i]
是从楼梯第 i
个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。
你可以选择从下标为 0
或下标为 1
的台阶开始爬楼梯。
请你计算并返回达到楼梯顶部的最低花费。
动规五部曲:
(1)dp[i]的含义:到达第i台阶所花费的最少体力为dp[i]。
(2)递推公式:到达第i个台阶两种选择,一种是从前1个台阶来的,另一种是从前2个台阶来的
从前1个台阶来:dp[i-1]+cost[i-1]
从前2个台阶来:dp[i-2]+cost[i-2]
dp[i]=min(dp[i-1]+cost[i-1], dp[i-2]+cost[i-2]) 选取最小花费
(3)初始化:可以选择从下标为 0
或下标为 1
的台阶开始爬楼梯,因此dp[0]=0,dp[1]=0
(4)遍历顺序:从前向后,从下标2开始
(5)举例推导dp数组
2. 不同路径Ⅱ
一个机器人位于一个 m x n
网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用 1
和 0
来表示。
动规五部曲:
(1)dp[i][j]的含义:表示到达第i行第j列的方格有dp[i][j]条不同的路径
(2)递推公式:
如果遇到障碍物,dp[i][j]=0
否则,路径存在从上来或从左来两种,dp[i][j]=dp[i-1][j]+dp[i][j-1]
(3)初始化:第一行和第一列只存在一种路径,但如果有障碍物,障碍物后面的数值都为0
(4)遍历顺序:从上到下,从左到右
类型二 背包问题
1. 分割等和子集
只包含正整数 的 非空 数组 nums
,判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等
背包思路:
元素和相等 -> 背包容量为sum/2
数字大小 -> 物体重量、价值
数组能够分割为两个子集元素和相等 -> 找到总和为sum/2的子集
数字只能用一次 -> 背包的物体不可重复放入
动规五部曲:
(1)dp[i]的含义:表示容量为i的背包能够最大放入的元素和为dp[i]
(2)递推公式:dp[i]=max(dp[i], dp[i-n]+n)
(3)初始化:dp=[0]*(sum/2)
(4)遍历顺序:先物品后背包,背包倒序遍历
why?先背包:只能放入一个物品;背包正序:物品重复放入
(5)举例推导dp数组
2. 粉碎石头问题
背包思路:让石头分成重量相同的两堆,相撞之后剩下的石头最小,这样就化解成01背包问题,与分割等和子集的题目类似。不同点是分割等和子集问题是判断背包是否正好装满(dp[m]==m),本题是求背包最多能装多少((sum(stones)-dp[m])-dp[m])
3. 目标和
给定一个非负整数数组 nums
和一个整数 target
。向数组中的每个整数前添加 '+'
或 '-'
,然后串联起所有整数,可以构造一个 表达式 。返回可以通过上述方法构造的、运算结果等于 target
的不同 表达式 的数目。
解题思路:
通过加减nums的数,使得和等于target。在数组nums中,设加法之和为x,则减法有sum-x,那么x-(sum-x)=target,则x=(target+sum)//2
对于除号,如果非整除,则无法构造出等于target的表达式,方案数为0;如果给定的target本身大于nums之和,也无法构造出表达式,方案数为0。(特殊情况)
背包思路:
找出等于 target
-> 找出加法之和为(target+sum)//2 -> 背包容量为(target+sum)//2
nums数组的数-> 物品重量、物品价值
nums的每个数只能用一次 -> 物品不可重复使用 (01背包)
动规五部曲:
(1)dp[i]的含义:表示目标加法和为i的方案数为dp[i]
(2)递推公式:方案数 -> 组合数 在存在nums[i]的情况下,凑成dp[j]就有dp[j - nums[i]] 种方法
那么所有方案数就是列举存在每种物品下,每种方案数的累加和,即:
dp[j]+= dp[j-nums[i]]
(3)初始化:因为要累加,所以dp[0]=1
(4)遍历顺序:先物品后背包,背包倒序
(5)举例推导dp数组
**如果求组合数就是外层for循环遍历物品,内层for遍历背包
**如果求排列数就是外层for遍历背包,内层for循环遍历物品
4. 一和零
给你一个二进制字符串数组 strs
和两个整数 m
和 n
。请你找出并返回 strs
的最大子集的长度,该子集中 最多 有 m
个 0
和 n
个 1
。
背包思路:题目求解最大子集长度-> 背包最大价值问题
strs数组的元素 -> 物品
m和n -> 二维背包
0和1的数量 -> 物品重量
字符串个数 -> 物品价值
动规五部曲:
(1)dp[i][j]的含义:表示含有i个0和j个1的最大子集长度为dp[i][j]
(2)递推公式:dp[i][j]=max(dp[i][j], dp[i-zeronum][j-onenum]+1)
(3)初始化:物品价值不会是负数,初始为0
(4)遍历顺序:先物体后背包,背包倒序,背包的m和n遍历顺序无要求
(5)举例推导dp数组
5. 完全平方数
给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
给你一个整数 n ,返回和为 n 的完全平方数的 最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
背包思路:
背包容量->正整数n
物品->完全平方数(可以重复使用,完全背包)
物品重量->完全平方数的大小
物品价值->完全平方数本身的个数
动规五部曲:
(1)dp[i]的含义:和为n的完全平方数的最少数量为dp[i]
(2)递推公式:dp[i]=min(dp[i], dp[i-nums[i]]+1)
(3)初始化:最小值,初始化为 float('inf'),dp[0]=0
(4)遍历顺序:先物品后背包/先背包后物品都可以
6. 单词拆分
给你一个字符串 s
和一个字符串列表 wordDict
作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s
则返回 true
。
背包思路:
物品->单词
背包->字符串s
完全背包问题->拆分时可以重复使用字典中的单词
动规五部曲:
(1)dp[i]的含义:字符串长度为i时,能够否拆分为一个或多个在字典中出现的单词true/false
(2)递推公式:dp[j]=true,且[j,i]这个区间的子串出现在字典里,则dp[i]=true
(3)初始化:dp[0]=true
(4)遍历顺序:排列数:先背包后物品
(5)举例推导dp数组
背包问题总结
递推公式
1. 问能否装满背包或最多装多少?dp[j]=max(dp[j], dp[j-nums[j]]+nums[j])
2. 问装满背包有几种方法? dp[j]+=dp[j-nums[i]]
3. 问背包装满的最大价值? dp[j]=max(dp[j], dp[j-weight[j]]+value[i])
4. 问装满背包所有物品的最小个数? dp[j]=min(dp[j-coins[i]]+1,dp[j])
遍历顺序
01背包:二维dp数组两种都可以,第二层是从小到大遍历;一维dp数组只能先物品后背包,背包倒序遍历
完全背包:纯完全背包问题:一维dp数组两种都可以,第二次是从小到大遍历;求组合数:先物品后背包;求排列数:先背包后物品;求最小数:两种都可以
类型三 打劫问题
1. 题一
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,能够偷窃到的最高金额。
解题思路:
成环有三种情况:①不考虑尾元素;②不考虑首元素;③不考虑首尾元素。情况①②包含情况③
动规五部曲:
(1)dp[i]的含义:表示是考虑下标i(包括i)范围内的房子,能够偷窃的最高金额为dp[i]
(2)递推公式:dp[i]=max(dp[i-2]+nums[i], dp[i-1] )
对于房子i,偷:dp[i-2]+nums[i] (说明第i-1间房子没偷)
对于房子i,不偷:dp[i-1]
(3)初始化:dp[0]=nums[0];dp[1]=max(nums[0], nums[1])
(4)遍历顺序:dp依赖于i-2和i-1,因此从前向后遍历
(5)举例推导dp数组
2. 树形DP
小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root
。
除了 root
之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。
给定二叉树的 root
。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。
递归三部曲+动规五部曲:
(1)确定递归函数的参数和返回值
参数:当且节点
返回值:当前节点偷与不偷两个状态得到的金钱,长度为2的dp数组
(2)终止条件:空节点,偷与不偷都是0(相当于dp数组的初始化)
(3)遍历顺序:后序遍历(左右中)递归左节点,得到左节点偷与不偷的金钱;递归右节点,得到右节点偷与不偷的金钱
(4)单层递归逻辑:
不偷当前节点,则偷左右孩子,选一个最大的:val2=max(left[0],left[1])+max(right[0],right[1])
偷当前节点,左右孩子不能偷:val1=cur.val+left[0]+right[0]
最后当前节点的状态为 {val2,val1},即{不偷当前节点得到的最大金钱,偷当前节点得到的最大金钱}
类型四 买卖股票问题
1. 一只股票只能买卖一次
给定一个数组 prices
,它的第 i
个元素 prices[i]
表示一支给定股票第 i
天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0
动规五部曲:
(1)dp[i][0]的含义:表示第i天持有股票所得的最多现金;dp[i][1]的含义:表示dii天不持有股票所得的最多现金
(2)递推公式
dp[i][0]:保持现状:dp[i-1][0],当天买入:-prices[i],选最大值:dp[i][0]=max(dp[i-1][0],-prices[i])
dp[i][1]:保持现状:dp[i-1][1],当天卖出:dp[i-1][0]+prices[i],选最大值:dp[i][1]=max(dp[i-1][1],dp[i-1][0]+prices[i])
(3)初始化:dp[0][0]=-prices[0],dp[0][1]=0
(4)遍历顺序:从前到后
(5)举例推导dp数组
2. 一只股票可以买卖多次
给你一个整数数组 prices
,其中 prices[i]
表示某支股票第 i
天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润
动规五部曲:
(1)dp[i][0]的含义:表示第i天持有股票所得的最大现金;dp[i][1]的含义:表示第i天不持有股票所得的最大现金
(2)递推公式:
dp[i][0]:保持现状,即dp[i-1][0];当天买入股票,即dp[i-1][1]-prices[i],取两者最大值
dp[i][1]:保持现状,即dp[i-1][1];当天卖出股票,即dp[i-1][0]+prices[i],取两者最大值
(3)初始化:dp[0][0]=-prices[0];dp[0][1]=0
(4)遍历顺序:从前向后
(5)举例推导dp数组
**与题1的区别在于本题可以买卖多次,所以当天买入股票是用前一天不持有股票的现金减去当天股票价格,而题1只能买卖一次,当天买入股票则是-prices[i]
3. 一只股票买卖K次
给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
动规五部曲:
(1)dp[i][j]的含义:表示第i天状态为j,所得的最大现金为dp[i][j]
j的状态表示为:0不操作 1第一次买入 2第一次卖出 3第二次买入 4第二次卖出···
除0以外,偶数买入,奇数卖出
(2)递推公式:
j为偶数,有保持现状和当天买入两种情况:dp[i][j]=max(dp[i-1][j],dp[i-1][j-1]-prices[i])
j为奇数,有保持现状和当天卖出两种情况:dp[i][j]=max(dp[i-1][j],dp[i-1][j-1]+prices[i])
(3)初始化:偶数买入,初始化为-prices[0],奇数初始化为0
(4)遍历顺序:从前向后,依赖于i-1
(5)举例推导dp数组
4. 买卖包含冷冻期
三个操作:买入、卖出、冷冻
四个状态:买入股票(0)、保持卖出状态(1)、当天卖出(2)、冷冻期(3)
why?本题有冷冻期,而冷冻期的前一天,只能是 「今天卖出股票」状态,如果是 「不持有股票状态」那么就很模糊,因为不一定是 卖出股票的操作
动规五部曲:
(1)dp[i][j]的含义:表示第i天处于状态j所得的最大金额为dp[i][j]
dp[i][0]:第i天买入股票所得的最大金额
dp[i][1]:第i天保持卖出股票状态所得的最大金额
dp[i][2]:第i天卖出股票所得的最大金额
dp[i][3]:第i天处于冷冻期所得的最大金额
(2)递推公式
dp[i][0]=max(dp[i-1][0],max(dp[i-1][1],dp[i-1][3])-prices[i])
dp[i][1]=max(dp[i-1][1],dp[i-1][3])
dp[i][2]=dp[i-1][0]+prices[i]
dp[i][3]=dp[i-1][2]
(3)初始化:只有dp[0][0]=-prices[0],其他初始化为0
(4)递推顺序:从前向后
(5)举例推导dp数组
类型五 子序列问题
1. 最长递增子序列
给你一个整数数组 nums
,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7]
是数组 [0,3,1,6,2,2,7]
的子序列。
动规五部曲:
(1)dp[i]的含义:表示i之前包括i的以nums[i]为结尾的最长递增子序列的长度
(2)递推公式/状态转移方程
位置i的最长递增子序列等于j从0到i-1各个位置的最长递增子序列+1的最大值
if nums[i]>nums[j]: dp[i]=max(dp[i],dp[j]+1)
因为j是遍历0到i-1得来的,得到很多个dp[i],因此需要取最大值
(3)初始化:对于每一个i,对应的dp[i](即最长递增子序列)起始大小至少为1
(4)遍历顺序:从前向后
(5)举例推导dp数组
2. 最长连续递增子序列
动规五部曲:
(1)dp[i]的含义:表示以下标i为结尾(并非以0起始)的连续递增子序列的长度为dp[i]
(2)递推公式:if nums[i]>nums[i-1]: dp[i]=dp[i-1]+1
(3)初始化:以下标i为结尾的连续递增的子序列长度至少为1
(4)遍历顺序:从前向后
(5)举例推导dp数组
**与题1的区别:不连续递增子序列的跟前0-i 个状态有关,连续递增的子序列只跟前一个状态有关
3. 最长重复子数组(实际为连续子序列)
给两个整数数组 nums1
和 nums2
,返回 两个数组中 公共的 、长度最长的子数组的长度 。
动规五部曲:
(1)dp数组的含义:dp[i][j]表示以下标i-1为结尾的A和以下标j-1为结尾的B,最长重复子数组长度为dp[i][j] (遍历时i,j从1开始)
(2)递推公式:if A[i-1]==B[j-1]: dp[i][j]=dp[i-1][j-1]+1
(3)初始化:dp[i][0] 和dp[0][j]没有意义,dp[i][0] 和dp[0][j]初始化为0
(4)遍历顺序:先A后B或先B后A都行
(5)举例推导dp数组
4. 最长公共子序列/不相交的线
给定两个字符串 text1
和 text2
,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0
。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
- 例如,
"ace"
是"abcde"
的子序列,但"aec"
不是"abcde"
的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
动规五部曲:
(1)dp[i][j]的含义:表示长度为[0,i-1]的字符串text1和长度为[0,j-1]的字符串text2的最长公共子序列为dp[i][j]
(2)递推公式
text1[i-1]==text2[j-1]:找到公共元素,dp[i][j]=dp[i-1][j-1]+1
text1[i-1] != text2[j-1]:dp[i][j]=max(dp[i-1][j], dp[i][j-1])
(3)初始化:0
(4)遍历顺序:从前向后
(5)举例推导dp数组
5. 最大子序和
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
动规五部曲:
(1)dp[i]的含义:表示包括下标i(以nums[i]为结尾)的最大连续子序列和为dp[i]
(2)递推公式:dp[i]=max(dp[i-1]+nums[i], nums[i])
dp[i-1]+nums[i]:当前数字加入连续子序列和
nums[i]:重新计算当前连续子序列和
(3)初始化:dp[i]依赖于dp[i-1],dp[0]=nums[0]
(4)遍历顺序:从前向后
(5)举例推导dp数组
6. 乘积最大子数组
给你一个整数数组 nums
,请你找出数组中乘积最大的非空连续 子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
测试用例的答案是一个 32-位 整数。
解题思路
因为可能存在负数,负数x负数则为正,可能乘积更大,负数x正数则为负,乘积变小,因此需要记录最大值、最小值,然后比较乘积哪个更大
代码
class Solution:
def maxProduct(self, nums: List[int]) -> int:
if not nums: return
res=nums[0]
pre_max=nums[0]
pre_min=nums[0]
for i in range(1,len(nums)):
#记录最大值
cur_max=max(pre_max*nums[i],pre_min*nums[i],nums[i])
cur_min=min(pre_min*nums[i],pre_max*nums[i],nums[i])
res=max(res,cur_max)
pre_max=cur_max
pre_min=cur_min
return res
类型六 编辑距离
1. 判断子序列
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"
是"abcde"
的一个子序列,而"aec"
不是)。
动规五部曲:(类似于最长公共子序列)
(1)dp[i][j]的含义:以下标i-1为结尾的字符串s和以下标j-1为结尾的字符串t,相同子序列的长度为dp[i][j]
(2)递推公式
t中找到了一个在s中也出现的字符 if s[i-1]==t[j-1]: dp[i][j]=dp[i-1][j-1]+1
s的当前字符与t的当前字符不匹配 if s[i-1] != t[j-1] : dp[i][j]=dp[i][j-1]
**最长公共子序列的两个字符串都可以删元素,而本题只能删字符串t
(3)初始化:初始化为0
(4)遍历顺序:从前到后
(5)举例推导dp数组
**dp[i][j]以下标i-1为结尾的字符串s和以下标j-1为结尾的字符串t 相同子序列的长度,如果dp[s.size()][t.size()] 与 字符串s的长度相同说明,s与t的最长相同子序列就是s,那么s 就是 t 的子序列。
2. 不同的子序列
给你两个字符串 s
和 t
,统计并返回在 s
的 子序列 中 t
出现的个数,结果需要对 109 + 7 取模。
动规五部曲:
(1)dp[i][j]:表示以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]
(2)递推公式:
s[i-1]==t[j-1]:一部分用s[i-1]匹配,不需要考虑当前s子串和t子串的最后一位字母(对个数没有影响),个数为dp[i-1][j-1];另一部分不用s[i-1]匹配,个数为dp[i-1][j]。dp[i][j]=dp[i-1][j-1]+dp[i-1][j]
why?s:bagg 和 t:bag ,s[3] 和 t[2]是相同的,但是字符串s也可以不用s[3]来匹配,即用s[0]s[1]s[2]组成的bag,当然也可以用s[3]来匹配,即:s[0]s[1]s[3]组成的bag。
s[i-1]!=t[j-1]:不用s[i - 1]来匹配,在s中删除元素,即dp[i][j] = dp[i-1][j]
(3)初始化:dp[i][0] 表示:以i-1为结尾的s可以随便删除元素,出现空字符串的个数,dp[i][0]=1
dp[0][j]:空字符串s可以随便删除元素,出现以j-1为结尾的字符串t的个数,dp[0][j]=0
dp[0][0]=1,空字符串s,可以删除0个元素,变成空字符串t。
3. 编辑距离
给你两个单词 word1
和 word2
, 请返回将 word1
转换成 word2
所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
动规五部曲:
(1)dp[i][j]的含义:表示以i-1为结尾的word1转换成以j-1为结尾的word2使用的最少操作数(word1转换为word2和word2转换为word1是等价的)
(2)递推公式:dp[i][j]=min(dp[i-1][j]+1, dp[i][j-1]+1, dp[i-1][j-1]+1)
①如果word1[i-1]==word2[j-1],不需要操作,那么延续之前的,即dp[i-1][j-1]
②如果word1[i-1] !=word2[j-1],则需要增、删、替换
增:word1增加,即dp[i-1][j]+1;word2增加,即dp[i][j-1]+1
删:word1删相当于word2增,增和删是等价的,所以相同
替换:word1[i-1]==word2[j-1],等于dp[i-1][j-1],那么不相等替换时,则为dp[i-1][j-1]+1
(3)初始化:
dp[i][0]=i, dp[0][j]=j
(4)遍历顺序:从上到下,从左到右
(5)举例推导dp数组
类型七 回文子串
1. 回文子串
给你一个字符串 s
,请你统计并返回这个字符串中 回文子串 的数目。
回文字符串 是正着读和倒过来读一样的字符串。
子字符串 是字符串中的由连续字符组成的一个序列。
解题思路:判断一个子字符串(字符串的下表范围[i,j])是否回文,依赖于,子字符串(下表范围[i + 1, j - 1])) 是否是回文。如果s[i]==s[j],且子字符串[i+1,j-1]是回文,呢吗该字符串为回文
动规五部曲:
(1)二维dp[i][j]的含义:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false。
(2)递推公式:
如果s[i] != s[j]:dp[i][j]=False
如果s[i]==s[j]:下标i与j相同(a),是回文子串;下标i与j相差1(aa),是回文子串;下标i与j相差大于1(cabac),看i到j区间的回文子串是不是回文,即dp[i+1][j-1]是否为True
(3)初始化:False
(4)遍历顺序:从下到上,从左到右,因为dp[i+1][j-1]
2. 最长回文子序列
给你一个字符串 s
,找出其中最长的回文子序列,并返回该序列的长度。
子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
动规五部曲:
(1)dp[i][j]的含义:表示区间[i,j]的字符串的回文子序列最长为dp[i][j]
(2)递推公式
如果s[i]==s[j]:回文子串[i+1,j-1]基础上长度加2,即dp[i][j]=dp[i+1][j-1]+2
如果s[i] != s[j]:删除元素,选最大长度,即dp[i][j]=max(dp[i+1][j], dp[i][j-1])
(3)初始化:递推公式没有包含i=j的情况,因此当i=j,只有一个元素,此时回文长度为1
(4)遍历顺序:从递推公式可知,遍历从下到上,从左到右
(5)举例推导dp数组