赞
踩
参考引用
问题:给定一个共有 n 阶的楼梯,你每步可以上 1 阶或者 2 阶,请问有多少种方案可以爬到楼顶?
/* 回溯 */ void backtrack(vector<int> &choices, int state, int n, vector<int> &res) { // 当爬到第 n 阶时,方案数量加 1 if (state == n) res[0]++; // 遍历所有选择 for (auto &choice : choices) { // 剪枝:不允许越过第 n 阶 if (state + choice > n) break; // 尝试:做出选择,更新状态 backtrack(choices, state + choice, n, res); // 回退 } } /* 爬楼梯:回溯 */ int climbingStairsBacktrack(int n) { vector<int> choices = {1, 2}; // 可选择向上爬 1 或 2 阶 int state = 0; // 从第 0 阶开始爬 vector<int> res = {0}; // 使用 res[0] 记录方案数量 backtrack(choices, state, n, res); return res[0]; }
d p [ i − 1 ] , d p [ i − 2 ] , … , d p [ 2 ] , d p [ 1 ] dp[i-1],dp[i-2],\ldots,dp[2],dp[1] dp[i−1],dp[i−2],…,dp[2],dp[1]
d p [ i ] = d p [ i − 1 ] + d p [ i − 2 ] dp[i]=dp[i-1]+dp[i-2] dp[i]=dp[i−1]+dp[i−2]
/* 搜索 */
int dfs(int i) {
// 已知 dp[1] 和 dp[2] ,返回之
if (i == 1 || i == 2)
return i;
// dp[i] = dp[i-1] + dp[i-2]
int count = dfs(i - 1) + dfs(i - 2);
return count;
}
/* 爬楼梯:搜索 */
int climbingStairsDFS(int n) {
return dfs(n);
}
/* 记忆化搜索 */ int dfs(int i, vector<int> &mem) { // 已知 dp[1] 和 dp[2] ,返回之 if (i == 1 || i == 2) return i; // 若存在记录 dp[i] ,则直接返回之 if (mem[i] != -1) return mem[i]; // dp[i] = dp[i-1] + dp[i-2] int count = dfs(i - 1, mem) + dfs(i - 2, mem); // 记录 dp[i] mem[i] = count; return count; } /* 爬楼梯:记忆化搜索 */ int climbingStairsDFSMem(int n) { // mem[i] 记录爬到第 i 阶的方案总数,-1 代表无记录 vector<int> mem(n + 1, -1); return dfs(n, mem); }
/* 爬楼梯:动态规划 */
int climbingStairsDP(int n) {
if (n == 1 || n == 2)
return n;
// 初始化 dp 表,用于存储子问题的解
vector<int> dp(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];
}
根据以上内容,总结出动态规划的常用术语
- 将数组 d p dp dp 称为 d p dp dp 表, d p [ i ] dp[i] dp[i] 表示状态 i i i 对应子问题的解
- 将最小子问题对应的状态(即第 1 和 2 阶楼梯)称为初始状态
- 将递推公式 d p [ i ] = d p [ i − 1 ] + d p [ i − 2 ] dp[i]=dp[i-1]+dp[i-2] dp[i]=dp[i−1]+dp[i−2] 称为状态转移方程
/* 爬楼梯:空间优化后的动态规划 */
// 空间复杂度从 O(n) 降低至 O(1)
int climbingStairsDPComp(int n) {
if (n == 1 || n == 2)
return n;
int a = 1, b = 2;
for (int i = 3; i <= n; i++) {
int tmp = b;
b = a + b;
a = tmp;
}
return b;
}
在动态规划问题中,当前状态往往仅与前面有限个状态有关,这时可以只保留必要的状态,通过 “降维” 来节省内存空间,这种空间优化技巧被称为 “滚动变量” 或 “滚动数组”
贪心算法是一种常见的解决优化问题的算法,其基本思想是:在问题的每个决策阶段,都选择当前看起来最优的选择,即贪心地做出局部最优的决策,以期望获得全局最优解
贪心算法和动态规划都常用于解决优化问题,它们之间的区别如下
问题:给定 n 种硬币,第 i 种硬币的面值为 coins[i-1],目标金额为 amt,每种硬币可以重复选取,问能够凑出目标金额的最少硬币个数,如果无法凑出目标金额则返回 -1
/* 零钱兑换:贪心 */ int coinChangeGreedy(vector<int> &coins, int amt) { // 假设 coins 列表有序 int i = coins.size() - 1; int count = 0; // 循环进行贪心选择,直到无剩余金额 while (amt > 0) { // 找到小于且最接近剩余金额的硬币 while (i > 0 && coins[i] > amt) { i--; } // 选择 coins[i] amt -= coins[i]; count++; } // 若未找到可行方案,则返回 -1 return amt == 0 ? count : -1; }
贪心算法不仅操作直接、实现简单,而且通常效率也很高。在以上代码中,记硬币最小面值为 m i n ( c o i n s ) min(coins) min(coins),则贪心选择最多循环 a m t / m i n ( c o i n s ) amt/min(coins) amt/min(coins) 次,时间复杂度为 O ( a m t / m i n ( c o i n s ) ) O(amt/min(coins)) O(amt/min(coins))。这比动态规划解法的时间复杂度 O ( n ∗ a m t ) O(n*amt) O(n∗amt) 提升了一个数量级
然而,对于某些硬币面值组合,贪心算法并不能找到最优解
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。