一、核心概念总览
1. 递归(DFS)本质
函数自身调用自身,“一路向前” 处理下一个元素 / 位置,直到触发终止条件后 “回溯返回”,核心作用是枚举所有可能情况(无需手动写多层循环)。
2. 回溯核心操作
“修改状态 → 递归推进 → 恢复状态”,避免前一种选择影响后一种(类似 “试衣服→脱下来→试下一件”)。
3. 三大问题核心区别(基础认知)
| 问题类型 | 顺序要求 | 选择规则 | 核心特征(n=3 为例) |
|---|---|---|---|
| 子集(选与不选) | 无顺序 | 每个元素可选 / 可不选 | 输出所有子集(含空集),共 2ⁿ 种 |
| 排列 | 有顺序 | 选 k 个元素,每个元素仅用一次 | 输出所有顺序组合,共 n!/(n-k)! 种 |
| 组合(固定个数) | 无顺序 | 选 k 个元素,每个元素仅用一次 | 输出无顺序组合,共 C (n,k) 种(C 为组合数) |
二、三大 DFS 详细拆解(以 n=3 为例)
(一)DFS1:子集问题(选与不选)
1. 功能定位
从 1~n 中选择任意个数元素(0~n 个),不考虑顺序,输出所有子集(可优化去空集)。
2. 核心代码
#include <iostream>
using namespace std;
const int N = 20;
int n;
int st[N]; // 0=未考虑,1=选中,2=不选
// u:当前处理第u个元素(按元素顺序推进)
void dfs(int u) {
// 终止条件:所有元素处理完毕(u > n)
if (u > n) {
bool flag = false;
for (int i = 1; i <= n; i++) {
if (st[i] == 1) {
cout << i << " ";
flag = true;
}
}
if (flag) cout << endl;
return;
}
// 选择1:不选第u个元素
st[u] = 2;
dfs(u + 1);
st[u] = 0; // 回溯:恢复状态
// 选择2:选中第u个元素
st[u] = 1;
dfs(u + 1);
st[u] = 0; // 回溯:恢复状态
}
int main() {
cin >> n;
dfs(1); // 从第1个元素开始处理
return 0;
}
3. 关键细节
| 组件 | 作用说明 |
|---|---|
变量 st[N] | 标记单个元素的状态(1 = 选,2 = 不选,0 = 未考虑),直接对应 “元素是否入选子集”。 |
递归参数 u | 按 “元素顺序” 推进,每个元素仅处理一次(处理完 u,直接推进到 u+1)。 |
| 选择逻辑 | 每个元素仅有 2 种固定选择(选 / 不选),无额外分支。 |
| 回溯操作 | 试完一种选择后,将 st[u] 恢复为 0,避免影响后续元素的选择判断。 |
| 终止条件 | 所有元素处理完毕(u > n),遍历 st 数组,输出选中的元素。 |
4. 运行结果(n=3)
plaintext
//还有个空
1
2
3
1 2
1 3
2 3
1 2 3
(二)DFS2:排列问题(选 k 个,有顺序)
1. 功能定位
从 1~n 中选择固定 k 个元素(k=1~n),考虑顺序,输出所有不重复排列(每个元素仅用一次)。
2. 核心代码
#include <iostream>
using namespace std;
const int N = 20;
int n;
int a[N];
bool vis[N];
void dfs(int v, int tar) {
// 终止条件:排列的tar个位置全部填充完毕(v = tar+1)
if (v == tar + 1) {
for (int i = 1; i <= tar; i++) {
if (i > 1) cout << " "; // 去掉末尾空格
cout << a[i];
}
cout << endl;
return;
}
// 选择逻辑:遍历所有未被选的元素,填入当前位置v
for (int i = 1; i <= n; i++) {
if (!vis[i]) {
vis[i] = true; // 标记为已选
a[v] = i; // 填入第v个位置
dfs(v + 1, tar); // 推进到下一个位置
vis[i] = false; // 回溯:取消标记
a[v] = 0; // 回溯:清空位置(可选,更严谨)
}
}
}
int main() {
cin >> n;
// 依次输出选1个、2个、...、n个的所有排列
for (int k = 1; k <= n; k++) {
dfs(1, k);
}
return 0;
}
3. 关键细节
| 组件 | 作用说明 |
|---|---|
变量 a[N] | 按 “位置” 存储排列结果(如 a [1]=1、a [2]=2 表示排列 “1 2”)。 |
变量 vis[N] | 标记元素是否被选,避免同一个元素重复填入多个位置(如 1 不能同时在两个位置)。 |
| 递归参数 | v:按 “位置顺序” 推进(填完 v,推进到 v+1);tar:控制排列长度(选 k 个)。 |
| 选择逻辑 | 每个位置遍历所有未被选的元素(多个选择),如第 1 个位置可选 1/2/3。 |
| 回溯操作 | 试完一个元素后,vis[i] 恢复为 false,a[v] 清空,方便下一个元素填入。 |
| 终止条件 | 排列的 tar 个位置填完(v=tar+1),直接输出 a 数组中的排列。 |
4. 运行结果(n=3)
plaintext
1
2
3
1 2
1 3
2 1
2 3
3 1
3 2
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
三)DFS3:组合问题(选 k 个,无顺序)
1. 功能定位
从 1~n 中选择固定 k 个元素(k=1~n),不考虑顺序,输出所有不重复组合(每个元素仅用一次)。
2. 核心代码
#include <iostream>
using namespace std;
const int N = 20;
int n;
int a[N];
void dfs(int v, int start, int tar) {
// 终止条件:组合的tar个位置全部填充完毕(v = tar+1)
if (v == tar + 1) {
for (int i = 1; i <= tar; i++) {
if (i > 1) cout << " ";
cout << a[i];
}
cout << endl;
return;
}
// 选择逻辑:从start开始选数,不回头(避免重复组合)
for (int i = start; i <= n; i++) {
a[v] = i; // 填入当前位置v
dfs(v + 1, i + 1, tar); // 下一个位置从i+1开始(不回头)
a[v] = 0; // 回溯:清空位置(可选,更严谨)
}
}
int main() {
cin >> n;
for (int k = 1; k <= n; k++) {
dfs(1, 1, k);
}
return 0;
}
3. 关键细节
| 组件 | 作用说明 |
|---|---|
变量 a[N] | 按 “位置” 存储组合结果(如 a [1]=1、a [2]=3 表示组合 “1 3”)。 |
| 递归参数 | v:按 “位置顺序” 推进;start:选数起始索引(核心去重手段);tar:组合长度。 |
| 选择逻辑 | 每个位置从start开始选数(不回头),如第 1 个位置选 1 后,第 2 个位置只能从 2/3 选。 |
| 回溯操作 | 仅需清空a[v](可选),无需额外标记(start已保证不重复选同一元素)。 |
| 终止条件 | 组合的 tar 个位置填完(v=tar+1),输出 a 数组中的组合。 |
| 核心去重 | start参数:下一个位置的选数起始索引 = 当前选数索引 + 1,避免 “1 2” 和 “2 1” 重复。 |
4. 运行结果(n=3)
plaintext
1
2
3
1 2
1 3
2 3
1 2 3
三、三大 DFS 核心差异总表(重点对比)
| 对比维度 | DFS1(子集:选与不选) | DFS2(排列:选 k 个) | DFS3(组合:选 k 个) |
|---|---|---|---|
| 核心目标 | 枚举所有无顺序子集(选任意个) | 枚举所有有顺序排列(选 k 个) | 枚举所有无顺序组合(选 k 个) |
| 推进方式 | 按 “元素” 推进(u:第 u 个元素) | 按 “位置” 推进(v:第 v 个位置) | 按 “位置” 推进(v:第 v 个位置) |
| 选择逻辑 | 每个元素 2 种选择(选 / 不选) | 每个位置遍历所有未被选元素(多选择) | 每个位置从start开始选数(不回头,多选择) |
| 关键变量 / 参数 | st[N](标记元素状态) | a[N](存排列)+ vis[N](标记已选) | a[N](存组合)+ start(起始索引) |
| 去重 / 防重复手段 | 元素仅处理一次,天然无重复子集 | vis[N]标记元素,避免重复使用 | start参数限制选数范围,避免重复组合 |
| 终止条件判断 | 所有元素处理完(u > n) | 所有位置填完(v == tar+1) | 所有位置填完(v == tar+1) |
| 输出结果特征 | 无顺序、元素不重复、可含空集(已优化去空) | 有顺序、元素不重复、长度固定(tar 个) | 无顺序、元素不重复、长度固定(tar 个) |
| 时间复杂度 | O (2ⁿ)(每个元素 2 种选择) | O (n! / (n-k)!)(排列数) | O (C (n,k))(组合数) |
| 核心关键词 | 子集、选或不选、任意个数 | 排列、有顺序、固定个数、vis 标记 | 组合、无顺序、固定个数、start 参数 |
四、适用场景速查(举一反三关键)
1. 选 DFS1(子集)的场景
- 需求是 “枚举所有可能的选择”(选多选少都行);
- 不考虑顺序,只关心 “元素是否被选中”。
- 举例:1~n 的所有子集、数组中所有和为 target 的子集、子集去重(含重复元素的数组)。
2. 选 DFS2(排列)的场景
- 需求是 “选 k 个元素,考虑顺序”;
- 同一元素组合,不同顺序算不同结果。
- 举例:1~n 的所有排列、字符串的所有字符排列(无重复字符)、从数组中选 k 个元素的所有排列。
3. 选 DFS3(组合)的场景
- 需求是 “选 k 个元素,不考虑顺序”;
- 同一元素组合,不同顺序算同一种结果。
- 举例:从 n 个元素中选 k 个的所有组合、组合总和(选 k 个元素和为 target)、组合去重(含重复元素的数组)。
五、常见修改技巧(小白必会)
(一)DFS1(子集)修改
- 保留空集:删除
flag判断,直接输出(哪怕无选中元素); - 只保留选 k 个元素的子集:加
cnt参数记录选中个数,终止时判断cnt==k:void dfs(int u, int cnt) { if (u > n) { if (cnt == 2) { // 只输出选2个的子集 for (int i=1; i<=n; i++) if (st[i]==1) cout << i << " "; cout << endl; } return; } st[u]=2; dfs(u+1, cnt); st[u]=0; // 不选,cnt不变 st[u]=1; dfs(u+1, cnt+1); st[u]=0; // 选,cnt+1 } // 调用:dfs(1, 0);
(二)DFS2(排列)修改
- 只输出选 k 个的排列:主函数去掉循环,直接输入 k 并调用
dfs(1, k); - 含重复元素的排列去重:先排序数组,再在循环中跳过重复元素:
// 假设数组为nums[],已排序 for (int i=1; i<=n; i++) { if (i>1 && nums[i]==nums[i-1] && !vis[i-1]) continue; // 去重 if (!vis[i]) { ... } }
(三)DFS3(组合)修改
- 只输出选 k 个的组合:主函数去掉循环,直接输入 k 并调用
dfs(1, 1, k); - 组合元素需满足特定条件(如和为 target):加
sum参数记录当前和,终止时判断:void dfs(int v, int start, int tar, int sum) { if (v == tar+1) { if (sum == 5) { // 只输出和为5的组合 for (int i=1; i<=tar; i++) cout << a[i] << " "; cout << endl; } return; } for (int i=start; i<=n; i++) { a[v] = i; dfs(v+1, i+1, tar, sum+i); // 累加当前元素值 } } // 调用:dfs(1, 1, 2, 0); // 选2个,和为5
六、易错点提醒
- 回溯操作不能漏:如 DFS1 的
st[u]=0、DFS2 的vis[i]=false,漏了会导致状态混乱; - 循环边界要精准:如子集 / 组合遍历从
i=1开始(避免a[0]无用数据); - 参数传递要正确:DFS3 的
start需传递i+1(而非start+1),否则无法去重; - 全局变量初始化:
st[N]、vis[N]作为全局变量默认值为 0/false,局部变量需用memset初始化; - 输出格式优化:避免末尾空格,用 “第一个元素不加空格,后续元素前加空格” 的逻辑。
七.给小白看的!
如果是第一次看dfs,这三种类型可能会把你搞晕,所以,加了点干的。
| DFS 类型 | 对应生活任务(一句话记住) | 核心动作 |
|---|---|---|
| DFS1(子集) | 收拾衣柜,每件衣服 “要么放进箱子,要么不放”(选任意件) | 逐个问 “要 / 不要”,不排序 |
| DFS2(排列) | 选 3 个同学排值日表(1st、2nd、3rd,顺序不同算不同) | 先选谁站 1st,再选谁站 2nd,要排序 |
| DFS3(组合) | 选 3 个同学组队参加比赛(没顺序,123 和 321 是同一队) | 从剩下的人里挑,不回头、不排序 |
举个 n=3(A、B、C)的例子,任务结果一眼分清:
1. DFS1(子集:要 / 不要)
- DFS1(子集):空、A、B、C、AB、AC、BC、ABC(8 种,选多选少都行);
2. DFS2(排列:选 k 个 + 要顺序)
- DFS2(排列,选 2 个):AB、BA、AC、CA、BC、CB(6 种,顺序算钱);
3. DFS3(组合:选 k 个 + 不要顺序)
- DFS3(组合,选 2 个):AB、AC、BC(3 种,顺序不算数)。
最后用 “最小代码片段” 对比,只看差异点
不用看全代码,只对比核心部分,差异一眼看穿:
| 部分 | DFS1(子集) | DFS2(排列) | DFS3(组合) |
|---|---|---|---|
| 函数参数 | dfs (u)(u = 当前元素) | dfs (v, tar)(v = 当前位置) | dfs (v, start, tar)(start = 选数起点) |
| 核心循环 | 无循环(只 2 个选择) | for (i=1;i<=n;i++)(遍历所有未选) | for (i=start;i<=n;i++)(从 start 开始) |
| 去重 / 防重复 | 天然无重复(元素只问一次) | vis [i] 标记(避免重复选同一人) | start 递增(避免回头选,去重组合) |

被折叠的 条评论
为什么被折叠?



