在算法世界中,有一类算法看似 “简单粗暴”,却能在很多场景下快速解决问题 —— 这就是枚举算法。它不依赖复杂的数学推导或数据结构优化,而是通过 “穷尽所有可能情况,筛选出符合条件的解” 的思路,直接找到问题的答案。 很多初学者会认为枚举是 “低效”“暴力” 的代名词,甚至不屑于使用。但实际上,枚举算法是解决问题的 “第一思路”:当问题规模较小时,枚举能以最简单的逻辑快速得到结果;当问题复杂时,枚举也能作为基础框架,通过优化逐步提升效率。在算法竞赛中,枚举更是 “签到题” 和 “中等题” 的常用解法,掌握枚举的思路和优化技巧,能帮你快速拿下基础分。 本文将从枚举算法的本质出发,详细讲解枚举的核心思想、常见类型(普通枚举、二进制枚举)、解题步骤、优化技巧及实战案例。全文采用 C++ 实现,兼容 ACM 竞赛提交格式,同时包含大量实例、易错点分析和练习建议,无论你是编程新手还是竞赛选手,都能彻底掌握枚举算法的应用。下面就让我们正式开始吧!
枚举算法(Enumeration Algorithm),又称 “穷举算法”,是指将问题的所有可能解逐一列举出来,检查每个解是否符合问题的条件,最终筛选出所有有效解的算法。它的核心逻辑可以概括为:“遍历所有可能,验证条件是否成立”。
例如:
这些问题的共同特点是:可能解的范围明确,且验证条件简单。只要能明确 “枚举什么” 和 “如何验证”,就能用枚举算法解决。
要写出高效、正确的枚举代码,必须把握以下 3 个核心要素,缺一不可:
枚举算法并非 “万能算法”,它有明确的适用场景,主要包括:
无论面对何种枚举问题,都可以按照以下 4 个步骤编写代码,确保逻辑清晰、无遗漏:
普通枚举是枚举算法的基础形式,指按顺序逐一枚举可能解,验证条件后筛选出有效解。它的核心是 “顺序遍历 + 条件判断”,适用于可能解范围连续、有序的场景。
题目描述:
为了准备颁奖典礼,组织者在矩形区域(第一象限)铺设 n 张地毯,编号从 1 到 n,按编号从小到大顺序铺设(后铺的地毯覆盖前铺的)。每张地毯的信息包括左下角坐标 (a, b) 和 x 轴、y 轴方向的长度 (g, k)。要求找到覆盖目标点 (x, y) 的最上面的地毯编号,若未被覆盖则输出 - 1。
输入:
输出:
覆盖目标点的最上面地毯编号,或 - 1。
示例输入:
3
1 0 2 3 # 地毯1:左下角(1,0),x长2(右到3),y长3(上到3)
0 2 3 3 # 地毯2:左下角(0,2),x长3(右到3),y长3(上到5)
2 1 3 3 # 地毯3:左下角(2,1),x长3(右到5),y长3(上到4)
2 2 # 目标点(2,2)示例输出:
3核心难点:
题目链接:https://www.luogu.com.cn/problem/P1003
#include <iostream>
using namespace std;
const int N = 1e4 + 10; // 地毯数量最大为1e4
int a[N], b[N], g[N], k[N]; // 存储每张地毯的信息:a[i]左下角x,b[i]左下角y,g[i]x长,k[i]y长
// 查找覆盖(x,y)的最上面地毯编号
int findCarpet(int n, int x, int y) {
// 逆序枚举:从最后一张地毯开始,找到第一个覆盖的就是最上面的
for (int i = n; i >= 1; --i) {
// 验证条件:x在[a_i, a_i+g_i],y在[b_i, b_i+k_i]
if (a[i] <= x && x <= a[i] + g[i] && b[i] <= y && y <= b[i] + k[i]) {
return i; // 找到,立即返回
}
}
return -1; // 未找到
}
int main() {
int n;
cin >> n;
for (int i = 1; i <= n; ++i) {
cin >> a[i] >> b[i] >> g[i] >> k[i];
}
int x, y;
cin >> x >> y;
cout << findCarpet(n, x, y) << endl;
return 0;
}题目描述:
用 8 位数字表示日期(YYYYMMDD),其中前 4 位是年份,中间 2 位是月份,最后 2 位是日期。一个日期是回文的,当且仅当 8 位数字从左到右读和从右到左读完全相同(如 20100102,即 2010 年 1 月 2 日)。给定两个 8 位日期 date1 和 date2,求两者之间(包含边界)的回文日期数量。
输入:
输出:
回文日期的数量。
示例输入 1:
20110101
20111231示例输出 1:
1 # 只有20111102(2011年11月2日)是回文日期示例输入 2:
20000101
20101231示例输出 2:
2 # 20011002(2001年10月2日)和20100102(2010年1月2日)核心难点:
题目链接:https://www.luogu.com.cn/problem/P2010
// 反转数字:如num=2010 → 返回102(注意:2010反转后是0102,即102,后续需补前导零到4位)
int reverseNum(int num) {
int res = 0;
while (num > 0) {
res = res * 10 + num % 10;
num /= 10;
}
return res;
}// 判断是否为闰年:能被4整除且不能被100整除,或能被400整除
bool isLeapYear(int year) {
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}// 验证日期是否有效:year-年份,mm-月份,dd-日期
bool isValidDate(int year, int mm, int dd) {
// 月份不合法
if (mm < 1 || mm > 12) return false;
// 每月最大天数
int maxDay[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
// 闰年2月多1天
if (mm == 2 && isLeapYear(year)) maxDay[2] = 29;
// 日期不合法
return dd >= 1 && dd <= maxDay[mm];
}#include <iostream>
using namespace std;
// 反转数字:如2010 → 102(后续补前导零到4位)
int reverseNum(int num) {
int res = 0;
while (num > 0) {
res = res * 10 + num % 10;
num /= 10;
}
return res;
}
// 判断是否为闰年
bool isLeapYear(int year) {
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
// 验证日期有效性
bool isValidDate(int year, int mm, int dd) {
if (mm < 1 || mm > 12) return false;
int maxDay[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
if (mm == 2 && isLeapYear(year)) maxDay[2] = 29;
return dd >= 1 && dd <= maxDay[mm];
}
// 统计[date1, date2]之间的回文日期数量
int countPalindromeDate(int date1, int date2) {
int count = 0;
// 枚举年份:从date1的前4位到date2的前4位
int startYear = date1 / 10000;
int endYear = date2 / 10000;
for (int year = startYear; year <= endYear; ++year) {
// 构造回文日期:YYYY * 10000 + 反转(YYYY)(补前导零到4位)
int reversed = reverseNum(year);
int palindromeDate = year * 10000 + reversed;
// 验证1:是否在范围内
if (palindromeDate < date1 || palindromeDate > date2) {
continue;
}
// 验证2:是否为有效日期
int mm = palindromeDate / 100 % 100; // 月份(第5-6位)
int dd = palindromeDate % 100; // 日期(第7-8位)
if (isValidDate(year, mm, dd)) {
count++;
}
}
return count;
}
int main() {
int date1, date2;
cin >> date1 >> date2;
cout << countPalindromeDate(date1, date2) << endl;
return 0;
}题目描述:
扫雷游戏的棋盘是 n×2 的矩阵,第一列有若干地雷,第二列没有地雷。第二列的每个格子中的数字表示 “与该格子连通的 8 个格子中地雷的数量”(但由于是 n×2 矩阵,实际连通的是上下左右及斜对角的第一列格子)。给定第二列的数字,求第一列地雷的摆放方案数(地雷用 1 表示,无地雷用 0 表示)。

输入:
输出:
第一列地雷的摆放方案数(0、1 或 2)。
示例输入:
2
1 1示例输出:
2 # 方案1:第一列[0,1];方案2:第一列[1,0]核心难点:
题目链接:https://www.luogu.com.cn/problem/P2327
#include <iostream>
using namespace std;
const int N = 1e4 + 10;
int a[N]; // 第一列地雷状态:a[1]~a[n]
int b[N]; // 第二列的数字:b[1]~b[n]
int n;
// 验证第一行状态为first(0或1)时,是否存在合法方案
bool check(int first) {
a[1] = first;
// 推导第2~n行的状态
for (int i = 2; i <= n; ++i) {
// 公式:a[i] = b[i-1] - a[i-1] - a[i-2](a[0]默认0,因为第一行上方无格子)
a[i] = b[i-1] - a[i-1] - (i >= 2 ? a[i-2] : 0);
// 状态必须是0或1,否则不合法
if (a[i] < 0 || a[i] > 1) {
return false;
}
}
// 验证最后一行:b[n]必须等于a[n-1] + a[n](a[n+1]默认0)
return b[n] == a[n-1] + a[n];
}
int main() {
cin >> n;
for (int i = 1; i <= n; ++i) {
cin >> b[i];
}
// 枚举第一行的两种可能状态,统计合法方案数
int count = 0;
count += check(0); // 第一行无地雷
count += check(1); // 第一行有地雷
cout << count << endl;
return 0;
}二进制枚举是枚举算法的进阶形式,指用一个整数的二进制位表示 “是否选择某个元素”,通过遍历所有可能的整数,枚举所有子集或组合情况。它的核心是 “位运算 + 状态映射”,适用于 “元素是否被选择” 的二选一场景。
假设有 n 个元素(编号 0~n-1),用一个 n 位的二进制数表示一个子集:
例如:
对于 n 个元素,二进制数的范围是 0~2ⁿ-1(共 2ⁿ种可能,对应所有子集,包括空集):
在二进制枚举中,常用的位运算操作包括:
(st >> i) & 1(st 为当前二进制数,将 st 右移 i 位,与 1 按位与,结果为 1 表示第 i 位是 1);__builtin_popcount(st),C++ 内置函数,统计 st 的二进制中 1 的个数)。题目描述:
给你一个整数数组 nums(元素互不相同),返回该数组所有可能的子集(幂集)。解集不能包含重复的子集,可以按任意顺序返回。
输入:
输出:
核心难点:
题目链接:https://leetcode.cn/problems/subsets/description/
#include <vector>
using namespace std;
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums) {
vector<vector<int>> result;
int n = nums.size();
// 枚举所有二进制状态:0 ~ 2^n - 1
for (int st = 0; st < (1 << n); ++st) {
vector<int> currentSubset;
// 检查每一位是否为1,若是则加入子集
for (int i = 0; i < n; ++i) {
if ((st >> i) & 1) {
currentSubset.push_back(nums[i]);
}
}
result.push_back(currentSubset);
}
return result;
}
};for (int st = 0; st < (1 << n); ++st) 遍历所有 2ⁿ种状态,对应所有子集;(st >> i) & 1 检查第 i 位是否为 1,若为 1 则将 nums [i] 加入当前子集;(1 << n)写成(1 << n) - 1,导致遗漏全集(st=2ⁿ-1);(st >> i) & 1写成st >> (i & 1),逻辑错误;题目描述:
有一个 5×5 的灯阵,每个灯有 “开(1)” 和 “关(0)” 两种状态。按下一个灯的开关,会使该灯及其上下左右相邻的灯状态反转(1 变 0,0 变 1)。给定初始状态,求最少需要按多少次开关才能使所有灯变亮(全 1),若超过 6 次则输出 - 1。
输入:
输出:
示例输入:
3
00111
01011
10001
11010
11100
11101
11101
11110
11111
11111
01111
11111
11111
11111
11111示例输出:
3
2
-1核心难点:
题目链接:https://www.luogu.com.cn/problem/P10449
// 反转(x,y)位置的灯及其相邻灯的状态(x,y从0开始,5×5矩阵)
void flip(int x, int y, vector<vector<int>>& grid) {
// 定义当前灯及上下左右的坐标偏移
int dx[] = {0, 0, 0, 1, -1};
int dy[] = {0, 1, -1, 0, 0};
for (int d = 0; d < 5; ++d) {
int nx = x + dx[d];
int ny = y + dy[d];
// 确保坐标在5×5范围内
if (nx >= 0 && nx < 5 && ny >= 0 && ny < 5) {
grid[nx][ny] ^= 1; // 异或1反转状态(0→1,1→0)
}
}
}#include <iostream>
#include <vector>
#include <cstring>
#include <climits>
using namespace std;
// 反转(x,y)及其相邻灯的状态
void flip(int x, int y, vector<vector<int>>& grid) {
int dx[] = {0, 0, 0, 1, -1};
int dy[] = {0, 1, -1, 0, 0};
for (int d = 0; d < 5; ++d) {
int nx = x + dx[d];
int ny = y + dy[d];
if (nx >= 0 && nx < 5 && ny >= 0 && ny < 5) {
grid[nx][ny] ^= 1;
}
}
}
// 计算初始状态grid下的最少按次数
int minPresses(vector<vector<int>>& original) {
int minCnt = INT_MAX;
// 枚举第一行的所有按法(0~31,32种可能)
for (int st = 0; st < (1 << 5); ++st) {
vector<vector<int>> grid = original; // 备份初始状态
int cnt = 0; // 记录当前按次数
// 1. 模拟第一行的按法
for (int j = 0; j < 5; ++j) {
if ((st >> j) & 1) { // 第j列需要按
flip(0, j, grid);
cnt++;
if (cnt > 6) break; // 超过6次,无需继续
}
}
if (cnt > 6) continue;
// 2. 推导第2~5行的按法
for (int i = 1; i < 5; ++i) {
for (int j = 0; j < 5; ++j) {
// 若上一行第j列是灭的,必须按当前行第j列的开关
if (grid[i-1][j] == 0) {
flip(i, j, grid);
cnt++;
if (cnt > 6) break;
}
}
if (cnt > 6) break;
}
if (cnt > 6) continue;
// 3. 验证最后一行是否全亮
bool allOn = true;
for (int j = 0; j < 5; ++j) {
if (grid[4][j] == 0) {
allOn = false;
break;
}
}
if (allOn && cnt < minCnt) {
minCnt = cnt;
}
}
// 若未找到有效方案,返回-1
return minCnt == INT_MAX ? -1 : minCnt;
}
int main() {
int n;
cin >> n;
while (n--) {
vector<vector<int>> original(5, vector<int>(5));
// 读取初始状态:'0'→0(灭),'1'→1(亮)
for (int i = 0; i < 5; ++i) {
string s;
cin >> s;
for (int j = 0; j < 5; ++j) {
original[i][j] = s[j] - '0';
}
}
// 计算并输出最少按次数
int res = minPresses(original);
cout << res << endl;
}
return 0;
}题目核心要求:
给定一个 n×n 的 01 矩阵(每个元素非 0 即 1),你可以将某些 0 修改为 1(不允许 1 修改为 0),使得最终矩阵成为 “偶数矩阵”。偶数矩阵的定义是:每个元素的上下左右相邻元素(若存在)之和为偶数。请计算最少的修改次数,若无法实现则输出 - 1。
输入格式:
输出格式:
示例输入:
3
3
0 0 0
0 0 0
0 0 0
3
0 0 0
1 0 0
0 0 0
3
1 1 1
1 1 1
0 0 0示例输出:
Case 1: 0
Case 2: 3
Case 3: -1题目链接:https://www.luogu.com.cn/problem/UVA11464
对于矩阵中的任意元素a[i][j](行号 i、列号 j,从 1 开始),其相邻元素之和为偶数,即:
a[i+1][j] + a[i][j+1] 为偶数;a[i-1][j] + a[i][j-1] 为偶数;a[i-1][j] + a[i+1][j] + a[i][j-1] + a[i][j+1] 为偶数;但通过推导可发现:矩阵的第一行状态确定后,后续所有行的状态均可唯一推导。原因如下:
对于第 i 行第 j 列的元素
a[i][j](i≥2),其上方元素a[i-1][j]的相邻元素之和需为偶数。a[i-1][j]的相邻元素包括a[i-2][j](上方,若存在)、a[i][j](下方)、a[i-1][j-1](左方)、a[i-1][j+1](右方)。整理可得推导公式:a[i][j] = a[i-2][j] ^ a[i-1][j-1] ^ a[i-1][j+1](“^” 为异或运算,异或结果为 0 表示和为偶数,1 表示和为奇数,符合偶数矩阵要求)。
n 的最大值为 15,第一行有 n 个元素,每个元素有 “保持原状态” 或 “修改为 1” 两种可能(但需满足 “不允许 1 变 0”)。因此第一行的可能状态数为 2ⁿ,当 n=15 时为 32768 种,完全在枚举范围内(不会超时)。
修改次数仅统计 “0 变 1” 的次数:对于每个位置,若初始状态为 0 且最终状态为 1,计数 + 1;若初始状态为 1 且最终状态为 0,属于非法操作(直接排除该方案)。
以第 i 行第 j 列(i≥2,1≤j≤n)为例:
a[i-1][j]的上下左右相邻元素之和为偶数;a[i-2][j](若 i≥3)、下方a[i][j]、左方a[i-1][j-1](若 j≥2)、右方a[i-1][j+1](若 j≤n-1);a[i-2][j]不存在(视为 0);当 j=1 时,a[i-1][j-1]不存在(视为 0);当 j=n 时,a[i-1][j+1]不存在(视为 0);a[i][j] = (i >= 3 ? a[i-2][j] : 0) ^ (j >= 2 ? a[i-1][j-1] : 0) ^ (j <= n-1 ? a[i-1][j+1] : 0)#include <iostream>
#include <cstring>
#include <climits>
using namespace std;
const int N = 20; // n最大为15,预留冗余
int n;
int initial[N][N]; // 初始矩阵(1-based)
int current[N][N]; // 当前枚举的最终矩阵(1-based)
// 检查并计算:第一行状态为st时的修改次数,非法返回-1
int check(int st) {
memset(current, 0, sizeof current);
int modify = 0; // 修改次数(0变1的次数)
// 1. 处理第一行,验证合法性并计算修改次数
for (int j = 1; j <= n; ++j) {
// 提取st的第j位(注意:st的第0位对应j=1,第1位对应j=2,...)
int bit = (st >> (j - 1)) & 1;
current[1][j] = bit;
// 合法性检查:初始为1,最终为0 → 非法
if (initial[1][j] == 1 && bit == 0) {
return -1;
}
// 计算修改次数:初始为0,最终为1 → +1
if (initial[1][j] == 0 && bit == 1) {
modify++;
}
}
// 2. 推导第2~n行的状态,验证合法性并计算修改次数
for (int i = 2; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
// 推导公式:current[i][j] = 上上行[j] ^ 上一行[j-1] ^ 上一行[j+1]
int up_up = (i >= 3) ? current[i-2][j] : 0; // 上上行(i-2),不存在为0
int up_left = (j >= 2) ? current[i-1][j-1] : 0; // 上一行左列(j-1),不存在为0
int up_right = (j <= n-1) ? current[i-1][j+1] : 0; // 上一行右列(j+1),不存在为0
current[i][j] = up_up ^ up_left ^ up_right;
// 合法性检查:初始为1,最终为0 → 非法
if (initial[i][j] == 1 && current[i][j] == 0) {
return -1;
}
// 计算修改次数:初始为0,最终为1 → +1
if (initial[i][j] == 0 && current[i][j] == 1) {
modify++;
}
}
}
// 3. 额外验证:最后一行的每个元素是否满足偶数矩阵要求(避免推导遗漏)
for (int j = 1; j <= n; ++j) {
int sum = 0;
// 最后一行的元素,仅需检查上方、左方、右方(无下方)
if (n >= 2) sum += current[n-1][j]; // 上方
if (j >= 2) sum += current[n][j-1]; // 左方
if (j <= n-1) sum += current[n][j+1]; // 右方
if (sum % 2 != 0) { // 之和为奇数,不满足偶数矩阵
return -1;
}
}
return modify; // 返回合法方案的修改次数
}
int main() {
int T;
cin >> T;
for (int case_num = 1; case_num <= T; ++case_num) {
cin >> n;
// 读取初始矩阵(1-based存储,方便后续推导)
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
cin >> initial[i][j];
}
}
int min_modify = INT_MAX; // 最少修改次数
// 枚举第一行的所有可能状态(0 ~ 2^n - 1)
for (int st = 0; st < (1 << n); ++st) {
int res = check(st);
if (res != -1 && res < min_modify) {
min_modify = res;
}
}
// 输出结果
if (min_modify == INT_MAX) {
printf("Case %d: -1\n", case_num);
} else {
printf("Case %d: %d\n", case_num, min_modify);
}
}
return 0;
}st表示第一行的最终状态:st的第j-1位(从 0 开始)对应第一行第j列的状态(1 表示最终为 1,0 表示最终为 0);st=5(二进制101)→ 第一行状态为[1,0,1](j=1 对应 bit0=1,j=2 对应 bit1=0,j=3 对应 bit2=1)。modify++;min_modify(初始为无穷大),最终取最小值。st的第 j 位对应第一行第 j 列(而非第 j-1 位),导致第一行状态错位;st的位序与列号的对应关系(bit0→j=1,bit1→j=2,...,bit (n-1)→j=n),提取位时用(st >> (j-1)) & 1。up_up = (i >= 3) ? current[i-2][j] : 0)。枚举算法的关键不是 “暴力遍历”,而是 “聪明地枚举”—— 通过分析问题特性,减少枚举次数,提升验证效率。在实际应用中,枚举往往是解决问题的 “第一思路”:当问题规模较小时,枚举能快速得到结果;当问题复杂时,枚举也能作为基础框架,逐步优化。 如果本文对你有帮助,欢迎点赞、收藏、转发,也欢迎在评论区交流讨论~