一、核心知识框架
本次课程聚焦算法中的核心思想 ------ 递归回溯(含剪枝)与动态规划,通过经典例题拆解逻辑,核心围绕 "大问题拆解为小问题" 的解题思路,覆盖全排列、组合数、爬楼梯、打家劫舍、最长公共子序列等高频题型,形成 "原理 - 代码 - 应用" 的完整学习链条。
二、递归与回溯剪枝
(一)核心逻辑
递归是通过调用自身函数拆解问题,回溯则是在递归后恢复状态以探索其他可能性,剪枝用于减少无效搜索,三者结合是解决排列、组合等问题的关键。
(二)经典例题:全排列
- 问题本质:输出数组所有不重复的排列方式(如 123 的排列包括 123、132、213 等),需遍历每个位置的所有可选元素。
- 关键设计 :
- 数组定义:
a存储原始输入数组,b暂存当前排列结果,vis数组标记元素是否已使用(0 未使用,1 已使用)。 - 递归边界:当当前排列的起始下标
l等于数组长度n时,输出b中的完整排列(元素间用逗号分隔,末尾换行)。 - 递归过程:从数组起始位置遍历,若元素未使用,将其存入
b并标记vis为 1,l+1后递归调用;递归返回后执行 "剪枝"------l-1并重置vis为 0,恢复初始状态以探索下一种排列。
- 数组定义:
- 与组合数的区别 :
- 组合数无需考虑元素顺序(如 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]表示字符串X前i个字符与字符串Y前j个字符的 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] = 0、dp[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
解题思路:将二维问题拆解为两次一维打家劫舍:
- 对每一行单独做 "一维打家劫舍",得到每行的最大金额数组
rowMax; - 对
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 语言实现需注意动态内存的申请与释放,避免内存泄漏。
四、作业与实践要求
- 必做题:完成前 6 道基础题,包括组合数(无需剪枝,仅回溯)、基础爬楼梯、一维打家劫舍;
- 选做题:最长公共子序列进阶、二维打家劫舍;
- 注意事项 :
- 组合数需关注数组覆盖与边界条件设计;
- 动态规划题需先明确状态定义,再推导转移方程;
- 编写 C 语言代码时,注意动态内存的管理(申请后必须释放)。
五、学习总结
- 递归回溯的关键是 "边界 + 状态恢复",适用于排列、子集等需要穷举所有可能性的问题;
- 动态规划的核心是 "状态存储 + 最优子结构",解题三步法:定义状态→推导转移方程→确定边界条件;
- 复杂的二维动态规划问题可拆解为多个一维问题解决,核心思路仍是 "大问题拆小问题"。后续课程将学习 01 背包、完全背包等进阶题型,需提前复习本次内容,夯实基础。