目录
[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 Even Parity](#2.3 Even Parity)
顾名思义,就是把所有情况全都罗列出来 ,然后找出符合题目要求的那⼀个 。因此,枚举是⼀种纯暴力的算法。
⼀般情况下,枚举策略都是会超时的 。此时要先根据题目的数据范围来判断暴力枚举是否可以通过。如果不行的话,就要用其他算法来进行优化(比如⼆分,双指针,前缀和与差分等)。
使用枚举策略时,重点思考枚举的对象(枚举什么),枚举的顺序(正序还是逆序),以及枚举的⽅式(普通枚举?递归枚举?⼆进制枚举)。(在这篇博客里的枚举题目都是不会超时的,注要是让大家了解枚举,后续的算法基础会跟深层次的学习枚举。)
1 普通枚举
1.1 铺地毯
题⽬来源: 洛⾕
题⽬链接: P1003 [NOIP2011 提⾼组] 铺地毯
难度系数: ★
【解法】
枚举所以的地毯,找出最后覆盖题目要求点的地毯即可。
策略:
从后往前枚举所有的地毯,第⼀次找到覆盖题目中给的点就是结果;
【参考代码】
cpp
#include<iostream>
using namespace std;
const int N = 1e4 + 10;
int a[N],b[N],g[N],k[N];
int 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 回文日期
题⽬来源: 洛⾕
题⽬链接:P2010 [NOIP2016 普及组] 回⽂⽇期
难度系数: ★

【解法】
枚举所有日月的组合,然后根据回文的特性推出年份,然后比较这个数字是否在题目给定的区间内。
策略一:枚举x~y之间所有的数字,然后判断是否回文。如果回文,那就拆分成年月日,判断是否是合法日期即可。(时间复杂度 1e10,符合题目要求的)
策略二:仅需枚举年份,然后拆分成回文形式的月日,判断是否合法即可;(时间复杂度 2e3)
策略三:枚举所有的日和月的组合,然后拼接成相应的年份,判断是否合法。(下面的参考代码就是策略三的解法)
【参考代码】
cpp
#include<iostream>
using namespace std;
int x,y;
int day[] = {0,31,29,31,30,31,30,31,31,30,31,30,31};
int ret;
int main(){
cin >> x >> y;
//枚举日月的组合
for(int i = 1;i <= 12;i++){
for(int j = 1;j < day[i];j++){
int num = j%10*1000 + j/10*100 + i%10*10 + i/10;
int k = num*10000 + i*100 +j;
if(k>= x && k<= y) ret++;
}
}
cout << ret << endl;
return 0;
}
1.3 扫雷
题⽬来源: 洛⾕
题⽬链接: P2327 [SCOI2005] 扫雷
难度系数: ★★

【解法】
当第⼀列中,第⼀行的小格子的状态确定了之后,其实后续行的状态也跟着固定下来。而第⼀列中,第⼀行的状态要么有雷,要么没有雷,所以最终的答案就在 0, 1, 2 中。
因此,我们枚举第⼀列中,第⼀行的两种状态:要么有雷,要么没雷。然后依次计算剩下⾏的值,看看是否能满⾜所给的数据。
【参考代码】
cpp
#include<iostream>
using namespace std;
const int N = 1e4 + 10;
int a[N],b[N];
int n;
int check1(){
a[1] = 1;
for(int i = 2;i <= n+1;i++){
a[i] = b[i-1] - a[i-2] - a[i-1];
if(a[i] < 0 || a[i] >1) return 0;
}
if(a[n+1] == 0) return 1;
else return 0;
}
int check2(){
a[1] = 0;
for(int i = 2;i <= n+1;i++){
a[i] = b[i-1] - a[i-2] - a[i-1];
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;
}
2 ⼆进制枚举
⼆进制枚举:用⼀个数⼆进制表示中的 0/1 表示两种状态,从而达到枚举各种情况。
- 利⽤⼆进制枚举时,会用到⼀些位运算的知识。
- 关于用⼆进制中的 0/1 表示状态这种方法,会在动态规划章节中的状态压缩 dp 中继续使用到。
- ⼆进制枚举的方式也可以用递归实现,后续递归算法中会再讲到
2.1 子集
题⽬来源: ⼒扣
题⽬链接:⼦集
难度系数: ★

【解法】
利用二进制枚举的方式,把所有情况都枚举出来( 用零表示不选某一位数,用1表示选择某一位数)
1、枚举0~1右移n-1之间所有的数
2、根据枚举的数中二进制表示中1的位置,把相应的数选出来
【参考代码】

2.2 费解的开关
题⽬来源: 洛⾕
题⽬链接: P10449 费解的开关
难度系数: ★★★

分析题目得出:
1.每一盏灯,最多只会点一次(对于一盏灯而言,只有按或者不按两种状态)
2.按法的先后顺序,是不影响最终结果的(不用关心按的顺序,只用关心按了什么)
3.第一行的按法确定之后,后续灯的按法就跟着确定了
【解法】
1.暴力枚举第一行的所有按法;
2.根据第一行的按法,计算出当前行以及下一行被按之后的结果;
3.根据第一行的结果,推导出第二行的按法;重复2/3过程
4.直到按到最后一行,然后判断所有灯是否全亮。
如何实现这个解法?
1.用二进制枚举第一行所有的按法,0 ~(1 << 5)-1
2.跟据二进制表示中一共有多少1,计算出一共按了多少次;
3.用二进制表示。来存储灯的初始状态。(存的时候,把0存成1,把1存成0)此时题目就从全亮变成全灭(这样特别好判断终止情况,同时代码更简单)
4.用位运算的知识,快速计算出被按之后的状态
5.根据按法,计算当前行以及下一行被按之后的状态。
【参考代码】
cpp
#include<iostream>
#include<cstring>
using namespace std;
const int N = 10;
int a[N];//二进制表示,来存储灯的状态
int t[N];//备份a数组
int n = 5;
//计算x的二进制表示中一共有多少个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;
//存成相反的
if(ch == '0') a[i] |= 1 << j;
}
}
//统计所有按法中的最小值
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;
}
2.3 Even Parity
题⽬来源: 洛⾕
题⽬链接: Even Parity
难度系数: ★★★

分析题目得出:
1.针对每一个0,只有变一次,或者不变两种清况,因此,每一个0都有两种状态
2.当我们决定改变哪些0之后,改变的顺序是不影响最终结果的,我们只需要关心哪些0最终被改变即可。
3,当我们第一行的"最终结果"确定之后,后续行的"最终结果"也跟着确定了
【解法】
1.暴力枚举第一行的最终状态
2.判断最终状态是否合法;
3.如果合法,推导出下一行的最终状态。重复2、3过程
4.直到推导到最后一行,并判断最后一行是否合法。
如何实现这个解法?
1.直接用二进制枚举第一行有可能得所有最终结果0~(1<<n)-1
2.一层for循环,判断a[i]和最终状态(change)相应的二进制表示中是否合法:
如果是0->1合法,搞一个变量sum记录一下
如果是1->0,不合法,返回一个-1
3.当前行的最终状态a[i]确定之后,如何推导出下一行的最终状态nextchange
【参考代码】
cpp
#include<iostream>
#include<cstring>
using namespace std;
const int N = 20;
int a[N];//用二进制存储状态
int 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 falg = 1;
for(int i = 1;i <= n;i++)
{
// 先判断change是否合法
int c = calc(t[i],change);
if(c == -1)
{
falg = 0;
break;
}
cnt += c;//累加次数
//当前行的最终状态
t[i] = change;
change = t[i-1] ^(t[i]<<1)^(t[i]>>1);
change &= (1 << n)-1;
}
if(falg)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;
}