枚举法
是一种将问题的所有可能的答案列举出来,然后再代入原问题中去验证是否正确,如果正确就保留,不正确就丢掉
枚举法是一种直接解决问题的方法,优点是解决思路清晰,编写程序简结,缺点是应付大规模问题时会非常冗长,做很多无用功,甚至会超出枚举者的枚举极
限(如无限个解)
但是我们也应该看到枚举法的优点,在一些规模小的问题,应用枚举法会快速解决问题。
应用枚举法的场合:
1.搜索解空间
解空间就是一个问题的解的集合,所以枚举解空间就最直接不过了。
我们先去考虑解空间的大小,如果解空间不大,就可以尝试用枚举
这方面的问题有:
http://acm.pku.edu.cn/JudgeOnline/problem?id=1013
该题目大意是有标有A-L的币,其中一枚就假的,但并不清楚假币比真币轻还是重,在输入处有3个比较的结果,输出假币的ID
解决该方法最直接是枚举,先看解空间,由于只有A-L 12个币,枚举的话只有12种情况,可以枚举。先假设某一币是假币,然后代入比较结果比较
另外一个有关这个问题是
http://acm.pku.edu.cn/JudgeOnline/problem?id=1029
当然另外一个解法是标记法
2.直接计算
可以直接代入公式计算,如求和,等差数列等
3.模拟法
先枚举,再模拟
如
http://acm.pku.edu.cn/JudgeOnline/problem?id=1753
一个4*4的棋盘,上面放有16个片,片的上下面是反色的,比如上面是黑,那么下面是白,现在要求根据输入的图案矩阵,求出将棋盘全部翻白或全部变黑的最
短移动次数。
此题看上去就有种要用广搜的感觉,但是由于题目限定了4*4,那么就有枚举的路可走,事实上枚举比BFS要快。
怎么枚举呢?从翻子的特点看出,如果翻一个棋子,那么该子周围的子都要被翻,似乎没有确定的顺序可循。如果我们从第1行开始翻,结果会影响第1,2行,
第2行翻的话会影响第1和2,3行,那么我们可以先翻第一行,在挑选第2行要翻的棋子时,查看第一行不满足要求的棋(这样就不会影响到已经翻好的棋子),
一直翻到末尾,如果符合要求就是一个可行解。
故我们只需要枚举第一行的所有可能:2^4 = 16种,然后进行模拟对比是否符合要求。
一下给出AC代码:
view plaincopy to clipboardprint?
#include <iostream>
#include <string>
#include <bitset>
#include <stdio.h>
using namespace std;
int map[6][6];
int state[6][6];
bool bCanSolve;
int shortest;
const int Max = 1 << 30;
//翻转
inline void Flip(const int x,const int y)
{
state[y-1][x] ^=1;
state[y][x] ^= 1;
state[y][x-1] ^=1;
state[y+1][x] ^=1;
state[y][x+1] ^=1;
}
//输出(调试用)
void Output()
{
for(int i = 1 ; i <= 4; ++i)
{
for(int j = 1; j <= 4; ++j)
cout << state[i][j];
cout << endl;
}
cout << endl;
}
//复制
void CopyToState()
{
for(int i = 1; i <= 4; ++i)
{
for(int j = 1; j <= 4; ++j)
state[i][j] = map[i][j];
}
}
int Try(const int value)
{
int min = 1 << 30;
for(int i = 0; i < 16; ++i)
{
int counter = 0;
bitset<4> bits(i);
CopyToState();
for(int j = 1; j <= 4;++j)//进行第一行枚举
{
if(bits[j-1] == 1)
{
Flip(j,1);
counter++;
}
}
//一下进行模拟验证
for(int j = 2; j <= 4;++j)
{
for(int k = 1;k <= 4; ++k)
{
if(state[j-1][k] == value)
{
Flip(k,j);
counter++;
}
}
}
bool flag = true;
for(int j = 1; j <= 4; ++j)
{
if(state[4][j] == value)
{
flag = false;
break;
}
}
if(flag && min > counter)
{
min = counter;
}
if(min == 0)
break;
}
return min;
}
void Input()
{
string str;
for(int i = 1; i <= 4; ++i)
{
cin >> str;
for(int j = 0; j < str.size();++j)
{
if(str[j] == 'b')
map[i][j+1] = 0;
else if(str[j] == 'w')
map[i][j+1] = 1;
}
}
}
int main()
{
shortest = Max;
Input();
int amin = Try(0);
int bmin = Try(1);
shortest = amin > bmin ? bmin:amin;
if(shortest != Max)
cout << shortest << endl;
else
cout << "Impossible" << endl;
return 0;
}
#include <iostream>
#include <string>
#include <bitset>
#include <stdio.h>
using namespace std;
int map[6][6];
int state[6][6];
bool bCanSolve;
int shortest;
const int Max = 1 << 30;
//翻转
inline void Flip(const int x,const int y)
{
state[y-1][x] ^=1;
state[y][x] ^= 1;
state[y][x-1] ^=1;
state[y+1][x] ^=1;
state[y][x+1] ^=1;
}
//输出(调试用)
void Output()
{
for(int i = 1 ; i <= 4; ++i)
{
for(int j = 1; j <= 4; ++j)
cout << state[i][j];
cout << endl;
}
cout << endl;
}
//复制
void CopyToState()
{
for(int i = 1; i <= 4; ++i)
{
for(int j = 1; j <= 4; ++j)
state[i][j] = map[i][j];
}
}
int Try(const int value)
{
int min = 1 << 30;
for(int i = 0; i < 16; ++i)
{
int counter = 0;
bitset<4> bits(i);
CopyToState();
for(int j = 1; j <= 4;++j)//进行第一行枚举
{
if(bits[j-1] == 1)
{
Flip(j,1);
counter++;
}
}
//一下进行模拟验证
for(int j = 2; j <= 4;++j)
{
for(int k = 1;k <= 4; ++k)
{
if(state[j-1][k] == value)
{
Flip(k,j);
counter++;
}
}
}
bool flag = true;
for(int j = 1; j <= 4; ++j)
{
if(state[4][j] == value)
{
flag = false;
break;
}
}
if(flag && min > counter)
{
min = counter;
}
if(min == 0)
break;
}
return min;
}
void Input()
{
string str;
for(int i = 1; i <= 4; ++i)
{
cin >> str;
for(int j = 0; j < str.size();++j)
{
if(str[j] == 'b')
map[i][j+1] = 0;
else if(str[j] == 'w')
map[i][j+1] = 1;
}
}
}
int main()
{
shortest = Max;
Input();
int amin = Try(0);
int bmin = Try(1);
shortest = amin > bmin ? bmin:amin;
if(shortest != Max)
cout << shortest << endl;
else
cout << "Impossible" << endl;
return 0;
}
另外一个用枚举的题目:
http://acm.pku.edu.cn/JudgeOnline/problem?id=2965
大意是一个冰箱有16个开关,用矩阵表示成4*4,需要把这些开关全部打开,但是没操作一个开关都会把这些开关相应的行列的所有开关都翻转一次,求最小步
骤并输出
看起来和上题有点像,关键是4*4是确定的,所以有可以用枚举法把所有解枚举验证
怎么枚举呢?先看这个操作的性质我们可以尝试这么一个操作:
-+--
----
----
-+--
把(1,1)翻转:
+-++
+---
+---
++--
把(1,3)翻转
-+--
+-+-
+-+-
+++-
再把(1,1)翻转
+-++
--+-
--+-
--+-
看这个结果相当于什么呢?其实就等效于(1,3)翻转,那么我们可以猜想:对于每个点,要么翻转一次,要么不翻转
其实可以这么想:将4*4阵列看作一个16位的整数,翻转就是将对应位置和某个数异或,那么异或两次就等于原状态,所以得证
得到这个性质,就很好枚举了:从0-2^16每个枚举(相应的1就是开关操作的位置),再进行模拟验证
代码:
+ expand sourceview plaincopy to clipboardprint?
#include <iostream>
#include <string>
using namespace std;
//翻转某一个位进行对应行列变换的数组
int Params[16] = {0x111F,0x222F,0x444F,0x888F,
0x11F1,0x22F2,0x44F4,0x88F8,
0x1F11,0x2F22,0x4F44,0x8F88,
0xF111,0xF222,0xF444,0xF888};
//取每一位的数组
int BitParams[16] = {0x01,0x02,0x04,0x08,
0x10,0x20,0x40,0x80,
0x100,0x200,0x400,0x800,
0x1000,0x2000,0x4000,0x8000};
void Input(int &nValue)
{
string str;
for(int i = 0; i < 4; ++i)
{
cin >> str;
for(int j =0; j < str.size(); ++j)
{
if(str[j] == '-')
nValue &= ~(BitParams[i*4+j]);
else if(str[j] == '+')
nValue |= BitParams[i*4+j];
}
}
}
void Output(int value)
{
int count = 0;
for(int i = 0; i < 16; ++i)
{
if(value & BitParams[i])
count++;
}
cout << count << endl;
for(int i = 0; i < 16; ++i)
{
if(value & BitParams[i])
cout << (i / 4)+1 << " " << (i % 4)+1 << endl;
}
}
//枚举验证
int Try(int nValue)
{
int temp = nValue;
int min = 1 << 30;
int record = -1;
int count;
for(int i = 0; i < (65536);++i)
{
count = 0;
temp = nValue;
for(int j = 0;j < 16;++j)
{
if(i & BitParams[j])
{
temp ^= Params[j];
++count;
}
}
if(temp == 0 && min > count)
{
min = count;
record = i;
}
}
return record;
}
int main()
{
int value = 0;
int result ;
Input(value);
result = Try(value);
if(result != -1)
Output(result);
else
cout << "no!"<<endl;
return 0;
}
#include <iostream>
#include <string>
using namespace std;
//翻转某一个位进行对应行列变换的数组
int Params[16] = {0x111F,0x222F,0x444F,0x888F,
0x11F1,0x22F2,0x44F4,0x88F8,
0x1F11,0x2F22,0x4F44,0x8F88,
0xF111,0xF222,0xF444,0xF888};
//取每一位的数组
int BitParams[16] = {0x01,0x02,0x04,0x08,
0x10,0x20,0x40,0x80,
0x100,0x200,0x400,0x800,
0x1000,0x2000,0x4000,0x8000};
void Input(int &nValue)
{
string str;
for(int i = 0; i < 4; ++i)
{
cin >> str;
for(int j =0; j < str.size(); ++j)
{
if(str[j] == '-')
nValue &= ~(BitParams[i*4+j]);
else if(str[j] == '+')
nValue |= BitParams[i*4+j];
}
}
}
void Output(int value)
{
int count = 0;
for(int i = 0; i < 16; ++i)
{
if(value & BitParams[i])
count++;
}
cout << count << endl;
for(int i = 0; i < 16; ++i)
{
if(value & BitParams[i])
cout << (i / 4)+1 << " " << (i % 4)+1 << endl;
}
}
//枚举验证
int Try(int nValue)
{
int temp = nValue;
int min = 1 << 30;
int record = -1;
int count;
for(int i = 0; i < (65536);++i)
{
count = 0;
temp = nValue;
for(int j = 0;j < 16;++j)
{
if(i & BitParams[j])
{
temp ^= Params[j];
++count;
}
}
if(temp == 0 && min > count)
{
min = count;
record = i;
}
}
return record;
}
int main()
{
int value = 0;
int result ;
Input(value);
result = Try(value);
if(result != -1)
Output(result);
else
cout << "no!"<<endl;
return 0;
}
最后总结一下枚举法的思路:
(1)确定枚举对象、枚举范围和判定条件;
(2)一一枚举可能的解,验证是否是问题的解