目录
斐波那契数列
509. 斐波那契数
dp定义:dp[i] 代表F(i)
递推公式:dp[i] = dp[i - 1] + dp[i - 2]
初始化:dp[0] = 0; dp[1] = 1;
70. 爬楼梯(数量和)
dp[i] 表示走到第 i 个楼梯的方法数目
第 i 个楼梯可以从第 i-1 和 i-2 个楼梯再走一步到达,走到第 i 个楼梯的方法数为走到第 i-1 和第 i-2 个楼梯的方法数之和
dp[i] = dp[i - 1] + dp[i - 2]
不考虑dp[0], 初始化dp[1] 和 dp[2]
746. 使用最小花费爬楼梯(最小值)
dp[i] 代表到达第i个台阶最低花费
到达i层,要么从i-1层走一步,要么从i-2层走两步
即,dp[i] = dp[i-1] + cost[i-1] 或 dp[i] = dp[i-2] + cost[i-2]
dp[i] = min( dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2] )
你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯,所以到达第0个台阶或第一个台阶不花费费用:dp[0] = 0; dp[1] = 0;
343. 整数拆分(最大值)
dp[i] 代表将i拆分为多个正整数的乘积
假设0<j<i, 那么i可以拆为j 和 i-j两个数,那么dp[i] 可以为 j *(i-j) 也可以为 dp[j] * (i-j)
j的值 要从1到 i 一个一个的试,看哪个 j 对应的 dp[i] 最大
dp[i] = max( j*(i-j), dp[j]*(i-j) )
for(int i = 3; i <= n; i++){
for(int j = 1; j < i; j++){
//找最大的dp[i]
dp[i] = Math.max(dp[i],Math.max(j*(i-j), dp[j]*(i-j)));
}
}
96. 不同的二叉搜索树(数量和)
dp[i] 代表i个结点组成的二叉树的个数, f(k)代表在 dp[i] 中以k为头结点的二叉树的个数
则dp[i] = f(1) + f(2) + ... + f(i)
每个f(k)中都有i个结点,左子树的结点是[1, k-1]共k - 1个结点,右子树的结点是[k+1, i]共有i - k个结点
所以左子树的结点可以构成 dp[k-1] 个平衡二叉树,右子树的结点可以构成 dp[i-k] 个平衡二叉树
根据排列组合,f(k) = dp[k - 1] * dp[i - k] (1 <= k <= i)
综上所述,dp[n] = (1 <= i <= n)
class Solution {
public int numTrees(int n) {
/**
* G(n)代表n个结点组成的二叉树的个数
* f(i)代表在G(n)以i为头结点的二叉树的个数
* 则G(n) = f(1)+f(2)+...+f(n)
* 在f(i)中,头结点左边有i-1个结点,右边有n-i个结点
* 则f(i) = G(i-1)*G(n-i)
* 最终:G(n) = G(0)G(n-1)+G(1)G(n-2)+...+G(n-1)G(0)
*/
int[] dp = new int[n+1];
dp[0] = 1;
dp[1] = 1;
for(int i = 2; i <= n; i++){
//G(i-1)*G(n-i) 相当于 dp[j-1]*dp[i-j]
for(int j = 1; j <= i; j++){
//把i=1,j=1代入,需要初始化dp[0]和dp[1]
dp[i] = dp[i] + dp[j-1]*dp[i-j];
}
}
return dp[n];
}
}
62. 不同路径(数量和)
dp[i][j] 代表到达坐标(i,j)不同的路径数量
到达左边位置的路径数量 + 到达上边位置的路径数量
dp[i][j] = dp[i-1][j] + dp[i][j-1]
初始化:dp[i][j]由上边和左边推导而来,初始化第一行和第一列
63. 不同路径 II(数量和)
dp[i][j] 代表到达坐标(i,j)不同的路径数量
石头在(i, j)的左边,只能从上边到达,dp[i][j] = dp[i-1][j]
石头在(i, j)的上边,只能从左边到达,dp[i][j] = dp[i][j-1]
如果我们把石头位置的 dp[i][j] 设置为 0
那么就不用管石头在哪了,直接dp[i][j] = dp[i-1][j] + dp[i][j-1],即碰到石头就跳过不进行计算
初始化还是第一行第一列
01背包
416. 分割等和子集(最大值)
背包容量就是所有元素的和的一半,判断能不能把背包装满
每个数只能选一次,01背包
dp[i] 背包容量为i时最多能装多少重量的物品
一维dp数组下,
选择不装入,dp[j] = dp[j]
选择装入,dp[j] = 背包剩余容量最多装入 + 当前物品重量 = dp[j-nums[i]] + nums[i]
即,dp[j] = Math.max(dp[j], dp[j-nums[i]] + nums[i])
一维dp数组下,先遍历物品再遍历容量,容量倒序遍历
初始化dp[0] = 0, 背包容量为0时最多能装重量为0
最后 dp[总和二分之一] == 总和二分之一,可以分为两个相等的集合
1049. 最后一块石头的重量 II(最大值)
求两堆石头的最小差值,一堆装入背包,一堆不装入背包
背包容量为石头总重量的二分之一,物品是石头
每个石头只能选一次,01背包
dp[i] 背包容量为i时最多能装多少重量的石头
一维dp数组下,
选择不装入,dp[j] = dp[j]
选择装入,dp[j] = 背包剩余容量最多装入 + 当前石头重量 = dp[j-stones[i]] + stones[i]
即,dp[j] = Math.max(dp[j], dp[j-stones[i]] + stones[i])
一维dp数组下,先遍历石头再遍历容量,容量倒序遍历
初始化dp[0] = 0, 背包容量为0时最多能装重量为0
一堆石头放入背包重量为 dp[sum/2], 另一堆石头没进背包重量为 sum-dp[sum/2]
两堆石头的差值 就是 结果
494. 目标和(数量和)
+号堆的和为x,-号堆的和为sum-x
x - (sum - x) = target x = (sum + target)/2
问题转化为:从nums中选一些数,使得这些数的和为(sum + target)/2
每个数只能选一次,01背包
dp[i] 背包容量为i时有几种选择nums的方法
一维dp数组下,
选择 不选,dp[j] = dp[j]
选择 选,dp[j] = 背包容量为 j-nums[i] 时有几种选择nums的方法 = dp[j-nums[i]]
即,dp[j] = dp[j] + dp[j-nums[i]]
一维dp数组下,先遍历物品再遍历容量,容量倒序遍历
初始化dp[0] = 1, 背包容量为0时有1种选择nums的方法就是都不选
474. 一和零(最大值)
选哪几个字符串组成一个字符数组,满足最多 有 m 个 0 和 n 个 1,且数组长度最长
dp[i][j] 字符数组中最多有i个0,j个1的最大长度
对于一个字符串,有x个0, y个1
若x > i 或 y > j , 无法加入当前字符数组, 因为字符数组中最多允许有i个0,j个1
若i >= x 且 j >= y, 可以加入
若不加入字符数组,dp[i][j] = dp[i][j]
若加入字符数组,dp[i][j] = 剩余容量最长 + 1 = dp[i - x][j - y] + 1
(为什么+1? 加入一个字符串长度肯定要+1)
(dp[i - x][j - y]? 加进来后,0的位数占了x,1的位数占了y,剩余容量就是i-x和j-y)
最终取值:看加入的值大 还是 不加入的大,即max一下
dp[i][j] = Math.max(dp[i][j], dp[i - x][j - y] + 1);
遍历顺序:遍历字符数组在外层,m和n就是容量在内层倒序遍历
初始化:字符数组中最多有0个0,0个1的最大长度为0, dp[0][0] = 0;
完全背包
518. 零钱兑换 II(组合数量和)
dp[j] 组成金额j的方案数量(方案数量 公式是相加)
每一种面额的硬币有无限个,完全背包
不选,dp[j] = dp[j]
选,dp[j] = dp[j - coins[i]]
dp[j] = dp[j] + dp[j - coins[i]];
遍历顺序:完全背包都是正序遍历,物品在外背包在内求组合,反之求排列
初始化:组成金额0的方案数量为1,dp[0] = 1
377. 组合总和 Ⅳ(排列数量和)
dp[i] 代表总和为 i 的元素排列的个数
数组元素可以重复选,完全背包
不选,dp[j] = dp[j]
选,dp[j] = dp[j - nums[i]]
dp[j] = dp[j] + dp[j - nums[i]];
遍历顺序:完全背包都是正序遍历,物品在外背包在内求组合,反之求排列
初始化:总和为 0的元素排列的个数为1,dp[0] = 1
322. 零钱兑换(最小值)
dp[i] 代表凑成金额 i 所需的最少硬币数量
不选数量不变: dp[j] = dp[j]
选数量+1: dp[j] = 金额 j-coins[i] 所需最少数量 + 1 = dp[j - coins[i]] + 1
遍历顺序:完全背包都是正序遍历,物品在外背包在内求组合,反之求排列
初始化:凑成0 需要0个硬币,dp[0] = 1
279. 完全平方数(最小值)
物品是完全平方数, n是背包的容量
完全平方数可以重复使用,完全背包
dp[i] 和为 i 的完全平方数的最少数量
不选,数量不变:dp[j] = dp[j]
选,数量+1: dp[j] = dp[ j - i*i ] + 1
dp[j] = Math.min(dp[j], dp[j - i*i] + 1)
遍历顺序:完全背包都是正序遍历,物品在外背包在内求组合,反之求排列
初始化:和为 0 的完全平方数的最少数量是0 ,dp[0] = 0
139. 单词拆分(排列布尔)
组成字符串是要有顺序的,排列完全背包
dp[i] 代表s[0, i-1]是否可以由字典组成
当前背包容量为j, 0............j-len.........j
s[j-len, j]存在于字典中,且dp[j-len] = true, 那么dp[j] = true
遍历顺序:先遍历背包 再遍历物品
初始化:空串可以由字典组成 dp[0] = true
打家劫舍
198. 打家劫舍(最大值)
dp[i] 表示[0, i-1]家 偷的最大金额
不偷i-1家,那么最大金额就是偷 [0,i-2] 的, dp[i] = dp[i-1]
偷i-1家,那么i-2家不能偷,dp[i] = 偷[0, i-3] 的 + i-1家的 = dp[i-2] + nums[i-1]
偷还是不偷,选收益最大的方案
dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i-1])
根据递推公式,i 要从2开始,需要初始化dp[1] 和 dp[0]
dp[0] = 0; 偷-1家,等于不偷,0
dp[1] = nums[0]; 偷第0家,第0家金钱,nums[0]
213. 打家劫舍 II
对于环形打家劫舍,分为不偷第0家 和 不偷第n家
p1 = 偷[1, n] 最大金额
p2 = 偷[0, n-1] 最大金额
取p1 和 p2 的最大值
337. 打家劫舍 III
树形打家劫舍,用后序遍历从底部向上返回最大金额
每层向上返回的是一个数组:{不偷本结点时最大金额,偷本结点时最大金额}
本结点不偷时最大金额:左右子树偷不偷都行: 左子树偷与不偷的最大值 + 右子树偷与不偷的最大值
本结点偷时最大金额:左右子树不能偷:本结点值 + 左子树不偷值 + 右子树不偷值
买卖股票
121. 买卖股票的最佳时机
存在两种状态:持有股票 和 未持有股票 且只能买卖一次
dp[i] 代表手中的现金
dp[1] 持有股票
1、昨天就持有股票,手中现金不变:dp[1] = dp[1]
2、今天买入的(只能买卖一次,这次就是第一次买入),0减去股票价格:dp[1] = -prices[i]
dp[0] 未持有股票
1、昨天就未持有股票,手中现金不变:dp[0] = dp[0]
2、今天卖出的,昨天手中现金加上今天股票价格:dp[0] = dp[1] + prices[i]
综上,dp[1] = Math.max(dp[1], -prices[i]);
dp[0] = Math.max(dp[0], prices[i] + dp[1]);
初始化: 第0天未持有股票,手中现金为0,dp[0] = 0
第0天持有股票,手中现金为 -prices[0]
122. 买卖股票的最佳时机 II
存在两种状态:持有股票 和 未持有股票 且能买卖多次
dp[i] 代表手中的现金
dp[1] 持有股票
1、昨天就持有股票,手中现金不变:dp[1] = dp[1]
2、今天买入的(不一定是第一次买入),昨天手中现金减去股票价格:dp[1] = dp[0] - prices[i]
dp[0] 未持有股票
1、昨天就未持有股票,手中现金不变:dp[0] = dp[0]
2、今天卖出的,昨天手中现金加上今天股票价格:dp[0] = dp[1] + prices[i]
综上,dp[1] = Math.max(dp[1], dp[0] - prices[i]);
dp[0] = Math.max(dp[0], prices[i] + dp[1]);
初始化: 第0天未持有股票,手中现金为0,dp[0] = 0
第0天持有股票,手中现金为 -prices[0]
123. 买卖股票的最佳时机 III
存在4种状态:第一次持有股票 和 第一次未持有股票
第二次持有股票 和 第二次未持有股票
且只能买卖两次
dp[i] 代表手中的现金
dp[1] 第一次持有股票
1、昨天就持有股票,手中现金不变:dp[1] = dp[1]
2、今天第一次买入的,0减去股票价格:dp[1] = - prices[i]
dp[2] 第一次未持有股票
1、昨天就未持有股票,手中现金不变:dp[2] = dp[2]
2、今天第一次卖出的,第一次持有时手中现金加上今天股票价格:dp[2] = dp[1] + prices[i]
dp[3] 第二次持有股票
1、昨天就持有股票,手中现金不变:dp[3] = dp[3]
2、今天第二次买入的,第一次未持时手中现金减去今天股票价格:dp[3] = dp[2] + prices[i]
dp[4] 第二次未持有股票
1、昨天就未持有股票,手中现金不变:dp[4] = dp[4]
2、今天第二次卖出的,第二次持有时手中现金加上今天股票价格:dp[4] = dp[3] + prices[i]
综上, dp[1] = Math.max(dp[1], - prices[i]);
dp[2] = Math.max(dp[2], dp[1] + prices[i]);
dp[3] = Math.max(dp[3], dp[2] - prices[i]);
dp[4] = Math.max(dp[4], dp[3] + prices[i]);
初始化: 第0天第一次持有股票,手中现金为-prices[0],dp[1] = -prices[0]
第0天第一次未持股票,手中现金为0,dp[2] = 0
第0天第二次持有股票,手中现金为-prices[0],dp[3] = -prices[0]
第0天第二次未持股票,手中现金为0,dp[1] = 0
188. 买卖股票的最佳时机 IV
买卖2次有四种状态,买卖k次,则有2k种状态
dp[i] 代表手中的现金
dp[j] j 是奇数,持有股票
1、昨天就持有股票,手中现金不变:dp[j] = dp[j]
2、今天买入的,昨天未持有时现金 减去 股票价格:dp[j] = dp[j-1]- prices[i]
dp[j] j 是偶数,未持有股票
1、昨天就未持有股票,手中现金不变:dp[j] = dp[j]
2、今天卖出的,昨天持有时现金 加上 股票价格:dp[j] = dp[j-1] + prices[i]
综上,
if(j % 2 == 1){
dp[j] = Math.max(dp[j], dp[j-1] - prices[i]);
}else{
dp[j] = Math.max(dp[j], dp[j-1] + prices[i]);
}
初始化:
for(int i = 1; i < 2*k + 1; i = i + 2){
//奇数,持有,都是-prices[0]
dp[i] = -prices[0];
}
309. 买卖股票的最佳时机含冷冻期
存在冷冻期,有四种状态:
持有股票、未持有股票且度过冷冻期、未持有股票且今天刚卖出、冷冻期
dp[0] 持有股票
1、昨天就持有股票:dp[0] = dp[0]
2、今天买入的且昨天是冷冻期:dp[0] = dp[3] - prices[i]
3、今天买入的且昨天非冷冻期:dp[0] = dp[1] - prices[i]
dp[1] 未持有股票且度过冷冻期
1、昨天是冷冻期:dp[1] = dp[3]
2、昨天非冷冻期:dp[1] = dp[1]
dp[2] 未持有股票且今天刚卖出
1、dp[2] = dp[0] + prices[i]
dp[3] 冷冻期
1、昨天卖出的:dp[3] = dp[2]
综上,
dp[0] = Math.max(Math.max(dp[0], dp[3]-prices[i]),dp[1]-prices[i]);
dp[1] = Math.max(dp[1], dp[3]);
dp[2] = dp[0] + prices[i]
dp[3] = dp[2]
我们知道,一维dp数组原理是利用数组的老数据计算新数据,并覆盖老数据
但是这里, 在计算dp[2]之前,dp[0]被更新了; 计算dp[3]之前,dp[2]被更新了
所以使用临时变量记录旧的dp[0]和dp[2]
int temp0 = dp[0];
int temp2 = dp[2];
dp[0] = Math.max(Math.max(dp[0], dp[3]-prices[i]),dp[1]-prices[i]);
dp[1] = Math.max(dp[1], dp[3]);
dp[2] = temp0 + prices[i];
dp[3] = temp2;
初始化: 只有dp[0]持有股票, dp[0] = -prices[0]
714. 买卖股票的最佳时机含手续费
可以多次买卖股票,但是每次卖出要扣一笔手续费
存在两种状态:持有股票 和 未持有股票
dp[i] 代表手中的现金
dp[1] 持有股票
1、昨天就持有股票,手中现金不变:dp[1] = dp[1]
2、今天买入的(不一定是第一次买入),昨天手中现金减去股票价格:dp[1] = dp[0] - prices[i]
dp[0] 未持有股票
1、昨天就未持有股票,手中现金不变:dp[0] = dp[0]
2、今天卖出的,昨天手中现金加上今天股票价格减去手续费:dp[0] = dp[1] + prices[i] - fee
综上,dp[1] = Math.max(dp[1], dp[0] - prices[i]);
dp[0] = Math.max(dp[0], dp[1] + prices[i] - fee);
初始化: 第0天未持有股票,手中现金为0,dp[0] = 0
第0天持有股票,手中现金为 -prices[0]
子序列问题
300. 最长递增子序列
子序列不是连续的, 子数组是连续的
dp[i] 代表结尾为nums[i]的递增子序列的最大长度
用 j 遍历 [0, i), 若nums[ j ] < nums[ i ]
nums[i]可以跟在dp[j]对应的序列后面,新序列长度为dp[j] + 1
for(int j = 0; j < i; j++){
if(nums[j] < nums[i]){
//并不是要比较 dp[i] 和 dp[j] + 1
//循环中每个j对应一个dp[i], 要找出最大的dp[i]
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
初始化: 数组总每个元素都可以组成长度为1的子序列, 即全部初始化为1
674. 最长连续递增序列
dp[i] 以nums[i]为结尾的连续递增子序列的最大长度
连续的序列,通常dp[i] 代表以nums[i] 为结尾的数组和字符串
不连续的序列,通常dp[i] 代表以nums[i - 1] 为结尾的数组和字符串
连续的话, 只和前一个元素比较即可,不用遍历整个字符串了
若nums[ i ] < nums[ i-1 ]
nums[i] 可以跟在 dp[ i-1 ] 对应的序列后面,新序列长度为dp[i-1] + 1
for(int i = 1; i < len; i++){
if(nums[i] > nums[i-1]){
dp[i] = dp[i-1] + 1;
}
res = Math.max(res, dp[i]);
}
初始化: 数组总每个元素都可以组成长度为1的子序列, 即全部初始化为1
718. 最长重复子数组
dp[i][j] 代表以nums1[i-1]为结尾 和 以nums2[j-1]为结尾 这两个数组中公共的长度最长的子数组的长度
如果nums1[i-1] == nums2[j-1]
nums1[0, i-2] 可以拼接上nums1[i-1] 和 nums2[0, j-2] 拼接上 nums2[j-1]
形成新的最长重复子数组: nums1[0, i-1] 和 nums2[0, j-1]
即: dp[i][j] = dp[i-1][j-1] + 1
因为要连续,所以如果最后一个元素不同,dp[i][j]就要清零
例如:2345 和 2347 的dp[i][j]是0,而不是3
i 和 j 遍历从1开始, 因为要判断nums1[i-1] == nums2[j-1]
从递推公式上看,递推依赖于左上角元素,我们从(1,1)开始遍历,初始化第一行第一列
dp[i][0] = 0, dp[0][j] = 0
1143. 最长公共子序列
dp[i][j] 代表以text1[i-1]结尾 和 以text2[j-1]结尾 这两个字符串中公共的长度最长的子串的长度
如果text1[i-1] == text2[j-1] 值相同,公共子序列长度至少为 1,再加上前面串的长度
即: dp[i][j] = dp[i-1][j-1] + 1
如果text1[i-1] != text2[j-1]
值不相同,当前公共子序列:
不选text2[j-1]: text1[0,i)和text2[0,j-1)的最长公共子序列
不选text1[i-1]: text1[0,i-1)和text2[0,j)的最长公共子序列
选一个最长的: dp[i][j] = Math.max(dp[i][j-1], dp[i-1][j]);
i 和 j 遍历从1开始, 因为要判断text1[i-1] == text2[j-1]
从递推公式上看,递推依赖于左上角 或 左边和上边 元素,我们从(1,1)开始遍历,初始化第一行第一列
dp[i][0] = 0, dp[0][j] = 0
1035. 不相交的线
转化为寻找两个数组的最长公共子序列
dp[i][j] nums1[0,i-1]和nums2[0,j-1]的最长公共子序列
53. 最大子数组和
dp[i] 以nums[i]结尾的连续子数组的最大和
连续的序列,通常dp[i] 代表以nums[i] 为结尾的数组和字符串
不连续的序列,通常dp[i] 代表以nums[i - 1] 为结尾的数组和字符串
nums[i]加入,前面累计的和 + 当前数组值, dp[i] = dp[i-1] + nums[i]
nums[i]不加入,前面累计的和清零 + 当前数组值, dp[i] = 0 + nums[i]
加入还是不加入,取最大值
dp[i] = Math.max(dp[i-1] + nums[i], nums[i]);
初始化: 以第0个元素结尾的连续数组最大和为 nums[0]
392. 判断子序列
dp[i][j] 表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度
若 s.charAt(i-1) == t.charAt(j-1)
相同子序列长度至少为1 + 之前串的相同子序列的长度 : dp[i][j] = dp[i-1][j-1] + 1;
若 s.charAt(i-1) != t.charAt(j-1)
那s[0,i-1] 和 t[0,j-1]相同子序列的长度 = s[0,i-1] 和 t[0,j-2] : dp[i][j] = dp[i][j-1];
从递推公式上看,递推依赖于左上角 或 上边 元素,我们从(1,1)开始遍历,初始化第一行第一列
dp[i][0] = 0, dp[0][j] = 0
115. 不同的子序列
dp[i][j]:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]
s[i-1] == t[j-1]时
用s[0, i-1]来匹配t[0,j-1] : dp[i][j] = dp[i-1][j-1]
(s[i-1] 和 t[j-1]相等不用匹配了, 相当于再匹配s[0, i-2] 和 t[0,j-2])
用s[0, i-2]来匹配t[0,j-1] : dp[i][j] = dp[i-1][j]
综上, dp[i][j] = dp[i-1][j-1] + dp[i-1][j]
s[i-1] != t[j-1]时
用s[0, i-2]匹配t[0, j-1], dp[i][j] = dp[i-1][j]
初始化 : dp[i][j]由上方和左上方 或 左方 推导而来, 所以dp[i][0] 和 dp[0][j] 要初始化,dp[i][0] 在s[0,i-1]中空字符串出现的个数,都是1, dp[0][j] 在空串中t[0, j-1]出现的个数,都是0
583. 两个字符串的删除操作
即求,两个字符串总长度 - 最长公共子序列 * 2
72. 编辑距离
dp[i][j] 表示将下标i-1为结尾的字符串word1 转化为 下标j-1为结尾的字符串word2 的最少操作数
当word1[i-1] == word2[j-1]
word1[i-1] 和 word2[j-1]相等不用操作
操作下标i-2为结尾的字符串word1 和 下标j-2为结尾的字符串word2, 即可
即,dp[i][j] = dp[i-1][j-1]
当word1[i-1] != word2[j-1]
删除word1[i-1]
即将word1[0, i-2]转化为word2[0,j-1], dp[i][j] = dp[i-1][j] + 1(等同添加)
删除word2[j-1]
即将word1[0, i-1]转化为word2[0,j-2], dp[i][j] = dp[i][j-1] + 1(等同添加)
替换word1[i-1]或word2[j-1]
使得word1[i-1] == word2[j-1], dp[i][j] = dp[i-1][j-1] + 1
综上, dp[i][j] = Math.min(Math.min(dp[i-1][j] + 1, dp[i][j-1] + 1), dp[i-1][j-1] + 1);
初始化 : dp[i][0] 将word1[0, i-1] 转化为 空串 的最少操作数 为 i
dp[0][j] 将 空串 转化为 word2[0,j-1]的最少操作数 为 j
回文子串
647. 回文子串
dp[i][j] s[i,j]是否是回文
若s[i] != s[j]
s[i,j]不是回文:dp[i][j] = false
若s[i] == s[j]
1、i - j = 0,单个字符肯定是回文,dp[i][j] = true
2、j - i = 1,两个字符肯定是回文,dp[i][j] = true
3、j - i > 1,要看s[i+1,j-1]是否是回文,dp[i][j] = dp[i+1][j-1]
综上, j-i <= 1 或者 s[i+1,j-1]是回文---> dp[i][j] = true;
遍历顺序: 存在dp[i][j] 通过 dp[i+1][j-1]判断,即左下角元素,所以遍历顺序从下到上,从左到右
初始化 : 单个字符肯定是回文 dp[i][i] = true;
516. 最长回文子序列
dp[i][j] 表示s[i,j]内的最长回文子序列的长度
当s[i] == s[j]时
s[i]和s[j]能和s[i+1,j-1]内的最长回文子序列组成一个新的最长回文子序列
即,dp[i][j] = dp[i+1][j-1] + 2
当s[i] != s[j]时
一起加入不能组成新的回文子序列,分别加入看看
1、把s[i]加入,即s[i,j-1], dp[i][j] = dp[i][j-1]
2、把s[j]加入,即s[i+1,j], dp[i][j] = dp[i+1][j]
综上,取最大值 dp[i][j] = max(dp[i][j-1], dp[i+1][j]);
遍历顺序 : 根据状态转移公式,dp[i][j]由左边或下边 以及 左下角推导而来,所以遍历:从下到上、从左到右
初始化 : 单个字符肯定是回文子序列,且长度为1 , dp[i][i] = 1;