回溯算法
回溯算法 (Backtracking)
像“走迷宫”一样,一条路一条路地试。如果发现这条路走不通(不满足约束条件),就退回上一个岔路口(回溯),换一条路继续走,直到找到所有解或一条可行解。
经典问题推演:N皇后问题
选择列表:当前行的每一列。
做选择:在 (row, col) 放置一个皇后。
约束判断:检查该位置是否与已放置的皇后同行、同列、同对角线。如果冲突,则跳过。
递归下一层:去 row + 1 行继续放皇后。
撤销选择:如果 row + 1 行怎么放都冲突,退回 row 行,把刚才放的皇后拿走,换下一列尝试。
核心特征:基于深度优先搜索(DFS),核心动作是“做选择 -> 递归 -> 撤销选择”。
适用场景:组合问题、排列问题、N皇后、迷宫等需要穷举所有可能解的场景。
括号生成问题 (Generate Parentheses)
问题描述:数字 n 代表生成括号的对数,请你设计一个函数,用于生成所有可能的并且有效的括号组合。
示例:输入 n = 3,输出 ["((()))", "(()())", "(())()", "()(())", "()()()"]。
为什么用回溯法?
这是一个典型的“多路径探索”问题。我们需要在每一步决定放左括号 ( 还是右括号 )。如果走错了路(比如右括号比左括号多了),就需要“反悔”(撤销选择),退回到上一步尝试另一种可能。回溯法完美契合这种“尝试 -> 失败 -> 回退”的闭环机制。
核心思想:有效括号的两大约束
数量约束:左括号和右括号的数量必须相等(都等于 n)。
合法性约束:在生成过程中的任意一个前缀子串中,左括号的数量必须 大于等于 右括号的数量(否则必然出现类似 )( 的非法情况)。
核心代码
#include <stdio.h>
#include <string.h>
// 结果集(简化演示,假设最多存100个结果,每个字符串最大长度20)
char result[100][20];
int resIndex = 0;
// 回溯核心函数
void backtrack(int left, int right, int n, char *path, int pathLen) {
// 1. 终止条件:右括号数量等于 n,说明找到了一组合法解
if (right == n) {
path[pathLen] = '\0'; // 字符串结尾
strcpy(result[resIndex++], path); // 收集结果
return;
}
// 2. 选择一:如果左括号数量小于 n,可以继续添加左括号
if (left < n) {
path[pathLen] = '('; // 做选择
backtrack(left + 1, right, n, path, pathLen + 1); // 递归深入
// 注意:这里不需要显式撤销,因为 pathLen 是值传递,下一层递归返回后自动恢复
}
// 3. 选择二:如果右括号数量小于左括号数量,可以添加右括号
if (right < left) {
path[pathLen] = ')'; // 做选择
backtrack(left, right + 1, n, path, pathLen + 1); // 递归深入
}
}
int main() {
int n = 3;
char path[20];
backtrack(0, 0, n, path, 0);
printf("生成的有效括号组合:\n");
for (int i = 0; i < resIndex; i++) {
printf("%s\n", result[i]);
}
return 0;
}
示例推演
以 n = 2 为例,我们来看看这棵“决策树”是如何展开的:
初始状态
目标:开始生成括号。
当前状态:left = 0, right = 0,路径 path = ""。
第 1 层探索
目标:决定第一个字符。
检查约束:left(0) < n(2),满足。
做选择:放入 (。当前路径变为 "(",left = 1。
递归进入下一层。
第 2 层探索
目标:决定第二个字符。
检查约束:left(1) < n(2),满足。放入 (。当前路径变为 "((",left = 2。
递归进入下一层。
第 3 层探索
目标:决定第三个字符。
检查约束:left(2) < n(2) 不满足,不能再放左括号了。
检查约束:right(0) < left(2),满足。放入 )。当前路径变为 "(()",right = 1。
递归进入下一层。
第 4 层探索(到达叶子节点)
目标:决定第四个字符。
检查约束:right(1) < left(2),满足。放入 )。当前路径变为 "(())",right = 2。
触发终止条件:right == n(2)。
收集结果:将 "(())" 加入结果集。
回溯:返回上一层,尝试其他可能(但本层已无其他选择,继续回退)。
回溯与横向遍历
退回到第 2 层(此时路径为 "(",left = 1, right = 0)。
刚才走了放 ( 的分支,现在尝试放 ) 的分支。
检查约束:right(0) < left(1),满足。放入 )。当前路径变为 "()",right = 1。
继续递归... 最终会生成 "()()"。
四步法:
定义递归函数:明确参数(如当前左右括号数量、当前路径)。
设定终止条件:明确何时收集结果(如 right == n)。
遍历选择列表:在循环或 if 中列出所有可能的选择(放左或放右)。
做选择与撤销选择(回溯):这是回溯的灵魂!如果是传引用(如C++的string),递归后必须 pop_back();如果是传值(如C语言的数组长度),则天然自带回溯效果。
剪枝优化(核心提效手段):
回溯本质是暴力穷举,效率不高。这个问题中 if (right < left) 就是极其关键的可行性剪枝,它提前砍掉了所有右括号多于左括号的无效分支。

浙公网安备 33010602011771号