递归、回溯与动态规划学习笔记

一、核心知识框架

本次课程聚焦算法中的核心思想 ------ 递归回溯(含剪枝)与动态规划,通过经典例题拆解逻辑,核心围绕 "大问题拆解为小问题" 的解题思路,覆盖全排列、组合数、爬楼梯、打家劫舍、最长公共子序列等高频题型,形成 "原理 - 代码 - 应用" 的完整学习链条。

二、递归与回溯剪枝

(一)核心逻辑

递归是通过调用自身函数拆解问题,回溯则是在递归后恢复状态以探索其他可能性,剪枝用于减少无效搜索,三者结合是解决排列、组合等问题的关键。

(二)经典例题:全排列

  1. 问题本质:输出数组所有不重复的排列方式(如 123 的排列包括 123、132、213 等),需遍历每个位置的所有可选元素。
  2. 关键设计
    • 数组定义:a存储原始输入数组,b暂存当前排列结果,vis数组标记元素是否已使用(0 未使用,1 已使用)。
    • 递归边界:当当前排列的起始下标l等于数组长度n时,输出b中的完整排列(元素间用逗号分隔,末尾换行)。
    • 递归过程:从数组起始位置遍历,若元素未使用,将其存入b并标记vis为 1,l+1后递归调用;递归返回后执行 "剪枝"------l-1并重置vis为 0,恢复初始状态以探索下一种排列。
  3. 与组合数的区别
    • 组合数无需考虑元素顺序(如 C32 仅需 12、13、23),无需回溯剪枝,遍历方向从左到右不回头;
    • 全排列需考虑顺序,必须通过vis数组标记使用状态,回溯恢复以确保所有可能性都被探索。

(三)学习要点

  • 边界条件是递归的 "终止开关",需精准匹配问题的 "最小子问题"(如全排列中l==n即完成一次排列);
  • 回溯的核心是 "状态恢复",避免前一次选择影响后续搜索,是实现多可能性探索的关键。

三、动态规划(DP)

(一)核心思想

灵活结合递归与递推,通过存储子问题的最优解(DP 数组)避免重复计算,核心是 "状态定义 + 状态转移方程 + 边界条件"。

(二)经典例题解析(C 语言实现)

1. 一维打家劫舍(基础题)

题目描述 :沿街房屋,相邻房屋不能同时偷窃,求可偷窃的最大金额。输入示例

复制代码
4
1 2 3 1

输出示例4

解题思路

  • 状态定义:dp[i]表示前i个房屋的最大偷窃金额;
  • 转移方程:dp[i] = max(dp[i-1](不偷第i个), dp[i-2] + nums[i-1](偷第i个))
  • 边界条件:dp[0]=0(0 个房屋),dp[1]=nums[0](1 个房屋)。

完整代码

复制代码
#include <stdio.h>
#include <stdlib.h>

int max(int a, int b) {
    return a > b ? a : b;
}

int main() {
    int n;
    scanf("%d", &n);
    int* nums = (int*)malloc(n * sizeof(int)); // 动态申请数组存储房屋金额
    for (int i = 0; i < n; i++) {
        scanf("%d", &nums[i]);
    }
    
    // 处理边界情况
    if (n == 0) {
        printf("0\n");
        free(nums); // 释放内存
        return 0;
    }
    if (n == 1) {
        printf("%d\n", nums[0]);
        free(nums);
        return 0;
    }
    
    // 初始化dp数组
    int* dp = (int*)malloc((n + 1) * sizeof(int));
    dp[0] = 0;
    dp[1] = nums[0];
    // 填充dp数组
    for (int i = 2; i <= n; i++) {
        dp[i] = max(dp[i-1], dp[i-2] + nums[i-1]);
    }
    printf("%d\n", dp[n]);
    
    // 释放动态内存,避免内存泄漏
    free(nums);
    free(dp);
    return 0;
}
2. 最长公共子序列(LCS)

题目描述 :给定两个字符串,求其最长公共子序列的长度(子序列无需连续)。输入示例

复制代码
ABCBDAB BDCABA

输出示例4

解题思路

  • 状态定义:dp[i][j]表示字符串Xi个字符与字符串Yj个字符的 LCS 长度;
  • 转移方程:
    • X[i-1] == Y[j-1],则dp[i][j] = dp[i-1][j-1] + 1(字符匹配,长度 + 1);
    • 否则dp[i][j] = max(dp[i-1][j], dp[i][j-1])(取上方 / 左方最优解);
  • 边界条件:dp[i][0] = 0dp[0][j] = 0(任一字符串为空时,LCS 长度为 0)。

完整代码

复制代码
#include <stdio.h>
#include <string.h>

int max(int a, int b) {
    return a > b ? a : b;
}

int main() {
    char s1[505], s2[505];
    // 循环读取输入(支持多组测试用例)
    while (~scanf("%s %s", s1, s2)) {
        int dp[505][505]; // dp数组存储子问题解
        int len1 = strlen(s1); // 字符串1长度
        int len2 = strlen(s2); // 字符串2长度
        
        // 初始化边界条件
        for (int i = 0; i <= len1; i++) {
            dp[0][i] = 0;
        }
        for (int i = 0; i <= len2; i++) {
            dp[i][0] = 0;
        }
        
        // 填充dp数组(注意字符串下标从0开始,dp数组从1开始)
        for (int i = 1; i <= len2; i++) {
            for (int j = 1; j <= len1; j++) {
                if (s1[j-1] == s2[i-1]) {
                    dp[i][j] = dp[i-1][j-1] + 1;
                } else {
                    dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
                }
            }
        }
        // 输出最终结果
        printf("%d\n", dp[len2][len1]);
    }
    return 0;
}
3. 二维打家劫舍(进阶题)

题目描述 :二维网格房屋,上下左右相邻房屋不能同时偷窃,求可偷窃的最大金额。输入示例

复制代码
2 3
1 2 3
4 5 6

输出示例10

解题思路:将二维问题拆解为两次一维打家劫舍:

  1. 对每一行单独做 "一维打家劫舍",得到每行的最大金额数组rowMax
  2. rowMax数组再做 "一维打家劫舍"(相邻行不能同时偷),得到最终结果。

完整代码

复制代码
#include <stdio.h>
#include <stdlib.h>

int max(int a, int b) {
    return a > b ? a : b;
}

// 子函数:计算单行房屋的最大偷窃金额(一维打家劫舍)
int rowRob(int* row, int n) {
    if (n == 0) return 0;
    if (n == 1) return row[0];
    
    int* dp = (int*)malloc((n + 1) * sizeof(int));
    dp[0] = 0;
    dp[1] = row[0];
    for (int i = 2; i <= n; i++) {
        dp[i] = max(dp[i-1], dp[i-2] + row[i-1]);
    }
    int res = dp[n];
    free(dp); // 释放子函数内的动态内存
    return res;
}

int main() {
    int m, n;
    scanf("%d %d", &m, &n); // 输入网格行数m、列数n
    // 动态申请二维数组存储网格金额
    int** grid = (int**)malloc(m * sizeof(int*));
    for (int i = 0; i < m; i++) {
        grid[i] = (int*)malloc(n * sizeof(int));
        for (int j = 0; j < n; j++) {
            scanf("%d", &grid[i][j]);
        }
    }
    
    // 第一步:计算每行的最大金额
    int* rowMax = (int*)malloc(m * sizeof(int));
    for (int i = 0; i < m; i++) {
        rowMax[i] = rowRob(grid[i], n);
    }
    
    // 第二步:对rowMax做一维打家劫舍
    int res;
    if (m == 0) {
        res = 0;
    } else if (m == 1) {
        res = rowMax[0];
    } else {
        int* dp = (int*)malloc((m + 1) * sizeof(int));
        dp[0] = 0;
        dp[1] = rowMax[0];
        for (int i = 2; i <= m; i++) {
            dp[i] = max(dp[i-1], dp[i-2] + rowMax[i-1]);
        }
        res = dp[m];
        free(dp);
    }
    printf("%d\n", res);
    
    // 释放所有动态内存
    for (int i = 0; i < m; i++) {
        free(grid[i]);
    }
    free(grid);
    free(rowMax);
    return 0;
}

(三)动态规划学习要点

  • 状态定义是核心,需明确dp[i]dp[i][j]的实际含义(如 "前 i 个房屋的最大金额");
  • 边界条件需覆盖 "空状态"(如空字符串、0 个房屋),避免数组越界;
  • 优先使用递推实现(双重循环),减少递归的内存占用;
  • C 语言实现需注意动态内存的申请与释放,避免内存泄漏。

四、作业与实践要求

  1. 必做题:完成前 6 道基础题,包括组合数(无需剪枝,仅回溯)、基础爬楼梯、一维打家劫舍;
  2. 选做题:最长公共子序列进阶、二维打家劫舍;
  3. 注意事项
    • 组合数需关注数组覆盖与边界条件设计;
    • 动态规划题需先明确状态定义,再推导转移方程;
    • 编写 C 语言代码时,注意动态内存的管理(申请后必须释放)。

五、学习总结

  1. 递归回溯的关键是 "边界 + 状态恢复",适用于排列、子集等需要穷举所有可能性的问题;
  2. 动态规划的核心是 "状态存储 + 最优子结构",解题三步法:定义状态→推导转移方程→确定边界条件;
  3. 复杂的二维动态规划问题可拆解为多个一维问题解决,核心思路仍是 "大问题拆小问题"。后续课程将学习 01 背包、完全背包等进阶题型,需提前复习本次内容,夯实基础。
相关推荐
眼镜哥(with glasses)2 小时前
网络技术三级考试综合题笔记整理(第二题、第三题)
网络·笔记·智能路由器
半壶清水2 小时前
[软考网规考点笔记]-数据通信基础之差错控制编码技术
网络·笔记·网络协议·tcp/ip
0 0 02 小时前
CCF-CSP 38-4 月票发行【C++】考点:动态规划DP+矩阵快速幂
c++·算法·动态规划·矩阵快速幂
左左右右左右摇晃2 小时前
Java List集合
笔记
OxyTheCrack2 小时前
【C++】详细拆解std::mutex的底层原理
linux·开发语言·c++·笔记
左左右右左右摇晃3 小时前
红黑树笔记整理
笔记
小光学长3 小时前
基于ssm的膳食健康管理系统e6whl4q7(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
java·开发语言·数据库·学习·ssm
weixin_458872614 小时前
东华复试OJ二刷复盘7
学习
盐水冰5 小时前
【Redis】学习(2)Redis常见命令
数据库·redis·学习