C语言算法:动态规划基础

本文献给:

准备学习动态规划的C语言程序员。如果你已经掌握了递归和分治算法,想要理解动态规划的核心思想并解决复杂优化问题------本文将为你奠定坚实的动态规划基础。

你将学到:

  1. 理解动态规划的基本思想和核心概念
  2. 掌握重叠子问题与最优子结构的识别方法
  3. 学会备忘录方法和自底向上求解技术
  4. 掌握动态规划问题的解题步骤和模式识别

让我们开始探索动态规划的强大威力!


目录

第一部分:动态规划入门

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);
}

重叠子问题的识别方法:

  1. 画出递归树,观察是否有重复节点
  2. 分析递归函数的时间复杂度
  3. 如果是指数级复杂度,很可能存在重叠子问题

2. 最优子结构验证

最优子结构是动态规划能够正确求解的关键保证。

c 复制代码
// 硬币找零问题的最优子结构验证
int coin_change(int coins[], int n, int amount) {
    // 最优子结构:
    // 如果找零amount的最优解使用了面值为c的硬币,
    // 那么找零amount-c也必须是最优解
    
    // 反证:如果amount-c不是最优解,存在更优解,
    // 那么可以用这个更优解加上硬币c得到amount的更优解,
    // 与假设矛盾
}

最优子结构的证明技巧:

  1. 剪切-粘贴法:假设子问题不是最优,用更优解替换会产生矛盾
  2. 反证法:假设最优解不包含子问题最优解,推导矛盾
  3. 构造法:直接说明如何用子问题最优解构造原问题最优解
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. 动态规划解题框架

  1. 分析问题:判断是否具有最优子结构和重叠子问题
  2. 定义状态:用恰当的方式描述问题状态
  3. 状态转移:建立状态之间的关系方程
  4. 初始化:设置边界条件的解
  5. 计算顺序:确定状态的计算顺序
  6. 空间优化:考虑是否能够降低空间复杂度

第八部分:常见问题解答

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基础 #算法设计 #优化算法

相关推荐
Elias不吃糖40 分钟前
LeetCode--130被围绕的区域
数据结构·c++·算法·leetcode·深度优先
sinat_602035361 小时前
翁恺 6-
c语言
誰能久伴不乏1 小时前
进程通信与线程通信:全面总结 + 使用场景 + 优缺点 + 使用方法
linux·服务器·c语言·c++
im_AMBER1 小时前
数据结构 12 图
数据结构·笔记·学习·算法·深度优先
STY_fish_20122 小时前
P11855 [CSP-J2022 山东] 部署
算法·图论·差分
myw0712052 小时前
湘大头歌程-Ride to Office练习笔记
c语言·数据结构·笔记·算法
H_BB2 小时前
算法详解:滑动窗口机制
数据结构·c++·算法·滑动窗口
Zero-Talent2 小时前
“栈” 算法
算法
橘子编程2 小时前
经典排序算法全解析
java·算法·排序算法