
大家好啊,我是云泽Q,欢迎阅读我的文章,一名热爱计算机技术的在校大学生,喜欢在课余时间做一些计算机技术的总结性文章,希望我的文章能为你解答困惑~
枚举 顾名思义,就是把所有情况全都罗列出来,然后找出符合题目要求的那一个。因此,枚举是一种纯暴力的算法
一般情况下,枚举策略都是会超时的。此时要根据题目的数据范围来判断暴力枚举是否可以通过,如果不行的话,就要用其他算法来进行优化
使用枚举策略时,重点思考枚举的对象(枚举什么),枚举的顺序(正序还是逆序),以及枚举的方式(普通枚举?递归枚举?二进制枚举)

解法: 枚举所有的地毯,判断哪一个地毯能够覆盖(x,y)这个位置 优化枚举方式:

#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;
}
该题目给出三种解法:

该策略会超时,但是整体思路还是很值得学习
#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;
}补充:
为什么需要这样的转换? 因为要判断数字是否为回文数,需要直接访问每一位数字的字符(比如第 1 位和最后 1 位、第 2 位和倒数第 2 位是否相等)。如果直接操作整数,需要通过取模、除法等运算拆分每一位,代码会更繁琐;而转换成字符串后,可以直接通过下标(如s[0]、s[7])访问每一位,判断回文会更简单直观。

#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之间,实则不然,还需要校验,原因如下:

//策略三:枚举所有的月日组合
#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天是正确的?
如果改为28天会有什么问题?

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

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

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;
}
};



解法 在这个拉灯游戏中我们可以得到三个性质
有了这三个性质,该题目的核心思路就是:



#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 行没有上一行),所以:
位运算的作用:用整数的二进制位替代数组存灯状态,用位操作替代 “逐个灯翻转” 的循环,让代码更高效、简洁。
三、逐环节拆解:逻辑 + 位运算 + 代码 环节 1:存储灯的初始状态(把 “灭灯” 标记为需要翻转) 实际需求 我们需要记录 “哪些灯需要被翻转”(灭灯需要翻转,亮灯不需要),用一个变量存一行的 5 个灯状态,避免用数组。 位运算实现
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 列灭):
环节 2:枚举第一行的所有按法 实际需求 第一行有 5 列,每列可 “按” 或 “不按”,共 32 种按法,需要遍历所有可能。 位运算实现
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对应的灯后,要完成:
位运算实现(分三步) 第一步:翻转当前行的自身 + 左右
第二步:清空无效位(避免移位越界) 左移可能超出 5 列(比如push = 10000 → push << 1 = 100000,第 5 列无效),用(1 << 5) - 1 = 31(二进制00011111)做按位与(&),保留低 5 位,清空高位。

第三步:翻转下一行 + 确定下一行按法
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:
环节 4:统计步数(数按了多少个灯) 实际需求 统计push中 “按了的灯” 数量(即二进制中1的个数)。 位运算实现 用x &= x-1消除最右边的1,每消除一次计数 + 1,直到x=0。
int calc(int x) {
int cnt = 0;
while(x) {
cnt++;
x &= x - 1; // 消除最右边的1
}
return cnt;
}例子:x = 01110(二进制)→ 消除过程:
环节 5:判断结果 + 输出 实际需求 最后一行如果全亮(即没有需要翻转的灯→t[4]=0),说明该按法有效,记录最小步数。
if(t[n - 1] == 0) ret = min(ret, cnt); // 最后一行全亮则更新最小步数
// 输出结果
if(ret > 6) cout << -1 << endl;
else cout << ret << endl;四、完整逻辑串起来




#include<iostream>
#include<cstring>
using namespace std;
const int N = 20;
int a[N], t[N];
int n;
//判断 x -> y 是否合法
//返回 -1 表示不合法
//其余的数,表示合法,并且表示 0 -> 1的次数
int calc(int x, int y)
{
int sum = 0;
for(int i = 0; i < n; i++)
{
if(((x >> i) & 1) == 0 && ((y >> i) & 1) == 1) sum++;
if(((x >> i) & 1) == 1 && ((y >> i) & 1) == 0) return -1;
}
return sum;
}
int solve()
{
//记录最小的改变次数
int ret = 0x3f3f3f3f;
//枚举第一行的最终状态
for(int st = 0; st < (1 << n); st++)
{
memcpy(t, a, sizeof a);
int change = st;
int cnt = 0;//统计0 -> 1的次数
bool flag = 1;
for(int i = 1; i <= n; i++)
{
//判断change是否合法
int c = calc(t[i], change);
if(c == -1)
{
flag = 0;
break;
}
//累加次数
cnt += c;
//当前行的最终状态
t[i] = change;
//计算下一行的最终状态
change = t[i - 1] ^ (t[i] << 1) ^ (t[i] >> 1);
//清空越位影响
change &= (1 << n) - 1;
}
if(flag) ret = min(ret, cnt);
}
//若所有状态都不合法
if(ret == 0x3f3f3f3f) return -1;
else return ret;
}
int main()
{
int T; cin >> T;
for(int k = 1; k <= T; k++)
{
//每进行一组测试,要清空前一组的测试数据
memset(a, 0, sizeof a);
cin >> n;
//读入初始状态
for(int i = 1; i <= n; i++)//行,避免越界
{
for(int j = 0; j < n; j++)//列
{
int x; cin >> x;
if(x) a[i] |= 1 << j;
}
}
printf("Case %d: %d\n", k, solve());
}
return 0;
}补充:该题目不会出现方案是合法的,但是最后构成的不是合法的偶数矩阵的情况
偶数矩阵的核心要求是:矩阵中每一行、每一列的 1 的个数都是偶数(假设是 0/1 矩阵场景,其他 “偶数条件” 同理)。而代码中判定 “方案合法” 的逻辑,本质是 “能构造出满足该要求的矩阵”,步骤通常是:
也就是说:代码中只有 “能构造出合法偶数矩阵” 的方案,才会被算作 “合法方案” —— 二者是强绑定的,不存在 “方案合法但矩阵不合法” 的情况。
因此,当 ret == 0x3f3f3f3f 时,说明不存在合法的构造方案,按照题目要求需要返回 -1;否则返回找到的最小改变次数。

总的来说,二进制枚举相关的数据范围不会很大,一旦稍微大一点,就会有超时的风险,一般题目所给的数据范围就是一个信号,可以借此来判断能不能使用暴力枚举或二进制枚举