題意:
有n個寶藏(1...n),然後n個數表示寶藏的初始狀態,0表示開啓,1表示關閉
有m個開關,每一個開關控制k個寶藏,
當你按下一個開關後,這個控制的k個寶藏的狀態都會發生改變(開啓->關閉,關閉->開啓)
只有當某一刻,所有寶藏都處於開啓狀態時,你才能獲得所有的寶藏,
問你能否獲得所有寶藏
解析:
下面是我對算法需要建立反邊的理解:
正着拓撲會一開始選擇影響因素過大的點導致整張圖的選擇
所以需要從影響因素小的點開始選擇,即倒着拓撲
倒着拓撲又決定了建立反邊,反邊決定了傳遞不選擇標記
模型一:兩者(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");
}