问题:
有一个由按钮组成的矩阵,其中每行有6个按钮,共5行。每个按钮的位置上有一盏灯。当按下一个按钮后,该按钮以及周围位置(上、下、左、右边的灯都会改变状态 (亮→暗,暗→亮))。与一盏灯毗邻的多个按钮被按下时,一个操作会抵消另一次操作的结果,给定矩阵中每盏灯的初始状态,求一种按按钮方案,使得所有的灯都熄灭,t组输入,输出一个5×6只包含0,1(0代表不按,1代表按)的矩阵,每一个值对应该位置灯泡的情况。
初次遇到熄灯问题时我的第一感觉是用枚举(穷举),但每一盏灯都有0(不按)或1(按)两种可能 ,对于一个5×6的矩阵,总共就有2^30种可能,显然这会导致TLE,且实现起来十分复杂繁琐(虽然这个题目的数据范围很小)。因此便排除了这种方法,之后就是十分茫然,不知从何下手。后面通过他人的讲解,我恍然大悟,觉得其方法十分妙。在结和他人与自己的理解后,我写下这篇博客,希望能对和我当初一样迷茫的人有所帮助。
思考:
其实解决这个问题最重要的是分析题目,通过二进制去解决(如果你在看到这里就已经有了方案,请允许我叫你一声大佬,如果还是没有思路也没有关系请继续往下看)。首先,我们知道在解决复杂数学题时,常用的思想是由特殊到一般,从简到繁去逐步分析解剖问题 。同样,这种思想也可以应用在算法竞赛上。因此我们先分析第一行的情况,假定我们已经知道了第一行每一个按钮的取值情况 ,那么我们就可以确定第二行的按钮的取值情况(因为按一次按钮只能够影响到它的上下,左右四盏灯,因此当a[i][[j]=1时则a[i+1][[j]就必须为1,这样才能使a[i][j]为0)。再以此类推,直到最后一行。我们只需判断最后一行的灯是否全灭(即全为0)即可得出答案。也就是说我们只要枚举第一行的情况就可以推出其他行的情况,进而得出答案。接下来我们就只需要去思考如何用代码去实现就行了。而在进行代码实现时我们需要借助二进制去完成。利用二进制是解决该问题的重要手段,同时也是难点。我们将矩阵的每一行都看成一个二进制数,则每一行灯泡的取值情况都对应一个数,这样我们就可以将n×m的矩阵转换为一个包含n个字符的字符数组(每个字符代表一行灯泡的取值情况,该字符二进制的第i位表示该行第i盏灯的取值情况),这样在进行代码实现时就可以大大提升效率,减少了空间,代码也会变得简单
代码如下:
1. #include<bits/stdc++.h> 2. using namespace std; 3. #define sc(x) scanf("%d",&x) 4. #define pr(x) printf("%d",x) 5. int GetBit(char c,int i) { //得到字符c二进制的第j位; 6. return (c>>i)&1; 7. } 8. void SetBit(char &c,int i,int x) { //将字符c二进制的第i位置为x(0或1); 9. if(x) c |= ( 1<<i ); //和1进行或运算可将数置为1; 10. else c &=~( 1<<i ); //和0进行与运算可将数置为0; 11. } 12. 13. 13. void ReBit(char &c,int i) { //将字符c二进制的第i位取反; 14. c^=(1<<i); //对c进行异或运算 15. } 16. void Output(char c[]) { //将字符数组以二进制的形式输出; 17. cout<<"答案如下"<<endl; 18. for(int i=0; i<5; i++) { 19. for(int j=0; j<6; j++) { 20. pr(GetBit(c[i],j)); 21. if(j<5) cout<<" "; 22. } 23. cout<<endl; 24. } 25. } 26. int main() { 27. int t; 28. char light1[6]; 29. char light2[6]; 30. char ans[6]; 31. sc(t); 32. while(t--) { 33. for(int i=0; i<5; i++) { 34. for(int j=0; j<6; j++) { 35. int x; 36. sc(x); //输入数据 37. SetBit(light1[i],j,x); 38. } 39. } 40. for(int i=0; i<64; i++) { 41. int n=i; //穷举第一行的情况,根据第一行得出其它行的情况; 42. memcpy(light2,light1,sizeof(light1)); //将原始字符数组拷贝到c中(这样可以方便对数组进行处理且不会改变原数组的值); 43. for(int j=0; j<5; j++) { 44. ans[j]=n; //将答案储存起来; 45. for(int k=0; k<6; k++) { 46. if(GetBit(n,k)) { //如果第k位为1,则要对周围灯泡进行取反运算 47. ReBit(light2[j],k); 48. if(k>0) ReBit(light2[j],k-1); 49. if(k<5) ReBit(light2[j],k+1); 50. } 51. } 52. if(j<4) light2[j+1] ^= n; /*对j+1行进行操作,这步算法十分巧妙,降低了运行时间,要仔细思考*/ 53. n=light2[j]; /* 此句话是重点,通过上一行的灯泡情况确定下一行灯泡的操作情况*/ 54. } 55. if(light2[4]==0) { //结束循环的条件,若light2[4]==0则说明所有灯泡均已熄灭 56. Output(ans); 57. break; 58. } 59. } 60. } 61. return 0; 62. } 63. 64. 64. ```