算法动态规划问题解读

目录

什么是动态规划?

动态规划问题解决步骤?

动态规划问题debug?

例题:

例1:斐波那契数列

例 2:爬楼梯

例 3 :使用最小花费爬楼梯

例 4:不同路径

例 5 :不同路径 II

例 6:整数拆分:


什么是动态规划?

动态规划,英⽂:Dynamic Programming,简称DP,如果某⼀问题有很多重叠⼦问题,使⽤动态规划是最有效的。所以动态规划中每⼀个状态⼀定是由上⼀个状态推导出来的,这⼀点就区分于贪⼼,贪⼼没有状态推导,⽽是从局部直接选最优的。

动态规划问题解决步骤?

1. 确定dp数组(dp table)以及下标的含义
2. 确定递推公式
3. dp数组如何初始化
4. 确定遍历顺序
5. 举例推导dp数组

动态规划问题debug?

把 dp 数组打印出来,看看是不是按照自己的思路推导的。

例题:

例1:斐波那契数列

斐波那契数,通常⽤ F(n) 表示,形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后⾯每⼀项数字都是前⾯两项数字的和。也就是:
F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1
给你n ,请计算 F(n) 。
示例 1:
输⼊:2
输出:1
解释:F(2) = F(1) + F(0) = 1 + 0 = 1


示例 2:
输⼊:3
输出:2
解释:F(3) = F(2) + F(1) = 1 + 1 = 2


示例 3:
输⼊:4
输出:3
解释:F(4) = F(3) + F(2) = 2 + 1 = 3

题解:

这里用一个一维数组来保存结果

1,确定 dp 数组以及下标的含义

dp [ i ] 的定义为:第 i 个数的斐波那契数值是 dp [ i ]

2,确定递推公式

题目中已给出:dp[i] = dp[i - 1] + dp[i - 2];

3. dp数组如何初始化

题目中已给出:dp [0] = 0;  dp [1] = 1;

4. 确定遍历顺序
从递归公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,dp[i]是依赖 dp[i - 1] 和 dp[i - 2],那么遍历的顺序是从前到后遍历的

5. 举例推导dp数组
按照这个递推公式dp[i] = dp[i - 1] + dp[i - 2],我们来推导⼀下,当N为10的时候,dp数组应该是如下的数列:0 1 1 2 3 5 8 13 21 34 55

Java代码如下:

public class 斐波那契数列 {
	public static void main(String[] args) {
		System.out.println(fib(10));
	}
	static 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:爬楼梯

假设你正在爬楼梯。需要 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. 确定dp数组以及下标的含义
dp [ i ]: 爬到第 i 层楼梯,有dp [ i ] 种⽅法

2. 确定递推公式
从dp [ i ]的定义可以看出,dp [ i ] 可以有两个⽅向推出来。
⾸先是dp[i - 1],上i-1层楼梯,有dp[i - 1]种⽅法,那么再⼀步跳⼀个台阶不就是dp[i]了么。
还有就是dp[i - 2],上i-2层楼梯,有dp[i - 2]种⽅法,那么再⼀步跳两个台阶不就是dp[i]了么。
那么dp[i]就是 dp[i - 1]与dp[i - 2]之和!
所以dp[i] = dp[i - 1] + dp[i - 2] 。

3. dp数组如何初始化

因为n要求为正整数,所以不考虑dp[0]如果初始化,只初始化dp[1] = 1,dp[2] = 2

4. 确定遍历顺序
从递推公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,遍历顺序⼀定是从前向后遍历的


5. 举例推导dp数组

i = 1,dp [i]=1;

i = 2,dp [i]=2;

i = 3,dp [i]=3;

i = 4,dp [i]=5;

i = 5,dp [i]=8;

和斐波那契十分相像,但是递推公式是需要自己推导出来;

Java代码如下:

public class 爬楼梯 {
	public static void main(String[] args) {
		System.out.println(fib(5));
	}
	static int fib(int n) {
		int []dp = new int [n];
		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 :使用最小花费爬楼梯

数组的每个下标作为⼀个阶梯,第 i 个阶梯对应着⼀个⾮负数的体⼒花费值 cost[i](下标从 0 开始)。每当你爬上⼀个阶梯你都要花费对应的体⼒值,⼀旦⽀付了相应的体⼒值,你就可以选择向上爬⼀个阶梯或者爬两个阶梯。请你找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯。


示例 1:
输⼊:cost = [10, 15, 20]
输出:15
解释:最低花费是从 cost[1] 开始,然后⾛两步即可到阶梯顶,⼀共花费 15 。


 示例 2:
输⼊:cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1]
输出:6
解释:最低花费⽅式是从 cost[0] 开始,逐个经过那些 1 ,跳过 cost[3] ,⼀共花费 6 。

题解:

1. 确定dp数组以及下标的含义
使⽤动态规划,就要有⼀个数组来记录状态,本题只需要⼀个⼀维数组dp[i]就可以了。
dp[i]的定义:到达第i个台阶所花费的最少体⼒为dp[i]。(注意这⾥认为是第⼀步⼀定是要花费)

2. 确定递推公式
可以有两个途径得到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]之类的,因为题⽬中说了:每当你爬上⼀个阶梯都要花费对应的体⼒值


3. dp数组如何初始化
根据dp数组的定义,dp数组初始化其实是⽐较难的,因为不可能初始化为第i台阶所花费的最少体⼒。那么看⼀下递归公式,dp[i]由dp[i-1],dp[i-2]推出,既然初始化所有的dp[i]是不可能的,那么只初始化dp[0]和dp[1]就够了,其他的最终都是dp[0]dp[1]推出。
所以初始化代码为:dp[0] = cost[0];   dp[1] = cost[1];


4. 确定遍历顺序
因为是模拟台阶,⽽且dp[i]⼜dp[i-1]dp[i-2]推出,所以是从前到后遍历cost数组就可以了。

5. 举例推导dp数组
拿示例2:cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1] ,来模拟⼀下dp数组的状态变化,如下:

dp = [1,100,2,3,3,103,4,5,104,6]

java代码:

public class 花费最小力气爬楼梯 {
	public static void main(String[] args) {
		int []cost = {1, 100, 1, 1, 1, 100, 1, 1, 100, 1};
		System.out.println(fib(cost));
	}
	static int fib(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 dp[cost.length-1];
	}
}

例 4:不同路径

⼀个机器⼈位于⼀个 m x n ⽹格的左上⻆ (起始点在下图中标记为 “Start” )。机器⼈每次只能向下或者向右移动⼀步。机器⼈试图达到⽹格的右下⻆(在下图中标记为 “Finish” )。问总共有多少条不同的路径?

示例1:

输⼊:m = 3, n = 7
输出:28

题解:

1. 确定dp数组(dp table)以及下标的含义
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],因为dp[i][j]只有这两个⽅向过来。


3. dp数组的初始化
如何初始化呢,⾸先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]都是从其上⽅和左⽅推导⽽来,那么从左到右⼀层⼀层遍历就可以了。
这样就可以保证推导dp[i][j]的时候,dp[i - 1][j] 和 dp[i][j - 1]⼀定是有数值的。

5. 举例推导dp数组
如图所示:

 Java代码:

public class 最短路径 {
	public static void main(String[] args) {
		Scanner sc=new Scanner(System.in);
		int m=sc.nextInt();
		int n=sc.nextInt();
		int [][]arr = new int[m][n];
		System.out.println(fib(arr));
	}
	static int fib(int [][]arr) {
		for (int i = 0; i < arr.length; i++) {
			arr[i][0]=1;
		}
		for (int i = 0; i < arr[0].length; i++) {
			arr[0][i]=1;
		}
		for (int i = 1; i < arr.length; i++) {
			for (int j = 1; j < arr[0].length; j++) {
				arr[i][j]=arr[i-1][j]+arr[i][j-1];
			}
		}
		return arr[arr.length-1][arr[0].length-1];
	}
}

例 5 :不同路径 II

⼀个机器⼈位于⼀个 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

题解:

1. 确定dp数组(dp table)以及下标的含义
dp[i][j] :表示从(0 ,0)出发,到(i, j) 有dp[i][j]条不同的路径。


2. 确定递推公式
递推公式和62.不同路径⼀样,dp[i][j] = dp[i - 1][j] + dp[i][j - 1]。但这⾥需要注意⼀点,因为有了障碍,(i, j)如果就是障碍的话应该就保持初始状态(初始状态为0)。所以代码为:

if (obstacleGrid[i][j] == 0) { // 当(i, j)没有障碍的时候,再推导dp[i][j]
 dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}

3. dp数组如何初始化

不同路径中我们给出如下的初始化:

for (int i = 0; i < m; i++) dp[i][0] = 1;
for (int j = 0; j < n; j++) dp[0][j] = 1;

因为从(0, 0)的位置到(i, 0)的路径只有⼀条,所以dp[i][0]⼀定为1,dp[0][j]也同理。
但如果(i, 0) 这条边有了障碍之后,障碍之后(包括障碍)都是⾛不到的位置了,所以障碍之后的dp[i][0]应该还是初始值0。

 所以本题初始化代码为:

for (int i = 0; i < m && obstacleGrid[i][0] == 0; i++) dp[i][0] = 1;
for (int j = 0; j < n && obstacleGrid[0][j] == 0; j++) dp[0][j] = 1;

注意代码⾥for循环的终⽌条件,⼀旦遇到obstacleGrid[i][0] == 1的情况就停⽌dp[i][0]的赋值1的操
作,dp[0][j]同理

4. 确定遍历顺序

从递归公式dp[i][j] = dp[i - 1][j] + dp[i][j - 1] 中可以看出,⼀定是从左到右⼀层⼀层遍历,这样保证推导dp[i][j]的时候,dp[i - 1][j] 和 dp[i][j - 1]⼀定是有数值。

5. 举例推导dp数组

题解:

public class 不同路径II {
	public static void main(String[] args) {
		int [][]arr = {{0,0,0},{0,1,0},{0,0,0}};
		System.out.println(fib(arr));
	}
	static int fib(int [][]arr) {
		int [][]dp = new int[arr.length][arr[0].length];
		for (int i = 0; i < arr.length&&arr[i][0]==0; i++) {
			dp[i][0]=1;
		}
		for (int i = 0; i < arr[0].length&&arr[0][i]==0; i++) {
			dp[0][i]=1;
		}
		for (int i = 1; i < dp.length; i++) {
			for (int j = 1; j < dp[0].length; j++) {
				if(arr[i][j]!=1) {					
					dp[i][j]=dp[i-1][j]+dp[i][j-1];
				}
			}
		}
//		for (int i = 0; i < dp.length; i++) {
//			for (int j = 0; j < dp[0].length; j++) {
//				System.out.print(dp[i][j]);
//			}
//			System.out.println();
//		}
		return arr[arr.length-1][arr[0].length-1];
	}
}

例 6:整数拆分:

给定⼀个正整数 n,将其拆分为⾄少两个正整数的和,并使这些整数的乘积最⼤化。 返回你可以获得的最⼤乘积


示例 1:
输⼊: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。


示例 2:
输⼊: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。
说明: 你可以假设 n 不⼩于 2 且不⼤于 58。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值