理论基础
动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。
所以动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的,
解题步骤
对于动态规划问题,我将拆解为如下五步曲,这五步都搞清楚了,才能说把动态规划真的掌握了!
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 举例推导dp数组
1.斐波那契数
递归
public int fib(int n) {if (n <= 1) {return n;}return fib(n - 1) + fib(n - 2);}
动态规划
public int fib(int n) {if (n <= 1) {return 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.爬楼梯
public int climbStairs(int n) {if (n <= 2) {return n;}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];}
3.使用最小花费爬楼梯⭐
- 确定dp数组以及下标的含义
- dp[i]的定义:到达第i个台阶所花费的最少体力为dp[i]。(注意这里认为是第一步一定是要花费)
- 确定递推公式
可以有两个途径得到dp[i],一个是dp[i-1] 一个是dp[i-2]。
那么究竟是选dp[i-1]还是dp[i-2]呢?
一定是选最小的,所以dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];
注意这里为什么是加cost[i],而不是cost[i-1],cost[i-2]之类的,因为题目中说了:每当你爬上一个阶梯你都要花费对应的体力值
- dp数组如何初始化
根据dp数组的定义,dp数组初始化其实是比较难的,因为不可能初始化为第i台阶所花费的最少体力。
那么看一下递归公式,dp[i]由dp[i-1],dp[i-2]推出,既然初始化所有的dp[i]是不可能的,那么只初始化dp[0]和dp[1]就够了,其他的最终都是dp[0]dp[1]推出。
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]);}
4.不同路径
动态规划
public int uniquePaths(int m, int n) {int[][] dp = new int[m][n];for (int j = 0; j < n ; j++) {dp[m - 1][j] = 1;}for (int i = 0; i < m; i++) {dp[i][n - 1] = 1;}for (int i = m - 2; i >= 0; i--) {for (int j = n - 2; j >= 0 ; j--) {dp[i][j] = dp[i + 1][j] + dp[i][j + 1];}}return dp[0][0];}
数论⭐


- 组合公式:
/*** 数论*/public int uniquePaths(int m, int n) {int all = m + n - 2;int part = all - (m - 1);long o1 = 1;long o2 = 1;for (int i = part + 1, j = 1; i <= all && j <= m - 1; i++, j++) {o1 *= i;o2 *= j;long gcd = gcd(o1, o2);o1 /= gcd;o2 /= gcd;}return (int) (o1 / o2);}//辗转相除法private long gcd(long o1, long o2) {return o2 == 0 ? o1 : gcd(o2, o1 % o2);}
5.不同路径Ⅱ
public int uniquePathsWithObstacles(int[][] obstacleGrid) {int M = obstacleGrid.length;int N = obstacleGrid[0].length;//dp[i][j]含义:从i,j出发到右下角有几条路径int[][] dp = new int[M][N];for (int i = M - 1; i >= 0; i--) {if (obstacleGrid[i][N - 1] == 1) {break;} else {dp[i][N - 1] = 1;}}for (int i = N - 1; i >= 0 ; i--) {if (obstacleGrid[M - 1][i] == 1) {break;} else {dp[M - 1][i] = 1;}}for (int i = M - 2; i >= 0; i--) {for (int j = N - 2; j >= 0 ; j--) {if (obstacleGrid[i][j] == 0) {dp[i][j] = dp[i + 1][j] + dp[i][j + 1];}}}return dp[0][0];}
6.整数拆分
public int integerBreak(int n) {//dp[i] 拆分数字i可以得到的最大乘积int[] dp = new int[n + 1];dp[2] = 1;for (int i = 3; i <= n ; i++) {//拆分出的第一个数是j,固定for (int j = 1; j < i - 1; j++) {//可能性1:只拆分成两个数//可能性2:拆分成多个数dp[i] = Math.max(dp[i], Math.max(j * (i - j), j * dp[i - j]));}}return dp[n];}
7.不同的二叉搜索树⭐
- 注意本题是二叉搜索树

n为1的时候有一棵树,n为2有两棵树,这个是很直观的。
来看看n为3的时候,有哪几种情况。
当1为头结点的时候,其右子树有两个节点,看这两个节点的布局,是不是和 n 为2的时候两棵树的布局是一样的啊!
(可能有人问了,这布局不一样啊,节点数值都不一样。别忘了我们就是求不同树的数量,并不用把搜索树都列出来,所以不用关心其具体数值的差异)
当3为头结点的时候,其左子树有两个节点,看这两个节点的布局,是不是和n为2的时候两棵树的布局也是一样的啊!
当2为头结点的时候,其左右子树都只有一个节点,布局是不是和n为1的时候只有一棵树的布局也是一样的啊!
发现到这里,其实我们就找到了重叠子问题了,其实也就是发现可以通过dp[1] 和 dp[2] 来推导出来dp[3]的某种方式。
思考到这里,这道题目就有眉目了。
dp[3],就是 元素1为头结点搜索树的数量 + 元素2为头结点搜索树的数量 + 元素3为头结点搜索树的数量
元素1为头结点搜索树的数量 = 右子树有2个元素的搜索树数量 左子树有0个元素的搜索树数量
元素2为头结点搜索树的数量 = 右子树有1个元素的搜索树数量 左子树有1个元素的搜索树数量
元素3为头结点搜索树的数量 = 右子树有0个元素的搜索树数量 左子树有2个元素的搜索树数量
有2个元素的搜索树数量就是dp[2]。
有1个元素的搜索树数量就是dp[1]。
有0个元素的搜索树数量就是dp[0]。
所以dp[3] = dp[2] dp[0] + dp[1] dp[1] + dp[0] dp[2]
如图所示:
public int numTrees(int n) {//dp[i] 从1到i可以组成几种二叉搜索树int[] dp = new int[n + 1];dp[0] = 1;for (int i = 1; i <= n; i++) {//以j为头for (int j = 1; j <=i ; j++) {dp[i] += dp[j - 1] * dp[i - j];}}return dp[n];}

