牛客寒假算法基础集训营2 F 处女座与宝藏(level 4)(2-sat)

题目链接

题意:

有n个宝藏(1...n),然后n个数表示宝藏的初始状态,0表示开启,1表示关闭

有m个开关,每一个开关控制k个宝藏,

当你按下一个开关后,这个控制的k个宝藏的状态都会发生改变(开启->关闭,关闭->开启)

只有当某一刻,所有宝藏都处于开启状态时,你才能获得所有的宝藏,

问你能否获得所有宝藏

 

解析:

2-sat讲解

2-sat模板

下面是我对算法需要建立反边的理解:

正着拓扑会一开始选择影响因素过大的点导致整张图的选择

所以需要从影响因素小的点开始选择,即倒着拓扑

倒着拓扑又决定了建立反边,反边决定了传递不选择标记

模型一:两者(A,B)不能同时取
  那么选择了A就只能选择B’,选择了B就只能选择A’
  连边A→B’,B→A’

模型二:两者(A,B)不能同时不取
  那么选择了A’就只能选择B,选择了B’就只能选择A
  连边A’→B,B’→A

模型三:两者(A,B)要么都取,要么都不取
  那么选择了A,就只能选择B,选择了B就只能选择A,选择了A’就只能选择B’,选择了B’就只能选择A’
  连边A→B,B→A,A’→B’,B’→A’

模型四:两者(A,A’)必取A
  那么,那么,该怎么说呢?先说连边吧。
  连边A’→A
  你想出来为什么了吗?也许你在想,哇塞,这样就保证了在反图中A在拓扑序中靠前,然后就会先选择A,呵呵,你错了。
  虽然,我在一些人的博客中见到了这样的解释,但是,我还是非常负责任的告诉你,这是不全面的。
  我们从A’向A连边,要的不仅是拓扑序,还有判可行与否。在2-sat图当中,若该图是可行的,就意味着如果从A到A’有路径的话,A’到A是没有路径的,因为如果有路径,它们就成一个块了,不会被判为可行,那么,如果A到A’是有路径的话,在反图中,A’就到A有路径,那么拓扑里,A’就会因为靠前被标记为“选择”,并未满足条件。并且,我们应当确信的是,解的多情况依赖的是拓扑排序的多情况,不按拓扑序来的话,解就是错误的,或者说是不成立的,也就是说,我们拓扑序先选择A的话,就会导致别的约束条件的不成立,那么在我们引入了A’到A的这条边后,A就与A’在同一块中了,算法会报告不可行信息。若是原图本来就满足A到A’有路径,然而A’到A无路径的话,我们添一条边,只是确定了其有解,但丝毫不会影响到拓扑排序,只有当A到A’之间根本不存在路径的时候,才会影响到其拓扑排序,所以,真相是这样的。
  是不是有人已经开始疑惑,讲了这么久的对称性,整个算法都是依赖对称性才得以成立的,那么怎么一下引入一条边让图不对称了算法还成立呢!关于这个问题,其实很好解释,仔细想想,对称性确保的是什么?它确保的可是原图有解,则一定可以构造出来。现在我们引入了一条边A’→A,若通过上述判断,让其无解了,那么对后面显然是没有影响的,毕竟算法都没执行下去了,还算什么影响。如果还有解呢?这说明了什么?这说明了A’到A原本就存在一条路径,或者A’到A之间根本就没有路径。如果有路径的话,那么我多增加一条有区别吗?它会影响到算法的任何一步吗?显然不会啊,那如果原本没有路径的话呢?没有路径意味着谁在拓扑序列中靠前都是可能的,在有了该边后(反边为A→A’),A将在拓扑序列中靠前,被标记为“选择”,那么我们会对A’进行直接标记,此时,A’到A是没有路可走的,它根本就无法访问到A,所以在标记的这个过程中,这条路径根本就没有影响到图的任何对称性,在拓扑排序后,你完全可以当其不存在。

 

 这里我摘取的是2-sat建图的模型,这些模型非常有用,这道题就是对它的应用,对理解2-sat非常有帮助。并且如果你不理解模型4的话,我会给出一组样例,方便理解。

2-sat是根据矛盾建图,这道题的矛盾,我一开始以为是不同的开关(因为他说一个宝箱最多由2个开关控制),后来想了想,

发现其实是开关的状态——按/不按。因为按/不按是两种对立的状态,并且一旦你选择其中一种,你一定会要选择其他开关的状态,使得宝藏处于开启状态。那么其实宝箱是用来建边用的。

那么对于每一宝箱初始状态我们分情况讨论

情况1:开启

(PS,这里判断初始是否开启,我们要用status[i]!=1来判断,不能用status[i]==0,因为这里好像数据有点问题,开启状态可能不止是0表示)

如果这个宝箱受0个开关控制:不用管它

如果这个宝箱受1个开关控制:那么这个开关一定不能按,L(按)->R(不按) (模型4)

(这里既然说到模型4,我就简单讲解一下,看来上面2-sat的讲解,我们知道我们建完图后用tarjan缩点,看对立的两个点有没有在同一个块上来得到答案能否可行。这里为什么这样建边不会对答案产生影响呢?

那么我们先把这条边删掉,假如最后建完的图中,我们可以找到一条从R->L的路径,那么说明这个开关一定是要按的,但是在当前的条件(初始开启,只受一个开关控制)下,这个开关又一定是不能按的,那么说明是无解的,但是怎么得到无解的答案呢,很简单,把L->R这条边加上去,L,R就会在一个块内了。

然后一样先把这边删掉,假如最后建完的图中,我们可以只能找到一条从L->R的路径,那么说明开关一定是不能按的,那么我们在加上L->R对答案并不会产生影响,只是多了一条从L->R的路径,L,R并不会在同一个块内

然后一样先把这边删掉,假如最后建完的图中,我们可以既能找到一条从L->R的路径,又能找到一条R->L的路径,那么一定是无解的,我们加上这条边,也一样无解,不会对答案产生贡献

后一样先把这边删掉,假如最后建完的图中,找不到L,R之间任何的路径,说明对于这个开关,我们按或不按都可以,但是按照条件,我们又一定不能按,所以要加L->R的边,使得L->R有路径,这样只有L->R的路径,没有R->L的路径,所以有解。

如果这个宝箱受2个开关控制,那么按了其中一个,必须要按另外一个;一个不按,另一个也不能按(模型3)

L1->L2,L2->L1,R1->R2,R2->R1

情况2:关闭

如果这个宝箱受0个开关控制:直接无解

如果这个宝箱受1个开关控制:必须按,R->L

如果这个宝箱受2个开关控制:两个开关里面必须只能按一个,(模型1+模型2)

L1->R2,L2->R1,   R1->L2,R2->L1

可以根据上面讲的构图方法,构建一下下面这组样例,加深模型4 的理解


4 3
0 1 1 1
3 1 2 4
2 2 3
1 1

NO

建完图,tarjan缩点,判断两个对立的点是否在同一个块中->是:无解;否则,有解

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <stack>
#define L(x) (2*x-1)
#define R(x) (2*x)
using namespace std;
const int MAXN = 2E5+10;
int status[MAXN];
int opp[MAXN][3];
 
typedef struct node
{
    int u,v;
    int next;
}node;
 
node edge[MAXN*4];
int head[MAXN*2],cnt,ind;
 
int LOW[2*MAXN],DFN[MAXN*2];
int visit[2*MAXN],ufset[MAXN*2];
 
 
 
stack<int> ms;
 
void addEdge(int u,int v)
{
    edge[cnt].next=head[u];
    edge[cnt].u=u;
    edge[cnt].v=v;
    head[u]=cnt++;
}
  
void tarjan(int x)
{
    DFN[x]=LOW[x]=++ind;  
    ms.push(x);  
    visit[x]=1;   //标记入栈
    for(int i=head[x];i!=-1;i=edge[i].next)
    {
        int v=edge[i].v;
        if(!DFN[v])   //如果这条边没有遍历过
        {
            tarjan(v);  //递归遍历他
            LOW[x]=min(LOW[x],LOW[v]);   //并将x加入到根节点比它小(入栈时间比它早)的树中,若还是low[x]比较小,那么x自己就是一棵一个节点的树
        }
        else if(visit[v])   //如果这个点已经被遍历过且现在还在栈中,说明存在环
        {
            LOW[x]=min(LOW[x],DFN[v]);   //得到环的最开始的判定就是从这一步开始的,因为只通过LOW[x]=min(LOW[x],LOW[v]),只能从自己下面(即后搜的点中得到父节点信息)
            //LOW[x]=min(LOW[x],LOW[v]);
        }                                  //而当自己连接的那条边是构成环的那条边时,就是通过这一步得到的,将自己现在的父节点的入栈时间low[x]与在自己之前已经入栈(遍历过)且还在栈中的节点的入栈时间比较
    }
    if(DFN[x]==LOW[x])  //任何一个强连通分量,必定是对原图的深度优先搜索树的子树
    {
        int u;
        do
        {
            u=ms.top();
            ms.pop();
            ufset[u]=x;  //一个环添加到一个并查集中
            visit[u]=0;
  
        }while(u!=x);
    }
}
 
 
 
int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    memset(head,-1,sizeof(head));
 
 
    for(int i=1;i<=n;i++) scanf("%d",&status[i]),opp[i][0]=1;
    for(int i=1;i<=2*m;i++) ufset[i]=i;
     
    cnt=ind=0;
    for(int i=1;i<=m;i++)
    {
        int k,tmp;
        scanf("%d",&k);
        for(int j=0;j<k;j++)
        {
            scanf("%d",&tmp);
            //if(!(opp[tmp][0]==2&&opp[tmp][1]==i))
                opp[tmp][opp[tmp][0]++]=i;
        }
    }
    int flag=1;
    for(int i=1;i<=n;i++)
    {
        int p,q;
        if(status[i]!=1)
        {
            if(opp[i][0]==2)
            {
                p=opp[i][1];
                addEdge(L(p),R(p));
            }
            else if(opp[i][0]==3)
            {
                p=opp[i][1];
                q=opp[i][2];
                addEdge(L(p),L(q));//要么都取,要么都不取
                addEdge(L(q),L(p));
                addEdge(R(q),R(p));
                addEdge(R(p),R(q));
            }
        }
        else
        {
            if(opp[i][0]==2)
            {
                p=opp[i][1];
                addEdge(R(p),L(p));
            }
            else if(opp[i][0]==3)
            {
                p=opp[i][1];
                q=opp[i][2];
                addEdge(L(p),R(q)); //不能同时取+不能同时不取->(A,B)只能取其中一个
                addEdge(R(q),L(p));
                addEdge(L(q),R(p));
                addEdge(R(p),L(q));
            }
            else if(opp[i][0]==1)
            {
                flag=0;
                break;
            }
        }
    }
    if(flag)
    {
        for(int i=1;i<=2*m;i++)
            if(!DFN[i]) tarjan(i);
     
        for(int i=1;i<=2*m;i+=2)
        {
            if(ufset[i]==ufset[i+1])
                {flag=0;break;}
        }
    }
    if(flag) printf("YES\n");
    else printf("NO\n");
     
}

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值