二进制枚举
一、什么是二进制枚举
在计算机科学中,二进制枚举(Binary Enumeration)是一种通过二进制数的位组合来枚举所有可能状态的算法技巧。它常用于解决组合优化、子集生成、状态压缩等问题。
核心思想
对于一个包含 n
个元素的集合,其所有子集的数量为(每个元素可以选择「包含」或「不包含」)。而一个
n
位的二进制数恰好有种不同的状态,每一位可以表示集合中对应元素的取舍。因此,可以用二进制数的每一种状态来表示集合的一个子集。
二、如和判断该不该使用二进制枚举?或是直接dfs?
1. 问题规模极小(n ≤ 20)
二进制枚举的时间复杂度是 O(2^n * n)
,当 n
超过 20 时,2^20 ≈ 1e6
(可接受),但 2^30 ≈ 1e9
(会超时)。因此:
- 若
n ≤ 20
:二进制枚举可行(如枚举 15 个元素的所有子集)。 - 若
n > 20
:二进制枚举几乎必然超时,优先考虑 DFS(结合剪枝)。
2. 状态是简单的 “二元选择”
二进制枚举的本质是用位表示 “选 / 不选”,仅适合每个元素只有两种状态的问题:
- 例如:枚举集合的所有子集、0-1 背包问题(物品要么选,要么不选)、判断某元素是否在子集中等。
- 若状态更复杂(如元素可选择多次、需要记录选择顺序、或状态包含数值信息),二进制枚举无法表示,必须用 DFS。
无需剪枝,必须遍历所有状态
如果问题要求遍历所有可能的状态(如统计所有满足条件的子集数量),且规模较小,二进制枚举比 DFS 更简洁。
- 例如:“找出所有和为 10 的子集”,若 n=15,二进制枚举直接遍历所有 32768 个子集即可,代码比 DFS 更短。
三、例题
洛谷传送门:P10449 费解的开关 - 洛谷
分析:
首先明确题义改变矩阵中任意一个地方的值,都会使得该点上下左右的点进行相应改变。题目要求给定一个矩阵,判断是否以上述规则在六步之内把整个矩阵改成全是1
先看输入
int n;scanf("%d",&n);while(n--){// 按行输入,把每一行当成一个字符串for(int i=0;i<5;i++){cin>>g[i];}
枚举
此处我们的原则是这一行确定了,就不在考虑范围之内
-
假设我们已经处理完第
i-1
行,并且通过第i
行的操作确保了第i-1
行的灯全部亮着。 -
接下来处理第
i
行时,所有操作都在第i+1
行进行(通过按下i+1,j
来修复i,j
的灯)。 -
由于第
i+1
行的操作只会影响第i
行、i+1
行、i+2
行,不会再改变第i-1
行的状态,因此第i-1
行的 “全亮” 状态是稳定的,无需再回头。
int res=2e9+10;//因为直接按第二行的灯不是全局最优,所以从第一行开始考虑//但是因为不知道到底怎么按,所以枚举第一行的所有可能 for(int op=0;op<32;op++){//op既可以当枚举的变量,他的二进制也可以表示第一行的状态//因为+1的步长可以使得枚举完所有可能的第一行 //但是要注意的是 因为第一行的状态早在输入的时候就确定了,所以我们枚举的一直都是操作//注意op的二进制比如(00101)表示按下那两个位置为1的灯 memcpy(backup, g, sizeof g);//备份数组 int step=0;//记录操作次数 // 第一行的按法(在这里1按了,0表示不按)for(int i=0;i<5;i++)if(op>>i&1){ //判断第i位是不是1 step++;turn(0,i);}// 然后通过第一行按完之后的状态,按234行for(int i=0;i<4;i++){//原则就是上一行是0就按下这一行,确定了的行一定不能更改 for(int j=0;j<5;j++ ){if (g[i][j]=='0'){step++;turn(i+1,j);// 如果这个位置是灭的,就按下一行对应的位置}} }bool dark=false;for(int j=0;j<5;j++){if (g[4][j]=='0'){dark=true;break;}}//最后一行无论如何都改不了因为没有下一行给他按了。所有但凡有黑的,这次一定不行 // 对于32种情况的这一种,如果所有的全亮就记录下步数(事实上只记录了最后一行是否dark)if (!dark) res=min(res,step);memcpy(g,backup, sizeof g);}if(res>6) res=-1;cout<<res<<endl;
关灯函数
void turn(int x,int y){//对于(x,y)的灯进行开关 for(int i=0;i<5;i++){int a=x+dx[i],b=y+dy[i];if(a<0||a>=5||b<0||b>=5) continue;g[a][b]^=1;}
}
Talk is cheap,show me the code
#include<bits/stdc++.h>
using namespace std;
const int N=6;
int dx[N]={-1,0,1,0,0},dy[N]={0,1,0,-1,0};
char g[N][N],backup[N][N];void turn(int x,int y){//对于(x,y)的灯进行开关 for(int i=0;i<5;i++){int a=x+dx[i],b=y+dy[i];if(a<0||a>=5||b<0||b>=5) continue;g[a][b]^=1;}
}int main(){int n;scanf("%d",&n);while(n--){// 按行输入,把每一行当成一个字符串for(int i=0;i<5;i++){cin>>g[i];}int res=2e9+10;//因为直接按第二行的灯不是全局最优,所以从第一行开始考虑//但是因为不知道到底怎么按,所以枚举第一行的所有可能 for(int op=0;op<32;op++){//op既可以当枚举的变量,他的二进制也可以表示第一行的状态//因为+1的步长可以使得枚举完所有可能的第一行 //但是要注意的是 因为第一行的状态早在输入的时候就确定了,所以我们枚举的一直都是操作//注意op的二进制比如(00101)表示按下那两个位置为1的灯 memcpy(backup, g, sizeof g);//备份数组 int step=0;//记录操作次数 // 第一行的按法(在这里1按了,0表示不按)for(int i=0;i<5;i++)if(op>>i&1){ //判断第i位是不是1 step++;turn(0,i);}// 然后通过第一行按完之后的状态,按234行for(int i=0;i<4;i++){//原则就是上一行是0就按下这一行,确定了的行一定不能更改 for(int j=0;j<5;j++ ){if (g[i][j]=='0'){step++;turn(i+1,j);// 如果这个位置是灭的,就按下一行对应的位置}} }bool dark=false;for(int j=0;j<5;j++){if (g[4][j]=='0'){dark=true;break;}}//最后一行无论如何都改不了因为没有下一行给他按了。所有但凡有黑的,这次一定不行 // 对于32种情况的这一种,如果所有的全亮就记录下步数(事实上只记录了最后一行是否dark)if (!dark) res=min(res,step);memcpy(g,backup, sizeof g);}if(res>6) res=-1;cout<<res<<endl;}return 0;
}