16道LeetCode学烂动态规划
前言
动态规划思路:
- 某一个问题可以被分割为子问题,且问题解决依赖于子问题,子问题又可以提前求出,那么就可以考虑动态规划。
- 动态规划关键是动态转移方程,而列出动态转移方程关键是设好dp的含义。就像小学求解应用题一样,要先【设】变量,再列方程。而dp的含义就是子问题的解决。
- 【设】了变量dp,即假设了子问题如何解决,那么就可以根据子问题去推导如何解决实际问题,得到动态转移方程,即dp之间的关系。
- 有了动态转移方程,再给定转移方程求解所需的初始条件,即可完成动态转移方程的求解。
- 本质就是用空间换时间,即保存计算过程,依据前面的计算得到后面的计算。
- 一般可以根据动态规划方程是否只是与相邻的前几个元素有关,如果是这样就可以将数组之类的结构优化,减少空间的开销。
1. 爬楼梯
- 问题
爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例1:
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶
示例2:
输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶
- 思路
1. 问题:你有多少种不同的方法可以【爬到楼顶】呢?
2. 设:dp[i]表示有多少种不同的方法可以【爬到第i阶】。
3. 得动态转移方程:dp[i]=dp[i-1]+dp[i-2]
4. 由动态转移方程可知,i>=2,dp[2]=dp[1]+dp[0],边界条件:dp[0]=1;dp[1]=1;
- 实现
public int climbStairs(int n) {
//自底向上求解
int[] dp=new int[n+1];
dp[0]=1;
dp[1]=1;
for (int i = 2; i <= n; i++) {
dp[i]=dp[i-1]+dp[i-2];
}
return dp[n];
}
2. 零钱兑换
- 问题
/**
* 零钱兑换 322
* 给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
* 计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
* 你可以认为每种硬币的数量是无限的。
*
* 示例 1:
* 输入:coins = [1, 2, 5], amount = 11
* 输出:3
* 解释:11 = 5 + 5 + 1
*
* 示例 2:
* 输入:coins = [2], amount = 3
* 输出:-1
*
* 示例 3:
* 输入:coins = [1], amount = 0
* 输出:0
*
* 示例 4:
* 输入:coins = [1], amount = 1
* 输出:1
*
* 示例 5:
* 输入:coins = [1], amount = 2
* 输出:2
*/
- 思路
1. 问题:可以【凑成总金额】所需的 最少的硬币个数?
2. 设:dp[i]表示可以【凑成总金额为i】所需的最少的硬币个数。
3. 得动态转移方程:dp[i]=1+min(dp[i-j]),j为所有零钱的可能。
4. 边界条件:dp[0]=0
- 实现
public static int coinChange_0(int[] coins, int amount) {
//1 定义dp数组
int[] dp=new int[amount+1];
Arrays.fill(dp,amount+1);//填充为不可能的最大组合个数,用于求最小值和判断是否能组成
dp[0]=0;
//2. 遍历amount,求dp
for (int i = 1; i <= amount; i++) {
//2.1 根据状态转移方程求dp[i]
for (int j = 0; j < coins.length; j++) {
if (coins[j]<=i) {
dp[i] = Math.min(dp[i], dp[i - coins[j]]+1);
}
}
}
//3. 如果可以组成amount,则返回dp[amount]即可
return dp[amount]>amount?-1:dp[amount];
}
3. 最长递增子序列
- 问题
/**
* 最长递增子序列 300
*
* 给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
* 子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。
* 例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
*
* 示例 1:
* 输入:nums = [10,9,2,5,3,7,101,18]
* 输出:4
* 解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
*
* 示例 2:
* 输入:nums = [0,1,0,3,2,3]
* 输出:4
*
* 示例 3:
* 输入:nums = [7,7,7,7,7,7,7]
* 输出:1
*/
- 思路
1. 问题:找到【nums中】最长严格递增子序列的长度。
2. 设:dp[i]表示以【第i个元素作为递增子序列的最后一个元素】的最长严格递增子序列的长度。
3. 得动态转移方程:dp[i]=max(dp[i],1+dp[j]),其中j<i,并且nums[i]>nums[j]
4. 边界条件:dp[i]=1;
- 实现
public int lengthOfLIS_0(int[] nums) {
//1. dp存储动态规划路上的计算结果
int[] dp=new int[nums.length];
//2. dp边界条件,result记录返回结果
dp[0]=1;
int result=1;
//3. 遍历nums,计算dp[i]
for (int i = 1; i < nums.length; i++) {
//3.1 dp[i]初始值为1
dp[i]=1;
//3.2 遍历[0,i-1],根据状态转移方程计算dp[i]
for (int j = 0; j < i; j++) {
if (nums[i]>nums[j])
dp[i]=Math.max(dp[i],dp[j]+1);
}
result=Math.max(result,dp[i]);
}
//4. 返回result即可
return result;
}
4. 完全平方数
- 问题
/**
* 完全平方数 279
* 给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
* 给你一个整数 n ,返回和为 n 的完全平方数的 最少数量。
* 完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,
* 而 3 和 11 不是。
*
*/
- 思路
1. 问题:找到【和为 n】 的完全平方数的 最少数量?
2. 设:dp[i]为【和为i】的完全平方数的 最少数量
3. 得动态转移方程:dp[i]=1+min(dp[i-j^2]),j^2<=i
4. 边界条件:dp[0]=0;
- 实现
public int numSquares_0(int n) {
//1. dp[i] 存储状态转移方程的所有状态
int[] dp=new int[n+1];
//2. 从小到大枚举n,计算所有的dp[i]
for (int i = 1; i <= n; i++) {
int min=Integer.MAX_VALUE;//记录dp[i]的最少平方个数
//2.1 根据状态转移方程计算dp[i]
for (int j = 1; j*j <= i; j++) {
//状态转移方程
min=Math.min(min,dp[i-j*j]);
}
dp[i]=1+min;
}
//3. 返回dp[n]即可
return dp[n];
}
5. 乘积最大子数组
- 问题
/**
* 乘积最大子数组 152
*
* 给你一个整数数组 nums ,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
*
* 示例 1:
* 输入: [2,3,-2,4]
* 输出: 6
* 解释: 子数组 [2,3] 有最大乘积 6。
*
* 示例 2:
* 输入: [-2,0,-1]
* 输出: 0
* 解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。
*
*/
- 思路
1. 问题:请你找出【数组中】乘积最大的连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积?
2. 思考:如果nums[i]为正数,那么我期望获取第 i-1 个元素结尾的乘积的最小值;
如果nums[i]为负数,那么我期望获取第 i-1 个元素结尾的乘积的最大值
3. 设:dp_max[i]表示【第 i 个元素结尾的】乘积最大子数组的乘积。
dp_min[i]表示【第 i 个元素结尾的】乘积最小子数组的乘积。
4. 得动态转移方程:dp_max[i]=max{dp_max[i-1]*nums[i],dp_min[i-1]*nums[i],nums[i]} 其中,1=<i<=nums.length;
dp_min[i]=min{dp_max[i-1]*nums[i],dp_min[i-1]*nums[i],nums[i]} 其中,1=<i<=nums.length
5. 边界条件:dp_max和dp_min初始都为nums对应的值(可以通过简单情况验证)
- 实现
public static int maxProduct_1(int[] nums) {
int length = nums.length;
int[] dp_max=new int[length];
int[] dp_min=new int[length];
System.arraycopy(nums,0,dp_max,0,length);
System.arraycopy(nums,0,dp_min,0,length);
//状态转移
for (int i = 1; i < nums.length; i++) {
dp_max[i]=Math.max(dp_max[i-1]*nums[i],Math.max(dp_min[i-1]*nums[i],nums[i]));
dp_min[i]=Math.min(dp_max[i-1]*nums[i],Math.min(dp_min[i-1]*nums[i],nums[i]));
}
//求最大值
int max=dp_max[0];
for (int i = 1; i < length; i++) {
max=Math.max(max,dp_max[i]);
}
return max;
}
6. 最大子序和
- 问题
/**
* 最大子序和 53
*
* 给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
* 输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
* 输出:6
* 解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
*/
- 思路
1. 问题:找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和?
2. 设:dp[i]表示【以i结尾的】连续子数组的最大和。
3. 得动态转移方程:dp[i]=max(nums[i],dp[i-1]+nums[i])
4. 边界条件:dp[0]=nums[0]
- 实现
public static int maxSubArray_4(int[] nums) {
int n = nums.length;
//0. max记录dp中的最大值
int max=nums[0];
//1. 定义dp,及边界
int[] dp=new int[n];
dp[0]=nums[0];
//2. 根据状态转移方程求dp
for (int i = 1; i < n; i++) {
dp[i]=Math.max(nums[i],dp[i-1]+nums[i]);
max=Math.max(max,dp[i]);
}
//3. 返回max即可
return max;
}
7. 括号生成
- 问题
/**
* 括号生成 22
*
* 数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
*
* 示例 1:
* 输入:n = 3
* 输出:["((()))","(()())","(())()","()(())","()()()"]
* 示例 2:
* 输入:n = 1
* 输出:["()"]
*
*/
- 思路
1. 问题:能够生成所有可能的并且 有效的 括号组合。
2. 思考:我们考虑整个括号排列中【最左边的括号】,它一定是一个左括号,那么它可以和它对应的右括号组成一组
完整的括号 "( )",那么,剩下的括号有可能在哪呢?因为我取出了最左边的一组括号,因此剩余的括号
组合,要么在括号内,要么在括号右侧。
3. 设:对于n组括号,除去最左边的一组括号外,剩余的n-1组括号,
在最左边的一组括号内部的有i个,设为dp[i];在最左边的一组括号右侧的有j个,设为dp[j],i+j=n-1;
4. 得动态转移方程:dp[n]="("+dp[i]+")"+dp[j],i+j=n-1;
5. 边界条件:dp[0]=""; dp[1]="()"
- 实现
/**
* 动态规划
*
* 思路:
* n对括号的所有组合情况是 "("+p+")"+q,p+q=n-1,即n对括号一定要么在第一对括号的中间,要么在第一对括号的右边。
* 那么p可以是从0到n-1对括号,则对应q是从n-1到0对括号。
*
* 可得如下递推过程:
* 已知:
* dp[0]=null;
* dp[1]="()";
*
* 求dp[2]?
* 取p=0,q=1,则dp[2]="("+dp[0]+")"+dp[1]="()()"
* 取p=1,q=0,则dp[2]="("+dp[1]+")"+dp[0]=“(())”
*
* 同理,求dp[3]?
* 取p=0,q=2,则dp[3]="("+dp[0]+")"+dp[2]="()()()" or “()(())”
* 取p=1,q=1,则dp[3]="("+dp[1]+")"+dp[1]=“(())()”
* 取p=2,q=0,则dp[3]="("+dp[2]+")"+dp[0]=“(()())” or “((()))”
*
* ...
*
* 由此可知,满足动态规划的条件,即下一步的结果取决于上一步的结果。
* @param n
* @return
*/
public static List<String> generateParenthesis(int n) {
//1. 定义dp链表,存储每步结果,索引代表括号数,值为存储所有组合情况的链表。curr存储当前括号数对应的组合
List<List<String>> dp=new ArrayList<>();
List<String> curr;
//2. 设置dp数组的已知初始值dp[0],dp[1]
curr=new ArrayList<>();
curr.add("");
dp.add(0,curr);
curr=new ArrayList<>();
curr.add("()");
dp.add(1,curr);
//3. 遍历2-n,获取括号个数 i
for (int k = 2; k <= n; k++) {
curr=new ArrayList<>();
//3.1 遍历第一对括号内的所有情况p=0~(i-1)
for (int i = 0; i <= k - 1; i++) {
//3.2 遍历第一对括号右边的所有情况q=(i-1)-p
int j = k-1-i;
//3.3 获取括号个数为i的所有组合情况,存储到curr中
for (String inner : dp.get(i)) {
for (String right : dp.get(j)) {
curr.add("("+inner+")"+right);
}
}
}
//3.4 将curr存入到dp
dp.add(k,curr);
}
//4. 返回dp.get(n)即可
return dp.get(n);
}
8. 不同路径
- 问题
/**
* 不同路径 62
* 一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )
* 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
* 问总共有多少条不同的路径?
*
* 示例1:
* 输入:m = 3, n = 7
* 输出:28
*
* 示例 2:
* 输入:m = 3, n = 2
* 输出:3
* 解释:
* 从左上角开始,总共有 3 条路径可以到达右下角。
* 1. 向右 -> 向下 -> 向下
* 2. 向下 -> 向下 -> 向右
* 3. 向下 -> 向右 -> 向下
*
*/
- 思路
1. 问题:机器人试图达到【网格的右下角】,总共有多少条不同的路径?
2. 设:dp[i][j]表示机器人试【图达到网格(i,j)】位置总共有多少条不同的路径。
3. 得动态转移方程:dp[i][j]=dp[i-1][j] + dp[i][j-1],即要么从上面来,要么从左边来的
4. 边界条件:dp[0][j]=1;dp[i][0]=1;即上边界和左边界上的任何一点只有一条路径
- 实现
public static int uniquePaths(int m, int n) {
//0. 特判
if (m==1 && n==1)
return 1;
//1. 二维数组 p 保存状态
int [][] p=new int[m+1][n+1];
//2. 初始化已知 p 的值
for (int i = 1; i <= m; i++) {
p[i][1]=1;
}
for (int i = 1; i <= n; i++) {
p[1][i]=1;
}
//3. 状态转移
for (int i = 2; i <= m; i++) {
for (int j = 2; j <= n; j++) {
p[i][j]=p[i-1][j]+p[i][j-1];
}
}
//4. 返回p[m][n]即可
return p[m][n];
}
9. 最小路径和
- 问题
/**
* 最小路径和 64
* 给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
* 说明:每次只能向下或者向右移动一步。
*
* 示例1:
* 输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
* 输出:7
* 解释:因为路径 1→3→1→1→1 的总和最小。
*
* 示例 2:
* 输入:grid = [[1,2,3],[4,5,6]]
* 输出:12
*
* 提示:
*
* m == grid.length
* n == grid[i].length
* 1 <= m, n <= 200
* 0 <= grid[i][j] <= 100
*
*/
- 思路
1. 问题:请找出一条从【左上角到右下角】的路径,使得路径上的数字总和为最小。
2. 设:dp[i][j]为从【左上角走到(i,j)位置】的最小路径之和
3. 得动态转移方程:dp[i][j]=grid[i][j]+min(dp[i-1][j],dp[i][j-1])
4. 边界条件:dp依赖上和左位置,上边界和左边界已知,先求出
- 实现
public static int minPathSum(int[][] grid) {
//1. 初始化dp,因为第一行和第一列的情况只有一种,先初始化它们
int rowNum = grid.length;
int columnNum = grid[0].length;
int[][] dp=new int[rowNum][columnNum];
int pathSum=0;
//上边界
for (int i = 0; i < columnNum; i++) {
pathSum+=grid[0][i];
dp[0][i]=pathSum;
}
pathSum=0;
//左边界
for (int i = 0; i < rowNum; i++) {
pathSum+=grid[i][0];
dp[i][0]=pathSum;
}
//2. 动态转移方程
for (int i = 1; i < rowNum; i++) {
for (int j = 1; j < columnNum; j++) {
dp[i][j]=Math.min(dp[i][j-1],dp[i-1][j])+grid[i][j];
}
}
//3. 返回dp右下角的值即可
return dp[rowNum-1][columnNum-1];
}
10. 不同的二叉搜索树
- 问题
/**
* 不同的二叉搜索树 96
*
* 给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?
* 返回满足题意的二叉搜索树的种数。
*
* 示例1:
* 输入:n = 3
* 输出:5
*
* 示例2:
* 输入:n = 1
* 输出:1
*/
- 思路
1. 问题: 互不相同的 二叉搜索树 有多少种?
2. 设:dp[n]表示【n个节点组成互不相同的 二叉搜索树 的个数】
3. 得动态转移方程:dp[n]=sum(dp[i-1]*dp[n-i]),dp[i-1]*dp[n-i]表示以i为根节点(1=<i<=n),有多少种不同的组合方法
4. 边界条件:dp[0]=1;dp[1]=1
- 实现
public static int numTrees(int n) {
//1. dp存储中间过程结果
int[] dp=new int[n+1];
//2. 已知dp赋值
dp[0]=1;
dp[1]=1;
//3. 遍历n
for (int i = 2; i <= n; i++) {
//3.1 遍历根节点情况
for (int j = 1; j <= i; j++) {
//3.2 状态转移
dp[i]+=dp[j-1]*dp[i-j];
}
}
//4. 返回dp[n]即可
return dp[n];
}
11. 单词拆分
- 问题
/**
* 单词拆分 139
*
* 给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
*
* 说明:
* 拆分时可以重复使用字典中的单词。
* 你可以假设字典中没有重复的单词。
*
* 示例 1:
* 输入: s = "leetcode", wordDict = ["leet", "code"]
* 输出: true
* 解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code"。
*
* 示例 2:
* 输入: s = "applepenapple", wordDict = ["apple", "pen"]
* 输出: true
* 解释: 返回 true 因为 "applepenapple" 可以被拆分成 "apple pen apple"。
* 注意你可以重复使用字典中的单词。
*
* 示例 3:
* 输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
* 输出: false
*/
- 思路
1. 问题:判定【 s 】是否可以被空格拆分为一个或多个在字典中出现的单词。
2. 设:dp[i]表示【s的前i个字符子串】是否能被空格拆分为一个或多个在字典中出现的单词。
3. 思考:判断一个字符串s是否能被拆分为一个或多个在字典中出现的单词,
【可以把s分为s1和s2两个部分,s2是拆分后的最后一个单词】,因此只要s1满足被拆分成功,
同时s2又包含在字典中,那么s就可以拆分成功。
4. 得动态转移方程:d[i]=d[j] and check(s[j...i-1]) 0=<j<=i-1;
5. 边界条件: d[0]=true; 验证i=0,说明整个s作为单词,此时只要check即可,j=0,即d[j]=true。
- 实现
public boolean wordBreak_0(String s, List<String> wordDict) {
//1. hash表 set 用于判断最后一个字符串s2是否在字典中
Set<String> set=new HashSet<>(wordDict);
//2. dp数组记录每个子串是否为符合条件的字符串
boolean[] dp=new boolean[s.length()+1];//dp[0]是作为额外的边界条件
dp[0]=true;
//3. 遍历s的所有位置,判断dp[i]是否能够被空格拆分为一个或多个在字典中出现的单词
for (int i = 1; i <= s.length(); i++) {
//3.1 遍历子串所有可能切割的位置j
for (int j = 0; j < i; j++) {
//3.2 如果满足状态转移方程,说明当前切割点已经可以完成满足条件的单词拆分,则结束循环
if (dp[j] && set.contains(s.substring(j,i)))
{
dp[i]=true;
break;
}
}
}
return dp[s.length()];
}
12. 最大正方形
- 问题
/**
* 最大正方形 221
*
* 在一个由 '0' 和 '1' 组成的二维矩阵内,找到只包含 '1' 的最大正方形,并返回其面积。
*
* 示例 1:
* 输入:matrix = [["1","0","1","0","0"],["1","0","1","1","1"],["1","1","1","1","1"],["1","0","0","1","0"]]
* 输出:4
*
* 示例2:
* 输入:matrix = [["0","1"],["1","0"]]
* 输出:1
*
* 示例3:
* 输入:matrix = [["0"]]
* 输出:0
*
*/
- 思路
1. 问题:找到只包含 '1' 的最大正方形,并返回其面积。
2. 设:设dp(i,j)表示以(i,j)为右下角,且只包含1的正方形的边长的最大值。
3. 得动态转移方程:dp(i,j)=min(dp(i,j-1),dp(i-1,j),dp(i-1,j-1))+1 ;即dp(i,j)是其左,上,左上dp值的最小值加1,为什么是最小值你画图就可以理解了,可以理解为三种情况的交集。
4. 边界条件:以上边和左边位置为正方形右下角的最大边长为1,当值不为0的时候。
- 实现
public int maximalSquare(char[][] matrix) {
//1. 特判:如果矩阵为null,或者矩阵行为0,或者矩阵的列为0,则面积为0
int maxEdgeLength=0;
if (matrix==null || matrix.length==0 || matrix[0].length==0)
return maxEdgeLength;
//2. 遍历矩阵,求状态转移方程
int rows = matrix.length;
int columns = matrix[0].length;
int[][] dp=new int[rows][columns];
for (int i = 0; i < rows; i++) {
for (int j = 0; j < columns; j++) {
if (matrix[i][j]=='1')
{
//已知边界条件
if (i==0 || j==0)
{
dp[i][j]=1;
}
else
{
//动态转移方程
dp[i][j]=Math.min(dp[i-1][j],Math.min(dp[i-1][j-1],dp[i][j-1]))+1;
}
maxEdgeLength=Math.max(maxEdgeLength,dp[i][j]);
}
}
}
//3. 返回最大边的平方即为最大面积
return maxEdgeLength*maxEdgeLength;
}
13. 打家劫舍
- 问题
/**
* 打家劫舍 198
* 你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
* 给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
*
* 示例1:
* 输入:[1,2,3,1]
* 输出:4
* 解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
* 偷窃到的最高金额 = 1 + 3 = 4 。
*
* 示例 2:
* 输入:[2,7,9,3,1]
* 输出:12
* 解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
* 偷窃到的最高金额 = 2 + 9 + 1 = 12。
*
* !!示例给的有一定的误区,容易让读者认为他是各一家就偷依次,其实他不一定隔一家偷一次,也可以隔好多家偷一次呀,只要不相邻即可!!
*/
- 思路
1. 问题:计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额?
2. 设:dp[i]表示偷到第i个房间,能够偷窃到的最高金额。
3. 得动态转移方程:dp[i]=max(dp[i-2]+nums[i],dp[i-1]) ,即偷第i个房间或者不偷第i个房间所盗取金额的最大值。
4. 边界条件:dp[0]=nums[0]; dp[1]=max(nums[0],nums[1]);
- 实现
public static int rob_0(int[] nums) {
//0. 特判
int length = nums.length;
if (nums==null || nums.length==0)
return 0;
if (length==1)
return nums[0];
//1. 边界条件
int[] dp=new int[length];
dp[0]=nums[0];
dp[1]=Math.max(nums[0],nums[1]);
//2. 状态转移
for (int i = 2; i < nums.length; i++) {
dp[i]=Math.max(dp[i-2]+nums[i],dp[i-1]);
}
//3. 最终结果
return dp[length-1];
}
14. 打家劫舍 III
- 问题
/**
* 打家劫舍 III
* 在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。
* 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。
* 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。
*
* 计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。
*
* 示例 1:
* 输入: [3,2,3,null,3,null,1]
*
* 3
* / \
* 2 3
* \ \
* 3 1
* 输出: 7
* 解释: 小偷一晚能够盗取的最高金额 = 3 + 3 + 1 = 7.
*
* 示例 2:
* 输入: [3,4,5,1,3,null,1]
*
* 3
* / \
* 4 5
* / \ \
* 1 3 1
*
* 输出: 9
* 解释: 小偷一晚能够盗取的最高金额 = 4 + 5 = 9.
*/
- 思路
1. 问题:计算在不触动警报的情况下,小偷一晚能够盗取的最高金额?
2. 设:f(o)表示偷o节点的情况下,o树的最大盗取金额;g(o) 表示不偷o节点的情况下,o树的最大盗取金额;left和right代表o节点的左右孩子
3. 得动态转移方程:f(o)=g(o.left)+g(o.right)+o.val;即偷o节点时,就不能选择它的左右孩子节点
g(o)=max(f(o.left),g(o.left))+max(f(o.right),g(o.right));
即不偷o节点时,左右孩子可以选择偷或者不偷,我们取最大值求和即为g(o)
4. 边界条件:f和g初始为0,因为是根据树的左右子结点推导根节点,可以利用深度优先搜索的后续遍历
- 实现
Map<TreeNode,Integer> f=new HashMap<TreeNode,Integer>();
Map<TreeNode,Integer> g=new HashMap<TreeNode,Integer>();
public int rob_1(TreeNode root) {
dfs(root);
return Math.max(f.getOrDefault(root,0),g.getOrDefault(root,0));
}
/**
* 后序深度优先遍历,获取所有节点偷与不偷两种情况下的盗取最大金额数
* @param curr
*/
private void dfs(TreeNode curr)
{
if (curr==null)
return;
//1. 搜索左子树
dfs(curr.left);
//2. 搜索右子树
dfs(curr.right);
//3. 根据动态规划方程,计算当前节点两种情况下盗取的最大金额
f.put(curr,curr.val+g.getOrDefault(curr.left,0)+g.getOrDefault(curr.right,0));
g.put(curr,Math.max(f.getOrDefault(curr.left,0),g.getOrDefault(curr.left,0))+
Math.max(f.getOrDefault(curr.right,0),g.getOrDefault(curr.right,0)));
}
15. 最长回文子串
- 问题
/**
* 最长回文子串 5
* 给你一个字符串 s,找到 s 中最长的回文子串。
*
* 示例 1:
* 输入:s = "babad"
* 输出:"bab"
* 解释:"aba" 同样是符合题意的答案。
*
* 示例 2:
* 输入:s = "cbbd"
* 输出:"bb"
*
* 示例 3:
* 输入:s = "a"
* 输出:"a"
*
* 示例 4:
* 输入:s = "ac"
* 输出:"a"
*/
- 思路
1. 问题:给你一个字符串 s,找到 s 中最长的回文子串。
2. 设:dp[i][j]表示s[i...j]子串是否为回文子串
3. 思考:一个字符串是否为回文子串可以分为该字符串的第一个字符和最后一个字符是否相等,在相等的前提下,
再判断出去第一个字符和最后一个字符后的子串是否为回文子串,若是则该字符串为回文子串。
4. 得动态转移方程:dp[i][j]=s[i]==s[j] and (j-i+1==1 || dp[i+1][j-1])
5. 边界条件:如果该字符串只有一个字符,则该字符串是回文串,即初始化dp[i][i]=true
- 实现
public static String longestPalindrome(String s) {
//1. 特殊情况判断,如果字符串长度2,则返回字符串本身
int len = s.length();
if (len<2)
return s;
//2. begin 记录回文子串开始索引,maxLen 记录回文子串长度
int begin=0;
int maxLen=1;
//3. dp二维数组,存储动态转移方程每一步的结果,表格对角线上的值都为ture(因为对角线上代表单个字符是否为回文串)
boolean dp[][]=new boolean[len][len];
for (int i = 0; i < len; i++) {
dp[i][i]=true;
}
//4. 依次从第一列开始向最后一列填表(每一列的每个单元的值依赖于该单元左下角的值)
for (int j = 1; j < len; j++) {
for (int i = 0; i < j; i++) {
//4.1 根据状态转移方程填表
//4.1.1 如果子串的首尾字符不相同,则该子串不是回文子串
if (s.charAt(i)!=s.charAt(j))
dp[i][j]=false;
else
{
//4.1.2 如果子串的长度小于2,及j-i+1<2 ==> j-i<3,这该子串是回文子串
if (j - i < 3)
dp[i][j] = true;
//4.1.3 如果子串除去首尾字符后,剩余字符串,仍是回文子串,则该子串为回文子串
else
dp[i][j]=dp[i+1][j-1];
}
//4.1.4 如果子串是回文串,并且子串长度大于maxLen,更新 begin 和 maxLen 的值
if (dp[i][j] && j-i+1>maxLen)
{
begin=i;
maxLen=j-i+1;
}
}
}
//5. 根据begin和maxLen截取字符串s即可。
return s.substring(begin,begin+maxLen);
}
16. 最佳买卖股票时机含冷冻期
- 问题
/**
* 最佳买卖股票时机含冷冻期 309
*
* 给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。
* 设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
* 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
* 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
* 示例:
* 输入: [1,2,3,0,2]
* 输出: 3
* 解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
*
*/
- 思路
1. 问题:在满足以下约束条件下,你可以尽可能地完成更多的交易,设计一个算法计算出最大利润?
2. 设:dp[i]表示第i天能够获取的最大利润,再根据条件限制具体变量
即dp[i][0]表示第i天手中有一支股票,能够获取的最大利润。(即前一天手中也有一支股票)
即dp[i][1]表示第i天手中没有股票,但处于冷冻期,能够获取的最大利润。(即前一天刚刚卖出)
即dp[i][2]表示第i天手中没有股票,但不处于冷冻期,能够获取的最大利润。(即前一天既没买入也未卖出)
3. 得动态转移方程:dp[i][0]=max(dp[i-1][0],dp[i-1][2]-prices[i]),即今天手中的股票是前一天就有的,还是今天刚买入的,取最大值
dp[i][1]=dp[i-1][0]-prices[i],即前一天获得的最大利润减去前一天买股票花的钱
dp[i][2]=max(dp[i-1][1],dp[i-1][2]),即前一天手中虽然没有股票,但不确定是否处于冷冻期,因此取最大利润
5. 边界条件:dp[0][0]=-prices[0];dp[0][1]=0;dp[0][2]=0;
- 实现
public int maxProfit_0(int[] prices) {
int n = prices.length;
//1. 特判
if (n==0)
return 0;
//2. f二维数组表示第i天结束之后3种情况的「累计最大收益」
int[][] dp =new int[n][3];
//3. 初始边界条件
dp[0][0]=-prices[0];
dp[0][1]=0;
dp[0][2]=0;
//4. 遍历prices,进行状态转移方程计算
for (int i = 1; i < n; i++) {
//4.1 第i天结束之后,我们目前持有一支股票,对应的「累计最大收益」f[i][0]=max(f[i−1][0],f[i−1][2]−prices[i])
dp[i][0]=Math.max(dp[i-1][0],dp[i-1][2]-prices[i]);
//4.2 第i天结束之后,我们目前不持有任何股票,并且处于冷冻期中,对应的「累计最大收益」f[i][1]=f[i−1][0]+prices[i]
dp[i][1]=dp[i-1][0]+prices[i];
//4.3 第i天结束之后,我们目前不持有任何股票,并且不处于冷冻期中,对应的「累计最大收益」f[i][2]=max(f[i−1][1],f[i−1][2])
dp[i][2]=Math.max(dp[i-1][1],dp[i-1][2]);
}
//5. 返回max(f[n−1][1],f[n−1][2])即可,因为最后一天手上持有的股票必须卖出才能获得尽可能大的收益
return Math.max(dp[n-1][1],dp[n-1][2]);
}