动态规划
动态规划五部曲
-
确定dp数组以及下标的含义
-
确定递推公式
-
dp数组如何初始化
-
确定遍历顺序
-
举例推导dp数组
【1】斐波那契数
动态规划入门题
- 确定dp数组以及下标的含义:第i个数的斐波那契数值是dp[i]
- 确定递推公式:
dp[i]=dp[i-1]+dp[i-2]
- dp数组如何初始化:
dp[0]=0,dp[1]=1
- 确定遍历顺序:从前往后
- 举例推导dp数组
public int fib(int n){
int[] dp=new int[n+1];
dp[0]=0;
dp[1]=1;
for(int i=2;i<=n;i++){
dp[i]=dp[i-1]+dp[i-2];
}
return dp[n];
}
【2】爬楼梯
爬楼梯
每次可以爬一阶或者是两阶,求方法
- 确定dp数组以及下标的含义:跳到第i阶的方法数是dp[i]
- 确定递推公式:
dp[i]=dp[i-1]+dp[i-2]
- dp数组如何初始化:
dp[0]=0,dp[1]=1
- 确定遍历顺序:从前往后
- 举例推导dp数组
public int climbStair(int n){
int[] dp=new int[n+1];
dp[0]=0;
dp[1]=1;
for(int i=2;i<=n;i++){
dp[i]=dp[i-1]+dp[i-2];
}
return dp[n];
}
爬楼梯进阶
每次最多可以爬m阶,其实是一个完全背包问题
- 确定dp数组以及下标的含义:跳到第i阶的方法数是dp[i]
- 确定递推公式:
dp[i]+=dp[i-j]
- dp数组如何初始化:
dp[0]=1
- 确定遍历顺序:从前往后
- 举例推导dp数组
public int climbStairI(int n,int m){
int[] dp=new int[n+1];
dp[0]=1;
for(int i=2;i<=n;i++){
for(int j=0;j<=m;j++){
if(i>=j)dp[i]+=dp[i-j];
}
}
return dp[n];
}
使用最小花费爬楼梯👑
数组的值代表花费,求爬到数组尾的最小花费
- 确定dp数组以及下标的含义:跳到第i个位置的最小花费是dp[i]
- 确定递推公式:
dp[i]=Math.min(dp[i-1],dp[i-2])+cost[i]
- dp数组如何初始化:
dp[0]=1
- 确定遍历顺序:从前往后
- 举例推导dp数组
🌟重点:最后是取最后两位的最小值,因为最后两位都可以跳到数组尾,选一个花费小的
public int climbStairII(int[] cost){
int[]dp=new int[cost.length];
dp[0]=cost[0];
dp[1]=cost[1];
for(int i=2;i<cost.length;i++){
dp[i]=Math.min(dp[i-1],dp[i-2])+cost[i];
}
return Math.min(dp[cost.length-1],dp[cost.length-2])
}
【3】不同路径
不同路径
start | |||||
---|---|---|---|---|---|
Finish |
网格mxn,每次只能向下或者向右走一格,求到达目标的方法数
- 确定dp数组以及下标的含义:跳到第i个格子的方法数是
dp[i][j]
- 确定递推公式:
dp[i][j]=dp[i-1][j]+dp[i][j-1]
- dp数组如何初始化:首行和首列的都是1,只有一种方法
- 确定遍历顺序:从左往右,从上往下
- 举例推导dp数组
public int path(int m,int n){
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];
}
不同路径II👑
网格mxn,网格中有障碍物每次只能向下或者向右走一格,求到达目标的方法数
- 确定dp数组以及下标的含义:跳到第i个格子的方法数是
dp[i][j]
- 确定递推公式:
dp[i][j]=dp[i-1][j]+dp[i][j-1]
- dp数组如何初始化:首行和首列在遇到障碍物之前都是1
- 确定遍历顺序:从左往右,从上往下
- 举例推导dp数组
public int pathI(int m,int n,int[][] obstacle){
int[][] dp=new int[m][n];
for(int i=0;i<m && obstacle[i][0]==0;i++)dp[i][0]=1;
for(int i=0;i<n && obstacle[0][i]==0;i++)dp[0][i]=1;
for(int i=1;i<m;i++){
for(int j=1;j<n;j++){
if(obstacle[i][j]==1)continue;//如果遇到障碍,跳过
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}
return dp[m-1][n-1];
}
🌟重点:初始化的时候就要注意到障碍,初始化错误后面会都错
【4】整数拆分
给定一个正整数 n,将其拆分为至少两个正整数的和, 返回可以获得的最大乘积
-
确定dp数组以及下标的含义:正整数i可以得到的最大乘积
-
确定递推公式:
dp[i]=Math.max(j*(i-j),j*dp[i-j])
-
j*(i-j)代表把i拆分为j和i-j两个数相乘
-
j*dp[i-j]代表把i拆分成j和继续把(i-j)这个数拆分,取(i-j)拆分结果中的最大乘积与j相乘
-
-
dp数组如何初始化:
dp[2]=1
,因为初始化dp[0]
和dp[1]
没有任何意义 -
确定遍历顺序:从左往右
-
举例推导dp数组
public int integerBreak(int n){
int[] dp=new int[n+1];
dp[2]=1;
for(int i=3;i<=n;i++){
for(int j=1;j<=i;j++){
dp[i]=Math.max(j*(i-j),j*dp[i-j])
}
}
return dp[n];
}
【5】不同的二叉搜索树👑
给定一个整数 n,求以 1 … n 为节点组成的二叉搜索树有多少种
-
确定dp数组以及下标的含义:以1…i为节点组成的二叉搜索树有多少种
-
确定递推公式:
dp[i]+=dp[j-1]*dp[i-j]
以j为头节点的左子树搜索树数量*以j为头节点的右子树搜索树数量
头节点是一个节点,左右子树一共i-1个节点,所以
dp[i]+=dp[j-1]*dp[i-j]
或dp[i]+=dp[j]*dp[i-j-1]
-
dp数组如何初始化:
dp[0]=1
-
确定遍历顺序:从左往右
-
举例推导dp数组
public int BST(int n){
int[] dp=new int[n+1];
dp[0]=1;
for(int i=1;i<=n;i++){
for(int j=1;j<=i;j++){
dp[i]+=dp[j-1]*dp[i-j];
}
}
return dp[n];
}
🌟重点:递归方程
【6】背包问题
Ⅰ 01背包理论
有N件物品和一个最多能背重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] ,每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
- 确定dp数组以及下标的含义:最多能背重量为j的背包,最多能背
dp[j]
的物品 - 确定递推公式:
dp[j]=Math.max(dp[j-weight[i]]+value[i],dp[j])
- dp数组如何初始化:
dp[0]=0
- 确定遍历顺序:正序遍历物品,倒序遍历背包,这样的保证物品只被放入一次
- 举例推导dp数组
public int bag(int W,int[] weight,int[] value){
int[]dp=new int[W+1];
dp[0]=0;
for(int i=0;j<weight.length;i++){
for(int j=W;j>=weight[i];j--){
dp[j]=Math.max(dp[j-weight[i]]+value[i],dp[j]);
}
}
return dp[W];
}
🌟重点:背包的遍历顺序是从后往前,这样的保证物品只被放入一次
分割等和子集
给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等
思路:对数组求和,问题转化为容量为 sum/2的背包能不能正好装满
- 确定dp数组以及下标的含义:最多能背重量为j的背包,最多能背
dp[j]
的物品 - 确定递推公式:
dp[j]=Math.max(dp[j-weight[i]]+value[i],dp[j])
- dp数组如何初始化:
dp[0]=0
- 确定遍历顺序:正序遍历物品,倒序遍历背包
- 举例推导dp数组
public boolean canPartition(int[] nums){
int sum=0;
for(int num:nums)sum+=num;
if(sum%2!=0)return false;//如果不能被2整除,不可能分割成功
int maxWeight=sum/2;
int[] dp=new int[maxWeight+1];
dp[0]=0;
for(int i=0;i<nums.length;i++){
for(int j=maxWeight;j>=nums[i];j--){
dp[j]=Math.max(dp[j],dp[j-nums[i]]+nums[i]);
}
}
return dp[maxWeight]==maxWeight;
}
最后一块石头的重量 II
有一堆石头,每块石头的重量都是正整数。每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
如果 x == y,那么两块石头都会被完全粉碎
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x
最后,最多只会剩下一块石头。返回此石头最小的可能重量。如果没有石头剩下,就返回 0
思路:和上一个题一样,转化为容量为 sum/2的背包最多能装多少
- 确定dp数组以及下标的含义:最多能背重量为j的背包,最多能背
dp[j]
的物品 - 确定递推公式:
dp[j]=Math.max(dp[j],dp[j-stones[i]]+stones[i])
- dp数组如何初始化:
dp[0]=0
- 确定遍历顺序:正序遍历物品,倒序遍历背包
- 举例推导dp数组
public int lastStoneWeightII(int[] stones){
int sum=0;
for(int num:nums)sum+=num;
int maxWeight=sum/2;
int[] dp=new int[maxWeight+1];
dp[0]=0;
for(int i=0;i<nums.length;i++){
for(int j=maxWeight;j>=stones[i];j--){
dp[j]=Math.max(dp[j],dp[j-stones[i]]+stones[i]);
}
}
return sum-2*dp[maxWeight];
}
目标和👑
给定一个非负整数数组,a1, a2, …, an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面,返回可以使最终数组和为目标数 S 的所有添加符号的方法数。
思路:数组总和为sum,设标+号的为x,则标-号的为sum-x,则满足x-(sum-x)=S,即x=(sum+S)/2
- 确定dp数组以及下标的含义:装满容量为j的背包的方法数
- 确定递推公式:
dp[j]=Math.max(dp[j-weight[i]]+value[i],dp[j])
- dp数组如何初始化:
dp[0]=0
- 确定遍历顺序:正序遍历物品,倒序遍历背包
- 举例推导dp数组
public int targetSum(int[] nums,int S){
int sum=0;
for(int num:nums)sum+=num;
if((sum+S)%2!=0 || S>sum)return 0;//如果不能被2整除,则没有方法
int maxWeight=Math.abs((sum+S)/2);//绝对值要注意!
int[] dp=new int[maxWeight+1];
dp[0]=1;
for(int i=0;i<nums.length;i++){
for(int j=maxWeight;j>=nums[i];j--){
dp[j]+=dp[j-nums[i]];
}
}
return dp[maxWeight];
}
一和零👑
给你一个二进制字符串数组 strs 和两个整数 m 和 n ,请你找出并返回 strs 的最大子集的大小,该子集中最多有 m 个 0 和 n 个 1
思路:就是两个零一背包,分边取1和取0
-
确定dp数组以及下标的含义:最多有i个0和j个1的strs的最大子集的大小为
dp[i][j]
-
确定递推公式:
dp[i][j]=Math.max(dp[i][j],dp[i-nums0][j-nums1]+1);
对每一个字符都要遍历一遍整个二维dp数组,所以要在遍历的过程中取最大值
-
dp数组如何初始化:
dp[0][0]=0
-
确定遍历顺序:正序遍历物品,倒序遍历两个背包
-
举例推导dp数组
public int findMaxForm(String[] strs,int m,int n){
int[] nums0=new int[strs.length];
int[] nums1=new int[strs.length];
for(int i=0;i<strs.length;i++){
String s=strs[i];
for(int j=0;j<s.length();j++){
if(s.charAt(j)=='0')nums0[i]++;
else nums1[i]++;
}
}
int[][] dp=new int[m+1][n+1];
dp[0][0]=0;
for(int i=0;i<strs.length;i++){
for(int j=m;j>=nums0[i];j--){
for(int k=n;k>=nums1[i];k--){
dp[j][k]=Math.max(dp[j][k],dp[j-nums0[i]][k-nums1[i]]+1);
}
}
}
return dp[m][n];
}
II 完全背包理论
有N件物品和一个最多能背重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] ,每件物品能用多次,求解将哪些物品装入背包里物品价值总和最大。
-
确定dp数组以及下标的含义:容量为j的背包最多能装价值
dp[j]
的货物 -
确定递推公式:
dp[j]=Math.max(dp[j],dp[j-weight[i]]+value[i])
-
dp数组如何初始化:
dp[0]=0
-
确定遍历顺序:外层正序遍历物品,内层正序遍历背包
-
举例推导dp数组
public int bag(int maxWeight,int[] weight,int[] value){
int[] dp=new int[maxweight+1];
dp[0]=0;
for(int i=0;i<weight.length;i++){
for(int j=weight[i];j<=maxWeight;j++){
dp[j]=Math.max(dp[j],dp[j-weight[i]]+value[i]);
}
}
return dp[maxWeight];
}
🌟重点一:
这里和01背包的区别是一件物品可以放多次所以正序遍历(每次搞不懂的时候都自己再去推导一遍)
🌟重点二:
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
零钱兑换
给定不同面额的硬币 coins[]和一个总金额,写出函数来计算可以凑成总金额amount的硬币组合数,假设每一种面额的硬币有无限个。
-
确定dp数组以及下标的含义:凑成总金额j的硬币组合数为
dp[j]
-
确定递推公式:
dp[j]+=dp[j-coin[i]];
-
dp数组如何初始化:
dp[0]=0
-
确定遍历顺序:外层遍历背包,内层遍历物品
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
-
举例推导dp数组
public int change(int amount,int[] coins){
int[] dp=new int[amount+1];
dp[0]=1;//因为是方法数,所以初始化要为1,不然就是0+0+0
for(int i=0;i<coins.length;i++){
for(int j=coins[i];j<=amount;j++){
dp[j]+=dp[j-coins[i]];
}
}
return dp[amount];
}
组合总和
给定一个由正整数组成且不存在重复数字的数组nums[],找出和为给定目标正整数target的组合的个数。
🌟重点:说的是组合,但其实是求排列,顺序不一样的是不同的排列
nums = [1, 2, 3] target = 4
所有可能的组合为: (1, 1, 1, 1) (1, 1, 2) (1, 2, 1) (1, 3) (2, 1, 1) (2, 2) (3, 1)
-
确定dp数组以及下标的含义:目标整数为i的组合的个数为
dp[I]
-
确定递推公式:
dp[i]+=dp[I-nums[j]]
-
dp数组如何初始化:
dp[0]=1
-
确定遍历顺序:外层遍历背包,内层遍历物品
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
-
举例推导dp数组
public int combinationSum4(int[] nums,int target){
int[] dp=new int[target+1];
dp[0]=1;
for(int i=0;i<=target;i++){
for(int j=0;j<nums.length;j++){
if(i-nums[j]>=0)dp[i]+=dp[i-nums[j]];
}
}
return dp[target];
}
爬楼梯进阶
之前已经写过了
零钱兑换
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。种硬币的数量是无限的。
- 确定dp数组以及下标的含义:凑成总金额为j的最少硬币个数为
dp[j]
- 确定递推公式:
dp[j]=Math.min(dp[j],dp[j-coins[i]]+1)
- dp数组如何初始化:
dp[0]=-1
- 确定遍历顺序:外层遍历背包,内层遍历物品
- 举例推导dp数组
public int coinChange(int[] coins, int amount) {
int[] dp=new int[amount+1];
dp[0]=0;
for(int i=1;i<=amount;i++){
dp[i]=Integer.MAX_VALUE;
}
for(int i=0;i<coins.length;i++){
for(int j=coins[i];j<=amount;j++){
if(dp[j-coins[i]]!=Integer.MAX_VALUE)dp[j]=Math.min(dp[j],dp[j-coins[i]]+1);
}
}
if(dp[amount]==Integer.MAX_VALUE)return -1;
return dp[amount];
}
完全平方数
给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
- 确定dp数组以及下标的含义:组成j的最少完全平方数为
dp[j]
- 确定递推公式:
dp[j]=Math.min(dp[j],dp[j-i*i]+1)
- dp数组如何初始化:
dp[0]=0
- 确定遍历顺序:外层遍历物品,内层遍历背包
- 举例推导dp数组
public int numSquares(int n) {
int[] dp=new int[n+1];
dp[0]=0;
for(int i=1;i<=n;i++){
dp[i]=Integer.MAX_VALUE;
}
for(int i=1;i<=n;i++){
for(int j=i*i;j<=n;j++){
if(j-i*i!=Integer.MAX_VALUE)dp[j]=Math.min(dp[j],dp[j-i*i]+1);
}
}
return dp[n];
}
单词拆分👑
给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
思路:单词列表是物品,字符串是背包,就看能不能装满
-
确定dp数组以及下标的含义:
dp[i]
长度为i的字符串能不能拆分为字典中出现的单词 -
确定递推公式:if([j, i] 这个区间的子串出现在字典里 && dp[j]是true) 那么 dp[i] = true
-
dp数组如何初始化:
-
确定遍历顺序:
-
举例推导dp数组
public static boolean wordBreak(String s, List<String> wordDict) {
boolean[] dp=new boolean[s.length()+1];
dp[0]=true;
for(int i=1;i<=s.length();i++){
for(int j=i;j>=0;j--){
if(wordDict.contains(s.substring(j,i)) && dp[j])dp[i]=true;
break;
}
}
return dp[s.length()];
}
【7】打家劫舍
打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
- 确定dp数组以及下标的含义:数组长度为i+1的数组能偷到的最高金额为
dp[i]
- 确定递推公式:
dp[i]=Math.max(dp[i-2]+nums[i],dp[i-1])
- dp数组如何初始化:
dp[0]=nums[0]
- 确定遍历顺序:从左到右
- 举例推导dp数组
public int rob(int[] nums){
int[] dp=new int[nums.length];
dp[0]=nums[0];
dp[1]=Math.max(nums[0],nums[1]);
for(int i=2;i<nums.length;i++){
dp[i]=Math.max(dp[i-2]+nums[i],dp[i-1]);
}
return dp[nums.length-1];
}
重做的错误:dp[1]=Math.max(nums[0],nums[1])
没写出来,一看要用到dp[i-2]
就应该想到
打家劫舍II
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,能够偷窃到的最高金额。
- 确定dp数组以及下标的含义:数组长度为i+1的数组能偷到的最高金额为
dp[i]
- 确定递推公式:
dp[i]=Math.max(dp[i-2]+nums[i],dp[i-1])
- dp数组如何初始化:
dp[0]=nums[0]
- 确定遍历顺序:从左到右
- 举例推导dp数组
public int robII(int[] nums){
int res1=processRob(int[] nums,int 0,int num.length-2);
int res2=processRob(int[] nums,int 1,int num.length-1);
return Math.max(res1,res2);
}
public int processRob(int[] nums,int l,int r){
int[] dp=new int[nums.length];
dp[0]=nums[l];
dp[1]=Math.max(nums[l],nums[l+1]);
for(int i=l+2;i<nums.length;i++){
dp[i]=Math.max(dp[i-2]+nums[i],dp[i-1]);
}
return dp[r];
}
打家劫舍III👑
在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。
思路:下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。后序遍历,为什么要后序遍历,我的理解:后序遍历就是左右头,分别得到左子树和右子树的信息然后决定头节点,最后到根节点也就完成了整棵树的遍历。感觉树型DP的题目全都是后序遍历。
-
确定dp数组以及下标的含义:下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。
-
确定递推公式:
不偷当前节点:
dp[0]=Math.max(left[0],left[1])+Math.max(right[0],right[1])
偷当前节点:
dp[1]=root.val+left[0]+right[0]
-
dp数组如何初始化:遇到空节点返回0
-
确定遍历顺序:后序遍历
-
举例推导dp数组
public int robIII(TreeNode root){
if(root==null)return 0;
int[] res=processRobIII(root);
return Math.max(res[0],res[1]);
}
public int[] processRobIII(TreeNode root){
int[] dp=new int[2];
if(root==null)return dp;
int[] left=processRobIII(root.left);
int[] right=processRobIII(root.right);
dp[0]==Math.max(left[0],left[1])+Math.max(right[0],right[1]);
dp[1]=root.val+left[0]+right[0];
return dp;
}
🌟结合了二叉树,多重考核了属于是
【8】买卖股票
买卖股票的最佳时机
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
-
确定dp数组以及下标的含义:
dp[i][0]
代表第i天持有股票所得的利润dp[i][1]
代表第i天不持有股票所得的利润 -
确定递推公式:
第i天持有有两种情况:第i天买入,第i天持有
-
第i天买入:
dp[i][0]=prices[i]
🌟注意:自己写错了这个地方,因为只有一次买入,所以不是
dp[i-1][1]-prices[i]
-
第i天持有:
dp[i][0]=dp[i-1][0]
-
取最大值:
dp[i][0]=Math.max(-prices[i],dp[i-1][0])
第i天不持有的两种情况:第i-1天就不持有,第i天买入
- 第i-1天就不持有:
dp[i][1]=dp[i][i-1]
- 第i天买入:
dp[i][1]=dp[i-1][0]+prices[i]
- 取最大值:
dp[i][1]=Math.max(dp[i-1][0]+prices[i],dp[i][i-1])
-
-
dp数组如何初始化:
dp[0][0]=-prices[0]
dp[0][1]=0
-
确定遍历顺序:从左到右遍历数组
-
举例推导dp数组
public int maxProfit(int[] prices){
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-1][1]-prices[i]
dp[i][0]=Math.max(-prices[i],dp[i-1][0]);
dp[i][1]=Math.max(dp[i-1][0]+prices[i],dp[i][i-1]);
}
return dp[prices.length-1][1];//一定是不持有的利润大
}
买卖股票的最佳时机II
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖)
-
确定dp数组以及下标的含义:
dp[i][0]
代表第i天持有股票所得的利润dp[i][1]
代表第i天不持有股票所得的利润 -
确定递推公式:
第i天持有有两种情况:第i天买入,第i天持有
- 第i天买入:
dp[i][0]=dp[i-1][1]-prices[i]
- 第i天持有:
dp[i][0]=dp[i-1][0]
- 取最大值:
dp[i][0]=Math.max(dp[i][1]-prices[i],dp[i-1][0])
第i天不持有的两种情况:第i-1天就不持有,第i天买入
- 第i-1天就不持有:
dp[i][1]=dp[i][i-1]
- 第i天买入:
dp[i][1]=dp[i-1][0]+prices[i]
- 取最大值:
dp[i][1]=Math.max(dp[i-1][0]+prices[i],dp[i][i-1])
- 第i天买入:
-
dp数组如何初始化:
dp[0][0]=-prices[0]
dp[0][1]=0
-
确定遍历顺序:从左到右遍历数组
-
举例推导dp数组
public int maxProfitII(int[] prices){
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][1]-prices[i],dp[i-1][0]);
dp[i][1]=Math.max(dp[i-1][0]+prices[i],dp[i][i-1]);
}
return dp[prices.length-1][1];
}
买卖股票的最佳时机III
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
-
确定dp数组以及下标的含义:
dp[i][0]
代表第i天不操作所得的利润dp[i][1]
代表第1天持有股票所得的利润dp[i][2]
代表第1天不持有股票所得的利润dp[i][3]
代表第2天持有股票所得的利润dp[i][4]
代表第2天不持有股票所得的利润 -
确定递推公式:
-
dp[i][1]=Math.max(dp[i-1][0]-prices[i],dp[i-1][1])
-
dp[i][2]=Math.max(dp[i-1][1]+prices[i],dp[i-1][2])
-
dp[i][3]=Math.max(dp[i-1][2]-prices[i],dp[i-1][3])
-
dp[i][4]=Math.max(dp[i-1][3]+prices[i],dp[i-1][4])
🌟递归方程还是写错了淦
-
-
dp数组如何初始化:
dp[0][0]=0
dp[0][1]=-prices[0]
dp[0][2]=0
dp[0][1]=-prices[0]
dp[0][1]=0
-
确定遍历顺序:从左到右遍历数组
-
举例推导dp数组
public int maxProfitIII(int[] prices){
int[][] dp=new int[prices.length][5];
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<prices.length;i++){
dp[i][1]=Math.max(dp[i-1][0]-prices[i],dp[i-1][1]);
dp[i][2]=Math.max(dp[i-1][1]+prices[i],dp[i-1][2]);
dp[i][3]=Math.max(dp[i-1][2]-prices[i],dp[i-1][3]);
dp[i][4]=Math.max(dp[i-1][3]+prices[i],dp[i-1][4]);
}
return dp[prices.length-1][4];
}
买卖股票的最佳时机IV
给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。
public int maxProfitIII(int[] prices,int k){
int[][] dp=new int[prices.length][2*k+1];
for(int i=0;i<=2*k;i++){
if(i%2==0)dp[0][i]=0;
else dp[0][i]=-prices[0];
}
for(int i=1;i<prices.length;i++){
for(int j=1;j<=2*k;j++){
if(j%2==1)dp[i][j]=Math.max(dp[i-1][j-1]-prices[i],dp[i-1][j]);
else dp[i][j]=Math.max(dp[i-1][j-1]-prices[i],dp[i-1][j]);
}
}
return dp[prices.length-1][2*k];
}
最佳买卖股票时机含冷冻期👑
给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 ,设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
- 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)
- 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)
public int maxProfitIV(int[] prices){
int[][] dp=new int[prices.length][3];
dp[0][0]=-prices[0];
dp[0][1]=0;//今天卖出
dp[0][2]=0;//不持有,一种是昨天刚卖了,在冷静期,一种是之前就没持有
for(int i=1;i<prices.length;i++){
dp[i][0]=Math.max(dp[i-1][0],dp[i-1][2]-prices[i]);
dp[i][1]=dp[i-1][0]+prices[i];
dp[i][2]=Math.max(dp[i-1][2],dp[i-1][1]);
}
return Math.max(dp[prices.length-1][1],dp[prices.length-1][2]);
}
买卖股票的最佳时机含手续费
给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;非负整数 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][1]-prices[i],dp[i-1][0]);
dp[i][1]=Math.max(dp[i-1][0]+prices[i]-fee,dp[i][i-1]);
}
return dp[prices.length-1][1];
}
【9】递增子序列
最长递增子序列
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
-
确定dp数组以及下标的含义:数组下标为i的最长递增子序列长度为
dp[i]
-
确定递推公式:
位置i的最长升序子序列等于j从0到i-1各个位置的最长升序子序列 + 1 的最大值
if(nums[i]>nums[j])dp[i]=Math.max(dp[i],dp[j]+1)
-
dp数组如何初始化:
Arrays.fill(dp,1)
-
确定遍历顺序:从左到右遍历数组
-
举例推导dp数组
public int lengthOfLIS(int[] nums){
int[] dp=new int[nums.length];
Arrays.fill(dp,1);
int max=0;
for(int i=1;i<nums.length;i++){//这里i不能从1开始,当nums为[0]会输出0
for(int j=0;j<i;j++){
if(nums[i]>nums[j])dp[i]=Math.max(dp[i],dp[j]+1);
}
max=Math.max(max,dp[i]);
}
return max;
}
最长连续递增序列
给定一个未经排序的整数数组,找到最长且连续递增的子序列,并返回该序列的长度。
-
确定dp数组以及下标的含义:数组下标为i的最长连续递增子序列长度为
dp[i]
-
确定递推公式:
if(nums[i]>nums[i-1])dp[i]=dp[i-1]+1
-
dp数组如何初始化:
Arrays.fill(dp,1)
-
确定遍历顺序:从左到右遍历数组
-
举例推导dp数组
public int findLengthOfLCIS(int[] nums) {
int[] dp=new int[nums.length];
Arrays.fill(dp,1);
int max=1;
for(int i=1;i<nums.length;i++){
if(nums[i]>nums[i-1])dp[i]=dp[i-1]+1;
max=Math.max(max,dp[i]);
}
return max;
}
【10】最长重复子数组/子序列👑
718.最长重复子数组
给两个整数数组 A 和 B ,返回两个数组中公共的、长度最长的子数组的长度。
动态规划五部曲:
-
确定dp数组以及下标的含义:以下标
i-1
结尾的数组A和以下标j-1
结尾的数组B最长公共子数组长度为dp[i][j]
-
确定递推公式:
if(A[i-1]==B[j-1])dp[i][j]=dp[i-1][j-1]+1
-
dp数组如何初始化:
dp[0][0]
是没有意义的所以遍历要从1开始 -
确定遍历顺序:从左到右遍历数组
-
举例推导dp数组
public int findLength(int[] nums1, int[] nums2) {
int[][] dp=new int[nums1.length+1][nums2.length+1];
int max=0;
for(int i=1;i<num1.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;
}
max=Math.max(max,dp[i][j]);
}
return max;
}
//可以改为一维数组
public int findLength(int[] nums1, int[] nums2) {
int[] dp=new int[nums2.length+1];
int max=0;
for(int i=1;i<=num1.length;i++){
for(int j=nums2.length-1;j>=1;j--){//必须从后向前遍历否则会重复覆盖
if(nums1[i-1]==nums2[j-1])dp[j]=dp[j-1]+1;
}
max=Math.max(max,dp[j]);
}
return max;
}
🌟感觉主要是dp数组的含义比较难想到,然后是改成一维数组的时候要从后向前遍历
1143.最长公共子序列
给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。
-
确定dp数组以及下标的含义:以下标
i-1
结尾的字符串1和以下标j-1
结尾的字符串2最长公共子数组长度为dp[i][j]
-
确定递推公式:
if(text1.charAt(i-1)==text2.charAt(j-1))dp[i][j]=dp[i-1][j-1]+1
-
dp数组如何初始化:不需要初始化,遍历从1开始
-
确定遍历顺序:从左到右遍历两个字符串
-
举例推导dp数组
public int longestCommonSubsequence(String text1, String text2) {
int[][] dp=new int[text1.length()+1][text2.length()+1];
for(int i=1;i<=text1.length();i++){
for(int j=1;j<=text2.length();j++){
if(text1.charAt(i-1)==text2.charAt(j-1))dp[i][j]=dp[i-1][j-1]+1;
else dp[i][j]=Math.max(dp[i-1][j],dp[i][j-1]);
}
}
return dp[text1.length()][text2.length()];
}
🌟本来以为这个题和上一个题完全一样,但其实不是的,重复子数组是要求数字都是连在一起的,公共子序列只是要求相对顺序
1035.不相交的线
绘制的最大连线数,其实就是求两个字符串的最长公共子序列的长度
-
确定dp数组以及下标的含义:以下标
i-1
结尾的数组1和以下标j-1
结尾的数组2最长公共子数组长度为dp[i][j]
-
确定递推公式:
if(text1.charAt(i-1)==text2.charAt(j-1))dp[j]=dp[j-1]+1
-
dp数组如何初始化:不需要初始化,遍历从1开始
-
确定遍历顺序:从左到右遍历两个数组
-
举例推导dp数组
public int maxUncrossedLines(int[] nums1, int[] nums2) {
int[][] dp=new int[nums1.length+1][nums2.length+1];
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;
else dp[i][j]=Math.max(dp[i-1][j],dp[i][j-1]);
}
}
return dp[nums1.length][nums2.length];
}
【11】53. 最大子序和
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
-
确定dp数组以及下标的含义:下标为i的数组最大和为
dp[i]
-
确定递推公式:
dp[i]=Math.max(nums[i],dp[i-1]+nums[i])
-
dp数组如何初始化:
dp[0]=nums[0]
-
确定遍历顺序:从左到右遍历数组
-
举例推导dp数组
public int maxSubArray(int[] nums) {
int[] dp=new int[nums.length];
dp[0]=nums[0];
int res=nums[0];
for(int i=1;i<nums.length;i++){
dp[i]=Math.max(nums[i],dp[i-1]+nums[i]);
res=Math.max(res.dp[i]);
}
return res;
}
【12】编辑字符串👑
392.判断子序列
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
-
确定dp数组以及下标的含义:下标为i-1的字符串s,和下标为j-1的字符串t,相同子序列长度为
dp[i][j]
-
确定递推公式:
如果当前字符相同:
s.charAt(i-1)==t.charAt(j-1)
dp[i][j]=dp[i-1][j-1]+1
如果不相同:相当于t删除当前字符
dp[i][j]=dp[i][j-1]
-
dp数组如何初始化:从1开始遍历
-
确定遍历顺序:正序遍历两个字符,先遍历s
-
举例推导dp数组
public boolean isSubsequence(String s, String t) {
int[][] dp=new int[s.length()+1][t.length()+1];
for(int i=1;i<=s.length();i++){
for(int j=1;j<=t.length();j++){
if(s.charAt(i-1)==t.charAt(j-1))dp[i][j]=dp[i-1][j-1]+1;
else dp[i][j]=dp[i][j-1];
}
}
return dp[s.length()][t.length()]==s.length();
}
115.不同的子序列👑
给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。
-
确定dp数组以及下标的含义:以i-1结尾的字符串s的子序列中以j-1结尾的子序列t出现的个数
-
确定递推公式:
如果当前字符相同:
s.charAt(i-1)==t.charAt(j-1)
dp[i][j]=dp[i-1][j-1]+1
如果不相同相当于把s删掉一个:
dp[i][j]=dp[i-1][j]
-
dp数组如何初始化:从1开始遍历
-
确定遍历顺序:从左到右遍历字符串
-
举例推导dp数组
public int numDistinct(String s, String t) {
int[][] dp=new int[s.length()+1][t.length()+1];
for(int i=0;i<=s.length();i++)dp[i][0]=1;
for(int i=1;i<=s.length();i++){
for(int j=1;j<t.length();j++){
if(s.charAt(i-1)==t.charAt(j-1))dp[i][j]=dp[i-1][j-1]+1+dp[i-1][j];
else dp[i][j]=dp[i-1][j];
}
}
return dp[s.length()][t.length()];
}
🌟重点:
- 当前字符一样的时候可以选也可以不选
- 初始化
583. 两个字符串的删除操作👑
给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。
-
确定dp数组以及下标的含义:以i-1结尾的单词 word1,和以j-1结尾的单词 word2相同所需要的最小步数
-
确定递推公式:
- 如果当前字符相同那就不用加步数
dp[i][j]=dp[i-1][j-1]
-
如果不相同:
删word1:
dp[i][j]=dp[i-1][j]+1
删word2:
dp[i][j]=dp[i][j-1]+1
两个都删:
dp[i][j]=dp[i-1][j-1]+2
-
dp数组如何初始化:
dp[i][0]
:以i-1
结尾的word1需要i步才能和word2一样 -
确定遍历顺序:从左到右遍历字符串
-
举例推导dp数组
public int minDistance(String word1, String word2) {
int[][] dp=new int[word1.length()+1][word2.length()+1];
for(int i=0;i<=word1.length();i++){
dp[i][0]=i;
}
for(int i=0;i<=word2.length();i++){
dp[0][i]=i;
}
for(int i=1;i<=word1.length();i++){
for(int j=1;j<=word2.length();j++){
if(word1.charAt(i)==word2.charAt(j))dp[i][j]=dp[i-1][j-1];
else dp[i][j]=Math.min(Math.min(dp[i-1][j]+1,dp[i][j-1]+1),dp[i-1][j-1]+2);
}
}
return dp[word1.length()][word2.length()];
}
//可以转换成两个字符串的长度减去最长公共子序列的长度
public int minDistance1(String word1, String word2) {
int len1=word1.length();
int len2=word2.length();
int[][] dp=new int[len1+1][len2+1];
for (int i = 1; i <=len1 ; i++) {
for (int j = 1; j <=len2 ; j++) {
if (word1.charAt(i-1)==word2.charAt(j-1))dp[i][j]=dp[i-1][j-1]+1;
else dp[i][j]=Math.max(dp[i-1][j],dp[i][j-1]);
}
}
return len1+len2-2*dp[len1][len2];
}
🌟可以转换成两个字符串的长度减去最长公共子序列的长度
72. 编辑距离
给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
-
确定dp数组以及下标的含义::以i-1结尾的单词 word1转换成以j-1结尾的单词 word2相同所需要的最小步数
-
确定递推公式:
如果相同:
dp[i][j]=dp[i-1][j-1]
如果不同有三种操作:
- word1插入一个字符相当于word2删除一个字符:
dp[i][j]=dp[i][j-1]+1
- word1删除一个字符:
dp[i][j]=dp[i-1][j]+1
- 替换一个字符:
dp[i][j]=dp[i-1][j-1]+1
- word1插入一个字符相当于word2删除一个字符:
-
dp数组如何初始化:
dp[i][0]
:以i-1
结尾的word1需要i步才能和word2一样 -
确定遍历顺序:
-
举例推导dp数组
public int minDistance2(String word1, String word2) {
int len1=word1.length();
int len2=word2.length();
int[][] dp=new int[len1+1][len2+1];
for (int i = 0; i <=len1 ; i++) {
dp[i][0]=i;
}
for (int i = 0; i <=len2; i++) {
dp[0][i]=i;
}
for (int i = 1; i <=len1 ; i++) {
for (int j = 1; j <=len2 ; j++) {
if (word1.charAt(i-1)==word2.charAt(j-1))dp[i][j]=dp[i-1][j-1];
else {
dp[i][j]=Math.min(Math.min(dp[i-1][j],dp[i][j-1]),dp[i-1][j-1])+1;
}
}
}
return dp[len1][len2];
}
【13】回文子串
647. 回文子串
给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
示例 1:
输入:“abc” 输出:3 解释:三个回文子串: “a”, “b”, “c”
动态规划五部曲:
-
确定dp数组以及下标的含义:
i~j
范围是否为回文子串 -
确定递推公式:
- 情况一:下标i 与 j相同,同一个字符例如a,当然是回文子串
- 情况二:下标i 与 j相差为1,例如aa,也是文子串
- 情况三:下标:i 与 j相差大于1的时候,例如cabac,此时s[i]与s[j]已经相同了,我们看i到j区间是不是回文子串就看aba是不是回文就可以了,那么aba的区间就是 i+1 与 j-1区间,这个区间是不是回文就看
dp[i+1][j-1]
是否为true。
-
dp数组如何初始化:
dp[0][0]=1
-
确定遍历顺序:
-
举例推导dp数组
//动态规划
public int countSubstrings(String s) {
boolean[][] dp=new boolean[s.length()][s.length()];
int res=1;
for(int i=s.length()-1;i>=0;i--){
for(int j=i;j<s.length();j++){
if(str.charAt(i)==str.charAt(j)){
if(j-i<=1){
res++;
dp[i][j]=true;
}else if(dp[i+1][j-1]){
res++;
dp[i][j]=true;
}
}
}
}
return res;
}
🌟重新写一遍这个题暴露了很大的问题,就是这个题的遍历顺序是跟递推公式有关的,是从i+1
推到i
的,所以遍历顺序一定是从后往前推
//双指针(这个方法也很重要)
public int countSubstrings(String s) {
int ans=0;
for(int i=0;i<s.length();i++){
ans+=process(s,i,i,s.length()-1);//以一个字符为中心
ans+=process(s,i,i+1,s.length()-1);//以两个字符为中心
}
return ans;
}
public int process(String s,int l,int r,int len){
int res=0;
while(l>=0 && r<=len && s.charAt(l)==s.charAt(r)){
res++;
l--;
r++;
}
return res;
}
516.最长回文子序列
给定一个字符串 s ,找到其中最长的回文子序列,并返回该序列的长度
🌟回文子序列可以不连续
-
确定dp数组以及下标的含义:字符串s在
i~j
范围内最长的回文子序列的长度为dp[i][j]
-
确定递推公式:
如果相等,那就是两个都可以加上
s.charAt(i)==s.charAt(j)
:dp[i][j]=dp[i+1][j-1]+2
如果不相等的话:
dp[i][j]=Math.max(dp[i+1][j],dp[i][j-1])
-
dp数组如何初始化:
-
确定遍历顺序:
-
举例推导dp数组
public int longestPalindromeSubseq(String s) {
int[][] dp=new int[s.length()][s.length()];
for(int i=s.length()-1;i>=0;i--){
dp[i][i]=1;
for(int j=i+1;j<s.length();j++){
if(s.charAt(i)==s.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][s.length()-1];
}