递推_二进制_方向向量_边界问题_异或_贪心_备份数组_AcWing 95. 费解的开关

#include

using namespace std;

const int N=10;

char a[N][N],backups[N][N];

int dx[5]={-1,0,1,0,0},dy[5]={0,0,0,1,-1};

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

void solve()
{
    for(int i=0;i<5;i++)
        for(int j=0;j<5;j++)
            cin>>a[i][j];
            
    int res=10;
    for(int op=0;op<(1<<5);op++)
    {
        int step=0;
        memcpy(backups,a,sizeof a);
        
        for(int i=0;i<5;i++)
            if(!(op>>i&1))
            {
                step++;
                turn(0,i);
            }
            
        for(int i=0;i<4;i++)
            for(int j=0;j<5;j++)
                if(backups[i][j]=='0')
                {
                    step++;
                    turn(i+1,j);
                }
        
        bool success=true;
        for(int i=0;i<5;i++)
            if(backups[4][i]=='0')
                success=false;
                
        if(success)
            res=min(res,step);
    }
    
    if(res>6)
        res=-1;
    cout<<res<<endl;
}

int main()
{
    int t;
    cin>>t;
    
    while(t--)
        solve();
        
    return 0;
}

算是对现在的我来说比较有挑战性的一道题,完全理解该题的时候感觉非常爽,有一种通透的感觉

该题的意思是输入一个5*5的矩阵,1表示灯是亮的,0表示灯是灭的,问最少经过多少次操作可以使得灯全部亮着,一次操作,该灯上下左右的灯都会受到影响,但是注意,上下左右的灯在该次操作时不会直接影响另外的灯

意思是说,每一次操作最多直接影响5盏灯,我刚开始以为牵一发而动全身,其实没有那么复杂

位运算

1 < < 5 = 2 5 1<<5=2^5 1<<5=25
o p > > i ∧ 1 op>>i\land1 op>>i1表示的意思是移除 i i i位数字之后,末位的数字是多少(用二进制表示的数字的末尾)

递推

把一盏灯状态修改,该灯的上下左右的灯全部变成和原来状态相反的情况,本来是亮的,现在变成熄灭,本来熄灭的,现在变成亮的

该题可以发现每一个位置最多被点击一次,如果可以被点击两次,相当于第一次点击白费了,反证法可以证明该观点成立

点击的前后顺序不影响答案,假设先点亮了某一盏灯,该灯周围的四盏灯都受到了影响,假设后点击该盏灯,有可能周围的四盏灯的状态确实和前面不一样,但是不影响最终的答案,答案只需要保证最后所有的灯都是亮的即可

上面的结论自己写还是比较难发现(至少对我来说)

第一行的情况固定之后,只有唯一一种方案进行操作,不管能不能使得灯全部变亮,第一行的数字只能由该数字下一行且同一列的数字来改变其状态,假设该数字是0,那么只有改变该一行同一列的数字,使得该数字变成1才有可能实现整个矩阵全部变成1

第一行有五个数字,每一个数字可以选择0或者1,有 2 5 2^5 25种选择,5个数字,每个数字都是0或者1,所以可以用031的二进制表示第一行的所有数字,对于每一个输入的矩阵,第一行都可以有32种情况,所以可以用上面代码中的操作,应该也可以直接对数组进行初始化,把第一行初始化为字符'1'和字符'0'

试了一下,直接初始化不可行,因为第一行左右两个点之间会互相影响

第一个循环,如下这个循环,

		for(int i=0;i<5;i++)
            if(!(op>>i&1))
            {
                step++;
                turn(0,i);
            }

表示的是二进制表示开关的情况,啥意思呢,意思就是,按不按开关,其实和原来的矩阵是啥样没有关系,就是表示第一行每一个点按不按开关,!(op>>i&1)加不加取反都是一样的,两种i情况的数目是一样多的,假设我们1表示按开关的话,就不加取反,假设我们0表示按开关的话,就加上取反

step表示的是每一种方案操作的次数,turn表示的是按开关会带来的影响

turn 函数

首先定义一个方向变量,表示上下左右中5个方向,我比较喜欢左中右,上下这个顺序,所有x的方向向量是-1,0,1,0,0,y的方向向量是0,0,0,1,-1,然后遍历几个方向向量,表示新的点,如果越界了就不使用,没有越界就把数组的元素变成和当前元素不同的元素,比如说'1'变成'0',实现该操作可以使用^,经过异或操作,可以实现该结果

'1'ASCII码是1100011ASCII码是000001,异或之后是110000,刚好是'0'ASCII码,比较巧妙

数组备份

每一次操作都需要修改数组的元素,但是我们需要进行32次操作,所以必须备份数组,保存原来的元素

所以可以使用一个备份数组,每一次都是使用备份数组进行操作

memcpy(backups,a,sizeof a);

该函数前面是备份数组,中间是来源数组,最后面是复制的长度

修改

只要数组是'0'表示下一行需要点击开关,总共五行,我们只需要遍历前面四行,因为每一次修改的都是该行的下一行

最后检查最后一行是否有'0',假设有'0'表示没有实现要求

你可能感兴趣的:(#,acwing算法提高,算法)