解扫雷 - 2-sat新用法
貌似是微软的一个面试题:扫雷游戏,给出一个雷图,让求出每一个未知块是雷的概率。身为数学盲,概率论61分的菜鸟表示概率这东西没有任何想法,但是想了下确定某一块是雷还是非雷这点还是可以搞定的...
于是就YY了一道ACM题,输入一个合法雷图,要求标记其中一定为雷和一定不为雷的块
开始想暴力,暴力可以解决任何问题,但是太麻烦了,复杂度也高,完全搞不来
然后想网络流,网络流可以求出一个可行解,但貌似没有好的方法确定某一块(我只能想到枚举删除每个点的拆边),这完全是没有意义的
后来想到一个块只有两种状态,是雷和非雷,就想到前一段练过的2-sat了,也看到松鼠会里面一篇文章介绍扫雷逻辑,把扫雷转化为一个逻辑门电路的问题,然后说这是一个SAT问题,最后说这是一个NP问题。既然每个块只有两种状态,那就是2-SAT了,2-SAT可不是NP啊,或许我理解的有问题,不过貌似可以写代码了...
基本思想:
基本思想就是把一个逻辑关系用一条边来表示,从而将逻辑的传递关系转变成了图论中的连通问题,比如说:
如果事件A发生,则事件B肯定发生,就建边 A->B
如果事件B发生,则事件C不能发生,就建边 B->~C
这样建出的图就是 A->B->~C,那么我们就可以得到 A->~C,即如果A发生,那么C不能发生
我们将一个事件的两种状态拆为两个点,然后按上面的逻辑关系建图,然后我们判断两个状态之间的连通性:
A->~A,如果A发生了,那么A不能发生,说起来有点怪,换句话说就是A不能发生,如果A发生那么就会出现矛盾的逻辑关系
同样,如果~A->A,则A肯定发生,如果两条边都有,那么有肯定这些逻辑关系中有矛盾,如果都没有边,那么就不能确定,两种情况都可以
建图:
从上面的分析来看,把一个未知块拆为两个点,一个表示该点为雷(x),另一个表示非雷(~x)
然后枚举每个数字建边
0:0周围不会有雷的(x->~x),起始时不会出现0周围有块的情况,但之后会出现,下面会提到
1~8:如果数字n周围有k(k>=n)个未知块,那么只有两种情况可以判断:k=n时,全是雷,对每一个雷块建边 (~x->x);k=n+1,如果有一块不是雷,那么其他块全是雷,对每一个x块,令其他块为x',则(~x->x')
然后还可以想到数字1还有一些情况:对于1周围有大于1块时,如果某一块是雷,那么其他都不是雷 (x->~x')
判断:
这样建好图,我们发现,我们人能判断的逻辑关系已经基本(后面还会说到一种情况)都加到图中了,然后重点到了:
对于一个点的两种状态,我们通过判断他们的连通情况来判断:
如果存在 x->~x且~x->x,也就是说存在矛盾,该情况不可能,我们保证是合法情况,不去考虑了
存在x->~x但不存在~x->x,说明某块不可能是雷
存在~x->x但不存在x->~x,说明某块肯定是雷
两个点不连通,好吧,这个块还不能确定
这样,就像人判扫雷一样,扫了一遍,但是我们发现,这样一次做过后并不能判断出全部的雷块和非雷块,对一些判断比较复杂的块还是可以确定他是雷非雷的,那么我们就多做几次好了,我们把前一次判断出来的雷和非雷加入新一轮的判断中,是雷的话就把周围数字减1,块数减1,非雷的话块数减1(这时候就会有0了),然后继续做,直到没有新的状态被确定为止,扫雷完成!!
输入:
9 9
.....1#1.
111..1#1.
##1..111.
#21.111..
#1.12#1..
#113#31..
#####2...
####31...
####2....
输出:
.....1O1.
111..1X1.
OX1..111.
X21.111..
O1.12X1..
O113X31..
OOXOX2...
###X31...
###X2....
(O不是雷,X是雷,#不确定)
下面代码:
1 #include<cstdio>
2 #include<cstring>
3 #include<cctype>
4 using namespace std;
5
6 char maze[15][15];
7 int id[15][15],tid;
8 int dir[8][2]={{-1,-1},{-1,0},{0,-1},{-1,1},{1,-1},{0,1},{1,0},{1,1}};
9 bool gra[100][100];
10 int lx,ly;
11
12 void floyd(int n){
13 for(int k=0;k<n;k++){
14 for(int i=0;i<n;i++){
15 for(int j=0;j<n;j++){
16 gra[i][j]|=gra[i][k]&gra[k][j];
17 }
18 }
19 }
20 }
21
22 void build(){
23 memset(gra,false,sizeof(gra));
24 for(int i=0;i<(tid<<1);i++) gra[i][i]=true;
25 for(int ii=0;ii<lx;ii++){
26 for(int jj=0;jj<ly;jj++){
27 if(isdigit(maze[ii][jj])){
28 int cnt=0,type=maze[ii][jj]-'0';
29 int block[10];
30 for(int kk=0;kk<8;kk++){
31 int tx=ii+dir[kk][0];
32 int ty=jj+dir[kk][1];
33 if(tx<0||ty<0||tx>=lx||ty>=ly) continue;
34 if(maze[tx][ty]=='X') type--;
35 if(maze[tx][ty]!='#') continue;
36 block[cnt++]=id[tx][ty];
37 }
38 if(type){
39 if(cnt==type){
40 for(int i=0;i<cnt;i++){
41 gra[block[i]<<1|1][block[i]<<1]=true;
42 }
43 }else if(cnt-1==type){
44 for(int i=0;i<cnt;i++){
45 for(int j=0;j<cnt;j++){
46 if(i==j) continue;
47 gra[block[i]<<1|1][block[j]<<1]=true;
48 }
49 }
50 }
51 }else{
52 for(int i=0;i<cnt;i++){
53 gra[block[i]<<1][block[0]<<1|1]=true;
54 }
55 }
56 if(type==1){
57 for(int i=0;i<cnt;i++){
58 for(int j=0;j<cnt;j++){
59 if(i==j) continue;
60 gra[block[i]<<1][block[j]<<1|1]=true;
61 }
62 }
63 }
64 }
65 }
66 }
67 }
68
69
70 int main(){
71 // freopen("ms.in","r",stdin);
72 // freopen("ms.out","w",stdout);
73
74 int t,cas=0;
75 scanf("%d",&t);
76 while(t--){
77 tid=0;
78 scanf("%d%d",&lx,&ly);
79 for(int i=0;i<lx;i++){
80 scanf("%s",maze[i]);
81 for(int j=0;j<ly;j++){
82 if(maze[i][j]=='#'){
83 id[i][j]=tid++;
84 }
85 }
86 }
87 bool flag=true;
88 while(flag){
89
90 build();
91 floyd(tid<<1);
92
93 flag=false;
94 for(int i=0;i<lx;i++){
95 for(int j=0;j<ly;j++){
96 if(maze[i][j]=='#'){
97 bool a=gra[id[i][j]<<1][id[i][j]<<1|1];
98 bool b=gra[id[i][j]<<1|1][id[i][j]<<1];
99 if(a&b){
100 puts("~~");
101 }else if(b){
102 maze[i][j]='X';flag=true;
103 }else if(a){
104 maze[i][j]='O';flag=true;
105 }
106 }
107 }
108 }
109 }
110
111 printf("Case %d:\n",++cas);
112 for(int i=0;i<lx;i++){
113 puts(maze[i]);
114 }
115 puts("");
116 }
117 }
我代码比较挫的,大致算起来O(n^4),不错,确实太大了,为了偷懒用了floyd判连通,这个地方可以改成做n次dfs,也就可以将O(n^3)的减到O(n^2),O(n^3)的总复杂度还是可以接受,另外那个while(true)循环,理论说可能做n次,不过我实在构造不来这样的数据,那个循环最多也就做两三次的样子,所以这个方法还是比较理想的~~(n为未知块个数×2,程序能处理最多50个未知块的数据,可以把数组范围适量改大)
扩展:
扫雷时候还有另外的一种情况的判断,那就是给出雷的个数,雷的个数在扫雷后期还是很有作用的,比如下面的雷图:
221
XX2
#X2
剩1个或是0个雷,还有一些更复杂的情况,都可以通过雷的个数来判断某些块中是否存在雷,这样的问题不能用上面的方法来搞定,但是用概率就简单多了,又转化成了求概率的问题...
经过上面的一通乱搞,与数字相关的未知块都扫的差不多了,当然还有,尤其是一排2和一排1的情况,不过在上面的基础上求概率应该就简单多了...
#3223#
######
#2112#
######
本文地址:http://www.cnblogs.com/ambition/archive/2011/09/22/Mine-sweeping.html