费解的开关和翻硬币问题其实归根到底都是递推问题,我们都可以用枚举所有的情况来找到最优的解,要观察其中存在的规律。
目录
前言
费解的开关和翻硬币问题其实归根到底都是递推问题,我们都可以用枚举所有的情况来找到最优的解,要观察其中存在的规律。
费解的开关
你玩过“拉灯”游戏吗?
25 盏灯排成一个 5×5 的方形。
每一个灯都有一个开关,游戏者可以改变它的状态。
每一步,游戏者可以改变某一个灯的状态。
游戏者改变一个灯的状态会产生连锁反应:和这个灯上下左右相邻的灯也要相应地改变其状态。
我们用数字 1 表示一盏开着的灯,用数字 0 表示关着的灯。
下面这种状态
10111
01101
10111
10000
11011
在改变了最左上角的灯的状态后将变成:
01111
11101
10111
10000
11011
再改变它正中间的灯后状态将变成:
01111
11001
11001
10100
11011
给定一些游戏的初始状态,编写程序判断游戏者是否可能在 6 步以内使所有的灯都变亮。
输入格式
第一行输入正整数 n,代表数据中共有 n 个待解决的游戏初始状态。
以下若干行数据分为 n 组,每组数据有 5 行,每行 5 个字符。
每组数据描述了一个游戏的初始状态。
各组数据间用一个空行分隔。
输出格式
一共输出 n 行数据,每行有一个小于等于 6 的整数,它表示对于输入数据中对应的游戏状态最少需要几步才能使所有灯变亮。
对于某一个游戏初始状态,若 6 步以内无法使所有灯变亮,则输出 −1。
数据范围
0<n≤500
输入样例:
3
00111
01011
10001
11010
11100
11101
11101
11110
11111
11111
01111
11111
11111
11111
11111
输出样例:
3
2
-1
算法思路
每一行开关的按法操作完成受上一行开关的亮灭状态所决定。
使用二维字符数组grap来记录每个灯的状态,同时再就用一个二维字符数组backup也来记录初始灯的状态,便于当一种情况修改灯的状态后重新回到初始数据。因为我们可以通过第一行灯的状态来决定后续每行的操作,所以我们只需要枚举第一行的所有情况即可,看看其中哪一种方案能够使得灯全亮且步数不多于7即可,第一行有5个灯,每个灯有修改和不修改两种状态,那么一共有种情况,故外层循环循环32次来表示每种情况,用op表示。
然后先将5行灯的状态备份到backup数组,用整型变量step来记录修改的次数;因op的最大值为32,那么二进制对应的是5位,那么我们通过对二进制来进行位运算并且与1做与运算来决定此时我们来对哪一个位置的灯进行操作。(其实就像相当于我们对第一行哪些位置来进行操作,op操作与灯的输入状态无关,是我们枚举的第一行我们是否进行操作的32中情况)
其中turn(x,y)函数就是对坐标(x,y)处的位置和上下左右的灯进行改变,把上下左右和本来的灯做改变直接和1进行异或操作即可;把上下左右和本身的横纵坐标变化的值直接用数组表示出来,然后遍历一下,当然记得做边界处理,因为边界情况有些位置不存在灯。
对应的 turn
函数调用(op
从 0 到 31):
-
op = 0
(二进制表示:00000
):- 没有 1,不执行任何
turn
操作。
- 没有 1,不执行任何
-
op = 1
(二进制表示:00001
):- 执行
turn(0, 0)
。
- 执行
-
op = 2
(二进制表示:00010
):- 执行
turn(0, 1)
。
- 执行
-
op = 3
(二进制表示:00011
):- 执行
turn(0, 0)
和turn(0, 1)
。
- 执行
-
op = 4
(二进制表示:00100
):- 执行
turn(0, 2)
。
- 执行
-
op = 5
(二进制表示:00101
):- 执行
turn(0, 0)
和turn(0, 2)
。
- 执行
-
op = 6
(二进制表示:00110
):- 执行
turn(0, 1)
和turn(0, 2)
。
- 执行
-
op = 7
(二进制表示:00111
):- 执行
turn(0, 0)
、turn(0, 1)
和turn(0, 2)
。
- 执行
-
op = 8
(二进制表示:01000
):- 执行
turn(0, 3)
。
- 执行
-
op = 9
(二进制表示:01001
):- 执行
turn(0, 0)
和turn(0, 3)
。
- 执行
-
op = 10
(二进制表示:01010
):- 执行
turn(0, 1)
和turn(0, 3)
。
- 执行
-
op = 11
(二进制表示:01011
):- 执行
turn(0, 0)
、turn(0, 1)
和turn(0, 3)
。
- 执行
-
op = 12
(二进制表示:01100
):- 执行
turn(0, 2)
和turn(0, 3)
。
- 执行
-
op = 13
(二进制表示:01101
):- 执行
turn(0, 0)
、turn(0, 2)
和turn(0, 3)
。
- 执行
-
op = 14
(二进制表示:01110
):- 执行
turn(0, 1)
、turn(0, 2)
和turn(0, 3)
。
- 执行
-
op = 15
(二进制表示:01111
):- 执行
turn(0, 0)
、turn(0, 1)
、turn(0, 2)
和turn(0, 3)
。
- 执行
-
op = 16
(二进制表示:10000
):- 执行
turn(0, 4)
。
- 执行
-
op = 17
(二进制表示:10001
):- 执行
turn(0, 0)
和turn(0, 4)
。
- 执行
-
op = 18
(二进制表示:10010
):- 执行
turn(0, 1)
和turn(0, 4)
。
- 执行
-
op = 19
(二进制表示:10011
):- 执行
turn(0, 0)
、turn(0, 1)
和turn(0, 4)
。
- 执行
-
op = 20
(二进制表示:10100
):- 执行
turn(0, 2)
和turn(0, 4)
。
- 执行
-
op = 21
(二进制表示:10101
):- 执行
turn(0, 0)
、turn(0, 2)
和turn(0, 4)
。
- 执行
-
op = 22
(二进制表示:10110
):- 执行
turn(0, 1)
、turn(0, 2)
和turn(0, 4)
。
- 执行
-
op = 23
(二进制表示:10111
):- 执行
turn(0, 0)
、turn(0, 1)
、turn(0, 2)
和turn(0, 4)
。
- 执行
-
op = 24
(二进制表示:11000
):- 执行
turn(0, 3)
和turn(0, 4)
。
- 执行
-
op = 25
(二进制表示:11001
):- 执行
turn(0, 0)
、turn(0, 3)
和turn(0, 4)
。
- 执行
-
op = 26
(二进制表示:11010
):- 执行
turn(0, 1)
、turn(0, 3)
和turn(0, 4)
。
- 执行
-
op = 27
(二进制表示:11011
):- 执行
turn(0, 0)
、turn(0, 1)
、turn(0, 3)
和turn(0, 4)
。
- 执行
-
op = 28
(二进制表示:11100
):- 执行
turn(0, 2)
、turn(0, 3)
和turn(0, 4)
。
- 执行
-
op = 29
(二进制表示:11101
):- 执行
turn(0, 0)
、turn(0, 2)
、turn(0, 3)
和turn(0, 4)
。
- 执行
-
op = 30
(二进制表示:11110
):- 执行
turn(0, 1)
、turn(0, 2)
、turn(0, 3)
和turn(0, 4)
。
- 执行
-
op = 31
(二进制表示:11111
):- 执行
turn(0, 0)
、turn(0, 1)
、turn(0, 2)
、turn(0, 3)
和turn(0, 4)
.
- 执行
因为第一层能够决定后续每一层的操作,第一层如果有灭灯,那么我们只能对第二层的灯进行处理来改变第一层的灯,并且第二层有灭灯,只能通过第三层的灯来进行处理,一次递推,那么最后一层存在灭灯就说明此时的方案是错误的。
然后设定一个布尔类型变量dark用来表示最后一层是否有灭的灯,true表示有灭灯,false代表没有
灭灯即全亮。从第1层开始遍历,如果存在灭风,那么就处理同一列的下一层灯即代码中的turn(i+1,j),同时steo加1;
最后遍历最后一层,如果发现存在灭灯就说明此时方案是失败的,直接退出循环,另dark = true;当dark为真时直接返回-1;当dark为假时,比较一下step是否大于6,大于6也是失败的方案,少于6的时候直接打印即可。
代码如下
import java.io.*;
import java.util.*;
public class Main {
static int N = 6;
static char[][] grap = new char[N][N];
static char[][] backup = new char[N][N];
public static void main(String[] args)throws Exception {
Scanner sc = new Scanner(System.in);
int t = sc.nextInt();
while(t-- > 0){
for(int i = 0;i < 5;i++){
grap[i] = sc.next().toCharArray();
}
int res = 10;
for(int op = 0;op < 32;op++){
for(int j = 0;j < 5;j++){
backup[j] = Arrays.copyOf(grap[j],5);
}
int step = 0;
for(int i = 0;i < 5;i++){
if((op >> i & 1) == 1){
step++;
turn(0,i);
}
}
for(int i = 0;i < 4;i++){
for(int j = 0;j < 5;j++){
if (grap[i][j] == '0'){
step++;
turn(i+1,j);
}
}
}
boolean dark = false;
for(int i = 0; i < 5;i++){
if(grap[4][i] == '0'){
dark = true;
break;
}
}
if(!dark){
res = Math.min(res,step);
}
for(int j=0;j<5;++j)
{
grap[j]=Arrays.copyOf(backup[j], 5);
}
}
if(res > 6){
res = - 1;
}
System.out.println(res);
}
}
public static void turn(int x, int y){
//上 右 下 左 中
int[] dx = {-1,0,1,0,0};
int[] dy = {0,1,0,-1,0};
for (int i = 0; i < 5; i ++ )
{
int a = x + dx[i], b = y + dy[i];
//如果在边界外边,直接忽略即可
if (a < 0 || a >= 5 || b < 0 || b >= 5) continue;
//等价于 grap[a][b] = '0' + ('1' - grap[a][b])
grap[a][b] ^= 1; //异或,不同的时候就变成相反的数
//[‘0’ ASCII值48 ‘1’ 49]
}
}
}
翻硬币
小明正在玩一个“翻硬币”的游戏。
桌上放着排成一排的若干硬币。我们用 * 表示正面,用 o 表示反面(是小写字母,不是零)。
比如,可能情形是:**oo***oooo
如果同时翻转左边的两个硬币,则变为:oooo***oooo
现在小明的问题是:如果已知了初始状态和要达到的目标状态,每次只能同时翻转相邻的两个硬币,那么对特定的局面,最少要翻动多少次呢?
我们约定:把翻动相邻的两个硬币叫做一步操作。
输入格式
两行等长的字符串,分别表示初始状态和要达到的目标状态。
输出格式
一个整数,表示最小操作步数
数据范围
输入字符串的长度均不超过100。
数据保证答案一定有解。
输入样例1:
**********
o****o****
输出样例1:
5
输入样例2:
*o**o***o***
*o***o**o***
输出样例2:
1
算法思路
当有10个灯的时候,每次只能对相邻的两个灯进行修改,那么其实我们有9个开关,而且我们每次的按法其实都是固定的,比如测试样例1我们每次都只能从第1个灯开始判断,当第1个灯跟最终结果不一样时,我们必须修改第1、2个灯,第2个灯与最终结果不一样十,我们必须修改第2、3个灯,依次类推。当最后一个灯与最终结果不一样时,就说明此时无解,当然题目中保证所有数据都有解,所以我们不需要考虑无解的情况。
直接用两个字符数组ch和target分别记录初始和目标的数据。然后从第一个字符遍历到倒数第二个,然后当第i个字符与结果不一样时,我们就转换第i、i+1个字符,同时步数加1,循环结束最后输出步数即可。
代码如下
import java.io.*;
public class Main {
static PrintWriter pw = new PrintWriter(new OutputStreamWriter(System.out));
static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
static StreamTokenizer st = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
static int N = 110;
static char[] ch = new char[N];
static char[] target = new char[N];
public static void main(String[] args) throws Exception {
ch = br.readLine().toCharArray();
target = br.readLine().toCharArray();
int res = 0;
for(int i = 0;i < ch.length - 1;i++){
if(ch[i] != target[i]){
res++;
turn(i);
turn(i+1);
}
}
pw.println(res);
pw.flush();
}
public static void turn(int i){
if(ch[i] == '*'){
ch[i] = 'o';
}else if(ch[i] == 'o'){
ch[i] = '*';
}
}
public static String next() throws Exception {
return br.readLine();
}
}
总结
"费解的开关"和"翻硬币"问题不仅考察了操作的策略性,还考察了如何在有限的操作次数内达到目标状态。通过分析这两个问题,我们可以掌握如何在面对复杂条件时,运用逻辑和数学方法来优化决策过程。无论是灯的开关状态变化,还是硬币的翻转操作,这些问题背后都体现了在不确定环境下如何进行有效的状态转换和最小化操作步骤。