牛客寒假算法基礎集訓營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");
     
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章