本文献给:
准备学习动态规划的C语言程序员。如果你已经掌握了递归和分治算法,想要理解动态规划的核心思想并解决复杂优化问题------本文将为你奠定坚实的动态规划基础。
你将学到:
- 理解动态规划的基本思想和核心概念
- 掌握重叠子问题与最优子结构的识别方法
- 学会备忘录方法和自底向上求解技术
- 掌握动态规划问题的解题步骤和模式识别
让我们开始探索动态规划的强大威力!
目录
- 第一部分:动态规划入门
-
- [1. 什么是动态规划?](#1. 什么是动态规划?)
- [2. 动态规划的适用场景](#2. 动态规划的适用场景)
- 第二部分:重叠子问题与最优子结构
-
- [1. 重叠子问题分析](#1. 重叠子问题分析)
- [2. 最优子结构验证](#2. 最优子结构验证)
- [3. 经典问题的结构分析](#3. 经典问题的结构分析)
- 第三部分:备忘录方法
-
- [1. 备忘录方法原理](#1. 备忘录方法原理)
- [2. 备忘录方法实现模式](#2. 备忘录方法实现模式)
- [3. 备忘录方法的优缺点](#3. 备忘录方法的优缺点)
- 第四部分:自底向上求解
-
- [1. 自底向上方法原理](#1. 自底向上方法原理)
- [2. 自底向上实现模式](#2. 自底向上实现模式)
- [3. 计算顺序的确定](#3. 计算顺序的确定)
- [4. 空间复杂度优化](#4. 空间复杂度优化)
- 第五部分:动态规划解题步骤
-
- [1. 四步解题法](#1. 四步解题法)
- [2. 经典问题解题示范](#2. 经典问题解题示范)
- 第六部分:经典动态规划问题
-
- [1. 斐波那契数列](#1. 斐波那契数列)
- [2. 背包问题](#2. 背包问题)
- [3. 最长公共子序列](#3. 最长公共子序列)
- 第七部分:总结
-
- [1. 核心要点总结](#1. 核心要点总结)
- [2. 动态规划解题框架](#2. 动态规划解题框架)
- 第八部分:常见问题解答
第一部分:动态规划入门
1. 什么是动态规划?
动态规划(Dynamic Programming)是一种解决复杂问题的算法设计范式,通过将问题分解为相互重叠的子问题,并存储子问题的解来避免重复计算,从而高效求解原问题。
动态规划的核心思想:
记住已经求解过的子问题的答案,避免重复计算,用空间换时间。
动态规划与分治算法的区别:
| 特性 | 分治算法 | 动态规划 |
|---|---|---|
| 子问题 | 相互独立 | 相互重叠 |
| 解的重用 | 不重用子问题解 | 重用子问题解 |
| 存储 | 通常不存储中间结果 | 存储中间结果 |
| 适用问题 | 子问题独立的问题 | 最优子结构问题 |
2. 动态规划的适用场景
动态规划并非万能,只有在问题满足特定条件时才适用:
最优子结构(Optimal Substructure):
问题的最优解包含其子问题的最优解。
c
// 最短路径问题的最优子结构
// 如果从A到C的最短路径经过B,那么从A到B和从B到C的路径也都是最短路径
typedef struct {
int from;
int to;
int distance;
} Path;
int shortest_path(Path paths[], int n, int start, int end) {
// 问题的最优解可以由子问题的最优解构成
// shortest(A, C) = shortest(A, B) + shortest(B, C)
}
重叠子问题(Overlapping Subproblems):
递归算法会反复求解相同的子问题。
c
// 斐波那契数列的重叠子问题
int fib(int n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}
// 调用fib(5)的计算树:
// fib(5)
// fib(4) + fib(3)
// (fib(3) + fib(2)) + (fib(2) + fib(1))
// 可以看到fib(2)、fib(3)被重复计算
无后效性(No Aftereffect):
当前状态一旦确定,后续决策只与当前状态有关,与如何到达此状态的路径无关。
c
// 背包问题的无后效性
int knapsack(int weights[], int values[], int n, int capacity) {
// 一旦确定了前i个物品和剩余容量j的状态,
// 后续决策只与这个状态有关,与之前选择了哪些物品无关
}
第二部分:重叠子问题与最优子结构
1. 重叠子问题分析
重叠子问题是动态规划适用的必要条件,通过分析问题是否包含重复计算来判断是否适合用动态规划。
c
// 斐波那契数列的重叠子问题可视化
void visualize_fib_overlap(int n) {
printf("斐波那契数列 F(%d) 的递归调用树:\n", n);
printf("重复计算的子问题:\n");
int call_count[100] = {0};
count_fib_calls(n, call_count);
for (int i = 0; i <= n; i++) {
if (call_count[i] > 1) {
printf("F(%d) 被计算了 %d 次\n", i, call_count[i]);
}
}
}
// 统计递归调用次数
int count_fib_calls(int n, int call_count[]) {
call_count[n]++;
if (n <= 1) return n;
return count_fib_calls(n - 1, call_count) +
count_fib_calls(n - 2, call_count);
}
重叠子问题的识别方法:
- 画出递归树,观察是否有重复节点
- 分析递归函数的时间复杂度
- 如果是指数级复杂度,很可能存在重叠子问题
2. 最优子结构验证
最优子结构是动态规划能够正确求解的关键保证。
c
// 硬币找零问题的最优子结构验证
int coin_change(int coins[], int n, int amount) {
// 最优子结构:
// 如果找零amount的最优解使用了面值为c的硬币,
// 那么找零amount-c也必须是最优解
// 反证:如果amount-c不是最优解,存在更优解,
// 那么可以用这个更优解加上硬币c得到amount的更优解,
// 与假设矛盾
}
最优子结构的证明技巧:
- 剪切-粘贴法:假设子问题不是最优,用更优解替换会产生矛盾
- 反证法:假设最优解不包含子问题最优解,推导矛盾
- 构造法:直接说明如何用子问题最优解构造原问题最优解
c
// 最长递增子序列的最优子结构
int lis(int arr[], int n) {
// 设L(i)是以arr[i]结尾的最长递增子序列长度
// 则L(i) = 1 + max{ L(j) },其中0 <= j < i且arr[j] < arr[i]
// 这满足最优子结构:整体最优解包含子问题最优解
}
3. 经典问题的结构分析
**3.1 背包问题
c
// 0-1背包问题的最优子结构
typedef struct {
int weight;
int value;
} Item;
int knapsack(Item items[], int n, int capacity) {
// 定义dp[i][w]:前i个物品,容量为w时的最大价值
// 最优子结构关系:
// 1. 不选第i个物品:dp[i][w] = dp[i-1][w]
// 2. 选第i个物品:dp[i][w] = dp[i-1][w-weight_i] + value_i
// 取两者最大值
// 这满足最优子结构,因为当前状态的最优解
// 由前一个状态的最优解决定
}
**3.2 矩阵链乘法
c
// 矩阵链乘法的最优子结构
int matrix_chain_order(int dimensions[], int n) {
// 定义m[i][j]:计算矩阵链A_i...A_j的最小代价
// 最优子结构关系:
// m[i][j] = min{ m[i][k] + m[k+1][j] + p_{i-1}*p_k*p_j }
// 对于 i <= k < j
// 整体最优的括号方案必然包含子链的最优括号方案
}
第三部分:备忘录方法
1. 备忘录方法原理
备忘录方法(Memoization)是动态规划的一种实现技术,通过存储已计算子问题的解来避免重复计算。
备忘录方法的特点:
- 自顶向下的求解方式
- 保持递归的思维模式
- 通过缓存避免重复计算
- 实现相对直观
c
// 斐波那契数列的备忘录方法
#define MAX_N 100
int memo[MAX_N]; // 备忘录数组
int fib_memo(int n) {
// 检查是否已计算
if (memo[n] != -1) {
return memo[n];
}
// 基本情况
if (n <= 1) {
memo[n] = n;
return n;
}
// 递归计算并存储结果
memo[n] = fib_memo(n - 1) + fib_memo(n - 2);
return memo[n];
}
// 初始化备忘录
void init_memo() {
for (int i = 0; i < MAX_N; i++) {
memo[i] = -1; // -1表示未计算
}
}
2. 备忘录方法实现模式
**2.1 通用模板
c
// 备忘录方法通用模板
Result dp_memo(State state, Result memo[]) {
// 1. 检查备忘录
if (is_computed(state, memo)) {
return get_from_memo(state, memo);
}
// 2. 处理基本情况
if (is_base_case(state)) {
Result base_result = solve_base_case(state);
store_to_memo(state, base_result, memo);
return base_result;
}
// 3. 递归求解
Result best_result = initial_value();
for (each possible choice) {
State next_state = apply_choice(state, choice);
Result sub_result = dp_memo(next_state, memo);
Result current_result = combine(state, sub_result, choice);
if (is_better(current_result, best_result)) {
best_result = current_result;
}
}
// 4. 存储结果并返回
store_to_memo(state, best_result, memo);
return best_result;
}
**2.2 硬币找零问题的备忘录解法
c
// 硬币找零问题的备忘录方法
int coin_change_memo(int coins[], int n, int amount, int memo[]) {
// 基本情况
if (amount == 0) return 0;
if (amount < 0) return -1;
// 检查备忘录
if (memo[amount] != -2) { // -2表示未计算
return memo[amount];
}
int min_coins = INT_MAX;
// 尝试每种硬币
for (int i = 0; i < n; i++) {
int sub_result = coin_change_memo(coins, n, amount - coins[i], memo);
if (sub_result != -1) {
min_coins = min(min_coins, sub_result + 1);
}
}
// 存储结果
memo[amount] = (min_coins == INT_MAX) ? -1 : min_coins;
return memo[amount];
}
// 用户接口
int coin_change(int coins[], int n, int amount) {
int memo[amount + 1];
// 初始化备忘录
for (int i = 0; i <= amount; i++) {
memo[i] = -2; // 特殊值表示未计算
}
return coin_change_memo(coins, n, amount, memo);
}
3. 备忘录方法的优缺点
优点:
- 实现直观,接近递归思维
- 只计算必要的子问题
- 代码可读性好
缺点:
- 递归调用有栈溢出风险
- 常数因子可能比迭代方法大
- 对于某些问题可能不如自底向上高效
c
// 备忘录方法的栈深度保护
int safe_dp_memo(State state, Result memo[], int* depth) {
// 栈深度检查
if (*depth > MAX_RECURSION_DEPTH) {
printf("警告:递归深度过大\n");
return ERROR_VALUE;
}
(*depth)++;
Result result = dp_memo(state, memo);
(*depth)--;
return result;
}
第四部分:自底向上求解
1. 自底向上方法原理
自底向上(Bottom-up)是动态规划的另一种实现方式,通过迭代从小问题开始求解,逐步构建大问题的解。
自底向上的特点:
- 迭代求解,无递归开销
- 可以优化空间复杂度
- 通常比备忘录方法更快
- 需要确定计算顺序
c
// 斐波那契数列的自底向上解法
int fib_bottom_up(int n) {
if (n <= 1) return n;
int dp[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. 自底向上实现模式
**2.1 通用模板
c
// 自底向上动态规划通用模板
Result dp_bottom_up(Problem problem) {
// 1. 定义DP表
Result dp[problem_size];
// 2. 初始化基本情况
for (each base case state) {
dp[state] = base_value;
}
// 3. 按顺序计算所有状态
for (int i = first_state; i <= target_state; i++) {
// 根据状态转移方程计算dp[i]
dp[i] = calculate_from_smaller_states(dp, i);
}
// 4. 返回目标状态的解
return dp[target_state];
}
**2.2 硬币找零问题的自底向上解法
c
// 硬币找零问题的自底向上解法
int coin_change_bottom_up(int coins[], int n, int amount) {
// 创建DP表
int dp[amount + 1];
// 初始化:0金额需要0个硬币,其他初始化为最大值
dp[0] = 0;
for (int i = 1; i <= amount; i++) {
dp[i] = amount + 1; // 使用一个大于最大可能值的数
}
// 计算所有金额的最小硬币数
for (int i = 1; i <= amount; i++) {
for (int j = 0; j < n; j++) {
if (coins[j] <= i) {
dp[i] = min(dp[i], dp[i - coins[j]] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
3. 计算顺序的确定
正确的计算顺序是自底向上方法成功的关键。
c
// 依赖关系分析与计算顺序
void analyze_dependencies(Problem problem) {
printf("状态依赖关系分析:\n");
for (each state) {
printf("dp[%d] 依赖于: ", state);
for (each state that current state depends on) {
printf("dp[%d] ", dependent_state);
}
printf("\n");
}
printf("计算顺序: ");
// 拓扑排序确定计算顺序
print_topological_order(problem);
}
**3.1 一维DP的计算顺序
c
// 一维DP通常从前向后或从后向前计算
void one_d_dp_order(int n) {
int dp[n + 1];
// 情况1:当前状态依赖前面的状态(正向计算)
dp[0] = base_value;
for (int i = 1; i <= n; i++) {
dp[i] = f(dp[i - 1], ...); // 依赖前一个状态
}
// 情况2:当前状态依赖后面的状态(反向计算)
dp[n] = base_value;
for (int i = n - 1; i >= 0; i--) {
dp[i] = f(dp[i + 1], ...); // 依赖后一个状态
}
}
**3.2 二维DP的计算顺序
c
// 二维DP需要确定行和列的计算顺序
void two_d_dp_order(int rows, int cols) {
int dp[rows][cols];
// 初始化边界
for (int i = 0; i < rows; i++) dp[i][0] = base_value;
for (int j = 0; j < cols; j++) dp[0][j] = base_value;
// 常见的计算顺序:行优先
for (int i = 1; i < rows; i++) {
for (int j = 1; j < cols; j++) {
// 依赖左边、上边或左上的状态
dp[i][j] = f(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]);
}
}
// 或者其他顺序,根据具体依赖关系确定
}
4. 空间复杂度优化
自底向上方法通常可以优化空间复杂度。
c
// 斐波那契数列的空间优化
int fib_optimized(int n) {
if (n <= 1) return n;
int prev2 = 0, prev1 = 1;
for (int i = 2; i <= n; i++) {
int current = prev1 + prev2;
prev2 = prev1;
prev1 = current;
}
return prev1;
}
// 二维DP降为一维DP(滚动数组)
int knapsack_optimized(Item items[], int n, int capacity) {
int dp[capacity + 1];
// 初始化
for (int w = 0; w <= capacity; w++) {
dp[w] = 0;
}
// 使用一维数组,从后向前更新
for (int i = 0; i < n; i++) {
for (int w = capacity; w >= items[i].weight; w--) {
dp[w] = max(dp[w], dp[w - items[i].weight] + items[i].value);
}
}
return dp[capacity];
}
第五部分:动态规划解题步骤
1. 四步解题法
动态规划问题可以通过系统化的四步法解决。
**步骤1:定义状态
c
// 状态定义示例
typedef struct {
// 根据问题确定状态变量
int param1;
int param2;
// ... 其他状态变量
} State;
// 或者使用多维数组索引作为状态
// dp[i][j] 表示某种含义
**步骤2:确定状态转移方程
c
// 状态转移方程模板
void state_transition_equation() {
// dp[state] = best_choice {
// option1: dp[next_state1] + cost1,
// option2: dp[next_state2] + cost2,
// ...
// }
}
**步骤3:初始化边界条件
c
// 边界条件初始化
void initialize_base_cases(int dp[], int n) {
// 设置最基本情况的解
dp[0] = base_value_0;
dp[1] = base_value_1;
// ... 其他边界情况
}
**步骤4:确定计算顺序
c
// 计算顺序确定
void determine_computation_order(int dp[], int n) {
// 根据状态依赖关系确定计算顺序
// 确保计算dp[i]时,它依赖的状态都已经计算过
for (int i = first_state; i <= target_state; i++) {
// 按依赖关系确定的顺序计算
}
}
2. 经典问题解题示范
**2.1 最长递增子序列(LIS)
c
// 最长递增子序列的DP解法
int longest_increasing_subsequence(int arr[], int n) {
// 步骤1:定义状态
// dp[i] 表示以arr[i]结尾的最长递增子序列长度
int dp[n];
// 步骤2:初始化
for (int i = 0; i < n; i++) {
dp[i] = 1; // 每个元素本身构成长度为1的序列
}
// 步骤3:状态转移
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++) {
if (arr[j] < arr[i]) {
dp[i] = max(dp[i], dp[j] + 1);
}
}
}
// 步骤4:获取结果
int max_length = 0;
for (int i = 0; i < n; i++) {
max_length = max(max_length, dp[i]);
}
return max_length;
}
**2.2 爬楼梯问题
c
// 爬楼梯问题的DP解法
int climb_stairs(int n) {
// 步骤1:定义状态
// dp[i] 表示爬到第i阶楼梯的方法数
if (n <= 2) return n;
int dp[n + 1];
// 步骤2:初始化边界
dp[0] = 1; // 起点,1种方法(不爬)
dp[1] = 1; // 爬1阶,1种方法
dp[2] = 2; // 爬2阶,2种方法:1+1 或 2
// 步骤3:状态转移
for (int i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
// 到达第i阶的方法数 =
// 从第i-1阶爬1阶的方法数 + 从第i-2阶爬2阶的方法数
}
return dp[n];
}
// 空间优化版本
int climb_stairs_optimized(int n) {
if (n <= 2) return n;
int prev2 = 1, prev1 = 2;
for (int i = 3; i <= n; i++) {
int current = prev1 + prev2;
prev2 = prev1;
prev1 = current;
}
return prev1;
}
第六部分:经典动态规划问题
1. 斐波那契数列
斐波那契数列是理解动态规划的最简单例子。
c
// 完整的斐波那契数列DP实现对比
#include <stdio.h>
#include <time.h>
// 朴素递归(指数时间复杂度)
int fib_naive(int n) {
if (n <= 1) return n;
return fib_naive(n - 1) + fib_naive(n - 2);
}
// 备忘录方法(O(n)时间,O(n)空间)
int fib_memoization(int n, int memo[]) {
if (memo[n] != -1) return memo[n];
if (n <= 1) return n;
memo[n] = fib_memoization(n - 1, memo) + fib_memoization(n - 2, memo);
return memo[n];
}
// 自底向上(O(n)时间,O(n)空间)
int fib_bottom_up(int n) {
if (n <= 1) return n;
int dp[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];
}
// 空间优化(O(n)时间,O(1)空间)
int fib_optimized(int n) {
if (n <= 1) return n;
int a = 0, b = 1, c;
for (int i = 2; i <= n; i++) {
c = a + b;
a = b;
b = c;
}
return b;
}
// 性能测试
void test_fib_performance() {
const int n = 40;
printf("计算斐波那契数列 F(%d):\n", n);
// 测试朴素递归
clock_t start = clock();
int result1 = fib_naive(n);
clock_t end = clock();
printf("朴素递归: %d (耗时: %f秒)\n", result1, (double)(end - start) / CLOCKS_PER_SEC);
// 测试备忘录方法
int memo[n + 1];
for (int i = 0; i <= n; i++) memo[i] = -1;
start = clock();
int result2 = fib_memoization(n, memo);
end = clock();
printf("备忘录方法: %d (耗时: %f秒)\n", result2, (double)(end - start) / CLOCKS_PER_SEC);
// 测试自底向上
start = clock();
int result3 = fib_bottom_up(n);
end = clock();
printf("自底向上: %d (耗时: %f秒)\n", result3, (double)(end - start) / CLOCKS_PER_SEC);
// 测试空间优化
start = clock();
int result4 = fib_optimized(n);
end = clock();
printf("空间优化: %d (耗时: %f秒)\n", result4, (double)(end - start) / CLOCKS_PER_SEC);
}
2. 背包问题
背包问题是动态规划的经典应用。
<br/**
c
// 0-1背包问题完整实现
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int weight;
int value;
} Item;
// 0-1背包问题DP解法
int knapsack_01(Item items[], int n, int capacity) {
// 创建DP表
int** dp = (int**)malloc((n + 1) * sizeof(int*));
for (int i = 0; i <= n; i++) {
dp[i] = (int*)malloc((capacity + 1) * sizeof(int));
}
// 初始化
for (int i = 0; i <= n; i++) {
for (int w = 0; w <= capacity; w++) {
if (i == 0 || w == 0) {
dp[i][w] = 0;
} else if (items[i - 1].weight <= w) {
// 选择当前物品或不选择当前物品的最大值
int include = items[i - 1].value + dp[i - 1][w - items[i - 1].weight];
int exclude = dp[i - 1][w];
dp[i][w] = (include > exclude) ? include : exclude;
} else {
dp[i][w] = dp[i - 1][w];
}
}
}
int result = dp[n][capacity];
// 释放内存
for (int i = 0; i <= n; i++) {
free(dp[i]);
}
free(dp);
return result;
}
// 空间优化的0-1背包问题
int knapsack_01_optimized(Item items[], int n, int capacity) {
int dp[capacity + 1];
// 初始化
for (int w = 0; w <= capacity; w++) {
dp[w] = 0;
}
// 填充DP表
for (int i = 0; i < n; i++) {
// 反向遍历,避免重复选择
for (int w = capacity; w >= items[i].weight; w--) {
if (dp[w] < dp[w - items[i].weight] + items[i].value) {
dp[w] = dp[w - items[i].weight] + items[i].value;
}
}
}
return dp[capacity];
}
// 打印最优解的具体物品选择
void print_knapsack_solution(Item items[], int n, int capacity) {
int** dp = (int**)malloc((n + 1) * sizeof(int*));
for (int i = 0; i <= n; i++) {
dp[i] = (int*)malloc((capacity + 1) * sizeof(int));
}
// 填充DP表
for (int i = 0; i <= n; i++) {
for (int w = 0; w <= capacity; w++) {
if (i == 0 || w == 0) {
dp[i][w] = 0;
} else if (items[i - 1].weight <= w) {
int include = items[i - 1].value + dp[i - 1][w - items[i - 1].weight];
int exclude = dp[i - 1][w];
dp[i][w] = (include > exclude) ? include : exclude;
} else {
dp[i][w] = dp[i - 1][w];
}
}
}
// 回溯找出选择的物品
printf("选择的物品: ");
int w = capacity;
for (int i = n; i > 0 && w > 0; i--) {
if (dp[i][w] != dp[i - 1][w]) {
printf("物品%d(重量:%d,价值:%d) ", i, items[i - 1].weight, items[i - 1].value);
w -= items[i - 1].weight;
}
}
printf("\n最大价值: %d\n", dp[n][capacity]);
// 释放内存
for (int i = 0; i <= n; i++) {
free(dp[i]);
}
free(dp);
}
3. 最长公共子序列
最长公共子序列是字符串处理中的经典动态规划问题。
c
// 最长公共子序列(LCS)实现
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// LCS长度计算
int lcs_length(char* X, char* Y, int m, int n) {
// 创建DP表
int** dp = (int**)malloc((m + 1) * sizeof(int*));
for (int i = 0; i <= m; i++) {
dp[i] = (int*)malloc((n + 1) * sizeof(int));
}
// 填充DP表
for (int i = 0; i <= m; i++) {
for (int j = 0; j <= n; j++) {
if (i == 0 || j == 0) {
dp[i][j] = 0;
} else if (X[i - 1] == Y[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = (dp[i - 1][j] > dp[i][j - 1]) ? dp[i - 1][j] : dp[i][j - 1];
}
}
}
int result = dp[m][n];
// 释放内存
for (int i = 0; i <= m; i++) {
free(dp[i]);
}
free(dp);
return result;
}
// 打印LCS具体内容
void print_lcs(char* X, char* Y, int m, int n) {
int** dp = (int**)malloc((m + 1) * sizeof(int*));
for (int i = 0; i <= m; i++) {
dp[i] = (int*)malloc((n + 1) * sizeof(int));
}
// 填充DP表
for (int i = 0; i <= m; i++) {
for (int j = 0; j <= n; j++) {
if (i == 0 || j == 0) {
dp[i][j] = 0;
} else if (X[i - 1] == Y[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = (dp[i - 1][j] > dp[i][j - 1]) ? dp[i - 1][j] : dp[i][j - 1];
}
}
}
// 回溯构造LCS
int index = dp[m][n];
char lcs[index + 1];
lcs[index] = '\0';
int i = m, j = n;
while (i > 0 && j > 0) {
if (X[i - 1] == Y[j - 1]) {
lcs[index - 1] = X[i - 1];
i--; j--; index--;
} else if (dp[i - 1][j] > dp[i][j - 1]) {
i--;
} else {
j--;
}
}
printf("最长公共子序列: %s\n", lcs);
printf("长度: %d\n", dp[m][n]);
// 释放内存
for (int i = 0; i <= m; i++) {
free(dp[i]);
}
free(dp);
}
// 空间优化的LCS(只计算长度)
int lcs_length_optimized(char* X, char* Y, int m, int n) {
int dp[2][n + 1];
// 初始化
for (int j = 0; j <= n; j++) {
dp[0][j] = 0;
}
dp[1][0] = 0;
// 使用滚动数组
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (X[i - 1] == Y[j - 1]) {
dp[i % 2][j] = dp[(i - 1) % 2][j - 1] + 1;
} else {
dp[i % 2][j] = (dp[(i - 1) % 2][j] > dp[i % 2][j - 1]) ?
dp[(i - 1) % 2][j] : dp[i % 2][j - 1];
}
}
}
return dp[m % 2][n];
}
第七部分:总结
1. 核心要点总结
动态规划思想:
- 通过存储子问题解避免重复计算
- 用空间换时间,提高算法效率
- 适用于具有最优子结构和重叠子问题的问题
关键技术:
- 备忘录方法:自顶向下,保持递归思维
- 自底向上:迭代求解,通常更高效
- 状态定义:准确描述问题状态
- 状态转移:明确状态之间的关系
2. 动态规划解题框架
- 分析问题:判断是否具有最优子结构和重叠子问题
- 定义状态:用恰当的方式描述问题状态
- 状态转移:建立状态之间的关系方程
- 初始化:设置边界条件的解
- 计算顺序:确定状态的计算顺序
- 空间优化:考虑是否能够降低空间复杂度
第八部分:常见问题解答
Q1:如何判断一个问题是否适合用动态规划解决?
A1:主要看两个特征:1) 最优子结构:问题的最优解包含子问题的最优解;2) 重叠子问题:递归求解时会重复计算相同的子问题。可以通过画递归树或分析递归函数的时间复杂度来判断。
Q2:备忘录方法和自底向上方法哪个更好?
A2:各有优势。备忘录方法更直观,接近递归思维,只计算必要的子问题;自底向上方法通常更快,没有递归开销,而且可以优化空间复杂度。对于初学者,建议先掌握备忘录方法,再学习自底向上方法。
Q3:动态规划的空间复杂度可以优化到什么程度?
A3:这取决于具体问题。很多一维DP可以优化到O(1),二维DP可以优化到O(n)(使用滚动数组)。但有些问题如最长公共子序列,如果要重建具体解,可能需要保持O(mn)的空间。
Q4:动态规划的时间复杂度通常是多少?
A4:动态规划的时间复杂度通常是状态数量乘以每个状态的转移代价。比如背包问题是O(nW),其中n是物品数量,W是背包容量;最长公共子序列是O(mn),其中m、n是两个字符串的长度。
Q5:如何设计状态转移方程?
A5:1) 明确状态定义;2) 考虑当前状态可能由哪些状态转移而来;3) 写出状态之间的关系;4) 确保状态转移覆盖所有可能情况;5) 验证边界条件。多练习经典问题有助于培养这种思维。
觉得文章有帮助?别忘了:
👍 点赞 👍 - 给我一点鼓励
⭐ 收藏 ⭐ - 方便以后查看
🔔 关注 🔔 - 获取更新通知
标签: #C语言算法 #动态规划 #DP基础 #算法设计 #优化算法