枚举算法实例应用(二)
Extended Lights Out
Description
In an extended version of the game Lights Out, is a puzzle with 5 rows of 6 buttons each (the actual puzzle has 5 rows of 5 buttons each). Each button has a light. When a button is pressed, that button and each of its (up to four) neighbors above, below, right and left, has the state of its light reversed. (If on, the light is turned off; if off, the light is turned on.) Buttons in the corners change the state of 3 buttons; buttons on an edge change the state of 4 buttons and other buttons change the state of 5. For example, if the buttons marked X on the left below were to be pressed,the display would change to the image on the right.
The aim of the game is, starting from any initial set of lights on in the display, to press buttons to get the display to a state where all lights are off. When adjacent buttons are pressed, the action of one button can undo the effect of another. For instance, in the display below, pressing buttons marked X in the left display results in the right display. Note that the buttons in row 2 column 3 and row 2 column 5 both change the state of the button in row 2 column 4,so that, in the end, its state is unchanged.
Note:
- It does not matter what order the buttons are pressed.
- If a button is pressed a second time, it exactly cancels the effect of the first press, so no button ever need be pressed more than once.
- As illustrated in the second diagram, all the lights in the first row may be turned off, by pressing the corresponding buttons in the second row. By repeating this process in each row, all the lights in the first four rows may be turned out. Similarly, by pressing buttons in columns 2, 3, all lights in the first 5 columns may be turned off. Write a program to solve the puzzle.
Input
The first line of the input is a positive integer n which is the number of puzzles that follow. Each puzzle will be five lines, each of which has six 0 or 1 separated by one or more spaces. A 0 indicates that the light is off, while a 1 indicates that the light is on initially.
Output
For each puzzle, the output consists of a line with the string: "PUZZLE #m", where m is the index of the puzzle in the input file. Following that line, is a puzzle-like display (in the same format as the input) . In this case, 1's indicate buttons that must be pressed to solve the puzzle, while 0 indicate buttons, which are not pressed. There should be exactly one space between each 0 or 1 in the output puzzle-like display.
Sample Input
2
0 1 1 0 1 0
1 0 0 1 1 1
0 0 1 0 0 1
1 0 0 1 0 1
0 1 1 1 0 0
0 0 1 0 1 0
1 0 1 0 1 1
0 0 1 0 1 1
1 0 1 1 0 0
0 1 0 1 0 0
Sample Output
PUZZLE #1
1 0 1 0 0 1
1 1 0 1 0 1
0 0 1 0 1 1
1 0 0 1 0 0
0 1 0 0 0 0
PUZZLE #2
1 0 0 1 1 1
1 1 0 0 0 0
0 0 0 1 0 0
1 1 0 1 0 1
1 0 1 1 0 1
题设分析:
根据题设可分析出以下两种潜在的规定:
- 开关按下的先后顺序与结果的正确性无关,即先按哪个后按哪个不改变最后一个开关按下时灯的亮灭情况。
- 每一个按钮按下多次时,将抵消前面按下时所产生的结果,即某按钮按下两次,将恢复至初始状态,即未按下按钮状态。所以按钮只有按下和没有按下两种情况,不妨用0表示没有按下,1表示按下。
解题分析:
- 枚举,即暴力破解。第一想法自然是枚举所有可能的开关状态,对每一个状态计算一下最后的灯的亮灭情况,若均熄灭则表示该状态为解。
- 每个开关有且仅有两种状态,且开关矩阵为5×6的矩阵,共30个开关,所以一共有2^30种可能的解,是一个相当大的数据量。所以应考虑剪枝,以期减少枚举的轮次。
- 不难想到,枚举一个“局部”,当局部被确定时,剩下的部分也随之确定,不可能发生变化,所以我们只需枚举确定的局部可能的情况即可,以此来极大程度上减少枚举的轮次。
- 而通过开关操作事第一行全部熄灭就是这样的一个局部。如:当按下若干开关使得第一行灯全部熄灭,开关会使第二行该开关下面的灯的亮灭情况翻转,所以,第二行的开关按动方案也随之确定,即将第二行仍然亮着的灯的开关按下,至此第一、二行全部熄灭一次类推,而当按下第三行的开关时,并不会影响第一行灯的亮灭。
- 所以,若按此方式到第五行开关完成按动之后第五行灯为全部熄灭状态则表明该第一行的局部是可行的,即找到一个解。若第5行开关按动完成之后后仍有未熄灭的灯,则表明以该第一行为局部的解是不可行方案,因此抛弃之,进行下一个第一行状态的试探直至出现第一个可行的方案即为解。
综上,只需以第一行的2^6种方案为局部进行2^6次枚举即可。
假若以int数组来存储灯源矩阵,当灯源矩阵规模足够大时,int数组占用空间大的劣势就体现出来了。
因此考虑数据类型的内存占用问题:
- int类型占用4B(字节)的内存大小,共4*8=32位,有符号数可表示-2^31 ~ 2^31 - 1内的整数,显然对于Extended Lights Out问题来说,远远超过了问题有效数值范围,以至于空间利用率极低。
- char类型占用1B(字节)的内存大小,共1*8=8位。有符号数可表示-2^{7}~2^7 - 1。
- 由于开关的有效的状态仅有两种,不难想到将其与二进制数及其运算挂钩。一个char类型变量由8位二进制数表示,因为该题灯源矩阵为5×6的矩阵,每一行共有6列,因此一个char类型变量足以存储一行的灯状态,共需5个char类型即可存储整个灯源矩阵。此时空间利用率将达到6/8=75%。同时,若等原矩阵为8列时,可达100%,当灯源矩阵大于8时,仅需再增加一个char类型变量表示即可。
- 相比于int类型存极大的增加了空间利用率。
解题步骤:
在考虑采用char类型存储灯源矩阵后,新的问题迎面而来——运算。
- char类型存储虽然大大减少了空间浪费,但是对于读取、设置某一行某一列灯的亮灭状态;以及对于当开关按下时,灯的亮灭状态的翻转是一个新的难题。
- 由于二进制数的特殊性,可以想到采用位运算将二进制数的某一位置1、置1、取反等操作。对于位运算的应用,应该充分理解二进制运算的本质,及逻辑运算规则,在此不再阐述。读者可自行查阅二进制逻辑运算相关知识自行补充。
下面具体介绍关于本题结合二进制数及其逻辑运算以减少空间复杂度的算法:
读取二进制数第index位二进制值
函数参数中str为第n行的char类型数据,因为char类型占用8bit(位),所以对于该题,第六位为有效数据,高两位补充为0,无异议,
假设传入str为00011001,有效数据为第六位(011001)。计算机规定从右到左为位数递增次序。即:
0 | 1 | 1 | 0 | 0 | 1 |
---|---|---|---|---|---|
第5位 | 第4位 | 第3位 | 第2位 | 第1位 | 第0位 |
- 若要取“00x000”中的x,则需通过00x000&001000=x,得到x,问题转化为如何获取001000,首先想到的是,将000000中第index位置为1,需要通过for循环index次到第index位,将其置为1。
- 但是CPU中有移位器,可以通过通过硬件快速实现读取第i位,比for循环要快的多且不占用更多的内存。
- 因此我们将00x000和001000同时右移3位,得到00000x、000001,通过与逻辑与运算00000x&000001=x得到x。
综上:该子问题的解将简化为读取00x000第3位的x时,将00x000右移3位,再与1做逻辑与运算得到目标值x。
举例:
- 若要取第1位的0,则应将str右移1位,变为001100,通过与“000001”做逻辑与运算,即可得到当前最低位“0”与1做与运算的结果,即0&1=0,取到结果为0。
- 若要取第3位的1,则应将str右移3位,变为000011,通过与“000001”做逻辑与运算,即可得到当前最低位“1”与1做与运算的结果,即1&1=1,取到结果为1。
- 若要取第5位的0,则应将str右移5位,变为000000,通过与“000001”做逻辑与运算,即可得到当前最低位“0”与1做与运算的结果,即0&1=0,取到结果为0。
代码:
int GetBitOfIndex(char str, int index) { //获取字符str的第index个比特
return (str >> index) & 1;
}
设置二进制数的第index位的值
因为需要修改二进制数串str的值,所以应读入二进制数串str的指针,设置为引用类型,否则只能在函数内修改,无法作用于调用函数处。参数index、value为修改第index位的数值为value。
将index位设置为1时,基本思路是将00x000的第index位和1做逻辑或运算,因为任何数与1做或运算恒等于1。
所以该子问题的解将简化为设置00x000第3位的x为1时,将001000与00x000做逻辑或运算。而001000可以通过将1左移3位获得。
该步代码描述为:
*str = *str | (1 << index);
将index位设置为0时,基本思路是将00x000与110111做逻辑与运算,其他位均为1,做与运算时数值保持不变,第三位和0做与运算时恒为0。
所以该子问题的解将简化为设置00x000第3位的x为0时,将00x000与110111做逻辑与运算。而110111可以通过将1左移3位得到001000,再取反,得到110111。
该步代码描述为:
*str = *str & (~(1 << index));
代码:
void SetBitOfIndex(char *str, int index, int value) { //设置字符str的第index位为val
if (value) {
//若要设置为1,则将str与00...100...00(第index位为1,其余位为0)做或运算。
*str |= (1 << index);
} else {
//若要设置为0,则将str与11...011...11(第index位为0,其余位为1)做与运算再取反。
*str &= ~(1 << index);
}
}
取反二进制数的第index位
因为需要修改二进制数串str的值,所以应读入二进制数串str的指针,设置为引用类型,否则只能在函数内修改,无法作用于调用函数处。参数index将第index位的数值取反。
对于某位取反运算的基本思路是做异或运算,即将00x000的第3位x与1做异或运算;
因为0 ^ 1 = 1,1 ^ 1=0。即任何数与1做异或运算为将该数取反。
用代码可描述为:
*str = *str ^ (1 << index);
代码:
void NegationBitOfIndex(char *str, int index) {//翻转字符str的第index位
//将字符str与00...100...00(第index位为1,其余位为0)做异或运算
*str ^= (1 << index);
}
输出开关 / 灯源 矩阵
较为简单不再赘述。
void MatrixOutput(char Matrix[]) {//输出开关矩阵
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 6; j++) {
printf("%d", GetBitOfIndex(Matrix[i], j));
if (j < 5) {
printf(" ");
}
}
printf("\n");
}
}
设计算法主函数
由于解题分析可知,应设置如下变量:
准备工作
存储题目输入的源灯矩阵;
char OriginLights[5]={0,}; //存储源灯矩阵,默认均为0
存储枚举时,开关起作用之后变化中的灯矩阵的亮灭状态;
char ChangingLights[5]={0,}; //存储枚举时变化的灯矩阵,默认均为0
存储能使所有灯都熄灭的开关矩阵;
char result[5]={0,}; //存储结果按钮矩阵,默认均为0
读入源灯矩阵。
for (int i = 0; i < 5; i++) { for (int j = 0; j < 6; j++) { int bit; scanf("%d", &bit); //设置源灯矩阵第i行第j列为bit SetBitOfIndex(&OriginLights[i], j, bit); } }
算法主体
有解题分析知,需要以第一行为局部进行枚举,因为第一行由6位二进制数组成,所以应有2^6=64种排列方式。用0~63记录这64次枚举。
再每一轮次的局部状态枚举中,首先应将源灯状态矩阵Origin Lights由变化中的灯矩阵Changing Lights暂存起来进行运算,以保护下次枚举时源灯矩阵的正确性。可以通过string.h头文件下memcpy()函数完成。
即:
//将源灯矩阵的前sizeof(OriginLights)位,即前6位存入ChangingLights中进行后续运算 memcpy(ChangingLights, OriginLights, sizeof(OriginLights));
在这64次以第一行局部状态开始的枚举中,开始依次处理第i行开关,由题设分析知:当第1行开关的按动次序确定之后,其余5行的开关次序也一并确定,所以,我们假设第i行开关已经确定为switchs。
- 将第i行开关状态存入结果矩阵的第i行中,即存入result[i]。
依次处理第i行的每一列:若第j列为1,表明灯是亮的,应按下(i,j)处按钮将其熄灭,但同时也会导致(i,j-1)、(i,j+1)两个位置的灯的状态翻转。故而应分情况讨论:
- 若j=0,即第一列时,影响本位、右位。
- 若0<j<5,即非两边届时,影响左位、本位、右位。
- 若j=5,则表示为最右侧一列,仅影响左位和本位。
化简后可表述为:
for (int j = 0; j < 6; j++) { if (GetBitOfIndex(switchs, j) == 1) { if (j > 0) //翻转第i行,j-1列的灯的亮灭情况 NegationBitOfIndex(&ChangingLights[i], j - 1); //翻转第i行,j列的灯的亮灭情况 NegationBitOfIndex(&ChangingLights[i], j); if (j < 5) //翻转第i行,j+1列的灯的亮灭情况 NegationBitOfIndex(&ChangingLights[i], j + 1); } }
若该行不为最后一行,则应处理对该行开关改动时所影响的下一行的灯的亮灭状态。
只需将该下一行的灯的状态与switchs做一次异或运算即可翻转该列的灯的状态。
比如:
本行按动开关的状态为011001,即按下第1,2,5列开关
下一行灯矩阵状态为100101,因为任何数与1做异或运算均可得到该数的相反数,所以通过下一行灯状态与本行开关状态做异或运算将本行开关状态影响的下一行灯的状态进行翻转。
如011001^100101=111100,将100101,1、2、5位翻转为111100
即:
if (i < 4) {//若当前不是最后一行则翻转下一行开关所影响的灯的亮灭。 //翻转i+1行switchs为1的bit位的灯亮灭情况 ChangingLights[i + 1] ^= switchs; }
此时第i行开关影响灯的亮灭状态的处理已经完成,那么第i+1行开关状态此时也就随之确定,即按动开关后应熄灭第i行的所有灯,所以switchs应修改为第i行的灯的状态。
即:
//当第i行确定之后,第i+1行也随之确定,即第i+1行开关熄灭第i行还亮着的灯即可 switchs = ChangingLights[i];
至此,本轮次局部状态的枚举处理已经完成,若此时最后一行的灯均为熄灭状态,则表示该局部状态为一个解,输出此时的开关矩阵即可,同时退出循环。
即:
if (ChangingLights[4] == 0) {//所有灯均已熄灭,表明本局部状态下的开关矩阵为解 //输出解,即开关矩阵 MatrixOutput(result); break; }
- 算法已经结束,只需再依据题目输入输出规则进行简单修改即可提交,此步不在赘述。
代码整合:
int GetBitOfIndex(char str, int index) { //获取字符str的第index个比特
return (str >> index) & 1;
}
void SetBitOfIndex(char *str, int index, int value) { //设置字符str的第index位为val
if (value) {
//若要设置为1,则将str与00...100...00(第index位为1,其余位为0)做或运算。
*str |= (1 << index);
} else {
//若要设置为0,则将str与11...011...11(第index位为0,其余位为1)做与运算。
*str &= ~(1 << index);
}
}
void NegationBitOfIndex(char *str, int index) {//翻转字符str的第index位
//将字符str与00...100...00(第index位为1,其余位为0)做异或运算
*str ^= (1 << index);
}
void MatrixOutput(char Matrix[]) {//输出开关矩阵
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 6; j++) {
printf("%d", GetBitOfIndex(Matrix[i], j));
if (j < 5) {
printf(" ");
}
}
printf("\n");
}
}
void ExtentedLightsOut() {
char OriginLights[5] = {0,}; //存储源灯矩阵,默认均为0
char ChangingLights[5] = {0,}; //存储枚举时变化的灯矩阵,默认均为0
char result[5] = {0,}; //存储结果按钮矩阵,默认均为0
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 6; j++) {
int bit;
scanf("%d", &bit);
//设置源灯矩阵第i行第j列为bit
SetBitOfIndex(&OriginLights[i], j, bit);
}
}
for (int n = 0; n < 64; n++) {
int switchs = n;//当前行的开关状态
//将源灯矩阵的前sizeof(OriginLights)位,即前6位存入ChangingLights中进行后续运算
memcpy(ChangingLights, OriginLights, sizeof(OriginLights));
for (int i = 0; i < 5; i++) {//处理第i行开关,此时第i行开关状态已经确定为switchs
result[i] = switchs;//暂存如result[i]中。
for (int j = 0; j < 6; j++) {
if (GetBitOfIndex(switchs, j) == 1) {
if (j > 0)
//翻转第i行,j-1列的灯的亮灭情况
NegationBitOfIndex(&ChangingLights[i], j - 1);
//翻转第i行,j列的灯的亮灭情况
NegationBitOfIndex(&ChangingLights[i], j);
if (j < 5)
//翻转第i行,j+1列的灯的亮灭情况
NegationBitOfIndex(&ChangingLights[i], j + 1);
}
}
if (i < 4) {//若当前不是最后一行则翻转下一行开关所影响的灯的亮灭。
//翻转i+1行switchs为1的bit位的灯亮灭情况
ChangingLights[i + 1] ^= switchs;
}
//当第i行确定之后,第i+1行也随之确定,即第i+1行开关熄灭第i行还亮着的灯即可
switchs = ChangingLights[i];
}
if (ChangingLights[4] == 0) {//所有灯均已熄灭,表明本局部状态下的开关矩阵为解
//输出解,即开关矩阵
MatrixOutput(result);
break;
}
}
}
by Ss1Two 2023/01/10