【题目总结】P2040 打开所有的灯

· · 题解

2018.6.10 华师一机房

原题:P2040 打开所有的灯

题目背景

pmshz在玩一个益(ruo)智(zhi)的小游戏,目的是打开九盏灯所有的灯,这样的游戏难倒了pmshz。。。

题目描述

这个灯很奇(fan)怪(ren),点一下就会将这个灯和其周围四盏灯的开关状态全部改变。现在你的任务就是就是告诉pmshz要全部打开这些灯。

例如

0 1 1

1 0 0

1 0 1

点一下最中间的灯【2,2】就变成了

0 0 1

0 1 1

1 1 1

再点一下左上角的灯【1,1】就变成了

1 1 1

1 1 1

1 1 1

达成目标。最少需要2步。

输出2即可。

输入输出格式

输入格式:

九个数字,3*3的格式输入,每两个数字中间只有一个空格,表示灯初始的开关状态。(0表示关,1表示开)

输出格式:

1个整数,表示最少打开所有灯所需要的步数。

输入输出样例

输入样例#1:

0 1 1
1 0 0
1 0 1

输出样例#1:

2

说明

这个题水不水,就看你怎么考虑了。。。。

明显的搜索练手题,可以快速用 DFS 完成,那么写写试试吧!

DFS思路

引理:

同一个开关不会被同时使用两次。

证明:

这不废话嘛用了两次就回去了啊,这还用证?

证毕。

(不要在意这个证明)

通过我们严谨的证明,我们知道,同一个开关不可能被使用两次,所以我们就可以放心大胆地直接深度优先搜索了。可以用G数组表示目前灯的开关状态,用use数组表示开关是否用过(用过为1,未用过为0),再加上数据范围较小,总的运算次数不会超过O(9^9),可以实现。

实现时间 程序耗时 占用内存 最终得分
10min 108ms 2113KB 100

代码:

#include<iostream>
using namespace std;
int G[5][5],ans=10;///由题可知,答案不会超过10,所以ans的初始值为10
bool use[5][5];///判断是否用过
///小技巧:用a和b两个数组快速完成位置变换
int a[5]={+0,+0,+1,-1,0};
int b[5]={+1,-1,+0,-0,0};
bool check()///判断函数
{
    for(int i=1;i<=3;i++)
        for(int j=1;j<=3;j++)
            if(!G[i][j])
                return false;
    return true;
}
void change(int x,int y)///变化函数
{
    for(int i=0;i<5;i++) G[x+a[i]][y+b[i]]=1-G[x+a[i]][y+b[i]];
}
void dfs(int step)///主体部分
{
    if(step>=ans) return ;///最(mei)优(shen)剪(me)枝(yong)
    if(check()) ans=min(ans,step);///找到答案
    else
    for(int i=1;i<=3;i++)
        for(int j=1;j<=3;j++)
            if(!use[i][j])
            {
                use[i][j]=1;
                change(i,j);
                dfs(step+1);
                ///回溯
                use[i][j]=0;
                change(i,j);
            }
}
int main()
{
    for(int i=1;i<=3;i++)
        for(int j=1;j<=3;j++)
            cin>>G[i][j];
    dfs(0);
    cout<<ans;
    return 0;
}

优点:写起来简单而且好想。

缺点:数据范围过大时比较鸡肋,容易 TLE

BFS+进制运算思路

其实一开始我看到这道题的时候是没有想到一个开关只能按一次的,所以就想打 BFS ,用方便判断是否到达目标的 string 储存当前状态,用方便判重的 STL 自带的 set 判断是否重复出现,这样就可以规避 DFS 思路的思考。本来代码打的好好的,突然脑子一糊涂,想着:“咦,只有九盏灯诶,可以用二进制储存啊,肯定也不会超过 1024 ,不是很方便吗”。然后 5min 打完的代码花了 20min ......

不过不得不说二进制运算和 BFS 是真的快,根本美好什么时间就运算完了,那就让 STL 见鬼去吧!

实现时间 程序耗时 占用内存 最终得分
23min 0ms 2062KB 100

代码:

#include<iostream>
using namespace std;
int tmp,h,t,step[100000];///step数组储存步数(也就是使用的开关数)
int bfs[100000];///储存当前的灯光状态
bool use[300];///判重数组,事实证明不是不到1024,连256都到不了
int change(int x,int pos)///神仙一般的变换函数!引以为傲但是超级难想,因为这个本机测试的时候RE了好多次
{
    int re=x;
    re^=1<<(8-pos);
    if(pos%3!=0) re^=1<<(9-pos); ///开关不在第一列
    if(pos%3!=2) re^=1<<(7-pos); ///开关不在最后一列
    if(pos/3!=0) re^=1<<(11-pos);///开关不在第一排
    if(pos/3!=2) re^=1<<(5-pos); ///开关不在最后一排
    return re;
}
int main()
{
    for(int i=0;i<9;i++)
    {
        cin>>tmp;
        bfs[0]=bfs[0]*2+tmp;///转换为二进制储存
    }
    use[bfs[0]]=true;
    while(h<=t)///标准BFS
    {
        for(int i=0;i<9;i++)
        {
            bfs[++t]=change(bfs[h],i);
            step[t]=step[h]+1;
            if(bfs[t]==(1<<9)-1)///到达最终状态
            {
                cout<<step[t];
                return 0;
            }
            if(use[bfs[t]]) t--;
            else use[bfs[t]]=true;
        }
        h++;
    }
    return 0;
}

优点:跑得超级快,时空甩 DFS 一大截。

缺点:累【这是真的】。

一定要提一下的一个小东西!

在我第一次用 DFS 思路写的时候其实是用 string 储存的,变换函数也和二进制思路比较相似,即:

string act(string a,int pos)
{
    string re=a;
    re[pos]=(a[pos]='0'?'1':'0');
    if(pos%3!=0) re[pos-1]=a[pos-1]='0'?'1':'0';
    if(pos%3!=2) re[pos+1]=a[pos+1]='0'?'1':'0';
    if(pos/3!=0) re[pos-3]=a[pos-3]='0'?'1':'0';
    if(pos/3!=2) re[pos+3]=a[pos+3]='0'?'1':'0';
    return re;
}

甚至还用了我从来没用过的三目运算符。

可是结果是60分,3WA。虽然错的不是这里,但我还是要说一下:用三目运算符一定要打括号,你又不是OYYZ,你不知道运算符优先级啊喂。

正确写法:

string act(string a,int pos)
{
    string re=a;
    re[pos]=(a[pos]='0'?'1':'0');
    if(pos%3!=0) re[pos-1]=(a[pos-1]='0')?'1':'0';
    if(pos%3!=2) re[pos+1]=(a[pos+1]='0')?'1':'0';
    if(pos/3!=0) re[pos-3]=(a[pos-3]='0')?'1':'0';
    if(pos/3!=2) re[pos+3]=(a[pos+3]='0')?'1':'0';
    return re;
}

2018.6.11 update1

好吧其实就错在这里,三目运算符中要打==判断而不是=。。。

真·正确写法:

string act(string a,int pos)
{
    string re=a;
    re[pos]=(a[pos]=='0'?'1':'0');
    if(pos%3!=0) re[pos-1]=(a[pos-1]=='0')?'1':'0';
    if(pos%3!=2) re[pos+1]=(a[pos+1]=='0')?'1':'0';
    if(pos/3!=0) re[pos-3]=(a[pos-3]=='0')?'1':'0';
    if(pos/3!=2) re[pos+3]=(a[pos+3]=='0')?'1':'0';
    return re;
}

或者写成:

string act(string a,int pos)
{
    string re=a;
    re[pos]=(a[pos]=='0')+'0';
    if(pos%3!=0) re[pos-1]=(a[pos-1]=='0')+'0';
    if(pos%3!=2) re[pos+1]=(a[pos+1]=='0')+'0';
    if(pos/3!=0) re[pos-3]=(a[pos-3]=='0')+'0';
    if(pos/3!=2) re[pos+3]=(a[pos+3]=='0')+'0';
    return re;
}

今天的题目总结就到这里吧,还有个 diggersun 老师提到的循环算法,还没有写,但是听说超级快,有时间再来补一下吧!