状态压缩入门 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的主要内容,如果未能解决你的问题,请参考以下文章