状态压缩入门 P2622 关灯问题II

Posted zhuanghuaijilie

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了状态压缩入门 P2622 关灯问题II相关的知识,希望对你有一定的参考价值。

本地是状态压缩的入门题目,在LeetCode LCP 13. 寻宝  中碰到此种类型的题,追根溯源,发现状态压缩多是从此题入门,故作此篇来加深印象。

本文较多内容会参考https://www.cnblogs.com/Tony-Double-Sky/p/9283254.html,部分内容会加入个人理解。

首先,要理解状态压缩,先要理解状态,比如下一题中灯的开和关就对应了两种状态。假设我们有10盏灯,要记录这10盏灯在某一时刻的状态,可以选用的做法为设置一个长度为10的整数数组lights,则lights[i] 的取值为0或者1,对应了第i+1盏灯(数组下标从0开始)的状态,这样要记录一个状态,就需要花费n的空间(n为灯的个数)。

灯的状态 0 1 0 1 1 1 0 1 0 1
灯号 0 1 2 3 4 5 6 7 8 9

这时,压缩就可以上场了。其实我们只需灯的状态,也就是1010111010(翻转了左右顺序,符合二进制存储顺序,方便叙述)就能获取到足够的信息,而这种形式刚好对应计算机中的二进制。我们可以用一个十进制存储该状态,比如1010111010对应十进制就是698,它与该时刻灯的状态唯一对应,可以大大减少存储的空间。

如果我们要获取灯号为i的灯的状态,只需要进行 x&(1<<i) ,即可获得其对应的0/1状态;

关闭灯号为i的灯(即将标号i的灯状态设置为0,前提是该灯是打开状态) x = x^(1<<i);

打开灯号为i的灯(即将标号i的灯状态设置为1)x = x|(1<<i)。

经过这样操作后,我们的处理会大大方便。例如,我们想记录某一状态是否到达过,则可直接设置一个状态数组state,state[x]既可以用来标记状态x是否出现过,又可以用来记录从原始状态走到当前状态经历的步数。可以发现,遍历过程的时间复杂度并没有降低,降低的是我们存储状态的空间复杂度。

下面落谷的 P2622 关灯问题II(https://www.luogu.com.cn/problem/P2622)就利用到了我们刚刚提到的状态压缩。

题目要求:

现有n盏灯,以及m个按钮。每个按钮可以同时控制这n盏灯——按下了第i个按钮,对于所有的灯都有一个效果。按下i按钮对于第j盏灯,是下面3中效果之一:如果a[i][j]为1,那么当这盏灯开了的时候,把它关上,否则不管;如果为-1的话,如果这盏灯是关的,那么把它打开,否则也不管;如果是0,无论这灯是否开,都不管。

现在这些灯都是开的,给出所有开关对所有灯的控制效果,求问最少要按几下按钮才能全部关掉。

数据范围:n<=10,m<=100。

解题思路:从最少按几下按钮才能全部关掉可以很容易联想到需要利用BFS搜寻最短路径。而图上的每一点就是一个状态。在一个状态时,我们通过逐个按下m个按钮,可以到达下一状态,这其中就涉及到两个问题:1、记录新状态是否已经到达过(不能再回头);2、记录到达当前状态走的步长。

结合上面提到的状态压缩,我们可以直接用一个整数x代表状态,而state[x]表示状态x是否到达过,而steps[x]表示从初始状态走过来的经历的步长。

进一步的,我们可以将两个数组合并为1个数组steps,初始时设置steps[x] = 999999 (表示不可达),当遍历到状态x时,如果更新的step小于steps[x],即代表之前未到达过该状态。

还有些需要讨论的细节是steps数组的长度设置,灯的数量为10,那我们至少要能存储状态1111111111,也就是2048,这样state[2047]不会超出范围。

以下是代码部分:

#include<vector>
#include<queue>
#include<iostream>
#include<cstring>
#include <algorithm> 
using namespace std;

int main(){
    int n,m;
    cin>>n>>m;
    vector<vector<int> > vec(m,vector<int>(n));
    // 输入矩阵
    for(int i=0;i<m;i++){
        for(int j=0;j<n;j++){
            cin>>vec[i][j];
        }
    }
    int steps[2048]; // 步长数组
    fill(steps,steps+2048,9999); // 初始化bfs长度
    queue<int> q;
    q.push((1<<n)-1); // 放入全开状态
    steps[(1<<n)-1] = 0; //标记全开状态为0
    while(!q.empty()){
        int oldState = q.front();
        q.pop();
        if(oldState==0){
            cout<<steps[oldState]<<endl; // 找到对应的状态
            return 0;
        }
        // 逐个按动所有按钮
        for(int i=0;i<m;i++){
            int newState = oldState; // 按动前的状态
            // 更新按动完成后的各个灯泡状态
            for(int j=0;j<n;j++){
                if(vec[i][j]==1){ // 关上,置为0
                    if(newState&(1<<j)) newState = newState^(1<<j);
                }
                else if(vec[i][j]==-1){ // 打开,置为1
                    newState = newState|(1<<(j));
                }
            }
            if(steps[newState]>steps[oldState]+1){ // 之前未出现过,放入队列中
                q.push(newState);
                steps[newState] = steps[oldState]+1;
            }
        }
    }
    cout<<-1<<endl;
    return 0;
} 

以上是关于状态压缩入门 P2622 关灯问题II的主要内容,如果未能解决你的问题,请参考以下文章

P2622 关灯问题II(状压&spfa)

p2622 关灯问题II

luogu p2622关灯问题II

P2622 关灯问题II

关灯问题II 状压DP

关灯问题II