我最愛的《挑戰程序設計競賽麼麼噠》上的例題
先放一下2-SAT模板。
也就是給一堆布爾變量(就是要麼是真的要麼是假的的一堆變量),和一堆布爾方程,問能否通過選擇這些變量是真的還是假的,使得所有布爾方程都成立。
注意一下,因爲是用scc跑強連通,所以下面這個既是2sat,又是強連通的模板。
強連通還有tarjan方法可以求,這是模板地址tarjan強連通模板
講一下scc求強連通分量的原理
1、這是原圖,這幅圖裏面ABC屬於一個強連通分量
2、跑一遍dfs。終點的序號爲1,起點的序號爲n。
3、所有的邊反向。所以在當初加邊的時候!一定要加了正向邊,又加上反向邊!
4、然後根據序號從大到小處理每個頂點。紅的表示正在走,藍的表示已經走過。
scc函數從頂點六開始走,rdfs函數發現它不存在反向邊。那麼頂點6獨自屬於一個強連通分量。
接下來scc函數從頂點五開始走,rdfs函數發現它存在反向邊指向6,但是剛剛已經訪問過了啦!所以它還是自己屬於一個強連通分量。
scc函數從頂點4開始走,rdfs函數發現它反向邊的頂點指向一個不曾訪問國過的點,那麼繼續往下rdfs,直到到達某個節點(比如C),它所有反向邊指向的節點都訪問過了。
5、這樣子先dfs一次。再按照序號從大到小逐漸rdfs(也就是反向邊的dfs一次),就可以找到所有的強連通分量。
總的複雜度是O(V+E)。
int V;
int used[MAX_N];
vector<int> vs;
int cmp[MAX_N];
void dfs(int ver)
{
//printf("ver = %d\n",ver);
used[ver] = 1;
for (int i = head[ver]; i != -1; i = pra[i].next){
int u = pra[i].v;
if (!used[u])
dfs(u);
}
vs.push_back(ver);
}
void rdfs(int ver,int k){//printf("ver = %d k = %d\n",ver,k);
used[ver] = 1;
cmp[ver] = k;
for (int i = rehead[ver]; i != -1; i = re[i].next){
int u = re[i].v;
if (!used[u])
rdfs(u,k);
}
}
int scc()
{
memset(used,0,sizeof(used));
vs.clear();
for (int v = 0; v < V; v++){
if (!used[v]) dfs(v);
}
memset(used,0,sizeof(used));
int k = 0;
for (int i = vs.size()-1; i >= 0; i--){
//printf("i=%d vector= %d",i,vs[i]);
if (!used[vs[i]])
rdfs(vs[i],k++);
}
return k;
}
//因爲連邊之後跑一遍scc,所以記得有反向邊的!
題意:有一個神父去主持婚禮,要麼在開始的時候主持,要麼在結束的時候主持。問你他能否參加小鎮上的所有婚禮而不互相沖突。
解答:對於婚禮A,把主持開頭設爲A爲真,把主持結尾設爲非A爲真。然後呢,根據已知的時間互相連邊。連線方向是已知的推出未知。
比如已知a和B非不能同時存在,那麼a可以推出b,連一個箭頭。非b可以推出非a,非b連向非a。
連邊完成後跑一遍SCC判斷強連通,如果後來A和非A處在同一個強連通分量裏面,那就會衝突,反之,輸出yes。
對於輸出結果。任取一個強連通分量爲真,比如對於婚禮a,輸出強連通分量標號較大的那個事件就可以了。
上代碼
/*注意反向邊,反向圖,init處理反向的head;
RE了。算n×n當做邊數好了
還是RE了。算2n×2n當做邊數好了
5000000的int數組佔用三萬多的內存。
*/
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
#define MAX_N 10000
#define SIZE_M 4000005
using namespace std;
int N;
int S[MAX_N],T[MAX_N],D[MAX_N];
struct pp
{
int v,next,w;
}pra[SIZE_M],re[SIZE_M];
int e,head[MAX_N],rehead[MAX_N];
void init()
{
e = 0;
memset(head,-1,sizeof(head) );
memset(rehead,-1,sizeof(rehead) );
}
void addedge(int x,int y)//,int z1,int z2)
{
pra[e].v = y;
pra[e].next = head[x];
head[x] = e;
re[e].v = x;
re[e].next = rehead[y];
rehead[y] = e++;
}
int V;
int used[MAX_N];
vector<int> vs;
int cmp[MAX_N];
void dfs(int ver)
{
//printf("ver = %d\n",ver);
used[ver] = 1;
for (int i = head[ver]; i != -1; i = pra[i].next){
int u = pra[i].v;
if (!used[u])
dfs(u);
}
vs.push_back(ver);
}
void rdfs(int ver,int k){//printf("ver = %d k = %d\n",ver,k);
used[ver] = 1;
cmp[ver] = k;
for (int i = rehead[ver]; i != -1; i = re[i].next){
int u = re[i].v;
if (!used[u])
rdfs(u,k);
}
}
int scc()
{
memset(used,0,sizeof(used));
vs.clear();
for (int v = 0; v < V; v++){
if (!used[v]) dfs(v);
}
memset(used,0,sizeof(used));
int k = 0;
for (int i = vs.size()-1; i >= 0; i--){
//printf("i=%d vector= %d",i,vs[i]);
if (!used[vs[i]])
rdfs(vs[i],k++);
}
return k;
}
//因爲連邊之後跑一遍scc,所以記得有反向邊的!
/*連線方向是已知的推出未知 其他的不用管了哈哈哈哈
比如已知a和B非不能同時存在,那麼a可以推出b,連一個箭頭。非b可以推出非a,非b連向非a
2-sat的原理就是。
在可以互相發生的事情之間連一條邊,然後
*/
void solve()
{
//設a是i,非a是i+N;b是j,非b是b+N
V = 2*N;
for (int i = 0; i < N; i++)
for (int j = 0; j < i; j++){//i > j, 連單向邊,兩邊方式不同
//for (int j = 0; j < N; j++){
// printf("i=%d j = %d",i,j);
if (min(S[i]+D[i], S[j]+ D[j])> max(S[i],S[j])){//由當前的a可以推出b非。由當前的b可以推出a非
addedge(i,j+ N);//從上往下連
addedge(j,i+N);// 從下往上連
//printf("ss i=%d j = %d\n",i,j);
}
if (min(S[i]+D[i],T[j] )> max(S[i],T[j]- D[j])){//由當前的a可以推出b,由當前的b非可以推出a非
addedge(i, j);
addedge(j+ N, i + N);
//printf("s ti=%d j = %d\n",i,j);
}
if (min(T[i],T[j])> max(T[i]-D[i],T[j] - D[j])){//由當前的a非可以推出b,由當前的b非可以推出a
addedge(i+N, j);
addedge(j+N,i);
//printf("tt i=%d j = %d\n",i,j);
/*addedge(i,j+ N);
addedge(j,i+ N);*/
}
if (min( T[i],S[j]+ D[j])> max(S[j], T[i] - D[i])){//由當前的a非可以推出b非,由當前的b可以推出a
addedge(i+ N, j+N);
addedge(j,i);
//printf("tsi=%d j = %d\n",i,j);
/*addedge(i,j);
addedge(j + N, i + N);*/
}
}
scc();
/*for (int i = 0; i < V; i++){
//printf("i=%d ",i);
for (int j = head[i]; j != -1; j = pra[j].next)
printf(" %d temp=%d",pra[j].v,cmp[pra[j].v]);
puts("");
}*/
for (int i = 0; i < N; i++)
if (cmp[i] == cmp[i+N]){
//printf("i=%d\n",i);
puts("NO");
return;
}
puts("YES");
for (int i = 0; i < N; i++){
if (cmp[i] > cmp[i+N]){// 任取一個聯通分量設爲真。看當前a還是非a在這個聯通分量中
printf("%02d:%02d %02d:%02d\n",S[i]/60,S[i]%60,(S[i]+ D[i])/60,(S[i]+ D[i])%60);
}else
printf("%02d:%02d %02d:%02d\n",(T[i] - D[i])/60,(T[i] - D[i])%60,T[i]/60,T[i]%60);
}
}
int main()
{
//freopen("input.txt","r",stdin);
int sum=0;
while (~scanf("%d",&N)){
init();
int temp1,temp2,temp3,temp4,temp5;
for (int i = 0; i < N ;i++){
scanf("%d:%d %d:%d %d",&temp1,&temp2,&temp3,&temp4,&temp5);
S[i] = temp1*60 + temp2;
T[i] = temp3 * 60 + temp4;
D[i] = temp5;
//printf("%d %d %d\n",S[i],T[i],D[i]);
}
solve();
//printf("%d\n",sum++);
}
return 0;
}
/*
2
08:00 09:00 30
08:15 09:00 20
*/