目录
动态规划是分治思想的延伸,通俗一点来说就是大事化小,小事化无的艺术。
在将大问题化解为小问题的分治过程中,保存对这些小问题已经处理好的结果,并供后面处理更大规模的问题时直接使用这些结果。
动态规划具备了以下三个特点
- 把原来的问题分解成了几个相似的子问题。
- 所有的子问题都只需要解决一次。
- 储存子问题的解。
动规五部曲分别为:
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
动态规划基础
力扣 509.斐波那契数
- dp[i]代表第i个数字斐波那契的值是dp[i]
- dp[i]=dp[i-1]+dp[i-2]
- 第一个数字是0,第二个数字是1,dp[0]=0,dp[1]=1
- 从递归公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,dp[i]是依赖 dp[i - 1] 和 dp[i - 2],那么遍历的顺序一定是从前到后遍历的
(注意初始化数组时,初始化了dp[0]和dp[1],所有定义数组长度时要+2(int[] dp=new int[n+2]),以免下标越界)
public class DpTest
{
public int fib(int n)
{
int[] dp=new int[n+2];
dp[0]=0;
dp[1]=1;
for (int i=2;i<=n;i++)
{
dp[i]=dp[i-1]+dp[i-2];
}
return dp[n];
}
}
力扣 70.爬楼梯
public class DpTest
{
public int climbStairs(int n)
{
if (n<=1)
{
return 1;
}
int[] dp=new int[n+1];
dp[1]=1;
dp[2]=2;
for (int i=3;i<=n;i++)
{
dp[i]=dp[i-1]+dp[i-2];
}
return dp[n];
}
}
力扣 746.使用最小花费爬楼梯
- dp[i]到达第i个台阶的最低花费dp[i]
- 有两个方式得到dp[i] ,一种是dp[i-1]和dp[i-2],选择这两个中最小的Math.min(dp[i-1],dp[i-2]),向上爬要支付费用,所以dp[i]=Math.min(dp[i-1),dp[i-2])+cost[i]。注意这里为什么是加cost[i],而不是cost[i-1],cost[i-2]之类的,因为题目中说了:每当你爬上一个阶梯你都要花费对应的体力值.
- 不需要全都初始化,根据递推公式只需要初始化dp[0]和dp[1]即可, dp[0]=cost[0],dp[1]=cost[1]
- dp[i]又dp[i-1]dp[i-2]推出,所以是从前到后遍历cost数组就可以了.
public class DpTest
{
public int minCostClimbingStairs(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] );
}
}
力扣 62.不同路径
1.dp[i][j]:从(0,0)出发到(i,j)有dp[i][j]条不同路径
2.dp[i][j]从两个方向来推导出来, 即dp[i - 1][j] 和 dp[i][j - 1], dp[i - 1][j] 表示啥,是从(0, 0)的位置到(i - 1, j)有几条路径,dp[i][j - 1]同理。即dp[i][j]=dp[i-1][j]+dp[i][j-1].
3.如何初始化呢,首先dp[i][0]一定都是1,因为从(0, 0)的位置到(i, 0)的路径只有一条,那么dp[0][j]也同理。
for (int i = 0; i < m; i++) dp[i][0] = 1;
for (int j = 0; j < n; j++) dp[0][j] = 1;
4.递归公式dp[i][j] = dp[i - 1][j] + dp[i][j - 1],dp[i][j]都是从其上方和左方推导而来,那么从左到右一层一层遍历就可以了。
public class DpTest
{
public static int uniquePaths(int m, int n)
{
int[][] dp=new int[m][n];
for (int i=0;i<m;i++)
{
dp[i][0]=1;
}
for (int j=0;j<n;j++)
{
dp[0][j]=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];
}
}
力扣 63.不同路径Ⅱ
此题和上题差不多,只是有个障碍物,遇到障碍物跳过即可,并且在初始化中,遇到障碍物了就终止。
public class DpTest
{
public int uniquePathsWithObstacles(int[][] obstacleGrid)
{
int[][] dp=new int[obstacleGrid.length][obstacleGrid[0].length];
for (int i=0;i<obstacleGrid.length&&obstacleGrid[i][0]!=1;i++)
{
dp[i][0]=1;
}
for (int j=0;j<obstacleGrid[0].length&&obstacleGrid[0][j]!=1;j++)
{
dp[0][j]=1;
}
for (int i=1;i<obstacleGrid.length;i++)
{
for (int j=1;j<obstacleGrid[0].length;j++)
{
if (obstacleGrid[i][j]==1)
{
continue;
}
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}
return dp[obstacleGrid.length-1][obstacleGrid[0].length];
}
}
力扣 343.整数拆分
- dp[i] :拆分数字i可以得到的最大乘积为dp[i]
- dp[i]有两种方法可以得到 从1到遍历到 j ,一种是直接相乘 i*(i-j) , 另一种是j*dp[i-j] (拆分i-j)j是从1开始遍历,拆分j的情况,在遍历j的过程中都计算过了。dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));
- p[0] dp[1] 就不应该初始化,也就是没有意义的数值。拆分0和拆分1的最大乘积是多少?这是无解的。所有直接从dp[2]开始初始化。dp[2]=1;
- dp[i] 是依靠 dp[i - j]的状态,所以遍历i一定是从前向后遍历,先有dp[i - j]再有dp[i]。
public class DpTest
{
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(dp[i], Math.max(j*(i-j),dp[i-j]*j));
}
}
return dp[n];
}
}
力扣 96.不同的二叉搜索树
- dp[i]: 第i个节点有dp[i]种互不相同的二叉搜索树
- dp[i] += dp[以j为头结点左子树节点数量] * dp[以j为头结点右子树节点数量] ,j相当于是头结点的元素,从1遍历到i为止。
- dp[i] += dp[j - 1] * dp[i - j]; ,j-1 为j为头结点左子树节点数量,i-j 为以j为头结点右子树节点数量
- 空树也能算一棵二叉搜索树,dp[0]=1;从递归公式上来讲,dp[以j为头结点左子树节点数量] * dp[以j为头结点右子树节点数量] 中以j为头结点左子树节点数量为0,也需要dp[以j为头结点左子树节点数量] = 1, 否则乘法的结果就都变成0了。所以初始化dp[0] = 1
public class DpTest
{
public int numTrees(int n)
{
int[] dp=new int[n+2];
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];
}
}
背包问题
01背包和完全背包
01背包:物品只能放到背包一次,不能重复放
二维dp数组01背包先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。
对于二维dp,dp[i][j]都是通过上一层即dp[i - 1][j]计算而来,本层的dp[i][j]并不会被覆盖!
一维dp数组01背包只能先遍历物品再遍历背包容量,且第二层for循环是从大到小遍历。
(因为一个物品会重复加入多次)
举一个例子:物品0的重量weight[0] = 1,价值value[0] = 15
如果正序遍历
dp[1] = dp[1 - weight[0]] + value[0] = 15
dp[2] = dp[2 - weight[0]] + value[0] = 30
此时dp[2]就已经是30了,意味着物品0,被放入了两次,所以不能正序遍历。
倒叙就是先算dp[2]
dp[2] = dp[2 - weight[0]] + value[0] = 15 (dp数组已经都初始化为0)
dp[1] = dp[1 - weight[0]] + value[0] = 15
所以从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。
完全背包:单个物品可以多次放到背包里(每种物品有无限件。)
纯完全背包的一维dp数组实现,先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。
但是仅仅是纯完全背包的遍历顺序是这样的,题目稍有变化,两个for循环的先后顺序就不一样了。
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
01背包
01背包的递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
力扣 416分割等和子集
- dp[i]: 背包总容量为i,可以凑成i的子集总和为dp[i]
- 01背包的递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);本题中:weight[i]和value[i]都是num[i],所以dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
- 从dp[j]的定义来看,首先dp[0]一定是0。如果如果题目给的价值都是正整数那么非0下标都初始化为0就可以了,如果题目给的价值有负数,那么非0下标就要初始化为负无穷。这样才能让dp数组在递归公式的过程中取的最大的价值,而不是被初始值覆盖了。本题题目中 只包含正整数的非空数组,所以非0下标的元素初始化为0就可以了。
- 01背包问题,先物品后背包。即用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒叙遍历。
public class DpTest
{
public boolean canPartition(int[] nums)
{
// 背包内总和不会大于20000,所以定义一个20000大的数组。
int[] dp=new int[20001];
int sum=0;
for (int i=0;i< nums.length;i++)
{
sum+=nums[i];
}
if (sum%2!=0)
{
return false;
}
int target=sum/2;
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]);
}
}
// 集合中的元素刚好可以凑成target
if(dp[target]==target)
{
return true;
}
return false;
}
}
力扣 1049最后一块石头重量Ⅱ
public class DpTest
{
public int lastStoneWeightII(int[] stones)
{
// dp[i] 表示容量为i的背包,最多可以背dp[i]重的石头
int[] dp=new int[15001];
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 - stones[i]]为 容量为j - stones[i]的背包最大所背重量。
dp[j]=Math.max(dp[j],dp[j-stones[i]]+stones[i]);
}
}
// 一堆石头的总重量是dp[target],另一堆就是sum - dp[target]。
// target=sum/2是向下取整,所有sum-dp[target]一定大于dp[target]
return (sum-dp[target])-dp[target];
}
}
力扣 494目标和
dp[i] :装满i这么大的背包,有dp[i]种方法
不考虑nums[i]的情况下,填满容量为j - nums[i]的背包,有dp[j - nums[i]]中方法。那么只要搞到nums[i]的话,凑成dp[j]就有dp[j - nums[i]] 种方法。(总的方案数为不考虑num[i]的方案数+有num[i]的方案数,即dp[j-num[i]]种方法和num[i]共同凑成了dp[j])。需要把 这些方法累加起来就可以了,dp[i] += dp[j - nums[j]](求组合问题的公式都是类似于这种)
dp[0]要初始化,dp[0]是一切递推公式的起源,装满容量为0的背包,有1种方法,就是装0件物品。的。dp[0]=1;
nums放在外层,target放在里面,里面倒叙遍历。
public class DpTest {
public int findTargetSumWays(int[] nums, int target) {
int sum = 0;
for (int i = 0; i < nums.length; i++) {
sum += nums[i];
}
// 目标和不能总和
if (sum < target) {
return 0;
}
// sum+taget =取+的数字和,不可能为小数。
if ((sum + target) % 2 == 1) {
return 0;
}
// 目标和加总和不能为负数
if (sum + target < 0) {
return 0;
}
// bagSize取+的数字和,问题等价于num[i]有多少种方法凑成bagSize
int bagSize = (sum + target) / 2;
// dp[i] 含义:填满容量为i的背包,有dp[i]种方法
// 填满j-num[i]的背包,有dp[j-num[i]]种方法
int[] dp = new int[bagSize + 1];
dp[0] = 1;
for (int i = 0; i < nums.length; i++) {
for (int j = bagSize; j >= nums[i]; j--) {
dp[j] += dp[j - nums[i]];
}
}
return dp[bagSize];
}
}
这道题回溯也可以哦:
public class DpTest {
public int findTargetSumWays(int[] nums, int target) {
int count=0;
public int findTargetSumWays(int[] nums, int target)
{
dfs(nums,target,0,0);
return count;
}
private void dfs(int[] nums, int target, int index, int sum)
{
if (index==nums.length)
{
if (sum==target)
{
count++;
}
}
else {
dfs(nums, target, index + 1, sum + nums[index]);
dfs(nums, target, index + 1, sum - nums[index]);
}
}
}
力扣 474.一和零
- dp[i][j]:有i个0和j个1的strs的最大子集个数是dp[i][j]
- dp[i][j] 可以由前一个strs里的字符串推导出来,strs里的字符串有zeroNum个0,oneNum个1。dp[i][j] 就可以是 dp[i - zeroNum][j - oneNum] + 1。然后我们在遍历的过程中,取dp[i][j]的最大值。所以递推公式:dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
- 01背包的dp数组初始化为0就可以。
- 我们讲到了01背包为什么一定是外层for循环遍历物品,内层for循环遍历背包容量且从后向前遍历
public class DpTest {
public int findMaxForm(String[] strs, int m, int n)
{
int[][] dp=new int[m+1][n+1];
for (String s:strs)
{
int zeroNum=0,oneNum=0;
for (char c:s.toCharArray())
{
if (c=='1')
{
oneNum++;
}
else
{
zeroNum++;
}
}
for (int i=m;i>=zeroNum;i--)
{
for (int j=n;j>=oneNum;j--)
{
dp[i][j]=Math.max(dp[i][j],dp[i-zeroNum][j-oneNum]+1);
}
}
}
return dp[m][n];
}
}
完全背包
力扣 518.零钱兑换
- dp[j]:凑成总金额为j的货币组合数为dp[j]
- dp[j] 所有的dp[j-coins[i]](不考虑coins[i])相加,即dp[j] += dp[j - coins[i]]
- 凑成金额为0个的货币组合数为1,dp[0]=1
- 完全背包中组合问题,先遍历物品后遍历背包,正序遍历
public class DpTest {
public int change(int amount, int[] coins)
{
int[] dp=new int[amount+1];
// 凑成金额为0有一种方法
// 下标非0的dp[j]初始化为0,dp[j-coin[i]]累加不影响dp[j]
dp[0]=1;
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];
}
}
力扣 377.组合总和Ⅳ
dp[i] :凑成总数为i的元素组合个数为dp[i]
求组合公式:dp[i]=dp[i-num[j]
dp[0]=1
此题不同顺序视为不同组合,所有外层遍历背包,内层遍历物品
public class DpTest {
public int combinationSum4(int[] nums, int target)
{
int[] dp=new int[target+1];
// dp[0]要初始化为1,这样递归其他dp[i]的时候才会有数值基础。
dp[0]=1;
for (int i=0;i<=target;i++)
{
for (int j=0;j<nums.length;j++)
{
if (i>=nums[i])
{
dp[i]+=dp[i-nums[j]];
}
}
}
return dp[target];
}
}
力扣 70.爬楼梯(再爬一次)
- dp[i] :爬到第i阶台阶有dp[i]种方法
- 求装满背包有几种方法,递推公式一般都是dp[i] += dp[i - nums[j]]; dp[i]有几种来源,dp[i - 1],dp[i - 2],dp[i - 3] 等等,即:dp[i - j]。递推公式为:dp[i] += dp[i - j]
- 递归公式是 dp[i] += dp[i - j],那么dp[0] 一定为1
- 这是背包里求排列问题,即:1、2 步 和 2、1 步都是上三个台阶,但是这两种方法。外层遍历背包,内层遍历物品
public class DpTest {
public int climbStairs(int n)
{
int[] dp=new int[n+1];
dp[0]=1;
for (int i=1;i<=n;i++)
{
// m表示最多可以爬m个台阶,代码中把m改成2就是本题70.爬楼梯
// for (int j=1;j<=m;j++)
for (int j=1;j<=2;j++)
{
if (i>=j)
{
dp[i]+=dp[i-j];
}
}
}
return dp[n];
}
}
力扣 322.零钱兑换
1.dp[i] :凑成总金额i的最少硬币个数dp[i]
2.dp[j]只有一个来源:dp[j-coins[i]], 即凑成总金额为j-coins[i]的最少硬币数为dp[j-coins[i],那么只要再加上这一个coins[i]就可以凑成dp[j].
- 当j<coins[i]时装不下,就只能继承dp[j]的值
- 当j>=coins[i]时装的下,可以选择不装或者装
即dp[j]=Math.min(dp[j],dp[j-coins[i]])
3.首先凑足总金额为0所需钱币的个数一定是0,那么dp[0] = 0;递推公式的特性,dp[j]必须初始化为一个最大的数,否则就会在min(dp[j - coins[i]] + 1, dp[j])比较的过程中被初始值覆盖。所以下标非0的元素都是应该是最大值。
4.求钱币最小个数,那么钱币有顺序和没有顺序都可以,都不影响钱币的最小个数。所以本题并不强调集合是组合还是排列。
public class DpTest {
public int coinChange(int[] coins, int amount)
{
int[] dp=new int[amount+1];
Arrays.sort(coins);
Arrays.fill(dp,Integer.MAX_VALUE);
dp[0]=0;
for (int i=0;i<coins.length;i++)
{
for (int j=coins[i];j<=amount;j++)
{
// 前面dp值在有计算过的基础上才能转移,如果不判断这一步,dp[j-coins[i]]=Integer.MAX_VALUE+1,那么最小的永远是dp[j]
if (dp[j-coins[i]]!=Integer.MAX_VALUE)
{
dp[j]=Math.min(dp[j],dp[j-coins[i]]+1);
}
}
}
return dp[amount]==Integer.MAX_VALUE?-1:dp[amount];
}
}
力扣 279.完全平方数
- dp[j]:和为j的完全平方数的最少数量dp[i]
- dp[j] 可以由dp[j - i * i]推出, dp[j - i * i] + 1 便可以凑成dp[j]。此时我们要选择最小的dp[j],所以递推公式:dp[j] = min(dp[j - i * i] + 1, dp[j]);
- dp[0]表示 和为0的完全平方数的最小数量,那么dp[0]一定是0。从递归公式dp[j] = min(dp[j - i * i] + 1, dp[j]);中可以看出每次dp[j]都要选最小的,所以非0下标的dp[i]一定要初始为最大值,这样dp[j]在递推的时候才不会被初始值覆盖。
- 如果求组合数就是外层for循环遍历物品,内层for遍历背包。如果求排列数就是外层for遍历背包,内层for循环遍历物品。本题求最小数,所以外层for遍历背包,里层for遍历物品,还是外层for遍历物品,内层for遍历背包,都是可以的。
public class DpTest {
public int numSquares(int n)
{
int[] dp=new int[n+1];
dp[0]=0;
Arrays.fill(dp,Integer.MAX_VALUE);
for (int i=1;i<=n;i++)
{
for (int j=1;j*j<=n;j++)
{
dp[j]=Math.min(dp[j],dp[j-i*i]+1);
}
}
return dp[n];
}
}
力扣 139.单词拆分
- dp[i] : 字符串长度为i,dp[i]=true,表示可以拆分为一个或者多个在字典中出现的单词
- 如果确定dp[j] 是true,且 [j, i] 这个区间的子串出现在字典里,那么dp[i]一定是true(j < i )。所以递推公式是 if([j, i] 这个区间的子串出现在字典里 && dp[j]是true) 那么 dp[i] = true。
- dp[0]表示如果字符串为空的话,说明出现在字典里。dp[0]=true;
- 本题使用求排列的方式,还是求组合的方式都可以。
public class DpTest {
public boolean wordBreak(String s, List<String> wordDict)
{
Set<String> set=new HashSet<>(wordDict);
boolean[] dp=new boolean[s.length()+1];
dp[0]=true;
for (int i=1;i<=s.length();i++)
{
for (int j=0;j<i;j++)
{
if (set.contains(s.substring(j,i))&&dp[j])
{
dp[i]=true;
break;
}
}
}
return dp[s.length()];
}
}
打家劫舍
力扣 198.打家劫舍
- dp[i] :考虑到dp[i]以内的房屋最多可以偷窃的金额是dp[i]
- dp[i]可以从两个方面推出,如果i房间不偷,那金额就和前一个一样,dp[i-1]; 另外就是这个房子偷,这个房子偷的话,那偷窃 i-2 房间的金额要加上这个房间的金额nums[i],即递推公式:dp[i]=Math.max(dp[i-1],dp[i-2]+nums[i]);
- 第一个房间的偷窃金额就是nums[0],第二个房间以内做多偷窃的金额就是Math.max(nums[0],nums[1])
- dp[i] 是根据dp[i - 2] 和 dp[i - 1] 推导出来的,应该从前向后推
public class DpTest {
public int rob(int[] nums)
{
if (nums.length==1)
{
return nums[0];
}
int[] dp=new int[nums.length+2];
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-1],dp[i-2]+nums[i]);
}
return dp[nums.length];
}
}
力扣 213.打家劫舍Ⅱ
和上一个题不一样的是,这个题成环了,考虑首元素就不能考虑尾部元素,考虑尾部元素就不能考虑首部元素。
public class DpTest {
public int rob(int[] nums)
{
if (nums.length==1)
{
return nums[0];
}
int ans=rob(nums,0,nums.length-2);
int ans2=rob(nums,1,nums.length-1);
return Math.max(ans,ans2);
}
private int rob(int[] nums, int start, int end)
{
// 只有两个元素
if (start==end)
{
return nums[start];
}
int[] dp=new int[nums.length];
dp[start]=nums[start];
dp[start+1]=Math.max(nums[start],nums[start+1]);
for (int i=start+2;i<nums.length;i++)
{
dp[i]=Math.max(dp[i-1],dp[i-2]+nums[i]);
}
return dp[end];
}
}
力扣 377.打家劫舍Ⅲ
递归(超时)
public class DpTest
{
public int rob(TreeNode root)
{
if (root==null)
{
return 0;
}
int money=root.val;
if (root.left!=null)
{
money+=rob(root.left.left)+rob(root.left.right);
}
if (root.right!=null)
{
money+=rob(root.right.left)+rob(root.right.right);
}
return Math.max(money,rob(root.left)+rob(root.right));
}
}
记忆化递归
public class DpTest
{
Map<TreeNode,Integer> map=new HashMap<>();
public int rob(TreeNode root)
{
if (root==null)
{
return 0;
}
if (root.right==null&&root.left==null)
{
return root.val;
}
// 可以使用一个map把计算过的结果保存一下,这样如果计算过孙子了,那么计算孩子的时候可以复用孙子节点的结果。
if (map.containsKey(root))
{
return map.get(root);
}
// 偷父节点
int money=root.val;
if (root.left!=null)
{
money+=rob(root.left.left)+rob(root.left.right);
}
if (root.right!=null)
{
money+=rob(root.right.left)+rob(root.right.right);
}
// 不偷父节点,偷子节点
int money2=rob(root.left)+rob(root.right);
map.put(root,Math.max(money,money2));
return Math.max(money,money2);
}
}
动态规划
public class DpTest
{
public int rob(TreeNode root)
{
int[] dp=rootTree(root);
return Math.max(dp[0],dp[1]);
}
private int[] rootTree(TreeNode root)
{
if (root==null)
{
return new int[2];
}
int[] left=rootTree(root.left);
int[] right=rootTree(root.right);
// 下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。
int[] dp=new int[2];
// 0 代表不偷,1 代表偷
// 当前节点选择偷:当前节点能偷到的最大钱数 = 左孩子选择自己不偷时能得到的钱 + 右孩子选择不偷时能得到的钱 + 当前节点的钱数
dp[1]=root.val+left[0]+right[0];
// 当前节点选择不偷:当前节点能偷到的最大钱数 = 左孩子能偷到的钱 + 右孩子能偷到的钱
dp[0]=Math.max(left[0],left[1])+Math.max(right[0],right[1]);
return dp;
}
}
股票系列
力扣 121.买卖股票的最佳时机(买卖一次)
1.dp[i][0]: 表示第i天所持有股票所得的现金,一开始现金是0,那么加入第i天买入股票现金就是 -prices[i], 这是一个负数。 dp[i][1] 表示第i天不持有股票所得现金。
2.如果第i天持有股票即dp[i][0], 那么可以由两个状态推出来
-
第i-1天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即:dp[i - 1][0]
-
第i天买入股票,所得现金就是买入今天的股票后所得现金即:-prices[i]
那么dp[i][0]应该选所得现金最大的,所以dp[i][0] = max(dp[i - 1][0], -prices[i]);
如果第i天不持有股票即dp[i][1], 也可以由两个状态推出来
-
第i-1天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金 即:dp[i - 1][1]
-
第i天卖出股票,所得现金就是按照今天股票佳价格卖出后所得现金即:prices[i] + dp[i - 1][0]
同样dp[i][1]取最大的,dp[i][1] = max(dp[i - 1][1], dp[i - 1][0]+prices[i]);
3.根据递推公式,需要得知dp[0][0]和dp[0][1]的值,dp[0][0]代表第0天持有股票,则dp[0][0]=-prices[0], dp[0][1]代表第零天不持有股票,dp[0][1]=0;
4.dp[i]都是由dp[i-1]推导出来的,是从前向后遍历
public class DpTest
{
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][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 dp[prices.length-1][1];
}
}
public class DpTest
{
public int maxProfit(int[] prices)
{
int[] dp=new int[2];
dp[0]=-prices[0];
dp[1]=0;
for (int i=1;i<prices.length;i++)
{
dp[0]=Math.max(dp[0],-prices[i] );
dp[1]=Math.max(dp[1],dp[0]+prices[i]);
}
return dp[1];
}
}
贪心
public class DpTest
{
public int maxProfit(int[] prices)
{
int low=Integer.MAX_VALUE;
int ans=0;
for (int i=0;i<prices.length;i++)
{
low=Math.min(low,prices[i]);
ans=Math.max(ans,prices[i]-low);
}
return low==Integer.MAX_VALUE?0:ans;
}
}
力扣 122.买卖股票的最佳时机Ⅱ(多次买卖)
和上一个题相比不同的是这个题可以多次买卖,那么在买的时候就要考虑利润的叠加了,第i天持有股票即dp[i][0],如果是第i天买入股票,所得现金就是昨天不持有股票的所得现金 减去 今天的股票价格 即:dp[i - 1][1] - prices[i]。
public class DpTest
{
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][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 dp[prices.length-1][1];
}
}
public class DpTest
{
public int maxProfit(int[] prices)
{
int[] dp=new int[2];
dp[0]=-prices[0];
dp[1]=0;
for (int i=1;i<prices.length;i++)
{
dp[0]=Math.max(dp[0],dp[1]-prices[i]);
dp[1]=Math.max(dp[1],dp[0]+prices[i]);
}
return dp[1];
}
}
力扣 123.买卖股票的最佳时机Ⅲ
一天一共就有五个状态,
0. 没有操作
-
第一次买入
-
第一次卖出
-
第二次买入
-
第二次卖出
dp[i][j]中 i表示第i天,j为 [0 - 4] 五个状态,dp[i][j]表示第i天状态j所剩最大现金。
dp[i][1] 表示的是第i天,买入股票的状态,并不是说一定要第i天买入股票,可以是沿用前一天买入的状态或者当天买入
i = 0天时五种状态 :dp[0][0] 第0天没有操作,dp[0][0]=0; dp[0][1] 第0天买入股票,dp[0][1]=-prices[0],dp[0][2]为在同一天买入并且卖出,dp[0][2]=0;
p[0][3] 同一天买入并且卖出后再以 -prices[0] 的价格买入股票,dp[0][3]=-prices[0];同理dp[0][4]=0;
public class DpTest
{
public int maxProfit(int[] prices)
{
int[][] dp=new int[prices.length][5];
dp[0][1]=-prices[0];
dp[0][3]=-prices[0];
for (int i=1;i<prices.length;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[prices.length-1][4];
}
}
public class DpTest
{
public int maxProfit(int[] prices)
{
int[] dp=new int[5];
dp[1]=-prices[0];
dp[3]=-prices[0];
for (int i=1;i<prices.length;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]);
}
return dp[4];
}
}
力扣 188.买卖股票的最佳时机
和上一题相比,区别就是本题可以买卖多次,通过规律我们可以发现,奇数为持有股票,偶数为不持有股票
public class DpTest
{
public int maxProfit(int k, int[] prices)
{
if (prices.length==0)
{
return 0;
}
int[][] dp=new int[prices.length][2*k+1];
// 初始化数组,第0天持有股票的初始值都为-prices[0]
for (int i=1;i<2*k;i+=2)
{
dp[0][i]=-prices[0];
}
for (int i=1;i<prices.length;i++)
{
for (int j=0;j<2*k-1;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]);
}
}
return dp[prices.length-1][2*k];
}
}
力扣 309.买卖股票最佳时机(含冷冻期)
dp[i][j]: 第i天状态为j所剩的最多现金为dp[i][j]
出现冷冻期后就有了四种状态:
-
状态一:买入股票状态(今天买入股票,或者是之前就买入了股票然后没有操作)
-
卖出股票状态,这里就有两种卖出股票状态
-
状态二:两天前就卖出了股票,度过了冷冻期,一直没操作,今天保持卖出股票状态
-
状态三:今天卖出了股票
-
-
状态四:今天为冷冻期状态,但冷冻期状态不可持续,只有一天!
状态一: 达到买入股票状态,前一天就是买入股票的状态或者今天买入,今天买入有两种情况:
- 前一天是冷冻期
- 前一天是保持卖出股票的状态
dp[i][0]=Math.max(dp[i-1][0],Math.max(dp[i-1][3],dp[i-1[1])-prices[i])
状态二: 达到卖出股票的状态,前一天就是卖出股票的状态或者昨天是冷冻期
dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]+prices[i])
状态三:今天卖出了股票
dp[i][2]=dp[i-1][0]+prices[i]
状态四: 今天是冷冻期,昨天刚卖了股票
dp[i][3]=dp[i-1][2]
public class DpTest
{
public int maxProfit(int[] prices)
{
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][3],dp[i-1][1])-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]));
}
}
力扣 714.买卖股票的最佳时机(含手续费)
可以多次交易就是每笔交易需要扣除手续费
public class DpTest
{
public int maxProfit(int[] prices, int fee)
{
int[] dp=new int[2];
dp[0]=-prices[0];
for (int i=1;i<prices.length;i++)
{
dp[0]=Math.max(dp[0],dp[1]-prices[i]);
dp[1]=Math.max(dp[1],dp[0]+prices[i]-fee);
}
return dp[1];
}
}
子序列问题
子序列(不连续)
力扣 300.最长上升子序列
- dp[i]: i 之前包括 i 的最长上升子序列
- if(num[i]>num[j])dp[i]=Math.max(dp[i],dp[j]+1)
- 每一个数字都是自己的一个上升子序列,每一个起始值都是1
- 从前向后遍历,遍历i的循环里外层,遍历j则在内层,
public class DpTest
{
public int lengthOfLIS(int[] nums)
{
if (nums.length==0)
{
return 0;
}
int[] dp=new int[nums.length];
Arrays.fill(dp,1);
int max=0;
for (int i=1;i<nums.length;i++)
{
for (int j=0;j<i;j++)
{
if (nums[i]>nums[j])
{
dp[i]=Math.max(dp[i],dp[j]+1);
}
}
if (dp[i]>max)
{
max=dp[i];
}
}
return max;
}
}
力扣 1143.最长公共子序列
- dp[i][j]:长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j]。之所以dp[i][j]的定义不是text1[0,i]和text2[0,j] 是为了方便当i=0或者j=0时,dp[i][j]表示为空字符串和另外一个字符串的匹配
- 当text1[i-1]=text2[j-1]时 最后一位相等,所以公共最长子序列又增加1 所以dp[i][j]=dp[i-1][j-1]+1
- 如果text1[i - 1] 与 text2[j - 1]不相同,那就看看text1[0, i - 2]与text2[0, j - 1]的最长公共子序列 和 text1[0, i - 1]与text2[0, j - 2]的最长公共子序列,取最大的。即:dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
public class DpTest
{
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++)
{
char c1=text1.charAt(i-1);
for (int j=1;j<=text2.length();j++)
{
char c2=text2.charAt(j-1);
if (c1==c2)
{
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()];
}
}
子序列(连续)
力扣 674.最长连续递增序列
public class DpTest
{
public int findLengthOfLCIS(int[] nums)
{
if (nums.length<=1)
{
return nums.length;
}
int[] dp=new int[nums.length];
Arrays.fill(dp,1);
int max=0;
for (int i=1;i<nums.length;i++)
{
if (nums[i]>nums[i-1])
{
dp[i]=dp[i-1]+1;
}
if (dp[i]>max)
{
max=dp[i];
}
}
return max;
}
}
力扣 718.最长重复数组
- dp[i][j] :以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dp[i][j]。
- 根据dp[i][j]的定义,dp[i][j]的状态只能由dp[i - 1][j - 1]推导出来。即当A[i - 1] 和B[j - 1]相等的时候,dp[i][j] = dp[i - 1][j - 1] + 1;
public class DpTest
{
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<=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 (dp[i][j]>max)
{
max=dp[i][j];
}
}
}
return max;
}
}
力扣 53.最大子序和
dp[i] : 包括下标i之前的最大连续子序列和为dp[i]。那么我们要找最大的连续子序列,就应该找每一个i为终点的连续最大子序列。所以在递推公式的时候,可以直接选出最大的dp[i]。
public class DpTest
{
public int maxSubArray(int[] nums)
{
int[] dp=new int[nums.length];
dp[0]=nums[0];
int max=nums[0];
for (int i=1;i< nums.length;i++)
{
dp[i]=Math.max(dp[i-1]+nums[i],nums[i]);
if (dp[i]>max)
{
max=dp[i];
}
}
return max;
}
}
方法二:
public class DpTest
{
public int maxSubArray(int[] nums)
{
int max=nums[0];
int sum=0;
for (int i:nums)
{
sum=Math.max(i,sum+i);
max=Math.max(sum,max);
}
return max;
}
}
编辑距离
力扣 392.判断子序列
public class DpTest
{
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];
}
}
}
if (dp[s.length()][t.length()] == s.length()) {
return true;
} else {
return false;
}
}
}
public boolean isSubsequence(String s, String t)
{
int i=0;
int j=0;
while (j<t.length()&&i<s.length())
{
if (s.charAt(i)==t.charAt(j))
{
i++;
}
j++;
}
return i==s.length();
}
力扣 115.不同的子序列
public class Num115
{
public int numDistinct(String s, String t)
{
// 以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]。
int[][] dp=new int[s.length()+1][t.length()+1];
for (int i=0;i<=s.length();i++)
{
dp[i][0]=1;
}
for (int j=1;j<=t.length();j++)
{
// 空字符串s可以随便删除元素,出现以j-1为结尾的字符串t的个数。
// 那么dp[0][j]一定都是0,s如论如何也变成不了t。
dp[0][j]=0;
}
// dp[0][0]应该是1,空字符串s,可以删除0个元素,变成空字符串t。
dp[0][0]=1;
for (int i=1;i<=s.length();i++)
{
for (int j = 1; j <=t.length() ; j++)
{
if (j>i)
{
continue;
}
if (s.charAt(i-1)==t.charAt(j-1))
{
dp[i][j]=dp[i-1][j-1]+dp[i-1][j];
}
else
{
dp[i][j]=dp[i-1][j];
}
}
}
return dp[s.length()][t.length()];
}
}
力扣 72.编辑距离
dp[i][j] :表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]。
if (word1[i - 1] == word2[j - 1]) 那么说明不用任何编辑,dp[i][j] 就应该是 dp[i - 1][j - 1],即dp[i][j] = dp[i - 1][j - 1];(word1[i - 1] 与 word2[j - 1]相等了,那么就不用编辑了,以下标i-2为结尾的字符串word1和以下标j-2为结尾的字符串word2的最近编辑距离dp[i - 1][j - 1] 就是 dp[i][j]了。)
if (word1[i - 1] != word2[j - 1])
- 操作一:word1增加一个元素,使其word1[i - 1]与word2[j - 1]相同,那么就是以下标i-2为结尾的word1 与 j-1为结尾的word2的最近编辑距离 加上一个增加元素的操作。即 dp[i][j] = dp[i - 1][j] + 1;
- 操作二:word2添加一个元素,使其word1[i - 1]与word2[j - 1]相同,那么就是以下标i-1为结尾的word1 与 j-2为结尾的word2的最近编辑距离 加上一个增加元素的操作。即 dp[i][j] = dp[i][j - 1] + 1;
这里有同学发现了,怎么都是添加元素,删除元素去哪了。word2添加一个元素,相当于word1删除一个元素,例如 word1 = "ad" ,word2 = "a",word2添加一个元素d,也就是相当于word1删除一个元素d,操作数是一样!
- 操作三:替换元素,word1替换word1[i - 1],使其与word2[j - 1]相同,此时不用增加元素,那么以下标i-2为结尾的word1 与 j-2为结尾的word2的最近编辑距离 加上一个替换元素的操作。即 dp[i][j] = dp[i - 1][j - 1] + 1;
综上,当 if (word1[i - 1] != word2[j - 1]) 时取最小的,
即:dp[i][j] = Math.min((dp[i - 1][j - 1], Math.min(dp[i - 1][j], dp[i][j - 1])) + 1;
public class DpTest
{
public int minDistance(String word1, String word2)
{
int[][] dp=new int[word1.length()+1][word2.length()+1];
for (int i=1;i<=word1.length();i++)
{
dp[i][0]=i;
}
for (int i=1;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-1)==word2.charAt(j-1))
{
dp[i][j]=dp[i-1][j-1];
}
else
{
dp[i][j]=Math.min(dp[i-1][j],Math.min(dp[i][j-1],dp[i-1][j-1]))+1;
}
}
}
return dp[word1.length()][word2.length()];
}
}
回文
力扣 647.回文子串
Boolean dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false。
s[i] !=s[j] dp[i][j]=false
s[i]=s[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。
public class DpTest
{
public int countSubstrings(String s)
{
boolean[][] dp=new boolean[s.length()][s.length()];
int count=0;
for (int i=s.length()-1;i>=0;i++)
{
for (int j=i;j<s.length();j++)
{
if (s.charAt(i)==s.charAt(j))
{
if (j-i<=1)
{
count++;
dp[i][j]=true;
}
else if (dp[i-1][j+1])
{
count++;
dp[i][j]=true;
}
}
}
}
return count;
}
}
力扣 516.最长回文子序列
public class DpTest
{
public int longestPalindromeSubseq(String s)
{
// dp[i][j]:字符串s在[i, j]范围内最长的回文子序列的长度为dp[i][j]。
int[][] dp=new int[s.length()][s.length()];
// dp[i][j]一定是等于1的,即:一个字符的回文子序列长度就是1。
for (int i=0;i<s.length();i++)
{
dp[i][i]=1;
}
// 那么分别加入s[i]、s[j]看看哪一个可以组成最长的回文子序列。
// 加入s[j]的回文子序列长度为dp[i + 1][j]。
// 加入s[i]的回文子序列长度为dp[i][j - 1]。
// 那么dp[i][j]一定是取最大的,即:dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
for (int i=s.length()-1;i>=0;i--)
{
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];
}
}