POJ 2811:熄灯问题 / POJ 1222

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;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
根据提供的引用内容,可以得知这是一道关于迷宫问题的题目,需要使用Java语言进行编写。具体来说,这道题目需要实现一个迷宫的搜索算法,找到从起点到终点的最短路径。可以使用广度优先搜索或者深度优先搜索算法来解决这个问题。 下面是一个使用广度优先搜索算法的Java代码示例: ```java import java.util.*; public class Main { static int[][] maze = new int[5][5]; // 迷宫地图 static int[][] dir = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; // 方向数组 static boolean[][] vis = new boolean[5][5]; // 标记数组 static int[][] pre = new int[5][5]; // 记录路径 public static void main(String[] args) { Scanner sc = new Scanner(System.in); for (int i = 0; i < 5; i++) { for (int j = 0; j < 5; j++) { maze[i][j] = sc.nextInt(); } } bfs(0, 0); Stack<Integer> stack = new Stack<>(); int x = 4, y = 4; while (x != 0 || y != 0) { stack.push(x * 5 + y); int t = pre[x][y]; x = t / 5; y = t % 5; } stack.push(0); while (!stack.empty()) { System.out.print(stack.pop() + " "); } } static void bfs(int x, int y) { Queue<Integer> qx = new LinkedList<>(); Queue<Integer> qy = new LinkedList<>(); qx.offer(x); qy.offer(y); vis[x][y] = true; while (!qx.isEmpty()) { int tx = qx.poll(); int ty = qy.poll(); if (tx == 4 && ty == 4) { return; } for (int i = 0; i < 4; i++) { int nx = tx + dir[i][0]; int ny = ty + dir[i][1]; if (nx >= 0 && nx < 5 && ny >= 0 && ny < 5 && maze[nx][ny] == 0 && !vis[nx][ny]) { vis[nx][ny] = true; pre[nx][ny] = tx * 5 + ty; qx.offer(nx); qy.offer(ny); } } } } } ``` 该代码使用了广度优先搜索算法,首先读入迷宫地图,然后从起点开始进行搜索,直到找到终点为止。在搜索的过程中,使用标记数组记录已经访问过的位置,使用路径数组记录路径。最后,使用栈来输出路径。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值