3·子问题(递归,备忘录,动态规划,滚动数组)
递归
- 思想:将问题分解为相同子问题(函数调用自身),从而解决问题
- 方向:自顶向下调用,自底向上执行,从子问题开始计算
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
int func(int n){//函数功能:返回n的阶乘
//结束条件
if(n <0)return-1;
if(n ==0|| n ==1 )return n;
//等价关系式
return n *func(n -1);
}
int fibonacci(int i) {
if(i <= 0) return 0;
if(i == 1) return 1;
return fibonacci(i-1) + fibonacci(i-2);
}
int fibonacci(int first, int second, int n) {//优化:滚动数组
if (n <= 0) {
return 0;
}
if (n < 3) {
return 1;
}
else if (n == 3) {
return first + second;
}
else {
return fibonacci(second, first + second, n - 1);
}
}
fibonacci(0, 1, n);
展开
- 阶乘:n! = 1……n的乘积
- 斐波那契数列:F (0)=0,F (1)=1, F (n)=F (n - 1)+F (n - 2)(n ≥ 2)
- 步骤:
- 明确函数功能
- 状态转移方程:分解问题,找出函数的等价关系式
- 返回条件(不能分解的基本问题)
- 优点:对于某些问题递归要比循环思想简单许多
- 缺点:存在函数调用的开销,子问题被大量重复调用,通常比迭代更低效
- 时间复杂度:每次递归的时间复杂度 * 递归的次数(节点数量)
- 空间复杂度:每次递归的空间复杂度 * 递归深度(树层数)
- 以上复杂度分别是:n, 2^n, n
- 图dfs:深度优先搜索
- 数据结构:栈(向量)
- 网格图:
- 返回条件:超过边界,被访问过,找到了期望结果
- 状态转移:从当前位置向4个递归
- 临接图:
- 返回条件:被访问过,找到了期望结果
- 状态转移:从当前节点对所有邻居递归
- 适用场景:快速找到可行路径(当首次找到目标,不能保证找到的是最短路径(构建树,目标节点出现在很多枝杈(路径)上,深度也不同))
- 优势:内存占用少
- 使用场景:通常问题有大量的组合方案,让我们计算可行的数量,或者最优选择
备忘录
1
2
3
4
5
6
7
8
9
10
11
vector<int> memo(n + 1, -1);
int fib_memo(int n, vector<int>& memo) {
if (n <= 1) return n;
// 如果已经计算过,直接返回结果
if (memo[n] != -1) {
return memo[n];
}
// 计算并存储结果
memo[n] = fib_memo(n-1, memo) + fib_memo(n-2, memo);
return memo[n];
}
展开
- 思想:将子问题计算结果保存,当遇到计算过的子问题,直接获取结果,避免重复计算子问题
- 优点:优化递归的时间复杂度
- 缺点:增加空间复杂度
动态规划DP
- 思想:将问题分解为相同子问题,从子问题开始计算,并存储计算结果(在备忘录的基础上,更改计算方向)
- 方向:自底向上,从子问题开始计算
- 优点:相比于备忘录减少函数调用的开销
- 步骤:
- 明确函数功能
- 状态转移方程
- 明确dp数组元素的功能:子问题结果
- 设置dp基本情况:不能分解的基本问题
- 循环填充dp元素:自底向上计算
01背包
1 2 3 4 5 6 7 8 9 10 11 12 13 14
int knapsack(vector<int>& weights, vector<int>& values, int capacity) { int n = weights.size(); vector<vector<int>> dp(n + 1, vector<int>(capacity + 1, 0));//个数 for (int i = 1; i <= n; i++) { for (int j = 1; j <= capacity; j++) { if (j >= weights[i-1]) { dp[i][j] = max(dp[i-1][j], dp[i-1][j-weights[i-1]] + values[i-1]); } else { dp[i][j] = dp[i-1][j]; } } } return dp[n][capacity]; }
展开
- 商品:重量wi,价值vi
- 背包:容量
- 问:如何才能获得最大的收益
- 01:所有物品不可再分,要么整个装入背包,要么放弃
- 状态转移方程:
- 每个物体都有选和不选两种可能
- 物体的重量<当前容量可以选和不选if (j >= w[i]):f[i][j] = max(f[i - 1][j], f[i - 1][j - w[i]] + v[j])
- 只能不选else:f[i][j] = f[i - 1][j];
- 横坐标 i 表示前i个物体,纵坐标 j 为当前背包容量
完全背包
1 2 3 4 5 6 7 8 9 10 11 12 13
int completeKnapsack_2D(int capacity, vector<int>& weights, vector<int>& values) { int n = weights.size(); vector<vector<int>> dp(n + 1, vector<int>(capacity + 1, 0)); for (int i = 1; i <= n; i++) { for (int j = 1; j <= capacity; j++) { dp[i][j] = dp[i-1][j]; if (j >= weights[i-1]) { dp[i][j] = max(dp[i][j], dp[i][j - weights[i-1]] + values[i-1]); } } } return dp[n][capacity]; }
展开
- 完全:每个物品可以取无限次放入背包中
- 状态转移方程:
- 物体的重量<当前容量可以选if (j >= w[i]):f[i][j] = max(f[i][j], f[i][j - w[i]] + v[j]),i-1变为i,因为当前物品可选
- 不能选else:f[i][j] = f[i - 1][j];
滚动数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//循环
int func(int n){
int d[n];
d[0] =0;
d[1] =1;
for(int i =2; i <= n; i++){
d[n] =d[n -1] +d[n -2];
}
return d[n];
}
//滚动数组
int func(int n){
int d[3];
d[1] =0;
d[2] =1;
for(int i =2; i <= n; i++){
d[0] =d[1];
d[1] =d[2];
d[2] =d[0] +d[1];
}
return d[n];
}
展开
- 思想:只分配依赖大小的空间
- 优点:相比于普通dp减少空间复杂度
使用场景:元素值只依赖前几个元素(一维)/ 前几行(二维)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
int knapsack_optimized(vector<int>& weights, vector<int>& values, int capacity) { int n = weights.size(); vector<int> dp(capacity + 1, 0); for (int i = 0; i < n; i++) { for (int j = capacity; j >= weights[i]; j--) { dp[j] = max(dp[j], dp[j - weights[i]] + values[i]); } } return dp[capacity]; } int completeKnapsack_1D(int capacity, vector<int>& weights, vector<int>& values) { int n = weights.size(); vector<int> dp(capacity + 1, 0); for (int i = 0; i < n; i++) { for (int j = weights[i]; j <= capacity; j++) { dp[j] = max(dp[j], dp[j - weights[i]] + values[i]); } } return dp[capacity]; }
展开
- 例子:斐波那契,爬楼梯,背包问题……
- 注意:
- dp数组:从二维变为一维
- for循环方向:对于01背包:j改为反方向,依赖于上一行的值,避免覆盖,对于完全背包则不用,依赖于当前行的左侧元素,所以应该先计算左侧
- 状态转移方程:本质不变,书写上变化
- 返回结果:变为一维
本文由作者按照 CC BY 4.0 进行授权