抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

求最值问题,比如最长序列、最短距离等,不考虑时间和空间都可以通过暴力穷举的方式遍历获得结果,通过对穷举的过程进行优化减少重复开销的过程,即动态规划。

动态规划的重点,就是如何确定 状态转移方程 和 通过 备忘录 空间换时间降低时间复杂度(优化穷举)。

以拿硬币举例,从 1,2,5 从拿硬币,求 amount 满足 11 时的最少硬币数量。

状态转移方程构建

1.确定 base case
即确定满足什么边界条件时 return 直接结束当前递归。
其中,例子的边界条件为:当 amount == 0 时 返回 0,当 amount < 0 时无解,返回 -1

2.确定 状态
即原问题和子问题都会变化的变量。
其中,例子的状态为 amount,可以看出每拿走一个硬币 amount 就会改变,并向 base case 靠近,直至匹配。

3.明确 选择
即导致状态改变的行为。
其中,例子的选择为拿硬币。

4.明确 dp 的定义
dp 函数即递归找最值的函数,递归一般分为 自顶向下 和 自底向上 两种,框架如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 自顶向下递归的动态规划
def dp(状态1, 状态2, ...):
for 选择 in 所有可能的选择:
# 此时的状态已经因为做了选择而改变
result = 求最值(result, dp(状态1, 状态2, ...))
return result

# 自底向上迭代的动态规划
# 初始化 base case
dp[0][0][...] = base case
# 进行状态转移
for 状态1 in 状态1的所有取值:
for 状态2 in 状态2的所有取值:
for ...
dp[状态1][状态2][...] = 求最值(选择1,选择2...)

对比如下解法看出区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 自顶向下
int fib(int N) {
// 备忘录全初始化为 0
int[] memo = new int[N + 1];
// 进行带备忘录的递归
return dp(memo, N);
}

// 带着备忘录进行递归
int dp(int[] memo, int n) {
// base case
if (n == 0 || n == 1) return n;
// 已经计算过,不用再计算了
if (memo[n] != 0) return memo[n];
memo[n] = dp(memo, n - 1) + dp(memo, n - 2);
return memo[n];
}


// 自底向上
int fib(int N) {
if (N == 0) return 0;
int[] dp = new int[N + 1];
// base case
dp[0] = 0; dp[1] = 1;
// 状态转移
for (int i = 2; i <= N; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}

return dp[N];
}
备忘录 memo

备忘录的作用,用于解决重复计算的问题。每次解决子问题的时候,先去备忘录查一下是否有结果,如果有直接使用,避免重复计算。例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int fib(int N) {
// 备忘录全初始化为 0
int[] memo = new int[N + 1];
// 进行带备忘录的递归
return dp(memo, N);
}

// 带着备忘录进行递归
int dp(int[] memo, int n) {
// base case
if (n == 0 || n == 1) return n;
// 已经计算过,不用再计算了
if (memo[n] != 0) return memo[n];
memo[n] = dp(memo, n - 1) + dp(memo, n - 2);
return memo[n];
}

评论