如果一个问题是有好多个重叠子问题,使用动态规划是最有效的。动态规划中每一个状态一定是由上一个状态推导出来的,这一点区别于贪心,贪心没有推导,而是从局部中直接选出最优的。
动态规划步骤:
1.
确定
dp
数组(
dp table
)以及下标的含义
2.
确定递推公式
3. dp
数组如何初始化
4.
确定遍历顺序
一.基础题目:
(1)斐波那契数列
1.
确定
dp
数组(
dp table
)以及下标的含义
dp[i]代表n==i时,斐波那契值;
2.
确定递推公式
dp[i] = dp[i-1] + dp[i-2];
3. dp
数组如何初始化
dp[0] = 0,dp[1] = 1;
4.
确定遍历顺序
由动态转移方程可知:dp[i]依赖dp[i-1]、dp[i-2]值,可确定遍历顺序为从前往后。
public int Fibonacci(int n) {
/*
int[] ret = new int[n+1];
ret[1] = 1;
ret[2] = 1;
for(int i=3;i<n+1;i++){
ret[i] = ret[i-2] + ret[i-1];
}
return ret[n];*/
//优化:不需要记录整个数组
int[] dp = new int[2];
dp[0] = 0;
dp[1] = 1;
for(int i=2;i<=n;i++){
int sum = dp[0]+dp[1];
dp[0] = dp[1];
dp[1] = sum;
}
return dp[1];
}
(2)不同路径的数目(一)
1.
确定
dp
数组(
dp table
)以及下标的含义
dp[i][j] 表示从(0,0)走到(i,j)有dp[i][j]条路径;
2.
确定递推公式
dp[i][j] = dp[i-1][j]+dp[i][j-1];
3. dp
数组如何初始化
由于机器人只能向下或者向右走,那么从(0,0)到(i,0)、(0,j)的方式都为1;即dp[i][0]=1,dp[0][j]=1;
4.
确定遍历顺序
从上到下,从左到右。
public int uniquePaths (int m, int n) {
// write code here
int[][] dp = new int[m][n];
for(int i=0;i<m;i++){
dp[i][0] = 1;
}
for(int i=0;i<n;i++){
dp[0][i] = 1;
}
for(int i=1;i<m;i++){
for(int j=1;j<n;j++){
dp[i][j] = dp[i-1][j]+dp[i][j-1];
}
}
return dp[m-1][n-1];
}
(3)不同的二叉搜索树
1.
确定
dp
数组(
dp table
)以及下标的含义
dp[i]表示i个节点的二叉搜索树的个数;
2.
确定递推公式
dp[i] += dp[j-1]*dp[i-j];
3. dp
数组如何初始化
dp[0] = 1;
dp[
以
j
为头结点左⼦树节点数量
] * dp[
以
j
为头结点右⼦树节点数量
]
中
以
j
为头结点左⼦树节点数量为
0
,也需要
dp[
以
j
为头结点左⼦树节点数量
] = 1
, 否则乘法的
结果就都变成
0
了;
4.
确定遍历顺序
遍历i中每个元素作为头节点的个数
public int numTrees (int n) {
// write code here
int[] dp = new int[n+1];
dp[0] = 1;
for(int i=1;i<n+1;i++){
for(int j=1;j<=i;j++){
dp[i] += dp[j-1]*dp[i-j];
}
}
return dp[n];
}
二.背包问题:
(一)01背包
01背包理论基础
有
N
件物品和⼀个最多能被重量为
W
的背包。第
i
件物品的重量是
weight[i]
,得到的价值是
value[i]
。
每件物品只能⽤⼀次
,求解将哪些物品装⼊背包⾥物品价值总和最⼤。
二维数组:
1.
确定
dp
数组(
dp table
)以及下标的含义
dp[i][j]
表⽰从下标为[0,i]
的物品⾥任意取,放进容量为j
的背包,价值总和最⼤是多少;
2.
确定递推公式
a.当不选取i物品,那dp[i][j]直接可以由dp[i-1][j]得出,即dp[i][j] = dp[i-1][j];
b.当选取i物品时,dp[i-1][j-weight[i]]是不放i物品的最大价值,那么dp[i-1][j-weight[i]]+value[i]就是放i物品的最大价值;
dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);
3. dp
数组如何初始化
dp[i][0]表示物品(0,i)中放入容量为0的背包里的最大价值,容量为0说明任何物品都不能放到背包中,那么价值都为0;
dp[0][j]说明下标为0的物品放入容量为j的背包中的最大价值,此时就分为两种情况:
如果j>weight[0],0物品可以放入背包中,那么dp[0][j]=value[0];
如果j<weight[0],0物品不能放入背包中,那么dp[0][j]=0;
// 正序遍历
for (int j = weight[0]; j <= bagWeight; j++) {
dp[0][j] = value[0];
}
4.
确定遍历顺序
先遍历物品,再遍历背包容量。
// weight数组的⼤⼩ 就是物品个数
for(int i = 1; i < weight.size(); i++) { // 遍历物品
for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量
if (j < weight[i]) {
dp[i][j] = dp[i - 1][j]; // 这个是为了展现dp数组⾥元素的变化
} else {
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] +value[i]);
}
}
}
一维数组:
1.
确定
dp
数组(
dp table
)以及下标的含义
dp[j]表示容量为j的物品,所背物品的最大价值为dp[j];
2.
确定递推公式
a.
dp[j]
可以通过
dp[j - weight[j]]
推导出来,
dp[j - weight[i]]
表⽰容量为
j - weight[i]
的背包所背
的最⼤价值;
b.
dp[j - weight[i]] + value[i]
表⽰ 容量为
j -
物品
i
重量 的背包 加上 物品
i
的价值。(也就是容
量为
j
的背包,放⼊物品
i
了之后的价值即:
dp[j]);
dp[j] = Math.max(dp[j],dp[j-weight[i]]+value[i];
3. dp
数组如何初始化
dp[j]当背包容量为0时,不能放入任何物品,价值只能为0;
dp[0] = 0;
4.
确定遍历顺序
倒序遍历,保证每个物品只被记录一次。
如果正序遍历,dp[1] = dp[1-weight[0]]+value[0],dp[2] = dp[2-weight[0]]+value[0],那么0物品就被放入了两次。
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
(1)分割等和子集
https://leetcode-cn.com/problems/partition-equal-subset-sum/
题目:给你一个
只包含正整数 的
非空 数组
nums
。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
1.
确定
dp
数组(
dp table
)以及下标的含义
dp[j]表示背包容量为j,最大可以凑成j的子集总和为dp[j];
2.
确定递推公式
dp[j] = Math.max(dp[j],dp[j-weight[i]]+value[i]);
本题中weight[i]和value[i]均为nums[i],所以
dp[j] = Math.max(dp[j],dp[j-nums[i]]+nums[i]);
3. dp
数组如何初始化
dp[0] = 0;
4.
确定遍历顺序
倒叙遍历
public boolean canPartition(int[] nums) {
int sum = 0;
for(int i=0;i<nums.length;i++){
sum += nums[i];
}
if(sum % 2 == 1){
return false;
}
int target = sum/2;
int[] dp = new int[target+1];
dp[0] = 0;
for(int i=0;i<nums.length;i++){
for(int j=target;j>=nums[i];j--){
dp[j] = Math.max(dp[j],dp[j-nums[i]]+nums[i]);
}
}
return dp[target]==target ? true : false;
}
(2)最后一块石头的重量Ⅱ
https://leetcode-cn.com/problems/last-stone-weight-ii/
题目:有一堆石头,用整数数组
stones
表示。其中
stones[i]
表示第
i
块石头的重量。
每一回合,从中选出
任意两块石头,然后将它们一起粉碎。假设石头的重量分别为
x
和
y
,且
x <= y
。那么粉碎的可能结果如下:
如果
x == y
,那么两块石头都会被完全粉碎;
如果
x != y
,那么重量为
x
的石头将会完全粉碎,而重量为
y
的石头新重量为
y-x
。
最后,
最多只会剩下一块 石头。返回此石头
最小的可能重量 。如果没有石头剩下,就返回
0
。
1.
确定
dp
数组(
dp table
)以及下标的含义
dp[j]表示背包容量为j,最多可以背dp[j]重的石头;
2.
确定递推公式
dp[j] = Math.max(dp[j],dp[j-weight[i]]+value[i]);
本题中weight[i]和value[i]均为stone,所以
dp[j] = Math.max(dp[j],dp[j-stone[i]]+stone[i]);
3. dp
数组如何初始化
dp[0] = 0;
4.
确定遍历顺序
倒叙遍历
public int lastStoneWeightII(int[] stones) {
int[] dp = new int[1501];
int sum = 0;
for(int i=0;i<stones.length;i++){
sum += stones[i];
}
int target = sum/2;
for(int i=0;i<stones.length;i++){
for(int j=target;j>=stones[i];j--){
dp[j] = Math.max(dp[j],dp[j-stones[i]]+stones[i]);
}
}
return sum - 2*dp[target];
}
(3)目标和
https://leetcode-cn.com/problems/target-sum/
题目:给你一个非负整数数组
nums
和一个整数
target
。
向数组中的每个整数前添加
'+'
或
'-'
,然后串联起所有整数,可以构造一个
表达式 :
例如,
nums = [2, 1]
,可以在
2
之前添加
'+'
,在
1
之前添加
'-'
,然后串联起来得到表达式
"+2-1"
。
返回可以通过上述方法构造的、运算结果等于
target
的不同
表达式 的数目。
假设加法的和为x,减法的和应为sum-x,那么有x-(sum-x)=target,->x = (target+sum)/2。问题就转化为填满容量为x的背包,有多少种方法;
1.
确定
dp
数组(
dp table
)以及下标的含义
dp[j]表示填满容量为j的背包,有dp[j]种方法;
2.
确定递推公式
dp[j] += dp[j-nums[i]];
3. dp
数组如何初始化
dp[0] = 1,填满容量为0的背包,只有一种方法就是放入0件物品;
4.
确定遍历顺序
倒叙遍历
public int findTargetSumWays(int[] nums, int target) {
int len = nums.length;
int sum = 0;
for(int i = 0; i < len; i++){
sum += nums[i];
}
//修枝 left为小数 而target全为整数
//全为正或全为负 都不能达到结果
if((sum + target) % 2 != 0 || sum < Math.abs(target)){
return 0;
}
int left = (sum + target) / 2;
int[] dp = new int[left + 1];
dp[0] = 1;
for(int i = 0; i < len; i++){
for(int j = left; j >= nums[i]; j--){
dp[j] = dp[j] + dp[j - nums[i]];
}
}
return dp[left];
}
(二)完全背包
完全背包理论
有
N
件物品和⼀个最多能背重量为
W
的背包。第
i
件物品的重量是
weight[i]
,得到的价值是
value[i]
。
每件物品都有⽆限个(也就是可以放⼊背包多次)
,求解将哪些物品装⼊背包⾥
物品价值总和最⼤。
完全背包较01背包,唯一不同的就是,完全背包物品有无数件。
01背包 一个物品只能被添加一次,所以要使用逆序从大到小遍历;而完全背包一个物品可以被多次添加,要使用正序从小到大遍历。同样循环嵌套也可以交换顺序。
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = weight[i]; j < bagWeight ; j++) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
(1)零钱兑换Ⅱ
https://leetcode-cn.com/problems/coin-change-2/
题目:
给定不同⾯额的硬币和⼀个总⾦额。写出函数来计算可以凑成总⾦额的硬币组合数。假设每
⼀种⾯额的硬币有⽆限个。
1.
确定
dp
数组(
dp table
)以及下标的含义
dp[j] 表示凑成总金额为j的组合数为dp[j];
2.
确定递推公式
dp[j] += dp[j-num[i]];
3. dp
数组如何初始化
d[0] = 1;
4.
确定遍历顺序
正序遍历,一个面额的硬币可以用多次。
int change(int amount, int[ coins) {
int[] dp = new int[amount+1];
dp[0] = 1;
for (int i = 0; i < coins.size(); i++) { // 遍历物品
for (int j = coins[i]; j <= amount; j++) { // 遍历背包
dp[j] += dp[j - coins[i]];
}
}
return dp[amount];
}
总结(背包递推公式):
1.问能否装满背包(或者做多装多少):dp[j] = math.max(dp[j],dp[j-num[i]]+nums[i]
2.问装满背包有多少种方法:dp[j] += dp[j-nums[i]]
3.问装满背包的最大价值:dp[j] = Math.max(dp[j],dp[j-weight[i]]+value[i])
4.问装满背包所有物品的最小个数:dp[j] = Math.min(dp[j-nums[i]]+1,dp[j])
三.股票问题
(1)买卖股票的最佳时机
题目:假设你有一个数组prices,长度为n,其中prices[i]是股票在第i天的价格,请根据这个价格数组,返回买卖股票能获得的最大收益
1.你可以买入一次股票和卖出一次股票,并非每天都可以买入或卖出一次,总共只能买入和卖出一次,且买入必须在卖出的前面的某一天
2.如果不能获取到任何利润,请返回0
3.假设买入卖出均无手续费
1.
确定
dp
数组(
dp table
)以及下标的含义
dp[i][0]表示第i天持有股票所得最多现金;
dp[i][1]表示第i天不持有股票所得最多现金;
2.
确定递推公式
dp[i][0]:
a.第i-1天就已经持有股票:dp[i][0] = dp[i-1][0];
b.第i天买入股票:dp[i][0] = -price[i];
dp[i][0] = Math.max(dp[i-1][0],-price[i]);
dp[i][1]:
a.第i-1天不持有股票:dp[i][1] = dp[i-1][1];
b.第i天卖出股票:dp[i][1] = dp[i-1][0]+price[i];
dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0]+price[i]);
3. dp
数组如何初始化
dp[0][0],第一天持有股票,那一定是第一天刚买的,一定步数从前一天继承下来的,那么dp[0][0] = -price[0];
dp[0][1],第一天不可能卖出股票,也不可能从前一天继承,那么dp[0][1] = 0;
4.
确定遍历顺序
从前往后正序遍历。
public int maxProfit (int[] prices) {
// write code here
int n = prices.length;
int[][] dp = new int[n][2];
dp[0][0] = -prices[0];
dp[0][1] = 0;
for (int i = 1; i < n; i++) {
dp[i][0] = Math.max(dp[i - 1][0], -prices[i]);
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
}
return Math.max(dp[n - 1][0], dp[n - 1][1]);
}
(2)买卖股票的最佳时机Ⅱ
假设你有一个数组prices,长度为n,其中prices[i]是某只股票在第i天的价格,请根据这个价格数组,返回买卖股票能获得的最大收益
1. 你可以多次买卖该只股票,但是再次购买前必须卖出之前的股票
2. 如果不能获取收益,请返回0
3. 假设买入卖出均无手续费
1. 确定dp
数组(
dp table
)以及下标的含义
dp[i][0]表示第i天持有股票所得最多现金;
dp[i][1]表示第i天不持有股票所得最多现金;
2.
确定递推公式
dp[i][0]:
a.第i-1天就已经持有股票:dp[i][0] = dp[i-1][0];
b.第i天买入股票:dp[i][0] = dp[i-1][1]-price[i];
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]-price[i]);
dp[i][1]:
a.第i-1天不持有股票:dp[i][1] = dp[i-1][1];
b.第i天卖出股票:dp[i][1] = dp[i-1][0]+price[i];
dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0]+price[i]);
3. dp
数组如何初始化
dp[0][0] = -price[0];
dp[0][1] = 0;
4.
确定遍历顺序
正序遍历。
public int maxProfit (int[] prices) {
// write code here
int n = prices.length;
int[][] dp = new int[n][2];
dp[0][0] = -prices[0];
dp[0][1] = 0;
for(int i=1;i<n;i++){
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]-prices[i]);
dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0]+prices[i]);
}
return Math.max(dp[n-1][0],dp[n-1][1]);
}
(3)买卖股票的最佳时机Ⅲ
题目:假设你有一个数组prices,长度为n,其中prices[i]是某只股票在第i天的价格,请根据这个价格数组,返回买卖股票能获得的最大收益
1. 你最多可以对该股票有两笔交易操作,一笔交易代表着一次买入与一次卖出,但是再次购买前必须卖出之前的股票
2. 如果不能获取收益,请返回0
3. 假设买入卖出均无手续费
1. 你最多可以对该股票有两笔交易操作,一笔交易代表着一次买入与一次卖出,但是再次购买前必须卖出之前的股票
2. 如果不能获取收益,请返回0
3. 假设买入卖出均无手续费
1.
确定
dp
数组(
dp table
)以及下标的含义
一天共个五个状态:0:没有操作,1:第一次买入,2:第一次卖出,3,第二次买入,4,第二次卖出;
dp[i][j] 表示第i天j状态下所得的最多现金;
2.
确定递推公式
dp[i][0] = dp[i-1][0];
dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0]-prices[i]);
dp[i][2] = Math.max(dp[i-1][2],dp[i-1][1]+prices[i]);
dp[i][3] = Math.max(dp[i-1][3],dp[i-1][2]-prices[i]);
dp[i][4] = Math.max(dp[i-1][4],dp[i-1][3]+prices[i]);
3. dp
数组如何初始化
dp[0][0] = 0;
dp[0][1] = -prices[0];
dp[0][2] = 0;
dp[0][3] = -prices[0];
dp[0][4] = 0;
4.
确定遍历顺序
从第一支股票到第n支股票,依次遍历。
public int maxProfit (int[] prices) {
// write code here
int n = prices.length;
int[][] dp = new int[n][5];
//0:无操作 1:第一次买入 2:第一次卖出 3:第二次买入 4:第二次卖出
dp[0][0] = 0;
dp[0][1] = -prices[0];
dp[0][2] = 0;
dp[0][3] = -prices[0];
dp[0][4] = 0;
for (int i = 1; i < n; i++) {
dp[i][0] = dp[i - 1][0];
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
dp[i][2] = Math.max(dp[i - 1][2], dp[i - 1][1] + prices[i]);
dp[i][3] = Math.max(dp[i - 1][3], dp[i - 1][2] - prices[i]);
dp[i][4] = Math.max(dp[i - 1][4], dp[i - 1][3] + prices[i]);
}
return dp[n - 1][4];
}
(4)买卖股票的最佳时机Ⅳ
题目:假设你有一个数组prices,长度为n,其中]prices[i]是某只股票在第i天的价格,请根据这个价格数组,返回买卖股票能获得的最大收益
1. 你最多可以对该股票有k笔交易操作,一笔交易代表着一次买入与一次卖出,但是再次购买前必须卖出之前的股票
2. 如果不能获取收益,请返回0
3. 假设买入卖出均无手续费
1. 你最多可以对该股票有k笔交易操作,一笔交易代表着一次买入与一次卖出,但是再次购买前必须卖出之前的股票
2. 如果不能获取收益,请返回0
3. 假设买入卖出均无手续费
1.
确定
dp
数组(
dp table
)以及下标的含义
一天共个五个状态:0:没有操作,1:第一次买入,2:第一次卖出,3,第二次买入,4,第二次卖出;
dp[i][j] 表示第i天j状态下所得的最多现金;
2.
确定递推公式
由买卖股票的最佳时机Ⅲ初始化结论可知,dp[i][j]递推公式存在规律;
3. dp
数组如何初始化
由买卖股票的最佳时机Ⅲ初始化结论可知,当j为奇数时,均等于-prices[i];
4.
确定遍历顺序
从第一支股票到第n支股票,依次遍历。
public int maxProfit (int[] prices,int k) {
int[][] dp = new int[n][2*k+1];
for(int j=1;j<2*k;j+=2){
dp[0][j] = -prices[0];
}
for(int i=1;i<n;i++){
for(int j=0;j<2*k;j+=2){
dp[i][j+1] = Math.max(dp[i-1][j+1],dp[i-1][j]-prices[i]);
dp[i][j+2] = Math.max(dp[i-1][j+2],dp[i-1][j+1]+prices[i]);
}
}
System.out.println(dp[n-1][2*k]);
}
(5)买卖股票的最佳时机含冷冻期
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-cool
down/
public int maxProfit(int[] prices) {
//0:买入 1:卖出已度过冷冻期丹没买入
//2:卖出 3:冷冻期
int[][] dp = new int[prices.length][4];
dp[0][0] = -prices[0];
for(int i=1;i<prices.length;i++){
dp[i][0] = Math.max(dp[i-1][0],Math.max(dp[i-1][1],dp[i-1][3])-prices[i]);
dp[i][1] = Math.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];
}
return Math.max(dp[prices.length-1][1],Math.max(dp[prices.length-1][2],dp[prices.length-1][3]));
}
(6)买卖股票的最佳时机含手续费
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-tran
saction-fee/
public int maxProfit(int[] prices, int fee) {
int[][] dp = new int[prices.length][2];
dp[0][0] = -prices[0];
dp[0][1] = 0;
for(int i=1;i<prices.length;i++){
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]-prices[i]);
//与买卖股票的最佳时机Ⅱ不同就是再卖出时-fee
dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0]+prices[i]-fee);
}
return Math.max(dp[prices.length-1][0],dp[prices.length-1][1]);
}
四.子序列问题
(一)子序列(不连续)
(1)最长上升子序列一
题目:给定一个长度为 n 的数组 arr,求它的最长严格上升子序列的长度。
所谓子序列,指一个数组删掉一些数(也可以不删)之后,形成的新数组。
1.
确定
dp
数组(
dp table
)以及下标的含义
dp[i]表示从0到i区间内的最长递增子序列;
2.
确定递推公式
dp[i],位置i的最长子序列为j到0->i-1各位置最长子序列+1
if(arr[i]>arr[j]){ dp[i]=Math.max(dp[i],dp[j]+1);
3. dp
数组如何初始化
数组上每个位置的子序列最小值都为它本身;
4.
确定遍历顺序
因为位置i的最长子序列与0->i-1位置最长子序列的值有关系,所以必须从前往后遍历。
public int LIS (int[] arr) {
// write code here
if(arr.length == 0){
return 0;
}
int[] dp = new int[arr.length];
for(int i=0;i<arr.length;i++){
dp[i] = 1;
}
int ret = 1;
for(int i=1;i<arr.length;i++){
for(int j=0;j<i;j++){
if(arr[i]>arr[j]){
dp[i] = Math.max(dp[i],dp[j]+1);
}
}
if(ret < dp[i]){
ret = dp[i];
}
}
return ret;
}
(二)子序列(连续)
(1)最长连续递增序列
https://leetcode-cn.com/problems/longest-continuous-increasing-subsequenc
e/
题目:给定一个未经排序的整数数组,找到最长且
连续递增的子序列,并返回该序列的长度。
1.
确定
dp
数组(
dp table
)以及下标的含义
dp[i]表示以i位置结尾的区间内的最长连续递增子序列;
2.
确定递推公式
dp[i],位置i+1的最长子序列为i位置最长子序列+1;
if(arr[i+1]>arr[i]){ dp[i+1]=dp[i]+1;
3. dp
数组如何初始化
数组上每个位置的子序列最小值都为它本身;
整个区间内都有:dp[i] = 1;
4.
确定遍历顺序
因为位置i+1的最长子序列与i位置最长子序列的值有关系,所以必须从前往后遍历。
public int findLengthOfLCIS(int[] nums) {
int[] dp = new int[nums.length];
for(int i=0;i<nums.length;i++){
dp[i] = 1;
}
int ret = 1;
for(int i=0;i<nums.length-1;i++){
if(nums[i+1]>nums[i]){
dp[i+1] = dp[i]+1;
}
if(ret < dp[i+1]){
ret = dp[i+1];
}
}
return ret;
}
不连续递增⼦序列的跟前0-i 个状态有关,连续递增的⼦序列只跟前⼀个状态有关.
(2)最长公共子序列一
题目:给两个整数数组
nums1
和
nums2
,返回
两个数组中 公共的 、长度最长的子数组的长度 。
1.
确定
dp
数组(
dp table
)以及下标的含义
dp[i][j]表示以i位置结尾的nums1数组,和以j位置结尾的nums2数组的最长公共子序列;
2.
确定递推公式
dp[i][j],i位置j位置的最长公共子序列与前一个位置的值是否相同,
相同时,dp[i][j] = dp[i-1][j-1]+1;
3. dp
数组如何初始化
dp[0][0] = 0,dp[i][0] = 0,dp[0][j] = 1;
4.
确定遍历顺序
因为位置i,j的最长子序列与i-1,j-1位置最长子序列的值有关系,所以必须从前往后遍历。
public int findLength(int[] nums1, int[] nums2) {
int[][] dp = new int[nums1.length+1][nums2.length+1];
int ret = 0;
for(int i=1;i<=nums1.length;i++){
for(int j=1;j<=nums2.length;j++){
if(nums1[i-1] == nums2[j-1]){
dp[i][j] = dp[i-1][j-1]+1;
}
if(ret < dp[i][j]){
ret = dp[i][j];
}
}
}
return ret;
}
(三)编辑距离
(1)编辑距离
题目:给定两个字符串 str1 和 str2 ,请你算出将 str1 转为 str2 的最少操作数。
你可以对字符串进行3种操作:
1.插入一个字符
2.删除一个字符
3.修改一个字符。
1.
确定
dp
数组(
dp table
)以及下标的含义
dp[i][j]表示以i-1结尾的str1与以i-1结尾的str2,最小编辑距离为dp[i][j;
2.
确定递推公式
当str1[i-1] == str2[j-1]时,dp[i][j] = dp[i-1][j-1];
当str1[i-1] != str2[j-1]时,dp[i][j] = Math.min(dp[i-1][j-1],Math.min(dp[i-1][j],dp[i][j-1]))+1;
3. dp
数组如何初始化
dp[i][0] = i,dp[0][j] = j;
4.
确定遍历顺序
因为dp[i][j]的值取决于dp[i-1][j],dp[i][j-1],dp[i-1][j-1],所以应该选择从前往后遍历。
public int editDistance (String str1, String str2) {
// write code here
int n = str1.length(),m = str2.length();
int[][] dp = new int[n+1][m+1];
for(int i=0;i<n+1;i++){
dp[i][0] = i;
}
for(int j=0;j<m+1;j++){
dp[0][j] = j;
}
for(int i=1;i<n+1;i++){
for(int j=1;j<m+1;j++){
if(str1.charAt(i-1) == str2.charAt(j-1)){
dp[i][j] = dp[i-1][j-1];
}else{
dp[i][j] = Math.min(dp[i-1][j-1],Math.min(dp[i-1][j],dp[i][j-1]))+1;
}
}
}
return dp[n][m];
}
五.回文
(1)回文子串
https://leetcode-cn.com/problems/palindromic-substrings/
题目:给你一个字符串
s
,请你统计并返回这个字符串中
回文子串 的数目。
1.
确定
dp
数组(
dp table
)以及下标的含义
dp[i][j]表示s从i位置到j位置的字符串是否为回文串;
2.
确定递推公式
dp[i][j] s.charAt(i)==s.charAt(j)
如果i-j==1 如aa 一定为回文串 dp[i][j] =true;
如果i-j>1 如abca 是否为回文串,取决于dp[i+1][j-1]的值;
3. dp
数组如何初始化
dp[i][j] = false;
4.
确定遍历顺序
因为要保证dp[i+1][j-1]都是经过计算的,所以要从下到上,从左到右遍历。
public int countSubstrings(String s) {
boolean[][] dp = new boolean[s.length()][s.length()];
int ret = 0;
for(int i=s.length()-1;i>=0;i--){
for(int j=i;j<=s.length()-1;j++){
if(s.charAt(i) == s.charAt(j)){
if(j-i<=1){
dp[i][j] = true;
ret++;
}else{
dp[i][j] = dp[i+1][j-1];
if(dp[i][j] == true){
ret++;
}
}
}
}
}
return ret;
}
(2)最长回文序列
题目:给定一个字符串,找到其中最长的回文子序列,并返回该序列的长度。
1.
确定
dp
数组(
dp table
)以及下标的含义
dp[i][j] 表示str从i到j区间内回文子序列的长度;
2.
确定递推公式
当str[i] == str[j]时,dp[i][j] = dp[i+1][j-1]+2;
当str[i] != str[j]时,dp[i][j] = Math.max(dp[i+1][j],dp[i][j-1];
3. dp
数组如何初始化
str的i位置到它本身就是一个回文子序列;
dp[i][i] = 1;
4.
确定遍历顺序
从下到上。
public int getLongestPalindrome (String str) {
int[][] dp = new int[str.length()][str.length()];
for(int i=0;i<str.length();i++){
dp[i][i] = 1;
}
for(int i=str.length()-1;i>=0;i--){
for(int j=i+1;j<str.length();j++){
if(str.charAt(i) == str.charAt(j)){
dp[i][j] = dp[i+1][j-1]+2;
}else{
dp[i][j] = Math.max(dp[i+1][j],dp[i][j-1]);
}
}
}
return dp[0][str.length()-1];
}