算法模板:递推算法--开关问题

Source

开关问题

这一类问题是,调用一些开关,每一次对某一个开关进行操作,会使周围的开关也进行转台的改变;
这一类问题属于递推问题,可以对某一些开关进行枚举,然后递推得到答案;

问题描述
你玩过“拉灯”游戏吗?
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

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;
const int N = 6;
char g[N][N],backup[N][N];
int dx[] = {
    
       -1, 0 , 1, 0, 0 }, dy[] = {
    
       0 , 0 , 0 , 1, -1};

void turn(int x, int y)
{
    
      
    for(int i = 0 ; i < 5; i ++ )
    {
    
      
        int a = x + dx[i]; int b = y + dy[i];
        if(a<0||a>=5||b<0||b>=5)continue;
        g[a][b] = '0' + '1' -g[a][b];
    }
}

int main(void)
{
    
      
    int T;
    cin>>T;
    while(T--)
    {
    
      
        for(int i = 0; i <5; i ++ )cin>>g[i];
        //这个是参数的录入;
        int res = 10;
        for(int op = 0; op <32; op++)
        {
    
      
            memcpy(backup,g,sizeof g);
            int step = 0;
            for(int i = 0 ; i < 5; i ++ )
            {
    
      
                if(op>>i&1)//在第op中操作中,第i个开关按动了
                {
    
      
                    step++;
                    turn(0,i);
                }
            }//这个循环是对第一行的操作;
            
            for(int i = 0 ; i <4; i ++ )
                for(int j = 0 ; j <5 ; j ++)
                    if(g[i][j] == '0')
                    {
    
      
                        step++;
                        turn(i+1,j);
                    }
            //这个循环是对后四行进行的操作;
            
            bool dark = false;
            for(int i =  0; i <5; i ++ )
            {
    
      
                if(g[4][i] =='0')dark = true;
            }//这个循环是检验最后一行有没有不亮的灯泡,即检验操作的合理性
            if(!dark)res = min(res ,step);
            memcpy(g,backup,sizeof g);
        }
        if(res>6)res = -1;
        cout<<res<<endl;
    }
}

代码分析
0、问题的思路:对于这种开关问题,我们要明确灯泡亮灭状态的影响因素,对于本题来说,开关的亮灭仅与周围开关的转台有关,与开关按动的次序无关,且每个开关只能按动一次,否则相当于没按,这样的话,会出现一个特殊的问题,在按完第一行的开关后,第一行的灯泡的亮灭仅与其正下方的开关有关,所以我们只需要枚举玩第一行开关的状态就行;
1、枚举方法:对于每一个开关来说,只有两种状态0和1,即按和不按。将第一行视为一个二进制的数的话,这个五位的二进制的数的范围是0~31;二进制枚举方法 将一连串的01视为一个二进制的数,将这个二进制的数化为十进制,通过循环与位运算进行枚各个元素的状态;

 for(int op = 0; op <32; op++)
 	for(int i = 0 ; i < 5; i ++ )
 		 if(op>>i&1)//在第op中操作中,第i个开关按动了
 		 {
    
      
              step++;
              turn(0,i);//turn(0,4-i);
         }
 		 
 

这一块代码就是二进制的枚举,这里运用了为运算来判断每一个开关的状态,将开关的01状态对应到二进制数字的时候有两种方式(小端和大端);

例题二:翻硬币

问题描述
小明正在玩一个“翻硬币”的游戏。
桌上放着排成一排的若干硬币。我们用 * 表示正面,用 o 表示反面(是小写字母,不是零)。
比如,可能情形是:oo*oooo
如果同时翻转左边的两个硬币,则变为:oooo***oooo
现在小明的问题是:如果已知了初始状态和要达到的目标状态,每次只能同时翻转相邻的两个硬币,那么对特定的局面,最少要翻动多少次呢?
我们约定:把翻动相邻的两个硬币叫做一步操作。
输入格式
两行等长的字符串,分别表示初始状态和要达到的目标状态。
输出格式
一个整数,表示最小操作步数
数据范围
输入字符串的长度均不超过100。
数据保证答案一定有解。
输入样例1:


oo
输出样例1:
5
输入样例2:
ooo***
ooo***
输出样例2:
1

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>

using namespace std;
const int N = 110;
int n;
char start[N],aim[N];

void turn(int u)
{
    
      
    if(u>n)return ;
    if(start[u] =='*')start[u] = 'o';
    else
        start[u] = '*';
}

int main(void)
{
    
      
    cin>>start>>aim;
    n = strlen(start);
    int step = 0;
    for(int i = 0; i <n; i ++ )
    {
    
      
        if(start[i] != aim[i])
        {
    
      
            step++;
            turn(i),turn(i+1);
        }
    }//这里完成了操作;
    
    bool flag = true;
    for(int i = 0 ; i < n ; i ++ )
    {
    
      
        if(start[i] != aim[i])
        {
    
      
            flag = false ;
            break;
        }
    }
    if(flag)cout<<step<<endl;
    return 0;
}


问题分析
这个问题相较于上一个问题更加的简单,我们可以发现,硬币只和他两侧的硬币有关;