目录
- 前言
- 一、普通枚举
-
- [1.1 铺地毯](#1.1 铺地毯)
- [1.2 回文日期](#1.2 回文日期)
- [1.3 扫雷](#1.3 扫雷)
- 二、二进制枚举
-
- [2.1 子集](#2.1 子集)
- [2.2 费解的开关](#2.2 费解的开关)
- [2.3 Even Parity](#2.3 Even Parity)
- 结语


🎬 云泽Q :个人主页
🔥 专栏传送入口 : 《C语言》《数据结构》《C++》《Linux》
⛺️遇见安然遇见你,不负代码不负卿~
前言
大家好啊,我是云泽Q,欢迎阅读我的文章,一名热爱计算机技术的在校大学生,喜欢在课余时间做一些计算机技术的总结性文章,希望我的文章能为你解答困惑~
枚举
顾名思义,就是把所有情况全都罗列出来,然后找出符合题目要求的那一个。因此,枚举是一种纯暴力的算法
一般情况下,枚举策略都是会超时的。此时要根据题目的数据范围来判断暴力枚举是否可以通过,如果不行的话,就要用其他算法来进行优化
使用枚举策略时,重点思考枚举的对象(枚举什么),枚举的顺序(正序还是逆序),以及枚举的方式(普通枚举?递归枚举?二进制枚举)
一、普通枚举
1.1 铺地毯
铺地毯

解法 :
枚举所有的地毯,判断哪一个地毯能够覆盖(x,y)这个位置
优化枚举方式:
- 因为要的是最后一个能够覆盖(x,y)位置的地毯,那么逆序枚举所有的地毯,第一次找到覆盖(x,y)位置的就是结果
- 如果从前往后枚举,至少要把所有地毯枚举完,才知道最后结果

cpp
#include <iostream>
using namespace std;
const int N = 1e4 + 10;
int n;
int a[N], b[N], g[N], k[N];
int x, y;
int find()
{
// 从后往前枚举
for(int i = n; i >= 1; i--)
{
// 判断是否覆盖
if(a[i] <= x && b[i] <= y && a[i] + g[i] >= x && b[i] + k[i] >= y)
{
return i;
}
}
return -1;
}
int main()
{
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i] >> b[i] >> g[i] >> k[i];
cin >> x >> y;
cout << find() << endl;
return 0;
}
1.2 回文日期
该题目给出三种解法:

该策略会超时,但是整体思路还是很值得学习
cpp
#include<iostream>
#include<string>
using namespace std;
int date1, date2;
int cnt;
int main()
{
cin >> date1 >> date2;
for (int num = date1; num <= date2; num++)
{
//判断是否回文
string s = to_string(num);
bool pal = (s[0] == s[7] && s[1] == s[6] && s[2] == s[5] && s[3] == s[4]);
if (!pal) continue;
//拆分年月日
int year = num / 10000;
int month = (num / 100) % 100;
int day = num % 100;
//判断月份是否合法
if (month < 1 || month > 12) continue;
//判断日期是否合法
int maxDay = 0;
if (month == 2)
{
bool isLeap = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
maxDay = isLeap ? 29 : 28;
}
else if (month == 4 || month == 6 || month == 9 || month == 11) {
maxDay = 30;
}
else {
maxDay = 31;
}
if (day >= 1 && day <= maxDay) cnt++;
}
cout << cnt << endl;
return 0;
}
补充:
- to_string(num)的功能 :
to_string是 C++ 标准库中的函数,属于< string >头文件,它能把数值类型 (如int、long、float等)转换成该数值对应的字符串形式。
比如:如果num的值是20241202,那么to_string(num)会生成字符串"20241202";如果num是123,则生成"123"。 - string s = ...的作用 :
定义一个string类型的变量s,并把to_string(num)转换得到的字符串结果赋值给s,这样后续就能通过字符串的方式操作num的每一位字符(比如判断回文时比较对称位置的字符)。
为什么需要这样的转换 ?
因为要判断数字是否为回文数,需要直接访问每一位数字的字符(比如第 1 位和最后 1 位、第 2 位和倒数第 2 位是否相等)。如果直接操作整数,需要通过取模、除法等运算拆分每一位,代码会更繁琐;而转换成字符串后,可以直接通过下标(如s[0]、s[7])访问每一位,判断回文会更简单直观。

cpp
#include<iostream>
using namespace std;
int date1, date2;
int count;
bool isLeap(int year)
{
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
bool isLegal(int year, int month, int day)
{
//检验月份是否合法
if(month < 1 || month > 12) return false;
//每月的最大天数
int maxDay;
switch(month)
{
case 1: case 3: case 5: case 7: case 8: case 10: case 12:
maxDay = 31; break;
case 4: case 6: case 9: case 11:
maxDay = 30; break;
case 2:
maxDay = isLeap(year) ? 29 : 28; break;
default:
maxDay = 0;
}
//检验天数是否合法
return (day >= 1 && day <= maxDay);
}
int main()
{
cin >> date1 >> date2;
//确定年份区间
int startYear = date1 / 10000;
int endYear = date2 / 10000;
for(int year = startYear; year <= endYear; year++)
{
//拆分年的每一位
int y1 = year / 1000;
int y2 = (year / 100) % 10;
int y3 = (year / 10) % 10;
int y4 = year % 10;
//根据回文规则生成月和日
int month = y4 * 10 + y3;
int day = y2 * 10 + y1;
//判断日期是否合法
if(!isLegal(year, month, day)) continue;
//将日期与年份拼接为8位
int trueDate = year * 10000 + month * 100 + day;
if(trueDate >= date1 && trueDate <= date2)
{
count++;
}
}
cout << count << endl;
return 0;
}
补充一下:这里日期判断合法后拼接起来的8位数看似就在date1到date2之间,实则不然,还需要校验,原因如下:
- 例子 1:起始年份的 "早期回文日期" 小于date1
假设输入的date1 = 20200501(2020 年 5 月 1 日),startYear = 2020:按回文规则,2020 年生成的回文日期是20200202(2020 年 2 月 2 日)。这个日期虽然年份在startYear范围内,但20200202 < 20200501,不在输入的起始区间内,因此需要排除。 - 例子 2:结束年份的 "晚期回文日期" 大于date2
假设输入的date2 = 20231001(2023 年 10 月 1 日),endYear = 2023:按回文规则,2023 年生成的回文日期是20233202(月份 32,不合法,会被过滤);再比如endYear = 2024,date2 = 20240301,2024 年的回文日期是20244202(月份 42,不合法),但如果是2025年,date2 = 20250201,2025 年的回文日期是20255202(月份 52,不合法)------若遇到合法但超期的情况(比如date2 = 20211201,2021 年的回文日期是20211202,则20211202 > 20211201),也需要排除。

cpp
//策略三:枚举所有的月日组合
#include<iostream>
using namespace std;
int day[] = {0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
int x, y;
int main()
{
cin >> x >> y;
int ret = 0;
//枚举月日组合
for(int i = 1; i <= 12; i++)
{
for(int j = 1; j <= day[i]; j++)
{
int k = j % 10 * 1000 + j / 10 * 100 + i % 10 * 10 + i / 10;
int num = k * 10000 + i * 100 + j;
if(num >= x && num <= y)
ret++;
}
}
cout << ret << endl;
return 0;
}
下面是策略三的一些补充解释:
为什么二月标记为29天是正确的?
- 回文日期的唯一性:
-
- 对于二月29日(i=2, j=29),生成的年份k为9220。9220年是闰年(因为9220是4的倍数且不是100的倍数),因此9220年二月29日是真实存在的日期。
-
- 类似地,对于其他月日组合,生成的年份k也能确保日期有效。例如,二月28日(j=28)生成年份8220,8220也是闰年,所以二月28日也存在。
- 避免漏掉有效日期:
-
- 如果將 day 数组中的二月改为28天,代码将不会枚举二月29日,从而漏掉像92200229这样的有效回文日期。尽管在输入日期范围较小(如样例中年份在2000到2010年)时,9220年可能超出范围,不影响结果,但代码需要保持通用性,以处理任何可能的输入范围。
- 闰年检查的隐含性:
-
- 代码通过回文结构自动确保了对于二月29日,生成的年份总是闰年。因此,无需单独检查闰年,直接枚举二月29日是安全的。如果改为28天,就破坏了这种隐含的闰年保证,导致代码不完整。
如果改为28天会有什么问题?
-
- 假设将 day 数组中的二月改为28天,代码只会枚举到二月28日。那么对于二月29日,即使生成的年份是闰年(如9220年),也不会生成日期92200229。这将导致在输入日期范围包含9220年时,漏掉一个有效的回文日期。
-
- 在实际应用中,输入日期范围可能很大(年份从1000到9999),因此9220年可能被包含在内。例如,如果输入日期是20000101和99991231,那么92200229就应该被计数。
1.3 扫雷
扫雷

解法 :
第一列中,第一行的小格子的状态确定了之后,后续行的状态也跟着固定下来,而第一列中,第一行的状态要么有雷,要么没有雷,所以最终的答案就在0,1,2中
因此我们枚举第一列中,第一行的两种状态:要么有雷,要么没雷。然后依次计算剩下行的值,看看是否能满足所给的数据

cpp
#include<iostream>
using namespace std;
const int N = 1e4 + 10;
int n;
int a[N], b[N];
//a[1]不放雷
int check1()
{
a[1] = 0;
for(int i = 2; i <= n + 1; i++)
{
a[i] = b[i - 1] - a[i - 1] - a[i - 2];
if(a[i] < 0 || a[i] > 1) return 0;
}
if(a[n + 1] == 0) return 1;
else{
return 0;
}
}
//a[1]放雷
int check2()
{
a[1] = 1;
for(int i = 2; i <= n + 1; i++)
{
a[i] = b[i - 1] - a[i - 1] - a[i - 2];
if(a[i] < 0 || a[i] > 1) return 0;
}
if(a[n + 1] == 0) return 1;
else{
return 0;
}
}
int main()
{
cin >> n;
for(int i = 1; i <= n; i++) cin >> b[i];
int ret = 0;
ret += check1(); // a[1]不放雷
ret += check2(); // a[1] 放雷
cout << ret << endl;
return 0;
}
二、二进制枚举
二进制枚举:用一个数二进制表示中的 0/1 表示两种状态,从而达到枚举各种情况
- 利用二进制枚举,经常会牵扯一些位运算的知识
- 关于用二进制中的 0/1 表示状态这种方法,后续也会在动态规划的文章中写
- 二进制枚举的方式也可以用递归实现
2.1 子集
子集

解法 :
枚举 0 到 1 << n -1 之间所有的数,每一个数的二进制中的 1 的位置可以表示数组中对应位置选上该元素,那么 0 到 1 << n -1 就可以枚举出原数组中所有的子集
根据枚举的每一个状态,选出原数组中对应的元素,然后存在结果数组中

cpp
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums) {
vector<vector<int>> ret;
int n = nums.size();
//枚举所有的状态
for(int st = 0; st < (1 << n); st++)
{
//根据st的所有状态,还原出要选的数
vector<int> tmp;// 从当前选的子集
for(int i = 0; i < n; i++)
{
if((st >> i) & 1) tmp.push_back(nums[i]);
}
ret.push_back(tmp);
}
return ret;
}
};


2.2 费解的开关
解法
在这个拉灯游戏中我们可以得到三个性质
- 每一个开关最多只会被按一次。因为按两次及以上是没有意义的,只会让按的次数增多
- 按每一个开关的先后顺序 不会影响最后的结果。可以想象,当所有开关按的方式确定之后,每一个开关被改变的次数也就被确定了,也就是不管你先按谁后按谁,改变的次数是固定的,那么结果就是固定的
- 如果确定了第一行 的按法,后续行的按法也就固定下来了(这里可以参考前面《扫雷》这道题,有相似点)。因为第一行的按法固定之后,第二行的按法需要把第一行全部点亮 ;当第二行的按法确定了之后,第三行的按法需要把第二行全部点亮...,依此类推,后续行的按法就都确定下来了。
有了这三个性质,该题目的核心思路就是:
- 暴力枚举第一行的所有按法
- 然后根据第一行的按法,计算出当前行(第一行)以及下一行被按之后的结果
- 根据上一行被按了之后的状态,确定当前行的按法,然后重复 2 操作
- 最后判断最后一行是否全部都亮



cpp
#include<iostream>
#include<cstring>
using namespace std;
const int N = 10;
int n = 5;
int a[N];//用二进制表示,来存储灯的状态
int t[N];//备份a数组
//统计当前行一共有多少个 1 - 当前行按了多少次
int calc(int x)
{
int cnt = 0;
while(x)
{
cnt++;
x &= x - 1;
}
return cnt;
}
int main()
{
int T; cin >> T;
while(T--)
{
//多组测试,要清空之前的数据
memset(a, 0, sizeof a);
//存储灯的初始状态
for(int i = 0; i < n; i++) //行
{
for(int j = 0; j < n; j++)
{
char ch; cin >> ch;
//把第j位设为1,用一个整数存下整行需要翻转的灯
if(ch == '0') a[i] |= 1 << j;
}
}
//统计所有合法的按法中的最小值,将ret初始化为无穷大
int ret = 0x3f3f3f3f;
//枚举第一行的所有按法
for(int st = 0; st < (1 << n); st++)
{
memcpy(t, a, sizeof a);
int push = st;//当前行的按法
int cnt = 0;//统计当前按法下一共按了多少次
//依次统计当前行的结果以及后续行的按法
for(int i = 0; i < n; i++)
{
cnt += calc(push);
//修改当前行被按的结果
t[i] = t[i] ^ push ^ (push << 1) ^ (push >> 1);
t[i] &= (1 << n) - 1;//清空影响
//修改下一行的状态
t[i + 1] ^= push;
//下一行的按法
push = t[i];
}
if(t[n - 1] == 0) ret = min(ret, cnt);
}
if(ret > 6) cout << -1 << endl;
else cout << ret << endl;
}
return 0;
}
一、先明确题目核心问题
5x5 的灯阵,每个灯有 "亮(1)" 和 "灭(0)" 两种状态。按一个灯,会翻转自身 + 上下左右的灯。目标是用 最少步数(≤6) 让所有灯变亮,求最小步数(无解则输出 - 1)。
二、解题核心逻辑:第一行决定一切
因为第 i 行的灯只能被第 i-1 行的按操作或自身行的按操作影响(第 1 行没有上一行),所以:
- 枚举第一行的所有按法(5 列有2^5=32种可能);
- 根据前一行的状态推导当前行的按法(比如第 2 行的按法由第 1 行剩下的灭灯位置决定);
- 模拟完所有行后,检查最后一行是否全亮,统计最小步数。
位运算的作用 :用整数的二进制位 替代数组存灯状态,用位操作替代 "逐个灯翻转" 的循环,让代码更高效、简洁。
三、逐环节拆解:逻辑 + 位运算 + 代码
环节 1:存储灯的初始状态(把 "灭灯" 标记为需要翻转)
实际需求
我们需要记录 "哪些灯需要被翻转"(灭灯需要翻转,亮灯不需要),用一个变量存一行的 5 个灯状态,避免用数组。
位运算实现
- 用整数的第 j 位(从 0 开始数)代表 "第 j 列的灯是否需要翻转":
-
- 灭灯(0)→ 需要翻转→第 j 位设为1;
-
- 亮灯(1)→ 不需要翻转→第 j 位设为0。
- 操作:a[i] |= 1 << j(把第 j 位设为 1)。
cpp
for(int i = 0; i < 5; i++) { // 遍历每一行
for(int j = 0; j < 5; j++) { // 遍历每一列
char ch; cin >> ch;
if(ch == '0') a[i] |= 1 << j; // 灭灯→第j位设为1
}
}
例子:输入某行是0 1 0 1 0(第 0、2、4 列灭):
- j=0:1<<0=1(二进制00001)→ a[i] = 00001;
- j=2:1<<2=4(二进制00100)→ a[i] |= 4 → a[i] = 00101;
- j=4:1<<4=16(二进制10000)→ a[i] |= 16 → a[i] = 10101(十进制 21)。
最终a[i]=21,用一个整数就存下了整行需要翻转的灯!
环节 2:枚举第一行的所有按法
实际需求
第一行有 5 列,每列可 "按" 或 "不按",共 32 种按法,需要遍历所有可能。
位运算实现
- 用整数st的二进制位代表 "第一行的按法":第 j 位是1→按第 j 列,是0→不按。
- 枚举范围:st从0到(1<<5)-1(即 0 到 31),覆盖所有 5 位二进制数。
cpp
for(int st = 0; st < (1 << 5); st++) { // 枚举32种按法
memcpy(t, a, sizeof a); // 备份初始状态
int push = st; // 当前行的按法(先赋值第一行的按法)
int cnt = 0; // 统计步数
}
例子:st=3(二进制00011)→ 第一行按第 0、1 列;st=16(二进制10000)→ 第一行按第 4 列。
环节 3:模拟 "按灯" 的连锁翻转(自身 + 左右 + 下一行)
实际需求
按push对应的灯后,要完成:
- 翻转当前行的自身 + 左右相邻灯;
- 翻转下一行的对应位置灯;
- 确定下一行的按法(当前行剩下的需要翻转的灯 = 下一行的按法)。
位运算实现(分三步)
第一步:翻转当前行的自身 + 左右
- 按push的灯→翻转自身:push;
- 翻转左边灯:push >> 1(push右移 1 位,比如push=00100→00010,对应左边列);
- 翻转右边灯:push << 1(push左移 1 位,比如push=00100→01000,对应右边列);
- 合并翻转:用异或( ^ )(0 ^ 1=1翻转,1 ^ 1=0翻转,0 ^ 0=0不变)→ t[i] ^ = push ^ (push<<1) ^ (push>>1)。
第二步:清空无效位(避免移位越界)
左移可能超出 5 列(比如push = 10000 → push << 1 = 100000,第 5 列无效),用(1 << 5) - 1 = 31(二进制00011111)做按位与(&) ,保留低 5 位,清空高位。

第三步:翻转下一行 + 确定下一行按法
- 翻转下一行:t[i+1] ^ = push(下一行对应位置翻转);
- 下一行按法:push = t[i](当前行剩下的需要翻转的灯 = 下一行要按的灯)。
cpp
for(int i = 0; i < 5; i++) { // 遍历每一行
cnt += calc(push); // 统计当前行按的步数(后面讲calc)
// 1. 翻转当前行自身+左右
t[i] ^= push ^ (push << 1) ^ (push >> 1);
// 2. 清空无效位
t[i] &= (1 << 5) - 1;
// 3. 翻转下一行+更新下一行按法
if(i + 1 < 5) t[i+1] ^= push;
push = t[i]; // 下一行按法=当前行剩余需要翻转的灯
}
例子:假设push = 00100(按第 2 列),当前行t[i] = 10101:
- 翻转自身 + 左右:00100 ^ 01000 ^ 00010 = 01110;
- t[i] ^ = 01110 → 10101 ^ 01110 = 11011;
- 清空无效位:11011 & 00011111 = 11011(无无效位);
- 翻转下一行:t[i+1] ^ = 00100(下一行第 2 列翻转);
- 下一行按法:push=11011(下一行要按第 0、1、3、4 列)。
环节 4:统计步数(数按了多少个灯)
实际需求
统计push中 "按了的灯" 数量(即二进制中1的个数)。
位运算实现
用x &= x-1消除最右边的1,每消除一次计数 + 1,直到x=0。
cpp
int calc(int x) {
int cnt = 0;
while(x) {
cnt++;
x &= x - 1; // 消除最右边的1
}
return cnt;
}
例子:x = 01110(二进制)→ 消除过程:
- 01110 & 01101 = 01100 → cnt=1;
- 01100 & 01011 = 01000 → cnt=2;
- 01000 & 00111 = 00000 → cnt=3;
最终返回 3(按了 3 个灯)。
环节 5:判断结果 + 输出
实际需求
最后一行如果全亮(即没有需要翻转的灯→t[4]=0),说明该按法有效,记录最小步数。
cpp
if(t[n - 1] == 0) ret = min(ret, cnt); // 最后一行全亮则更新最小步数
// 输出结果
if(ret > 6) cout << -1 << endl;
else cout << ret << endl;
四、完整逻辑串起来
- 用位运算把 "需要翻转的灯" 存成整数(a[i] |= 1<<j);
- 枚举第一行的 32 种按法(st < (1<<5));
- 对每种按法,用位运算模拟 "按灯→翻转当前行 + 下一行→推导下一行按法";
- 用位运算统计步数(x &= x-1);
- 最后一行全亮则记录最小步数,最终输出。
2.3 Even Parity
结语


