参考引用
问题:给定一个共有 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) 提升了一个数量级
然而,对于某些硬币面值组合,贪心算法并不能找到最优解
