递归出口
if (n <= 1) return 1;
最小问题直接回答,不再递归。出口必须能被所有合法输入最终到达。
Easy explanation
打开最大的套娃,会看到一个更小但结构相同的套娃,直到最小的不能再打开。递归函数也要有“最小情况”作为出口,其余情况把问题缩小后再次调用自己。没有出口,函数就会不停压入调用栈,最终耗尽空间。
走迷宫时,在路口选择一条路,走不通就退回路口并尝试另一条。回溯算法会记录当前选择,深入搜索,失败后撤销选择。它不是盲目重复,而是系统地枚举可能性并剪掉不可能路线。
Core concepts
if (n <= 1) return 1;
最小问题直接回答,不再递归。出口必须能被所有合法输入最终到达。
return n * factorial(n - 1);
每次调用都让参数更接近出口,并使用小问题结果构造当前答案。
dfs(nextRow, nextCol);
沿一条路线尽量深入,不能继续时返回,再探索其他分支。
path.pop_back();
递归返回后恢复进入前状态,让下一分支从干净状态开始。
sum(n) = n + sum(n - 1)先写数学关系,再决定 n=0 时的直接答案。
if (visited[r][c]) return false;访问标记防止在迷宫环路中无限来回。
choice.push_back(x); solve(); choice.pop_back();做选择、深入、撤销是回溯最常见骨架。
if (path.size() == target) record(path);达到完整答案时保存副本,然后返回继续寻找其他答案。
Complete examples
Example 1
n % 10 得到最后一位,n / 10 得到剩余数字。问题从多位数逐步缩小到零。
调用返回时,各层把自己保存的最后一位相加。画出 527 的三层调用能看清顺序。
int digitSum(int n)
{
n = abs(n);
if (n == 0) return 0;
return n % 10 + digitSum(n / 10);
}
cout << digitSum(527) << '\n';
// 7 + digitSum(52)
// 7 + 2 + digitSum(5)
// 7 + 2 + 5 + digitSum(0)
Example 2
超出边界、遇到墙或已经访问都立即失败;到达出口则成功。其余格子先标记,再递归尝试上下左右。
若需要打印路径,可以进入时加入坐标,所有方向失败后删除坐标;成功路线则保留。
bool solve(int r, int c)
{
if (r < 0 || r >= rows || c < 0 || c >= cols) return false;
if (maze[r][c] == '#' || visited[r][c]) return false;
if (maze[r][c] == 'E') return true;
visited[r][c] = true;
path.push_back({r, c});
int dr[] = {1, -1, 0, 0};
int dc[] = {0, 0, 1, -1};
for (int i = 0; i < 4; ++i)
if (solve(r + dr[i], c + dc[i])) return true;
path.pop_back();
return false;
}
Algorithm thinking
不要试图同时在脑中展开几十层。先假设小问题函数已经正确,再说明当前层怎样利用它。然后确认参数严格缩小并最终到达出口。用三层小输入画调用和返回,通常就能发现错误。
回溯适合搜索组合、排列和路径,但可能尝试很多可能性。可以提前检查规则,发现选择必然失败就不再深入,这叫剪枝。剪枝不能漏掉合法答案,因此必须基于确定条件。
找出不用继续调用就能直接回答的情况。
每次改变参数,使它严格接近出口。
进入下一层前记录路径、已用元素或访问状态。
递归返回时撤销本层影响,再尝试下一个候选。
Common mistakes
参数没有缩小或出口条件写错,会导致栈溢出。
上一条失败路线残留在 path 中,会污染后续答案。
相邻格子互相递归,程序在环中无限调用。
每层按值复制网格成本很高,应使用引用并谨慎恢复修改。
先准备一个最小输入,只保留能够重现问题的几行数据;再在关键步骤输出变量值,确认程序究竟在哪一步偏离预期。编译错误从第一条开始处理,运行错误则比较“实际结果”和“期望结果”。修复后别只重跑原来的例子,还要增加空输入、边界值和错误输入,防止问题换一个形式再次出现。
Homework
建议按顺序完成。前四题帮助巩固语法和基本操作,第五到第七题要求把多个知识点组合起来,第八题是小项目。每道题都要先写输入、处理、输出三行计划,再开始敲代码;程序运行后至少测试正常情况、边界情况和错误情况。
从 n 输出到 1,再在返回阶段从 1 输出到 n,观察两个位置的区别。
递归计算 base 的非负整数次幂,再挑战用折半方法减少调用。
比较两端字符并缩小区间,忽略大小写,处理空字符串。
把上一课二分查找改成递归,参数明确表示剩余区间。
输出三个不同字符的所有排列,使用 used 数组并正确撤销。
用 1、2、5 组成目标金额,输出所有不重复组合并进行剪枝。
用 DFS 找到一条路,再思考为什么它不保证最短,为下一阶段算法学习留下解释。
在 4×4 障碍地图中寻找出口,打印路径、访问格数,并处理无解地图。
本课验收标准:每个递归函数都有可达出口和严格缩小步骤;能画出至少三层调用栈;回溯代码成对执行选择与撤销;迷宫不会因环路无限递归。
提交内容应包括源代码、三组测试输入与输出、一个曾经出现的错误及修复方法。最后请用自己的话解释本课最重要的概念;如果只能照着代码念,还需要再独立重写一次核心例子。