POJ 2811:熄灯问题 / POJ 1222
- 暴力解法:对第一行的各种状态,直接使用6重循环来实现(每个点都有6种可能)
样例输入/输出后面有~
- 先想明白这些:
1.按一个开关2次等于没有按,等价于 每个开关只能按1次 -> 转换成选哪些开关按的问题
2.按的先后顺序对结果没有影响() -> 不需要考虑次序问题(动态 -> 静态) - 思路1:
1.枚举所有的switch方案,由上面的分析知一种方案,只对应一种结果,通过判断各方案是否满足要求,来筛选出合理方案,即结果
2.所有情况,共2^30种组合会超时,要进行优化筛选出不必要的情况 - 优化1:
局部即整体思想:虽然整体情况看起来巨多,眼花缭乱,但有时候问题的一小部分,其实对应了所有的结果,因为所有的结果都是从这一局部出发生长出来的(鸡汤:细节决定成败,丢了马蹄铁导致战争失败)
本题:第一行就是这样的“局部”,任意给出一个第一行的按开关方案,后面所有行,要想成功,一定是确定的。
具体为:因为按任意一个元素a(i, j)
,都会使同列上一行元素a(i-1, j)
受影响,第一行的方案如果确定,会导致第一行的灯有亮有灭,所以第二行一定要按那些第一行亮着的灯下面对应的开关(不这样按,一定失败,只有第二行能改变第一行,第三行以上都影响不了第一行),如第一行亮的灯是(0, 2, 3, 5)->对应第二行按的方案就一定是(0, 2, 3, 5),由此递推到最后一行,从这里我们知道了上一行按后的结果(亮的灯-–矩阵中的1),就一定是下一行按的方案
结束条件:每种按第一行的方案(一共2^5=64种),对应的最后一行(第5行)是否符合条件(全灭),因为只有最后一行状态不确定,包括最后一行在内,每一行都完成了使上一行全灭的任务,等最后一行按完以后,前n-1行全灭了已经,故只需考虑最后一行(即第n行)
存储方式上来看:只有0/1矩阵 想到 -> 位运算(时间和空间上都会优化很多),位运算的相关技巧 见文末(需要相关的计算机组成原理知识,涉及到机器数的表示)
这里用到的技巧是:用0 ~ 2^5-1=63 对应的二进制(即0000 0000B ~ 0011 1111B)来模拟第一行(for(int n=0; n < 64; ++n)
)
- code1:
#include <memory>
#include <string>
#include <iostream>
#include <cstring>
using namespace std;
char oriLights[5];
char lights[5];
//测试用的灯矩阵
char result[5];
int GetBit(char c, int i){
// 取c的第i位bit
return (c >> i) & 1; //(1)10 = 0000 0001B, &:与操作
}
void SetBit(char & c, int i, int v){
//将c的第i位改成1:
if(v){
//v==1
c |= (1 << i);
}
else
c &= ~(1 << i);
}
void FlipBit(char & c, int i){
//翻转第i位,异或:与0异或不变,与1异或翻转
c ^= (1 << i);
}
void OutputResult(int t, char result[]){
//输出开关矩阵
cout << "PUZZLE #" << t <<endl;
for(int i = 0; i < 5; ++i){
for(int j = 0; j < 6; ++j){
cout << GetBit(result[i], j);
if(j < 5)
cout << " ";
}
cout <<endl;
}
}
int main(){
int T;
cin >> T;
for(int t = 1; t <= T; ++t){
for(int i = 0; i < 5; ++i)
for(int j = 0; j < 6; ++j){
int s;
cin >> s;
SetBit(oriLights[i], j, s);
}
// OutputResult(t, oriLights); //测试读入
for(int n = 0; n < 64; ++n){
//第1行一共2^6种状态,用整数0~63对应的二进制来表示,即0000 0000B ~ 0011 1111B
int switchs = n;
memcpy(lights, oriLights, sizeof(oriLights));
//每次模拟开始,都先把原灯状态oriLights 复制给测试矩阵lights
for(int i = 0; i < 5; ++i){
//先用result记录这次模拟,如果成功直接Output(res),失败则重来
result[i] = switchs;
//按下第i行 开关后对应行的变化
for(int j = 0; j < 6; j++){
if(GetBit(switchs, j)){
if(j > 0)
FlipBit(lights[i], j-1);
FlipBit(lights[i], j);
if(j < 5)
FlipBit(lights[i], j+1);
}
}
//按下第i行 开关后下一行的变化(上一行一定全灭,故不用考虑)
if(i < 4)
lights[i+1] ^= switchs;
//将第i行灯的状态赋值给switchs: 从而得到第i+1行的开关策略
//因为:下一行一定要灭掉上一行亮的灯
//如i行灯01001001,则下一行按开关为01001001
switchs = lights[i];
//在下一次循环的开始,result[i+1] = switchs;
}
if(lights[4] == 0){
//如果最后一行全1(灯全灭),输出本次结果
OutputResult(t, result);
break;
}
}
}
return 0;
}
- 英文版:
样例输入:
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
样例输出:
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
-
优化:
由于矩阵为5行6列,如果从以行(5个点)来模拟,只有2^5 = 32种组合,
既然每次测试中press的列,就是的puzzle上一列亮的灯(下一列要按的是上一列亮着的灯),所以就没必要多开一个数组每次存储灯亮的结果,而是press多开一列,记录出等价的最后一行灯情况,从而节省空间和时间
-
思路2:
如果不使用二进制数的运算,可以用数组来模拟二进制数(编写一个进位函数,获取连续的“二进制数”),即
puzzle[7][7]
、press[7][8]
即:
puzzle: + 为增加区域,0为有效数据区 0 1 2 3 4 5 6 0 + + + + + + + 1 + 0 0 0 0 0 0 2 + 0 0 0 0 0 0 3 + 0 0 0 0 0 0 4 + 0 0 0 0 0 0 5 + 0 0 0 0 0 0 6 + + + + + + + press: //x :press第7列就是对应每轮模拟中puzzle最后一列的情况 0 1 2 3 4 5 6 7 0 + + + + + + + + 1 + 0 0 0 0 0 0 x 2 + 0 0 0 0 0 0 x 3 + 0 0 0 0 0 0 x 4 + 0 0 0 0 0 0 x 5 + 0 0 0 0 0 0 x 6 + + + + + + + +
直接按题目规则强行模拟出给出第一行后对应整个矩阵的情况,在对边界的处理上使用虚拟思想(哨兵,外面加一圈虚拟的边,使原来的边界和中间元素的操作相同),原来的边界可以像中间普通元素一样对新加的外圈操作,相应的产生的结果无足轻重,直接忽视掉
-
具体模拟:上一列的灯的最终状态
PuzzleEnd
,由其对应的那个开关加四周(上下左右)开关的状态确定,比如:灯(2,3)由开关(2,3)(2,4)(2,2)(1,3)(3,3)决定,具体为,该灯的原始状态与这5个开关做’’异或’’(也可以表示成它们的和模2,开关是0无影响,开关是1则变号,刚好模2可以做到,其实就是二进制进位的原因)PuzzleEnd = puzzle[i][j-1]^press[i][j-1]^press[i-1][j-1]^press[i+1][j-1]^pressp[i][j-2]^press[i][j] ①
由①式,要使上一行的灯灭,
PuzzleEnd = 0
,很容易得出该列的press,即press[i][j] = 0^puzzle[i][j-1] ^ press[i][j-1] ^ press[i-1][j-1] ^ press[i+1][j-1] ^ press[i][j-2]
-
code2:
#include <iostream>
using namespace std;
int press[7][8] = {0};
int puzzle[7][7];
void Carry(int a[][8]){
//数组模拟二进制进位函数
for(int i = 1; i < 6; ++i){
if(a[i][1] == 0){
a[i][1] = 1;
break;
}
if(a[i][1] != 0){
a[i][1] = 0;
}
}
}
void Input(){
int n;
for(int i = 1; i < 6; ++i){
for(int j = 1; j < 7; ++j){
scanf("%d", &n);
puzzle[i][j] = n;
}
}
}
void Output(int n){
printf("PUZZLE #%d\n", n);
for(int i = 1; i < 6; ++i){
for(int j = 1; j < 7; ++j){
printf("%d ", press[i][j]);
}
printf("\n");
}
}
bool Test(){
for(int j = 2; j < 8; ++j){
for(int i = 1; i < 6; ++i){
press[i][j]= puzzle[i][j-1] ^ press[i][j-1] ^ press[i-1][j-1] ^ press[i+1][j-1] ^ press[i][j-2];
//也可以写成:
//press[i][j]= (puzzle[i][j-1] + press[i][j-1] + press[i-1][j-1] + press[i+1][j-1] + press[i][j-2]) % 2;
//更新本列press 即 上列puzzle
}
}
bool flag = true;
for(int i = 1; i < 6; ++i){
if(press[i][7] == 1) flag = false;
//通过press第7列的状态,得到puzzle第6列,即最后一列的情况
}
return flag;
}
int main(){
int n;
scanf("%d", &n);
for(int i = 1; i < n + 1; ++i){
Input();
for(int j = 0; j < 32; ++j){
//枚举32种不同的press第一列
Carry(press);
if(Test()){
Output(i);
}
}
}
return 0;
}