
| 🔭 个人主页: 散峰而望 |
|---|
《C语言:从基础到进阶》《编程工具的下载和使用》《C语言刷题》
《C++》《算法竞赛从入门到获奖》《人工智能》《AI Agent》
愿为出海月,不做归山云
🎬博主简介



【基础算法】从入门到实战:递归型枚举与回溯剪枝,暴力搜索的初级优化指南
- 前言
- [1. 搜索](#1. 搜索)
-
- [1.1 暴力搜索](#1.1 暴力搜索)
- [1.2 自适应搜索](#1.2 自适应搜索)
- [1.3 回溯和剪枝](#1.3 回溯和剪枝)
- [2. 递归型枚举与回溯剪枝初识](#2. 递归型枚举与回溯剪枝初识)
-
- [2.1 递归型枚举与回溯剪枝大致思路](#2.1 递归型枚举与回溯剪枝大致思路)
- [2.2 枚举子集(递归实现指数型枚举)](#2.2 枚举子集(递归实现指数型枚举))
- [2.3 组合型枚举](#2.3 组合型枚举)
- [2.4 枚举排列](#2.4 枚举排列)
- [2.5 全排列问题](#2.5 全排列问题)
- 结语
前言
在算法设计与问题求解中,搜索是解决复杂问题的核心方法之一。暴力搜索通过穷举所有可能的解来确保答案的正确性,但其效率往往难以应对大规模问题。递归型枚举与回溯剪枝技术通过系统性地探索解空间并提前终止无效路径,显著提升了搜索效率。
本指南从暴力搜索的基础出发,逐步引入自适应搜索策略,重点探讨回溯与剪枝的优化原理。通过递归实现子集枚举、组合生成、排列构造等经典问题,揭示如何将理论转化为实战代码。无论是算法竞赛选手还是编程爱好者,都能从中掌握高效搜索的初级优化技巧,为更复杂的算法学习奠定基础。
1. 搜索
搜索算法用于在数据结构(例如数组、链表、树或图)中搜索一个或一组满足特定条件的元素。
搜索算法可根据实现思路分为以下两类。
- 通过遍历数据结构来定位目标元素,例如数组、链表、树和图的遍历等。
- 利用数据组织结构或数据包含的先验信息,实现高效元素查找,例如二分查找、哈希查找和二叉搜索树查找等。
1.1 暴力搜索
暴力搜索通过遍历数据结构的每个元素来定位目标元素。
- "线性搜索"适用于数组和链表等线性数据结构。它从数据结构的一端开始,逐个访问元素,直到找到目标元素或到达另一端仍没有找到目标元素为止。
- 深度优先搜索 和宽度优先搜索是图和树的两种遍历策略。宽度优先搜索从初始节点开始逐层搜索,由近及远地访问各个节点。深度优先搜索从初始节点开始,沿着一条路径走到头,再回溯并尝试其他路径,直到遍历完整个数据结构。
暴力搜索的优点是简单且通用性好,无须对数据做预处理和借助额外的数据结构。
之前的【算法竞赛】树 我们曾讲过相关的深度优先搜索 和宽度优先搜索 ,这和深度优先搜索 和宽度优先搜索有什么区别吗?
- 遍历是形式,搜索是目的。
不过,在一般情况下,我们不会去纠结概念的差异,两者可以等同。
1.2 自适应搜索
自适应搜索利用数据的特有属性(例如有序性)来优化搜索过程,从而更高效地定位目标元素。
- 二分查找利用数据的有序性实现高效查找,仅适用于数组。
- 哈希查找利用哈希表将搜索数据和目标数据建立为键值对映射,从而实现查询操作。
- 树查找在特定的树结构(例如二叉搜索树)中,基于比较节点值来快速排除节点,从而定位目标元素。
然而,使用这些算法往往需要对数据进行预处理。例如,二分查找需要预先对数组进行排序,哈希查找和树查找都需要借助额外的数据结构,维护这些数据结构也需要额外的时间和空间开销。

这些在之前或多或少的都讲过一些,不过这里我们主要研究的是深度优先搜索 和宽度优先搜索。
由于深度优先搜索 和宽度优先搜索 本质上是「暴力搜索」,所以需要进行优化。又由之前的学习中我们知道深度优先搜索 和宽度优先搜索 可以转化成树 来讨论,因此可能还会有类似于找父节点 的操作。此时,我们就需要用到回溯 和剪枝。
1.3 回溯和剪枝
- 回溯: 算法在搜索过程中遇到某个状态无法继续前进或无法得到满足条件的解时,它会撤销上一步的选择,退回到之前的状态,并尝试其他可能的选择。值得说明的是,回退并不仅仅包括函数返回,在后续的讲解中可以感受到。

- 剪枝: 剪掉在搜索过程中,剪掉重复出现或者不是最优解的分支。

2. 递归型枚举与回溯剪枝初识
2.1 递归型枚举与回溯剪枝大致思路
- 画决策树;
- 根据决策树写递归。
2.2 枚举子集(递归实现指数型枚举)

算法原理:
设一共有 3 个数,分别是 [1, 2, 3] 。从前往后考虑每一个数,针对当前这个数选或者不选,我们可以画出如下「决策树」:

设计递归函数:
- 重复子问题:针对某一位,「选」或者「不选」这个数。因为最终结果要按照「字典序」输出,我们可以「先考虑不选」,然后「再考虑选」;
- 实现方式参考代码和注释,结合「决策树」一起看会很清晰。
参考代码:
cpp
#include <iostream>
using namespace std;
int n;
string path;//记录每一步决策
void dfs(int pos)//pos是进行到哪一个位置
{
if(pos > n)
{
cout << path << endl;
return;
}
//不选
path += 'N';
dfs(pos + 1);
path.pop_back();//回溯,清空现场
//选
path += 'Y';
dfs(pos + 1);
path.pop_back();
}
int main()
{
cin >> n;
dfs(1);
return 0;
}
2.3 组合型枚举

算法原理:
设 n = 4,m = 3,从前往后考虑 3 个位置应该选哪个数,我们可以画出如下的决策树:

设计递归函数:
- 重复子问题:当前这一位,应该放哪个数上去。因为这是一个「组合」问题,不涉及排列,所以我们当前位置开始放的数,应该是「上次决策的数的下一位」。
- 实现方式参考代码和注释,结合「决策树」一起看会很清晰。
参考代码:
cpp
//代码一
#include <iostream>
#include <vector>
using namespace std;
int n, m;
vector<int> path;
void dfs(int pos, int begin)//pos表示当前位置,begin表示该从哪个数开始
{
if(pos > m)
{
for(auto x : path) cout << x << " ";
cout << endl;
return;
}
for(int i = begin; i <= n; i++)
{
path.push_back(i);
dfs(pos + 1, i + 1);
path.pop_back();
}
}
int main()
{
cin >> n >> m;
dfs(1, 1);
return 0;
}
cpp
//代码二
#include <iostream>
#include <vector>
using namespace std;
int n, m;
vector<int> path;
//path.size();可以知道在什么位置
void dfs(int begin)//pos表示当前位置,begin表示该从哪个数开始
{
if(path.size() == m)
{
for(auto x : path) cout << x << " ";
cout << endl;
return;
}
for(int i = begin; i <= n; i++)
{
path.push_back(i);
dfs(i + 1);
path.pop_back();
}
}
int main()
{
cin >> n >> m;
dfs(1);
return 0;
}
2.4 枚举排列

算法原理:
设 n = 3,k = 2,一共要选出两个数,可以依次「考虑要选出来的数」是谁,画出如下决策树:

设计递归函数:
- 重复子问题:考虑这一位要放上什么数。因为是排列问题,所以我们直接从 1 开始枚举要放的数。
- 剪枝:在这一条路径中,我们「不能选择之前已经选择过的数」。
- 实现方式参考代码和注释,结合「决策树」一起看会很清晰。
算法原理:
cpp
#include <iostream>
#include <vector>
using namespace std;
const int N = 15;
int n, k;
vector<int> path;
bool st[N];
void dfs()
{
if(path.size() == k)
{
for(auto x : path) cout << x << " ";
cout << endl;
return;
}
for(int i = 1; i <= n; i++)
{
if(st[i]) continue;
path.push_back(i);
st[i] = true;
dfs();
path.pop_back();
st[i] = false;
}
}
int main()
{
cin >> n >> k;
dfs();
return 0;
}
2.5 全排列问题

算法原理:
跟上一道题的决策一样,我们可以枚举每一位应该放上什么数,只不过少了 k 的限制。剪枝的策略还是一样的,那就是在路径中,不能选择之前已经选过的数。

关于「全排列问题」,我们也可以调用 STL 里面的「next_permutation 函数」
参考代码:
cpp
#include <iostream>
#include <vector>
using namespace std;
const int N = 15;
int n;
bool st[N];
vector<int> path;
void dfs()
{
if(path.size() == n)
{
for(auto x : path)
{
printf("%5d", x);
}
cout << endl;
return;
}
for(int i = 1; i <= n; i++)
{
if(st[i]) continue;
path.push_back(i);
st[i] = true;
dfs();
path.pop_back();
st[i] = false;
}
}
int main()
{
cin >> n;
dfs();
return 0;
}
结语
递归型枚举与回溯剪枝是算法学习中不可或缺的核心内容,为暴力搜索提供了高效的优化路径。从基础的指数型枚举到复杂的全排列问题,掌握递归与剪枝技巧能显著提升问题求解能力。
通过系统学习枚举子集、组合型枚举和排列生成,可以逐步理解如何减少冗余计算并优化搜索空间。回溯剪枝的核心在于及时终止无效分支,将理论转化为实际代码能力。
无论是竞赛还是实际开发,这些方法都能帮助更高效地解决组合优化问题。持续练习并深入理解其思想,将为后续学习动态规划、贪心算法等高级主题奠定坚实基础。
愿诸君能一起共渡重重浪,终见缛彩遥分地,繁光远缀天。
