枚举在大家看来可能是一个非常简单的问题,不就是一个遍历算法嘛,有什么好说的,然而,在参加了北京大学MOOC的算法基础后,我直接被震惊了。原来枚举算法还能这么玩!
好吧,不说有的没得没得了,先来看第一个例子——熄灯问题
熄灯问题
这个问题的描述如下:
一个由按钮组成的矩阵,其中每行有6个按钮,共5行。每个按钮的位置上有一盏灯。当按下一个按钮后,该按钮以及周围位置(上边、下边、左边、右边)的灯都会改变一次。即,如果灯原来是点亮的,就会被熄灭;如果灯原来是熄灭的,则会被点亮。在矩阵角上的按钮改变3盏灯的状态;在矩阵边上的按钮改变4盏灯的状态;其他的按钮改变5盏灯的状态。
在上图中,左边矩阵中用X标记的按钮表示被按下,右边的矩阵表示灯状态的改变。对矩阵中的每盏灯设置一个初始状态。请你按按钮,直至每一盏等都熄灭。与一盏灯毗邻的多个按钮被按下时,一个操作会抵消另一次操作的结果。在下图中,第2行第3、5列的按钮都被按下,因此第2行、第4列的灯的状态就不改变。
请你写一个程序,确定需要按下哪些按钮,恰好使得所有的灯都熄灭。
这个问题的难度简直比鸡百鸭百高上数倍!当然,毕竟这是计算机相关内容,所以免不了会有下列图片的问题:
。。。。。。。。
但是,其实他的基本思路是和枚举算法思路是差不多的。枚举的思路在于,先找到一个不变量,在课程中,老师将它定义为局部,不是那个菊。。。
这个局部有什么特征呢?特征是当你确定了这个局部的具体值时,其他所有的数据都只能依照这个局部的数值来变化。
在这个问题中,局部就是第一行,为什么这么说呢?当我们只要确定了第一行按下的状态,那么其余行的按下状态就也已经确定了!如果你仔细思考一下,会发现这是理所当然的,第一行经历了一次按下后,那么势必会出现灯亮或灯不亮的一些场景,那么只有第二行的按下状态才能影响第一行灯的状态了。也就是说,只有第二行按钮才能熄灭第一行亮着的灯!
同理,也只有第三行的按钮才能熄灭第二行的灯,于是乎,一轮不可抗的力量就会向后面的灯按钮涌去,致使你不按也得按!再挣扎也没用了!人类!哈啊哈哈(好中二。。。
就像这样
我们回过头来再分析一下算法的时间,我们首先一一枚举第一行等的所有按钮按下的状态,第一行有6盏灯,每盏灯可以有按下和不按两种状态,于是他们所有的结果就是2^6=64,然后要将所有的灯遍历一次,那么总遍历数就是64*5*6。如果我们从列的一行开始枚举,那么枚举状态数量将是2^5=32,但是在现实中,还要考虑高速缓存的关系。所以随便哪种都可以。
宽度=w,高度=h,w小于h
我们可以很容易的推算出时间复杂度为theta(2^w)。指数级也是没办法的事=_=#
下面就看一下它的伪代码实现:
得到输入puzzle[5][6]二维数组
初始化按下数组Press[5][6]
for 第一行Press[1]的每一种按下去的状态
计算之后的的Press数组的按下状态
看看puzzle数组的地五行是否全部灯灭 puzzle[5]
如果全部灯灭,则打印Press数组
否则继续
如果没有正确的Press数组,那么就报告说,没有方案可以使得灯全部熄灭!
下面我们来把伪代码转换成C++代码
经过作者一小时的编程。。。才觉得课程中的代码简直不要太好!它的优点是没有进行赋值操作,如果我按照自然顺序的话,也就是检测按下灯,而改变灯的状态,那是相当费时费空间的。。。。。。可是老师好像没有给源码的样子。。。
#include <iostream>
void PrintTowDimensionArray(size_t row, size_t col);
void PrintResult(int n);
bool guess();
int EachConvertOneZero(int val);
bool CheckIsInRange(int value, int lower = 0, int upper = 5);
static int Press[5][6], Puzzle[5][6];
int main() {
/*得到输入puzzle[5][6]二维数组
初始化按下数组Press[5][6]
for 第一行Press[1]的每一种按下去的状态
计算之后的的Press数组的按下状态
看看puzzle数组的地五行是否全部灯灭 puzzle[5]
如果全部灯灭,则打印Press数组
否则继续
如果没有正确的Press数组,那么就报告说,没有方案可以使得灯全部熄灭!*/
size_t count;
std::cin >> count;
for (size_t n = 0; n < count; n++) {
//Get Input Puzzle,Init Press
for (size_t row = 0; row < 5; row++) {
for (size_t col = 0; col < 6; col++) {
std::cin >> Puzzle[row][col];
Press[row][col] = 0;
}//for col
}//for row
if (guess()) {
PrintResult(n);
continue;
}
bool IsOK = false;
//every case in first row of Press
for (size_t i = 1; i < 64; i++) {
Press[0][0]++;
int c = 0;
while (Press[0][c]>1) {
Press[0][c] = 0;
c++;
Press[0][c]++;
}
if (guess()) {
PrintResult(n);
IsOK = true;
break;
}
}
if (IsOK) {
std::cout << "Puzzle#" << n + 1 << " No Result\0";
}
}//for case Number
getchar();
getchar();
return 0;
}
void PrintTowDimensionArray(size_t row, size_t col) {
for (size_t i = 0; i < row; i++) {
for (size_t j = 0; j < col; j++) {
std::cout << Press[i][j] << " ";
}
std::cout << '\n';
}
}
void PrintResult(int i) {
std::cout << "Puzzle#" << i << '\0';
PrintTowDimensionArray(5, 6);
std::cout << std::endl;
}
bool guess() {
int PuzzleCopy[5][6];
for (size_t i = 0; i < 5; i++) {
for (size_t j = 0; j < 6; j++) {
PuzzleCopy[i][j] = Puzzle[i][j];
}
}
auto PressCopy = Press;
for (size_t row = 0; row < 5; row++) {
for (size_t col = 0; col < 6; col++) {
if (row > 0) {
PressCopy[row][col] = PuzzleCopy[row - 1][col];
}
if (PressCopy[row][col]) {
if (CheckIsInRange(row - 1, 0, 4)) {
PuzzleCopy[row - 1][col] = EachConvertOneZero(PuzzleCopy[row - 1][col]);
}
if (CheckIsInRange(row + 1, 0, 4)) {
PuzzleCopy[row + 1][col] = EachConvertOneZero(PuzzleCopy[row + 1][col]);
}
if (CheckIsInRange(col - 1, 0, 5)) {
PuzzleCopy[row][col - 1] = EachConvertOneZero(PuzzleCopy[row][col - 1]);
}
if (CheckIsInRange(col + 1, 0, 5)) {
PuzzleCopy[row][col + 1] = EachConvertOneZero(PuzzleCopy[row][col + 1]);
}
PuzzleCopy[row][col] = EachConvertOneZero(PuzzleCopy[row][col]);
}
}
}
for (size_t i = 0; i < 6; i++) {
if (PuzzleCopy[4][i]) {
return false;
}
}
return true;
}
int EachConvertOneZero(int val) {
return val == 0 ? 1 : 0;
}
bool CheckIsInRange(int value, int lower, int upper) {
return value >= lower&&value <= upper;
}
虽然我这个要比老师的代码直观一点点。。。不过在运行效率和空间效率上以及代码量上都不太理想。。。
好了,还有一个青蛙跳的问题,以及两道作业,额。。。人生好艰难=_=#