目录
1.DFS
1.1核心思想
1.2适用场景
1.3问题分类
1.3.1固定长度组合问题
1.3.2不固定长度组合问题
1.3.3两类问题的代码模板对比
1.3.4总结
1.3.5✌️延伸思考
2.例题
2.1全排列
2.1.1题目描述
2.1.2解题思路
2.1.3代码展示
2.2组合数
2.2.1题目描述
2.2.2解题思路
2.2.3代码展示
2.3指数型
2.3.1题目描述
2.3.2解题思路
2.3.3 对比学习(本题与全排列的不同之处)
2.3.4代码展示
2.4n-皇后问题
2.4.1解题思路
2.4.3代码展示
2.5飞机降落
2.5.1题目描述
2.5.2解题思路
2.5.3代码展示
2.6 safecracker
2.6.1题目描述
2.6.2解题思路
2.6.3代码展示
2.7选数问题
2.7.1题目描述
2.7.2解题思路
2.7.3代码展示
2.8最大团问题
2.8.1题目描述
2.8.2解题思路(与选数问题类似)
2.8.3代码展示
2.9最佳组队问题
2.9.1题目描述
2.9.2解题思路
2.9.3代码展示
DFS的核心思想可以用 “尝试所有可能,逐步构建解,不满足条件则回退” 来概括。它本质是一种有策略的穷举搜索,通过剪枝和状态回退机制高效地在解空间中寻找可行解。以下从三个维度深入解析其核心思想:
一、解空间树与决策路径
DFS将问题抽象为一棵解空间树,树的每个节点代表一个部分解(或决策状态):
- 根节点:初始状态(尚未做出任何选择)。
- 中间节点:已做出部分选择,尚未完成整个解。
- 叶节点:完整解或无效解。
核心操作:从根节点出发,通过递归向下扩展路径(做出选择),若发现当前路径不可能通向有效解,则回溯到父节点(撤销选择),尝试其他分支。
二、剪枝函数:避免无效搜索
关键在于剪枝策略,通过两类函数判断路径有效性:
- 约束函数:判断当前路径是否满足问题的约束条件(如组合问题中元素是否重复)。
- 限界函数:判断当前路径是否可能产生最优解(常用于优化问题,如旅行商问题)。
剪枝效果:若某节点被判定为无效,直接跳过其所有子树,大幅减少搜索空间。
示例:在全排列问题中,若当前路径已包含元素2
,则后续选择跳过2
,避免生成重复排列。三、状态回退:恢复现场的艺术
每次递归返回前,必须撤销当前选择,恢复到选择前的状态,确保后续分支不受影响。关键步骤:
- 选择:在当前节点做出一个选择,进入下一层递归。
- 递归:处理子问题。
- 撤销:递归返回后,撤销之前的选择,尝试其他可能性。
DFS用于解决需在复杂解空间中穷举所有可能组合、排列或路径,并通过约束条件剪枝筛选符合要求解的问题(如组合生成、棋盘布局、路径搜索等)。
DFS 可大致分为两类问题:固定长度组合问题&不固定长度组合问题。
⚠️这两类问题的递归树有本质的不同,解体思路也有差异。
- 目标:从 n 个元素中选出固定 k 个元素,生成所有不重复的组合。
- 递归树特征:
- 树的深度固定为 k(层数即已选元素数)。
- 每个节点的分支数逐渐减少(避免重复组合)。
- 关键参数:当前层数(控制递归深度)、起始下标(控制元素选择范围)。
- 剪枝条件:剩余元素不足时提前终止。
典型例题:全排列;飞机降落
递归树示例(从
[1,2,3]
选 2 个数):dfs(0, []) ├── 选1 → dfs(1, [1]) │ ├── 选2 → dfs(2, [1,2]) ✅ │ └── 选3 → dfs(2, [1,3]) ✅ └── 选2 → dfs(1, [2]) └── 选3 → dfs(2, [2,3]) ✅
- 目标:从 n 个元素中选出任意数量元素,满足特定条件(如和为 k、元素个数最少等)。
- 递归树特征:
- 树的深度不固定,直到满足条件或无法继续。
- 每个节点有选 / 不选两个分支(或根据题意调整)。
- 关键参数:当前元素下标(控制选哪个元素)、当前状态(如和、元素个数)。
- 剪枝条件:根据目标条件动态剪枝(如和超过 k 时终止)。
典型例题:选数问题;最大团问题
递归树示例(从
[1,2,3]
选和为 3 的组合):dfs(0, 0) ├── 选1 → dfs(1, 1) │ ├── 选2 → dfs(2, 3) → [1,2] ✅ │ └── 不选2 → dfs(2, 1) │ └── 选3 → dfs(3, 4) ❌ └── 不选1 → dfs(1, 0) ├── 选2 → dfs(2, 2) │ └── 选3 → dfs(3, 5) ❌ └── 不选2 → dfs(2, 0) └── 选3 → dfs(3, 3) → [3] ✅
1. 固定长度组合模板
vector> result;
vector path;
void dfs(int start, int depth, int k) {
if (depth == k) { // 层数达到k,收集结果
result.push_back(path);
return;
}
for (int i = start; i < n; i++) { // 从start开始选,避免重复
path.push_back(nums[i]);
dfs(i + 1, depth + 1, k); // 层数+1,继续递归
path.pop_back();
}
}
2. 不固定长度组合模板
vector> result;
vector path;
void dfs(int idx, int current_sum, int target) {
if (current_sum == target) { // 满足条件,收集结果
result.push_back(path);
return;
}
if (current_sum > target || idx == n) return; // 剪枝
// 选当前元素
path.push_back(nums[idx]);
dfs(idx + 1, current_sum + nums[idx], target);
path.pop_back();
// 不选当前元素
dfs(idx + 1, current_sum, target);
}
递归树结构不同:
参数设计逻辑不同:
剪枝策略不同:
遇到 DFS 问题时,可按以下步骤判断类型:
是否需要选满固定数量元素?
是 → 固定长度问题(层数参数);
否 → 不固定长度问题(下标参数)。是否有动态约束条件(如和为 k、元素个数最少)?
是 → 需在递归中维护状态并剪枝;
否 → 仅需控制组合不重复。是否允许元素重复使用?
是 → 递归时idx
不变(如组合总和问题);
否 → 递归时idx+1
(如子集问题)。
给定一个整数 nn,将数字 1∼n1∼n 排成一排,将会有很多种排列方法。
现在,请你按照字典序将所有的排列方法输出。
输入格式
共一行,包含一个整数 nn。
输出格式
按字典序输出所有排列方案,每个方案占一行。
数据范围
1≤n≤7 1≤n≤7
输入样例:
3
输出样例:
1 2 3 1 3 2 2 1 3 2 3 1 3 1 2 3 2 1
- 约束条件:在生成排列的过程中,每个元素只能使用一次。
- 剪枝实现:使用visited数组标记已选择的元素,若当前元素已被使用,则跳过该分支。
3.状态回退-->恢复现场
- 路径记录:移除最后选择的元素(通过
path.pop()
)。- 元素使用标记:将当前元素的使用状态重置为
False
。
#include
#include
#include
#define int long long
using namespace std;
int path[10]; //记录路径
bool visited[10];//记录每个数的状态,有利于剪枝
int n;
void dfs(int u) {
if (u == n) { //如果u==n代表每个位置上都有数字了,输出对应的路径
for (int i = 0; i < n; i++) {
cout << path[i] << ' ';
}
cout << endl;
return;
}
for (int i = 1; i <= n; i++) {
if (!visited[i]) {
path[u] = i;
visited[i] = true;
dfs(u + 1);
//恢复现场
visited[i] = false;
path[u] = 0; //写不写都可以,因为path[u]每一次都会被覆盖掉
}
}
}
signed main() {
cin >> n;
dfs(0);//从第0个位置开始搜
return 0;
}
从n个数中挑出m个数的组合,按字典序输出。其中相同的m个数算1个组合。
输入格式:
两个正整数n和m.(1≤m≤n≤10)
输出格式:
所有m个数的组合数,每行m个整数。
输入样例:
在这里给出一组输入。例如:
5 3
输出样例:
在这里给出相应的输出。例如:
1 2 3 1 2 4 1 2 5 1 3 4 1 3 5 1 4 5 2 3 4 2 3 5 2 4 5 3 4 5
规则:下一层起始点为
i+1
,避免重复组合
代码:for (int i = start; i <= n; i++)
3.状态回退:移除path路径的最后一个选择
#include
#include
#include
#include
为了方便理解递归调用过程,下边以n=4,m=2举例,画出其对应的递归树:
递归深度 0: dfs(0,1)
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
↓ ↓ ↓ ↓
递归深度 1: 选1→dfs(1,2) 选2→dfs(1,3) 选3→dfs(1,4) 选4→dfs(1,5)
┌───┼───┐ ┌───┼ ┌───┐
↓ ↓ ↓ ↓ ↓ ↓
递归深度 2: 选2 选3 选4 选3 选4 选4
[1,2][1,3][1,4] [2,3][2,4] [3,4]
从1∼n个数中选择任意多个,输出所有选择方案。
输入格式:
一个正整数n。
输出格式:
每行一种方案
没有选择任何数,输出空行
同一行数按字典顺序
输入样例:
在这里给出一组输入。例如:
3
在这里给出相应的输出。例如:
1 1 2 1 2 3 1 3 2 2 3 3
通过控制
start
参数避免生成重复子集:
- 约束条件:每个子集中的元素必须按升序排列(如
[1,2]
合法,[2,1]
非法)。- 剪枝实现:递归时传入
i+1
作为下一层的start
,确保后续选择的数字大于当前数字。
3.状态回退
对比项 | 子集生成(本题) | 全排列 |
---|---|---|
输出时机 | 每次递归进入时立即输出当前路径 | 仅当路径长度达到 n 时输出 |
解空间结构 | 子集树(每个节点代表一个子集) | 排列树(每个叶节点代表一个排列) |
路径约束 | 元素升序,不可重复 | 必须包含所有元素,顺序不同即不同解 |
剪枝策略 | 通过 start 参数控制后续选择范围 |
通过 visited 数组标记已使用元素 |
输出结果为什么会有这种差异?
- 子集问题:需要生成所有可能的子集(包括空集),因此每个中间状态都是有效的解,需要立即输出。
- 全排列问题:需要生成所有元素的排列,因此只有当路径包含所有元素时才是有效的解,需要达到固定深度后输出。
#include
#include
#include
using namespace std;
int n;
vector path;
void dfs(int start) {
// 输出当前子集
for (int num : path) cout << num << ' ';
cout << endl;
// 从start开始尝试添加数字
for (int i = start; i <= n; i++) {
path.push_back(i);
dfs(i + 1); // 递归处理剩余数字,避免重复
path.pop_back(); // 回溯
}
}
int main() {
cin >> n;
dfs(1); // 从数字1开始
return 0;
}
用固定长度DFS逐行放置皇后,每行选一列。通过标记列、主对角线(i+u)、副对角线(i-u+n)避免冲突。递归n层(每行一层),全放置成功时输出布局,利用回溯撤销状态。
#include
#include
#include
#include
N(N<10)架飞机准备降落到某只有一条跑道的机场。
第 i 架飞机在Ti时刻到达机场上空,到达时它的剩余油料还可继续盘旋Di个单位时间,降落过程需要Li单位时间。
一架飞机降落完毕时,另一架飞机可以立即在同一时刻开始降落,但是不能在前一架飞机完成降落前开始降落。
请你判断 N架飞机是否可以全部安全降落,可以降落则输出降落顺序。
如果有多个可以安全降落的顺序,按字典顺序输出,每行一个。
输入格式:
第1行为1个正整数N
接下来N行,每行3个整数,分别是到达时刻Ti,盘旋时间Di,降落过程的时间Li
输出格式:
安全的降落顺序,每行1个,按字典顺序。如果没有则输出NO
输入样例:
在这里给出一组输入。例如:
3 0 100 10 10 10 10 0 2 20
输出样例:
在这里给出相应的输出。例如:
3 2 1
核心思路:
当前的这架飞机可以加入path的条件-->其前一架飞机的降落时间在当前这架飞机的盘旋时间内。
#include
#include
#include
using namespace std;
struct plane {
int t;
int d;
int l;
};
vector p(1000);
vector path(1000);
bool visited[1000] = { false };
int n, last;
bool hasSolution = false;
void dfs(int u) {
if (u == n + 1) { // 所有飞机都已安排降落
hasSolution = true;
for (int i = 1; i <= n; i++) {
cout << path[i] << ' ';
}
cout << endl;
return;
}
for (int i = 1; i <= n; i++) {
if (!visited[i]) {
int earliestStart = max(last, p[i].t); // 最早可能的降落开始时间
if (earliestStart <= p[i].t + p[i].d) { // 检查是否在盘旋时间内
visited[i] = true;
path[u] = i;
int prevLast = last;
last = earliestStart + p[i].l; // 更新last为当前飞机降落结束时间
dfs(u + 1);
last = prevLast; // 回溯
visited[i] = false;
}
}
}
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> p[i].t >> p[i].d >> p[i].l;
}
last = 0; // 初始化last为0
dfs(1);
if (!hasSolution) {
cout << "NO" << endl;
}
return 0;
}
给你一个target数和一串字符串,这串字符都是A~Z,并且规定A~Z分别代表1~26。要求从这一串字符中找出5个字符(而且要是按字典序排序最大的),使公式v−w2+x3−y4+z5=target成立,如果不存在则输出no solution.
输入格式:
输入一行,一个正整数target,一个由A~Z组成的字符串
输出格式:
字符串中的五个字母,能使公式成立,且字典序最大。
输入样例:
在这里给出一组输入。例如:
11700519 ZAYEXIWOVU
输出样例:
在这里给出相应的输出。例如:
YOXUZ
输入处理与预处理
- 读取目标值
target
和候选字符串str
- 对字符串降序排序(如 "ABC" → "CBA"),确保优先尝试字典序大的字母组合
状态初始化
- 创建长度为 5 的路径数组
path
存储当前组合- 使用布尔数组
used
标记每个字母是否已被选择深度优先搜索(DFS)
- 递归函数
dfs(u)
:处理路径的第u
个位置- 终止条件:当
u == 5
时,检查当前组合是否满足表达式- 遍历候选:按降序遍历每个字母,选择未使用的字母并递归
- 恢复现场
剪枝优化
- 找到第一个有效解后立即终止搜索(利用字典序最大特性)
- 在计算表达式时使用整数幂替代浮点数函数,避免精度误差
#include
#include
#include
#include
using namespace std;
string str;
int target;
bool used[26]; // 标记字母是否已被使用
string path; // 存储当前路径
bool found = false;
bool check() {
int res = 0;
for (int i = 0; i < 5; i++)
res += (i % 2 ? -1 : 1) * pow(path[i] - 'A' + 1, i + 1);
return res == target;
}
void dfs(int u) {
if (u == 5) {
if (check() && !found) {
cout << path << endl;
found = true;
}
return;
}
for (char c : str) { // 遍历降序排列后的字符
if (!used[c - 'A']) {
used[c - 'A'] = true;
path[u] = c;
dfs(u + 1);
if (found) return; // 提前终止
used[c - 'A'] = false;
}
}
}
int main() {
cin >> target >> str;
sort(str.rbegin(), str.rend()); // 直接降序排序
path.resize(5); // 预分配路径长度
dfs(0);
if (!found) cout << "no solution";
return 0;
}
string的sort函数补充:
- 正向排序:
sort(str.begin(), str.end())
会将字符串按升序排列(如 "CBA" → "ABC")。- 反向排序:
sort(str.rbegin(), str.rend())
会将字符串按降序排列(如 "ABC" → "CBA")。
给定若干个正整数a0、a0 、…、an-1 ,从中选出若干数,使它们的和恰好为k,
要求找选择元素个数最少的解。如果有多个最优解,输出字典序最小的。
输入格式:
输入有两行,第一行给出2个正整数n,k,用空格分隔。第二行是用空格分隔的n个整数。
输出格式:
输出有两行,第一行从小到大输出选择的元素,第二行输出元素的个数。
输入样例:
在这里给出一组输入。例如:
5 9 1 1 4 5 7
输出样例:
在这里给出相应的输出。例如:
4 5 2
核心思路(非固定长度的变形)
- DFS 遍历:递归尝试每个元素的选 / 不选,生成所有可能组合。
- 剪枝优化:若当前和超过 k 或路径长度≥已知最优解,提前终止。
- 字典序控制:数组升序排序,递归时优先选小元素。
#include
#include
#include
#include
#include
给定图G(V, E),团是一个子图g(v, e),以至于对于v中的所有顶点对v1、v2,在e中都存在一条边(v1, v2)。最大团是具有最多顶点数的团。
输入格式:
输入包含多组测试。对于每组测试:第一行有一个整数n,表示顶点数。(1 < n <= 50)接下来的n行,每行有n个0或1,表示顶点i(行号)和顶点j(列号)之间是否存在边。当n = 0时,表示输入结束。此组测试不应处理。
输出格式:
每组测试输出一个数字,即最大团中的顶点数。
输入样例:
5 0 1 1 0 1 1 0 1 1 1 1 1 0 1 1 0 1 1 0 1 1 1 1 1 0 0
输出样例:
4
核心思路:非固定长度的DFS
- 首先这道题目是不固定长度的,所以递归每个点。
- 判断当前点是否可以成团的条件是看当前点和path中的点是否都有边连接,如果有连接则可以成团,递归有这个点。
- 每次递归进去后要先更新最大成团的数量和最大成团点的集合。
#include
#include
using namespace std;
int n; // 节点数
int grid[50][50]; // 邻接矩阵
int max_size = 0; // 最大团的大小
vector best_path; // 最大团的节点集合
// 检查当前节点是否可以加入团
bool check(int node, const vector& path) {
for (int num : path) {
if (grid[node][num] == 0) return false;
}
return true;
}
// DFS函数:使用"选与不选"模板
void dfs(int idx, vector& path) {
// 处理完所有节点,更新最大团
if (idx == n) {
if (path.size() > max_size) {
max_size = path.size();
best_path = path;
}
return;
}
// 不选当前节点,直接处理下一个节点
dfs(idx + 1, path);
// 选当前节点(需先检查是否满足条件)
if (check(idx, path)) {
path.push_back(idx);
dfs(idx + 1, path); // 递归处理下一个节点
path.pop_back(); // 回溯
}
}
signed main() {
while (cin >> n && n) {
// 初始化
max_size = 0;
best_path.clear();
// 输入邻接矩阵
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
cin >> grid[i][j];
}
}
// 开始DFS
vector path;
dfs(0, path);
// 输出结果
cout << max_size << endl;
}
return 0;
}
双人混合ACM程序设计竞赛即将开始,因为是双人混合赛,故每支队伍必须由1男1女组成。现在需要对n名男队员和n名女队员进行配对。由于不同队员之间的配合优势不一样,因此,如何组队成了大问题。
给定n×n优势矩阵P,其中P[i][j]表示男队员i和女队员j进行组队的竞赛优势(0
输入格式:
测试数据有多组,处理到文件尾。每组测试数据首先输入1个正整数n(1≤n≤9),接下来输入n行,每行n个数,分别代表优势矩阵P的各个元素。
输出格式:
对于每组测试,在一行上输出n支队伍的竞赛优势总和的最大值。
输入样例:
3 10 2 3 2 3 4 3 4 5
输出样例:
18
核心思路:本题按照固定长度的DFS解决
- 判断本题是固定长度的DFS,按照定长的模板每次递归每一层。
- 当层数为n时,更新最大竞争优势总和ma。
- 循环遍历每一个男生,如果没有被访问则可以与当前的女生u组队,继续递归。
#include
#include
#include
#include
#define int long long
using namespace std;
int n, ma = -1e9;
int grid[10][10];
bool visited[10];
void dfs(int u ,int cur) {
if (u == n) {
ma = max(ma, cur);
return;
}
for (int i = 0; i < n; i++) { //循环男生
if (!visited[i]) {
visited[i] = true;
dfs(u + 1, cur + grid[i][u]);
visited[i] = false;
}
}
}
signed main()
{
while (cin >> n) {
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
cin >> grid[i][j]; //i表示男生,j表示女生
}
}
ma = -1e18;
dfs(0, 0);
cout << ma << endl;
}
return 0;
}