动态规划 DP
- 3. 动态规划 DP
- 什么是动态规划
- 动态规划和其他算法的区别
- 解题方法
- 解题步骤
- [509. 斐波那契数](https://leetcode.cn/problems/fibonacci-number/) (easy)
- 暴力递归
- 递归 + 记忆体
- 动态规划
- 滚动数组优化
- 动态规划 + 降维
- [62. 不同路径](https://leetcode.cn/problems/unique-paths/)(midium)
- 1. 动态规划
- 2. 状态压缩
- [63. 不同路径 II ](https://leetcode.cn/problems/unique-paths-ii/)(midium)
- 1. 动态规划
- 2. 状态压缩
- [70. 爬楼梯](https://leetcode.cn/problems/climbing-stairs/)
- 思路
- 1.动态规划
- 2.状态压缩
- 3.降维
- [279. 完全平方数](https://leetcode.cn/problems/perfect-squares/)
- 思路
- 方法1:动态规划
- [120. 三角形最小路径和](https://leetcode.cn/problems/triangle/)
- 1.动态规划
- 2.状态压缩
3. 动态规划 DP
什么是动态规划
动态规划,英文:Dynamic Programming
,简称DP
,将问题分解为互相重叠的子问题,通过反复求解子问题来解决原问题就是动态规划,如果某一问题有很多重叠子问题,使用动态规划来解是比较有效的。
Q:什么是重叠子问题
A:比如这里要求的f(4) f(3) … f(1)就是重叠子问题
求解动态规划的核心问题是穷举,但是这类问题穷举有点特别,因为这类问题存在「重叠子问题
」,如果暴力穷举的话效率会极其低下。动态规划问题一定会具备「最优子结构」,才能通过子问题的最值得到原问题的最值。另外,虽然动态规划的核心思想就是穷举求最值,但是问题可以千变万化,穷举所有可行解其实并不是一件容易的事,只有列出**正确的「状态转移方程」**才能正确地穷举。重叠子问题、最优子结构、状态转移方程
就是动态规划三要素
Q:什么是最优子结构?
A:问题寻找从顶部到底部相加和最小的路径,现在已经在5这个位置,可以向1和8走,显然5+1比5+8小,那么5往1走就是最优的子结构
动态规划和其他算法的区别
- 动态规划和分治的区别:动态规划和分治都有最优子结构 ,但是
分治的子问题不重叠
分治没有重复的子问题
- 动态规划和贪心的区别:动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从
局部直接选最优解
,所以它永远是局部最优,但是全局的解不一定是最优的。
比如
- 动态规划和递归的区别:递归和回溯可能存在非常多的重复计算,动态规划可以用递归加记忆化的方式减少不必要的重复计算
解题方法
- 递归+记忆化(自顶向下)
- 动态规划(自底向上)
解题步骤
- 根据重叠子问题定义状态
- 寻找最优子结构推导状态转移方程
- 确定dp初始状态
- 确定输出值
509. 斐波那契数 (easy)
- dp(n) 表示第n个位置斐波那契数为多少
- 状态转移方程 dp(n) = dp(n-1) + dp(n-2)
- dp初始状态 dp(0) = 0 dp(1) = 1
- 确定输出值
暴力递归
class Solution {
public int fib(int n) {
if (n == 0) {
return 0;
} else if (n == 1) {
return 1;
}
return fib(n - 1) + fib(n - 2);
}
}
递归 + 记忆体
也就是自顶向下
class Solution {
//用于存储已经计算过的值
static HashMap<Integer,Integer> hashMap = new HashMap<>();
public int fib(int n) {
//如果已经计算过,直接返回结果
if(hashMap.containsKey(n)){
return hashMap.get(n);
}
if (n == 0) return 0;
if (n == 1) return 1;
//计算完后放入HashMap
hashMap.put(n,fib(n - 1) + fib(n - 2));
return hashMap.get(n);
}
}
动态规划
沿着一条路往上走,不会去走其他路了
class Solution {
static int[] dp = new int[50];
public int fib(int n) {
if (n == 0) return 0;
if (n == 1) return 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];
}
}
滚动数组优化
因为每个斐波那契数只与前两个斐波那契数有关,所以其实可以把数组的长度设为3或2,优化空间复杂度
class Solution {
static int[] dp = new int[3];
public int fib(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
dp[0] = 0;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
//自底向上计算每个状态
dp[2] = dp[0] + dp[1];
//数组滚动
dp[0] = dp[1];
dp[1] = dp[2];
}
return dp[2];
}
}
//再次优化,把数组变为2
class Solution {
static int[] dp = new int[2];
public int fib(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
dp[0] = 0;
dp[1] = 1;
int sum = 0;
for (int i = 2; i <= n; i++) {
//自底向上计算每个状态
sum = dp[0] + dp[1];
//数组滚动
dp[0] = dp[1];
dp[1] = sum;
}
return sum;
}
}
动态规划 + 降维
降维能减少空间复杂度,但不利于程序的扩展
比如如果我们要获取过程中的每一个斐波那契数,这种方法就不好扩展了
class Solution {
public int fib(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
int prev0 = 0;
int prev1 = 1;
int sum = 0;
for (int i = 2; i <= n; i++) {
//自底向上计算每个状态
sum = prev0 + prev1;
prev0 = prev1;
prev1 = sum;
}
return sum;
}
}
- 思路:自底而上的动态规划
- 复杂度分析:时间复杂度
O(n)
,空间复杂度O(1)
62. 不同路径(midium)
难度中等
一个机器人位于一个 m x n
网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
示例 1:
输入:m = 3, n = 7
输出:28
示例 2:
输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右
3. 向下 -> 向右 -> 向下
示例 3:
输入:m = 7, n = 3
输出:28
示例 4:
输入:m = 3, n = 3
输出:6
提示:
1 <= m, n <= 100
- 题目数据保证答案小于等于
2 * 109
1. 动态规划
- 根据重叠子问题定义状态
dp[i][j]
表示到达i j的位置有多少种路径
- 寻找最优子结构推导状态转移方程
- 由于在每个位置只能向下或者向右, 所以每个坐标的路径和等于上一行相同位置和上一列相同位置不同路径的
总和
,状态转移方程:dp[i][j] = dp[i-1][j] + dp[i][j-1]
;
- 由于在每个位置只能向下或者向右, 所以每个坐标的路径和等于上一行相同位置和上一列相同位置不同路径的
- 确定dp初始状态
dp[0][0...n] = 1
dp[0...m][0] = 1
- 确定输出值
- 复杂度:时间复杂度
O(mn)
。空间复杂度O(mn)
,优化后空间复杂度O(n)
class Solution {
public 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 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];
}
}
2. 状态压缩
- 可以先建一个都是1的一维数组表示第一行
- 我们注意到2这个位置是左边加上上边的和
- 左边就是数组中的前一位
- 上边其实就是自己的值
- 自己等于自己加上左边
class Solution {
public int uniquePaths(int m, int n) {
//定义状态
int[] dp = new int[n];
//初始化
Arrays.fill(dp, 1);
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
//状态转移方程
dp[j] = dp[j] + dp[j-1];
}
}
return dp[n-1];
}
}
class Solution {
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
//1. 根据重叠子问题定义状态
int m = obstacleGrid.length;
int n = obstacleGrid[0].length;
int dp[][] = new int[m][n];
//2. 确定dp初始状态
//上边和左边都是1
for (int i = 0; i < m; i++) {
//不是障碍物才填1
if(obstacleGrid[i][0] != 1){
dp[i][0] = 1;
}else {
break;
}
}
for (int i = 0; i < n; i++) {
//不是障碍物才填1
if(obstacleGrid[0][i] != 1){
dp[0][i] = 1;
}else {
break;
}
}
//3. 寻找最优子结构推导状态转移方程
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
//如果是障碍物
if(obstacleGrid[i][j] == 1){
dp[i][j] = 0;
continue;
}
//不是障碍物 dp[i][j] = dp[i-1][j] + dp[i][j-1]
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[m-1][n-1];
}
}
63. 不同路径 II (midium)
难度中等
一个机器人位于一个 m x n
网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用 1
和 0
来表示。
示例 1:
输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右
示例 2:
输入:obstacleGrid = [[0,1],[0,0]]
输出:1
提示:
-
m == obstacleGrid.length
-
n == obstacleGrid[i].length
-
1 <= m, n <= 100
-
obstacleGrid[i][j]
为0
或1
-
思路:和62题一样,区别就是遇到障碍直接返回0
-
复杂度:时间复杂度
O(mn)
,空间复杂度O(mn)
,状态压缩之后是o(n)
1. 动态规划
class Solution {
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
//1. 根据重叠子问题定义状态
int m = obstacleGrid.length;
int n = obstacleGrid[0].length;
int dp[][] = new int[m][n];
//2. 确定dp初始状态
//上边和左边都是1,遇到障碍物开始后面都是0
for (int i = 0; i < m; i++) {
//不是障碍物填1
if(obstacleGrid[i][0] != 1){
dp[i][0] = 1;
}else {
//遇到障碍物返回
break;
}
}
for (int i = 0; i < n; i++) {
//不是障碍物填1
if(obstacleGrid[0][i] != 1){
dp[0][i] = 1;
}else {
//遇到障碍物返回
break;
}
}
//3. 寻找最优子结构推导状态转移方程
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
//如果是障碍物
if(obstacleGrid[i][j] == 1){
dp[i][j] = 0;
continue;
}
//不是障碍物 dp[i][j] = dp[i-1][j] + dp[i][j-1]
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[m-1][n-1];
}
}
2. 状态压缩
class Solution {
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
//1. 根据重叠子问题定义状态
int m = obstacleGrid.length;
int n = obstacleGrid[0].length;
int dp[] = new int[n];
//2. 确定dp初始状态
dp[0] = obstacleGrid[0][0] == 1 ? 0 : 1;
//dp[0] = 1; //也可以直接先用1填充,因为后面会遍历到
//3. 寻找最优子结构推导状态转移方程
//注意这里从0开始
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
//遇到障碍物
if(obstacleGrid[i][j] == 1){
dp[j] = 0;
continue;
}
//最左边的时候.左边没有元素
if(j >= 1){
dp[j] = dp[j] + dp[j-1];
}
//等于上边(自己)和左边的和
}
}
return dp[n-1];
}
}
70. 爬楼梯
难度简单
假设你正在爬楼梯。需要 n
阶你才能到达楼顶。
每次你可以爬 1
或 2
个台阶。你有多少种不同的方法可以爬到楼顶呢?
示例 1:
输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶
示例 2:
输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶
提示:
1 <= n <= 45
思路
- 思路:因为每次可以爬 1 或 2 个台阶,所以到第n阶台阶可以从第n-2或n-1上来,其实就是斐波那契的dp方程
- 复杂度分析:时间复杂度
O(n)
,空间复杂度O(1)
1.动态规划
当前台阶可以由前一个台阶走过来,也可以由前两个台阶走过来
所以走法数 = 前一个台阶走法数 + 前两个台阶走法数
class Solution {
public int climbStairs(int n) {
if(n == 1){
return 1;
}
//1.定义状态
int[] dp = new int[n];
//2.初始化
dp[0] = 1;
dp[1] = 2;
//3.状态转移方程
for (int i = 2; i < n; i++) {
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n-1];
}
}
2.状态压缩
class Solution {
public int climbStairs(int n) {
if (n == 1) {
return 1;
} else if (n == 2) {
return 2;
}
//1.定义状态
int[] dp = new int[2];
//2.初始化
dp[0] = 1;
dp[1] = 2;
int result = 0;
//3.状态转移方程
for (int i = 2; i < n; i++) {
result = dp[0] + dp[1];
dp[0] = dp[1];
dp[1] = result;
}
return result;
}
}
3.降维
class Solution {
public int climbStairs(int n) {
if (n == 1) {
return 1;
} else if (n == 2) {
return 2;
}
//1.定义状态
//2.初始化
int prev1 = 1;
int prev2 = 2;
int result = 0;
//3.状态转移方程
for (int i = 2; i < n; i++) {
result = prev1 + prev2;
prev1 = prev2;
prev2 = result;
}
return result;
}
}
279. 完全平方数
难度中等
给你一个整数 n
,返回 和为 n
的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1
、4
、9
和 16
都是完全平方数,而 3
和 11
不是。
示例 1:
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4
示例 2:
输入:n = 13
输出:2
解释:13 = 4 + 9
提示:
1 <= n <= 104
思路
dp[i]
的i减去所有比i小的完全平方数的平方dp[i- j * j]
然后再+1,比较
dp[i]
和dp[i- j * j] + 1
得到小的那个
方法1:动态规划
-
思路:
dp[i]
表示i
的完全平方和的最少数量,dp[i - j * j] + 1
表示减去一个完全平方数j
的完全平方之后的数量加1就等于dp[i]
,只要在dp[i]
,dp[i - j * j] + 1
中寻找一个较少的就是最后dp[i]
的值。 -
复杂度:时间复杂度
O(n* sqrt(n))
,n是输入的整数,需要循环n次,每次计算dp方程的复杂度sqrt(n)
,空间复杂度O(n)
class Solution {
public int numSquares(int n) {
//1.定义状态
int[] dp = new int[n + 1];
for (int i = 1; i <= n; i++) {
//2.初始化 表示都由1这个完全平方数组成
dp[i] = i;
for (int j = 1; i - j * j >= 0; j++) {
//3.状态转移方程
dp[i] = Math.min(dp[i], dp[i - j * j] + 1);
}
}
return dp[n];
}
}
120. 三角形最小路径和
难度中等
给定一个三角形 triangle
,找出自顶向下的最小路径和。
每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i
,那么下一步可以移动到下一行的下标 i
或 i + 1
。
示例 1:
输入:triangle = [[2],[3,4],[6,5,7],[4,1,8,3]]
输出:11
解释:如下面简图所示:
2
3 4
6 5 7
4 1 8 3
自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。
示例 2:
输入:triangle = [[-10]]
输出:-10
提示:
1 <= triangle.length <= 200
triangle[0].length == 1
triangle[i].length == triangle[i - 1].length + 1
-104 <= triangle[i][j] <= 104
进阶:
- 你可以只使用
O(n)
的额外空间(n
为三角形的总行数)来解决这个问题吗?
- 思路:从三角形最后一层开始向上遍历,每个数字的最小路径和是它下面两个数字中的较小者加上它本身
- 复杂度分析:时间复杂度
O(n^2)
,空间复杂O(n)
1.动态规划
class Solution {
public int minimumTotal(List<List<Integer>> triangle) {
//1. 定义状态
int n = triangle.size();
//表示从底部到nn位置的路径长度
int dp[][] = new int[n][n];
//2. 初始化
for (int i = 0; i < n; i++) {
//获取三角形最后一行
dp[n-1][i] = triangle.get(n-1).get(i);
}
//3. 状态转移方程
//每个数字的最小路径和是它下面两个数字中的较小者加上它本身
//从倒数第2层开始
for (int i = n-2; i >= 0; i--) {
//第几层就有有多少个元素
for (int j = 0; j <= i; j++) {
dp[i][j] = triangle.get(i).get(j) + Math.min(dp[i+1][j] , dp[i+1][j+1]);
}
}
return dp[0][0];
}
}
2.状态压缩
class Solution {
public int minimumTotal(List<List<Integer>> triangle) {
//1. 定义状态
int n = triangle.size();
int dp[] = new int[n];
//2. 初始化
for (int i = 0; i < n; i++) {
//获取三角形最后一行
dp[i] = triangle.get(n-1).get(i);
}
//3. 状态转移方程
//每个数字的最小路径和是它下面两个数字中的较小者加上它本身
//从倒数第2层开始
for (int i = n-2; i >= 0; i--) {
//第几层就有有多少个元素
for (int j = 0; j <= i; j++) {
dp[j] = triangle.get(i).get(j) + Math.min(dp[j] , dp[j+1]);
}
}
return dp[0];
}
}