文章總數35祭+tarjan的多種用法:求強連通分量,縮點,割點、橋,LCA,點雙連通圖、邊雙聯通圖、2-SAT問題

文章總數35祭+tarjan的多種用法:求強連通分量,縮點,割點、橋,LCA,點雙連通圖、邊雙聯通圖、2-SAT問題
tarjan算法十分奇妙,有許多不同的運用。下面我們一起看看。

0.背景知識

先度娘一下tarjan:
Robert Tarjan,計算機科學家,以LCA、強連通分量等算法聞名。他擁有豐富的商業工作經驗,1985年開始任教於普林斯頓大學。
Robert Tarjan他還在多所大學擔任學術職務,如:康奈爾大學(1972-1973年),加州大學伯克利分校(1973-1975),斯坦福大學(1974-1980),紐約大學(1981-1985)。 他也加入過NEC研究所(1989-1997),並在美國麻省理工學院(1996年)擔任Visiting Scientist 。
Tarjan:他曾在AT&T貝爾實驗室(1980-1989),浩信科技(1997-2001),康柏(2002年)和惠普(2006年至今)工作。 他曾加入ACM和IEEE委員會,並曾爲幾家期刊的編輯。
Robert Tarjan出生在波莫納,加利福尼亞州。他的父親是一個專業兒童精神科醫生,以前在國家醫院任職。還是孩子的Robert Tarjan就閱讀了大量的科學小說,從此對天文學產生興趣,並夢想成爲一名天文學家。他在Scientific American雜誌上看完Martin Gardner的數學遊戲後又對數學產生了興趣。他的一位中學老師發現了他對數學的興趣,從八年級就開始培育他的數學能力。之後Robert開始深入研究數學。
Robert Tarjan上高中就找到了一份工作:從事IBM卡片校對機的工作。 他第一次真正用計算機工作是在1964年,那時他參與Summer Science Program在其中研究天文學。
Robert Tarjan在1969年獲得了加州理工學院數學學士學位。在斯坦福大學,他獲得了他的計算機科學碩士學位(1971)和博士學位(1972)。在斯坦福,他由羅伯特·弗洛伊德和高德納指導,兩位都是非常突出的計算機科學家。他的博士論文是An Efficient Planarity Algorithm。Robert Tarjan選定計算機科學領域作爲他的主要研究方向,是因爲他認爲計算機科學是實踐數學理論的方式,有現實價值。
Robert Tarjan設計了求解的應用領域的許多問題的廣泛有效的算法和數據結構。 他已發表了超過228篇理論文章(包括雜誌,一些書中的一些章節文章等)。Robert Tarjan以在數據結構和圖論上的開創性工作而聞名。 他的一些著名的算法包括 Tarjan最近共同祖先離線算法 ,Tarjan的強連通分量算法 以及Link-Cut-Trees算法等。其中Hopcroft-Tarjan平面嵌入算法是第一個線性時間平面算法。
Tarjan也開創了重要的數據結構如:斐波納契堆和splay樹(splay發明者還有Daniel Sleator)。另一項重大貢獻是分析了並查集。他是第一個證明了計算反阿克曼函數的樂觀時間複雜度的科學家。
Tarjan與約翰霍普克羅夫特共同於1986年獲得圖靈獎。
Tarjan還於1994年當選爲ACM院士。
Tarjan其他獎項包括:
奈望林納獎信息科學(1983第一個獲獎者)
國家科學院的研究倡議獎 (1984)
巴黎Kanellakis獎-理論與實踐( ACM1999)
帕斯卡獎章數學與計算機科學( 歐洲科學院2004)
加州理工學院傑出校友獎( 美國加州技術研究所2010)

好了,BB了那麼久,是該講講tarjan的前置知識了。

1.DFS
2.沒了。

1.求強連通分量

什麼叫強連通分量?
如圖:

在這裏插入圖片描述
這整個圖都是一個強連通分量
它的定義是:圖中任意兩個節點可以互相到達
我們藉助兩個數組:DFN和LOW
DFN的定義是:節點的時間戳(即第幾個DFS到)
LOW的定義是:可以回溯的最小的棧中的時間戳
模擬一遍樣例:

在這裏插入圖片描述
然後如何判斷一個點是不是在某個強連通分量上的呢?
如果DFN[u]==LOW[u]既是
爲什麼呢?
我們思考一下兩個數組的定義
如果節點u所能回溯的最小DFN值
如果u只能回溯到自己,說明當前棧中起始一段都是帶有節點u的強連通分量
因爲u不能再回溯了。
來段僞代碼壓壓驚:

void tarjan(int u){
	DFN[u]=LOW[u]=++time_line;
	s.push(u);
	for(枚舉u的出邊){
		if(DFN[v]0){
			tarjan(v);
			LOW[u]=min(LOW[u],LOW[v]);
		}
		else if(v在棧中){
			LOW[u]=min(LOW[u],DFN[v]);
		}
	}
	if(判斷u是強連通分量){
		while(棧頂元素≠u) 輸出棧頂元素,退棧
		輸出棧頂元素,退棧
	}
}

這樣即可輸出當前圖中所有的強連通分量
你百度了一圈,也沒有發現模板題
因爲只用單獨輸出每個元素即可通過SPJ並AC
............
所以強連通分量一般與其他題目結合
例題:luogu P2863 [USACO06JAN]牛的舞會The Cow Prom
題目描述
The N (2 <= N <= 10,000) cows are so excited: it’s prom night! They are dressed in their finest gowns, complete with corsages and new shoes. They know that tonight they will each try to perform the Round Dance.
Only cows can perform the Round Dance which requires a set of ropes and a circular stock tank. To begin, the cows line up around a circular stock tank and number themselves in clockwise order consecutively from 1…N. Each cow faces the tank so she can see the other dancers.
They then acquire a total of M (2 <= M <= 50,000) ropes all of which are distributed to the cows who hold them in their hooves. Each cow hopes to be given one or more ropes to hold in both her left and right hooves; some cows might be disappointed.
約翰的N (2 <= N <= 10,000)只奶牛非常興奮,因爲這是舞會之夜!她們穿上禮服和新鞋子,別 上鮮花,她們要表演圓舞.
只有奶牛才能表演這種圓舞.圓舞需要一些繩索和一個圓形的水池.奶牛們圍在池邊站好, 順時針順序由1到N編號.每隻奶牛都面對水池,這樣她就能看到其他的每一隻奶牛.
爲了跳這種圓舞,她們找了 M(2<M< 50000)條繩索.若干只奶牛的蹄上握着繩索的一端, 繩索沿順時針方繞過水池,另一端則捆在另一些奶牛身上.這樣,一些奶牛就可以牽引另一些奶 牛.有的奶牛可能握有很多繩索,也有的奶牛可能一條繩索都沒有.
對於一隻奶牛,比如說貝茜,她的圓舞跳得是否成功,可以這樣檢驗:沿着她牽引的繩索, 找到她牽引的奶牛,再沿着這隻奶牛牽引的繩索,又找到一隻被牽引的奶牛,如此下去,若最終 能回到貝茜,則她的圓舞跳得成功,因爲這一個環上的奶牛可以逆時針牽引而跳起旋轉的圓舞. 如果這樣的檢驗無法完成,那她的圓舞是不成功的.
如果兩隻成功跳圓舞的奶牛有繩索相連,那她們可以同屬一個組合.
給出每一條繩索的描述,請找出,成功跳了圓舞的奶牛有多少個組合?
For the Round Dance to succeed for any given cow (say, Bessie), the ropes that she holds must be configured just right. To know if Bessie’s dance is successful, one must examine the set of cows holding the other ends of her ropes (if she has any), along with the cows holding the other ends of any ropes they hold, etc. When Bessie dances clockwise around the tank, she must instantly pull all the other cows in her group around clockwise, too. Likewise,
if she dances the other way, she must instantly pull the entire group counterclockwise (anti-clockwise in British English).
Of course, if the ropes are not properly distributed then a set of cows might not form a proper dance group and thus can not succeed at the Round Dance. One way this happens is when only one rope connects two cows. One cow could pull the other in one direction, but could not pull the other direction (since pushing ropes is well-known to be fruitless). Note that the cows must Dance in lock-step: a dangling cow (perhaps with just one rope) that is eventually pulled along disqualifies a group from properly performing the Round Dance since she is not immediately pulled into lockstep with the rest.
Given the ropes and their distribution to cows, how many groups of cows can properly perform the Round Dance? Note that a set of ropes and cows might wrap many …
input file
Line 1: Two space-separated integers: N and M
Lines 2…M+1: Each line contains two space-separated integers A and B that describe a rope from cow A to cow B in the clockwise direction.
output file
Line 1: A single line with a single integer that is the number of groups successfully dancing the Round Dance.
一句話題意:輸出元素個數2≥2的元素個數
近乎模板題
只用把輸出強連通分量改爲統計答案+求和即可
代碼:

#include<bits/stdc++.h>
using namespace std;
const int N=200005;
int n,m;
int hed[N],tal[N],nxt[N],cnt=0;
int dfn[N]={0};//時間戳 
int low[N];//能返回的最小的棧中時間戳 
int indexy=0;//當前時間戳 
int ans=0;//答案 
stack<int> s;//棧 
bool vis[N]={0};//是否在棧中 
void add(int x,int y){
 	cnt++;
 	tal[cnt]=y;
 	nxt[cnt]=hed[x];
	 hed[x]=cnt;
}
void tarjan(int x){
	 low[x]=dfn[x]=++indexy;
	 s.push(x);
	 vis[x]=1;
	 for(int i=hed[x];i;i=nxt[i]){
  		int y=tal[i];
  		if(!dfn[y]){
  			 tarjan(y);
   			 low[x]=min(low[x],low[y]);
	     }
  		else if(vis[y]){
   			low[x]=min(low[x],dfn[y]);
  		}
 	} 
 	if(low[x]==dfn[x]){//找到一個強連通分量 
  		int sum=1;
  		vis[x]=0;
  		while(s.top()!=x){
   			sum++;
   			vis[s.top()]=0;
  			 s.pop();
  		} 
  		s.pop();
 		 if(sum>1) ans++;
 	}
}
int main(){
 	scanf("%d%d",&n,&m); 
 	while(m--){
  		int x,y;
 	 	scanf("%d%d",&x,&y);
 	 	add(x,y);
	}
 	for(int i=1;i<=n;i++){
  		if(!dfn[i]){
   			tarjan(i);
  		}
 	}
 	printf("%d\n",ans);
 	return 0;
}

你以爲強連通分量就這樣結束了?

真的就這樣結束了?

真的?

NO!

(敲黑板)你是否注意到

else if(vis[y]){
      low[x]=min(low[x],dfn[y]);
}

爲啥不是

else if(vis[y]){
      low[x]=min(low[x],low[y]);
}

呢?
事實上,99.99999999.999999%的強連通分量都可以用第二種寫法水過
(幾乎是全部的)
(話說輸出所有元素可以通過100%呢!)
但是............

割點!

什麼是割點?
即爲刪除後使圖不連通
十分好理解
比如下圖:
在這裏插入圖片描述
圖中唯一的割點即是4
怎麼找呢?
具體見下文(逃)
小代碼:

#include<bits/stdc++.h>
using namespace std;
const int N=300005;
int DFN[N]={0},LOW[N];
int n,m;
int hed[N]={0},tal[N],nxt[N];
int cnt=0;
int ida=0;
bool cut[N]={0};
int ans=0;
int f[N];
void addege(int x,int y)
{
    cnt++;  
       tal[cnt]=y;  
       nxt[cnt]=hed[x];  
	   hed[x]=cnt;  
}
void tarjan(int u,int fa)//tarjan求割點 
{
	//cout << u << " -> " << fa << endl;
    DFN[u]=LOW[u]=++ida;
    int ch=0;
    for(int i=hed[u];i;i=nxt[i])
    {
   	  int v=tal[i];
   	  //cout << v << endl;
		 if(!DFN[v])
		 {
 			 tarjan(v,fa);
 			 LOW[u]=min(LOW[u],LOW[v]);
			  if(LOW[v]>=DFN[u]&&u!=fa)
 			  cut[u]=1;
			  if(u==fa)
 				  ch++;
   	  }
		 LOW[u]=min(LOW[u],DFN[v]);
    }
    if(ch>=2&&u==fa&&cut[u]==0)
		 cut[u]=1; 
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++)
    {
		 int a,b;
 		scanf("%d%d",&a,&b);
		 addege(a,b);
   	  addege(b,a);
    }
	ida=0;
	for(int i=1;i<=n;i++)
		 if(DFN[i]==0)
			  tarjan(i,i);
	ans=0;
	for(int i=1;i<=n;i++) if(cut[i]) ans++;
	printf("%d\n",ans);
	for(int i=1;i<=n;i++) if(cut[i]) printf("%d ",i);
	return 0;
}

而在下面這個樣例中,第二種方法求割點將失效:

在這裏插入圖片描述解釋完畢。
接下來進入縮點環節

縮點

有嘛用?
十分有用!
一句話:把強連通分量裏的點打包成新的一個點
HOW TO DO IT?
看圖:
在這裏插入圖片描述
我們縮縮縮,縮成這樣:

在這裏插入圖片描述 成功!
如何操作?
樸素做法:
1.存下原圖
2.導出每個強連通分量,把裏面的點標記爲++cnt
3.對於縮後的點重新建圖
4.根據題目要求重新DFS
來個例題吧!

luogu P2341 [HAOI2006]受歡迎的牛

題目描述

每頭奶牛都夢想成爲牛棚裏的明星。被所有奶牛喜歡的奶牛就是一頭明星奶牛。所有奶
牛都是自戀狂,每頭奶牛總是喜歡自己的。奶牛之間的“喜歡”是可以傳遞的——如果A喜
歡B,B喜歡C,那麼A也喜歡C。牛欄裏共有N 頭奶牛,給定一些奶牛之間的愛慕關係,請你算出有多少頭奶牛可以當明星。

輸入輸出格式

輸入格式:

 第一行:兩個用空格分開的整數:N和M
 第二行到第M + 1行:每行兩個用空格分開的整數:A和B,表示A喜歡B

輸出格式:

 第一行:單獨一個整數,表示明星奶牛的數量

輸入輸出樣例

輸入樣例1:

3 3
1 2
2 1
2 3

輸出樣例1:

1

說明

只有 3 號奶牛可以做明星

數據範圍

n &lt;=&lt;= 10000, m &lt;=&lt;= 50000
考慮一頭奶牛,他要受歡迎,必須所有兒子都直接/間接喜歡它
直接考慮太複雜,我們考慮縮點,將互相愛慕的奶牛縮在一起
如樣例

在這裏插入圖片描述
縮成:

在這裏插入圖片描述
保證了圖中無環,這樣就可以愉快地DFS了
當然,答案是作爲“answer”的強連通分量中元素個數
還有,答案一定是出度爲0的強連通分量的元素數
原因:無出度,那麼必然沒有再愛慕的對象了
還有,出度爲0的==0或者>1答案都爲0
上代碼

#include<bits/stdc++.h>
using namespace std;
const int N=100005;
struct edge{
    int x,y;
}s[N];
int n,m; 
int hed[N],tal[N],nxt[N],cnt1=0;//簡單的前向星存圖   
int dfn[N]={0},low[N],indexy=0;//和強連通分量一毛一樣 
stack<int> sta;//存儲元素的棧    
bool vis[N]={0};//是否在棧中  
int num[N];//第i個點的所屬強連通分量編號     
int cnt=0;//強連通分量的數量 
int d[N]={0};//第i個強連通分量的出度  
int sum[N]={0};//第i個強連通分量的元素個數  
void add(int x,int y){
    cnt1++;
    tal[cnt1]=y;
    nxt[cnt1]=hed[x];
    hed[x]=cnt1;
}
void tarjan(int x){
    dfn[x]=low[x]=++indexy;
    sta.push(x);
    vis[x]=1;
    for(int i=hed[x];i;i=nxt[i]){
        int y=tal[i];
        if(!dfn[y]){
            tarjan(y);
            low[x]=min(low[x],low[y]);
        } 
        else if(vis[y]){
            low[x]=min(low[x],dfn[y]);//重要!!!!!! 
        }
    }
    if(dfn[x]==low[x]){
        cnt++;
        while(sta.top()!=x){
            num[sta.top()]=cnt;
            vis[sta.top()]=0;
            sum[cnt]++; 
            sta.pop();
        }
        sum[cnt]++;
        num[sta.top()]=cnt;
        sta.pop();
        vis[x]=0; 
    }
}
int main(){
    ios::sync_with_stdio(false);
    cin >> n >> m;
    for(register int i=1;i<=m;i++){
        int x,y;
        cin >> x >> y;
        s[i].x=x,s[i].y=y;
        add(x,y);
    } 
    for(register int i=1;i<=n;i++){
        if(!dfn[i]){
            tarjan(i);
        }
    }
    for(register int i=1;i<=m;i++){
        if(num[s[i].x]!=num[s[i].y]) d[num[s[i].x]]++;
    }
    int CNT=0;
    for(register int i=1;i<=cnt;i++){
        if(d[i]==0){
            CNT++;
        }
    }
    if(CNT==0||CNT>1) cout<<0<<endl;
    else{
        for(register int i=1;i<=cnt;i++){
            if(d[i]==0){//沒入度,必爲終點 
                cout <<sum[i]<<endl;
                break;
            }
        }
    } 
    return 0;
}

ou yes!
next 難點:

割點

之前閒着沒講,現在來搞搞?
我相信大家一定了解了割點的定義
現在我們要思考一下怎麼判斷一個點是不是割點
先放上定義:
1.如果u是根(root),如果一次可以將它的子樹dfs完,它就不是割點/反之,需要2次及以上dfs才能搜完它的子樹,它就是割點
2.如果u不是根,如果存在一個子節點v使得low[v]>=dfn[u],那麼u就是割點
額,所以是爲什麼得出來的呢?
我們思考第一條
如果我可以通過不經過根節點從一個子樹到另外的子樹,根的存在就不影響圖的連通性了,根也自然不是割點,反之,從某個子樹到另一個子樹必經根,根就是割點
第二條,如果去掉了u使得v和u的父親們失去了聯繫,u必然是割點
說了那麼多,我也喝口水,來看幾張圖:
第一條,是割點的:
在這裏插入圖片描述
第一條,不是割點的:

在這裏插入圖片描述
第二條,是割點的
在這裏插入圖片描述
額,第二條不是割點的就不畫了
問題就十分簡單了
把之前的代碼改改即可

#include<bits/stdc++.h>
using namespace std;
const int N=200005;
int n,m;
int hed[N],tal[N],nxt[N],cnt=0;
int dfn[N]={0},low[N],indexy=0;
bool cut[N];
int ans=0;
void add(int x,int y){
    cnt++;
    tal[cnt]=y;
    nxt[cnt]=hed[x];
    hed[x]=cnt;
}
void tarjan(int x,int fa){
    dfn[x]=low[x]=++indexy;
    int times=0;//特判根節點 
    for(int i=hed[x];i;i=nxt[i]){
        int y=tal[i];
        if(!dfn[y]){
            if(x==fa) times++;
            tarjan(y,fa);
            low[x]=min(low[x],low[y]);
            if(low[y]>=dfn[x]&&x!=fa) cut[x]=1;
        }
        low[x]=min(low[x],dfn[y]);
    }
    if(x==fa&&times>1) cut[x]=1;
}
int main(){
    ios::sync_with_stdio(false);
    cin >> n >> m;
    while(m--){
        int x,y;
        cin >> x >> y;
        add(x,y);
        add(y,x);
    }
    for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i,i);//做自己的父親 
    for(int i=1;i<=n;i++) if(cut[i]) ans++;
    cout << ans << endl;
    for(int i=1;i<=n;i++) if(cut[i]) cout << i << " ";
    cout <<endl;
    return 0;
} 

AC!
下一個:

我…
來了一個割點,怎麼又來一個橋?
err…
事實上十分簡單
如果u的子節點有一個的low[v]>dfn[u],dfn[u]即爲橋
原因和割點差不多
上代碼:

#include<cstdio> 
#include<vector>
#include<cstring>
#include<iostream>
#include<algorithm>
#define hi printf("hi!");
using namespace std;

vector<pair<int,int> >bridge;
vector<int> g[10010];
int dfn[10010],low[10010];
int deep,root,n,m,ans;

int tarjan(int u,int fa)
{
    int lowu;
    lowu=dfn[u]=++deep;
    int sz=g[u].size();
    for(int i=0;i<sz;i++)
    {
        int v=g[u][i];
        if(!dfn[v])
        {
            int lowv=tarjan(v,u);
            lowu=min(lowu,lowv);
            if(lowv>dfn[u])
            {
                int from,to;
                from=u;
                to=v;
                if(from>to)
                {
                    swap(from,to);
                }
                bridge.push_back(make_pair(from,to));
            }
        }
        else
        {
            if(v!=fa&&dfn[v]<dfn[u])
            {
                lowu=min(lowu,dfn[v]);
            }
        } 
    }
    low[u]=lowu;
    return lowu;
} 

int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++)
    {
        int from,to;
        scanf("%d%d",&from,&to);
        g[from].push_back(to);
        g[to].push_back(from);
    }
    for(int i=1;i<=n;i++)
    {
        if(!dfn[i])
        {
            root=i;
            tarjan(i,-1);
        }
    }
    for(int i=0;i<bridge.size();i++)
    {
        printf("%d %d\n",bridge[i].first,bridge[i].second);
    }
}

沒有例題…
下一個!下一個!

LCA

啥啥啥?tarjan還可以求LCA?
先放上倍增求LCA。
相信大家都會
f[u][x]表示u點往上跳2的x次方次所到達的點是啥
可以O(nlogn)預處理dp出
具體思想:任何數字都可以二進制拆分,故可以這麼做

注意:

1.預處理的外層循環枚舉x,內層循環枚舉u!

2.卡卡常

總時間複雜度:O((n+m)logn)
代碼:

#include<bits/stdc++.h>
using namespace std;
const int N=500005;
#define re register 
int n,m,root;
int dep[N]={0};
int f[N][21];
int hed[N]={0},tal[N*2],nxt[N*2],cnt=0;
void add(int x,int y){
    cnt++;
    tal[cnt]=y;
    nxt[cnt]=hed[x];
    hed[x]=cnt;
}
void dfs(int x,int fa,int deep){//dfs求深度 
    dep[x]=deep;
    f[x][0]=fa;
    //for(re int i=1;(1<<i)<=dep[x];i++) f[x][i]=f[f[x][i-1]][i-1];
    for(re int i=hed[x];i;i=nxt[i]){
        int y=tal[i];
        if(y!=fa) dfs(y,x,deep+1);
    }
}
int LCA(int x,int y){
    if(dep[x]>dep[y]) swap(x,y);
    for(re int i=20;i>=0;i--) if(dep[f[y][i]]>=dep[x]) y=f[y][i];
    if(x==y){
        return x;
    }
    //cout << x << " " << y << endl;
    for(re int i=20;i>=0;i--){
        if(f[x][i]!=f[y][i]){
            //cout << x << " " << y << " " << i << " " << f[x][i]<< " " << f[y][i] << " ***"<<endl;
            x=f[x][i];
            y=f[y][i];
        }
    }
    return f[x][0];
}
int main(){
    scanf("%d%d%d",&n,&m,&root);
    for(re int i=2;i<=n;i++){
        int x,y;
        scanf("%d%d",&x,&y);
        add(x,y);
        add(y,x);
    }
    dfs(root,0,1);
    for(int j=1;j<=20;j++){//預處理dp數組 
        for(int i=1;i<=n;i++){
            f[i][j]=f[f[i][j-1]][j-1];
        }
    }
    while(m--){
        int x,y;
        scanf("%d%d",&x,&y);
        printf("%d\n",LCA(x,y));
    } 
    return 0;
} 

過了easy的LCA,我們來看看樹鏈剖分求LCA
我還不會
口胡一下DFS序+ST表/線段樹求LCA
思考一下DFS序的優秀性質
是啥?
當然是:區間對應樹上的路徑
考慮存入每個數的DFS序
在in區間裏找到深度最小的點的編號就是LCA
神奇不神奇!
當然這是一定可行的
因爲另外深度&gt;=&gt;=答案深度的點一定不在in區間裏。區間上的最值,線段樹or樹狀數組orST表均可
簡單維護一下即可
優點:單次詢問O(1),對於詢問較多的數據展現得比較優秀
代碼丟失!

接下來:我們來看看tarjan求LCA

來個可愛的小圖!
在這裏插入圖片描述
這是一顆樹。假設現在要求5和6的LCA,我們可以這樣操作:DFS
同時還可以求出它的順序,它並沒有對算法起到作用,但是可以方便我們理解順序
在這裏插入圖片描述
然後我們可以開始考慮tarjan的算法
當然,它是離線的
首先,我們可以記錄每個點查詢的對應點,記錄
再定一個數組叫vis,表示是否搜索過
每次先遞歸子節點,再更新答案
每次回溯到父節點時更新fa(也就是並查集)
這樣,dfs(第一次)到一個節點,我們判斷它的每個對應點是否vis過
如果沒就不做,如果有就find一下調用並查集
例如下圖
在這裏插入圖片描述
5、6節點均有一條指向父親的邊
這樣當我們搜到6時,指向了4
事實上LCA(5,6)就是4
全圖:
在這裏插入圖片描述
這樣我們就只用維護一個並查集就可以了
時間複雜度O(n+m)
優點:飛快
缺點:常數大,是離線的
代碼:無
接下來:

2-SAT!

簡單介紹一下2-sat問題
就是給你一堆關係,兩個裏面只能滿足一條,問是否可行以及一種方案
很迷啊???
那麼:
看看這個:

題目背景

2-SAT 問題 模板題目描述有n個布爾變量x1x_1x1​~xnx_nxn​,另有m個需要滿足的條件,每個條件的形式都是“xix_ixi​爲true/false或xjx_jxj​爲true/false”。比如“x1x_1x1​爲真或x3x_3x3​爲假”、“x7x_7x7​爲假或x2x_2x2​爲假”。2-SAT 問題的目標是給每個變量賦值使得所有條件得到滿足。

輸入輸出格式

輸入格式:

第一行兩個整數n和m,意義如體面所述。接下來m行每行4個整數 i a j b,表示“xix_ixi​爲a或xjx_jxj​爲b”(a,b∈{0,1})

輸出格式:

如無解,輸出“IMPOSSIBLE”(不帶引號); 否則輸出"POSSIBLE"(不帶引號),下 一行n個整數x1​~n​(xi​∈{0,1}),表示構造出的解。

輸入輸出樣例

輸入樣例#1:

3 1
1 1 3 0

輸出樣例#1:

POSSIBLE
0 0 0

說明

1<=n,m<=1e6 , 前3個點卡小錯誤,後面5個點卡效率,由於數據隨機生成,可能會含有( 10 0 10 0)之類的坑,但按照最常規寫法的寫的標程沒有出錯,各個數據點卡什麼的提示在標程裏。
然後…
我們考慮2-SAT
這不是廢話嘛…
看下圖:

在這裏插入圖片描述
爲了簡化問題,我們給每個i克隆一個i+n,但是新點的opt=1,原點的是0。
看圖:
在這裏插入圖片描述
當然,我們只能取其中的n個,且1、7不能同時取,2、8不能同時取,i、i+n不能同時取(一個點不能有兩個權值對嗎?)
然後考慮加邊
我們想:如果1 0 2 0,那麼我們可以給1、8連有向邊,7、2連有向邊
表示我選了i就一定得選d[i]
比如我們有三個關係:
1 0 2 1
3 0 4 0
3 0 6 1
我們可以得到下圖:
在這裏插入圖片描述
然後,事情就明朗了。
我們可以給這個圖染色(tarjan求強連通分量)
簡單,對嗎?
如果i和i+n在同一塊內,一定就無解了。
否則輸出be[i]>be[i+n]
也就是判斷一下i和i+n的先後順序
還有存圖
記得開2倍空間
以及加邊操作代碼:

while(m--){
  	int x,phix,y,phiy,deltax,deltay;
  	scanf("%d%d%d%d",&x,&phix,&y,&phiy);
  	deltax=phix^1,deltay=phiy^1;
	addege(x+deltax*n,y+phiy*n);
  	addege(y+deltay*n,x+phix*n);
 }

完整代碼:

#include<bits/stdc++.h>
using namespace std;
const int N=5000005; 
int n,m;
int hed[N],tal[N],nxt[N],cnt=0;
int dfn[N]={0},low[N];
int opt=0,indexy=0;
int ssp[N];
stack<int> s;
bool vis[N]={0}; 
void addege(int x,int y){
    cnt++;
    tal[cnt]=y;
    nxt[cnt]=hed[x];
    hed[x]=cnt; 
}
void tarjan(int u){
    dfn[u]=low[u]=++indexy;
    vis[u]=1;
    s.push(u);
    for(int i=hed[u];i;i=nxt[i]){
        int v=tal[i];
        if(!dfn[v]){
            tarjan(v);
            low[u]=min(low[u],low[v]);
        }
        else if(vis[v]){
            low[u]=min(low[u],dfn[v]);
        }
    }
    if(dfn[u]==low[u]){
        opt++;
        while(!s.empty()&&s.top()!=u){
            ssp[s.top()]=opt;
            vis[s.top()]=0;
            s.pop();
        } 
        ssp[s.top()]=opt;
        vis[s.top()]=0;
        s.pop();
    }
}
bool Double_SAT(){
    for(int i=1;i<=2*n;i++) if(!dfn[i]) tarjan(i);
    for(int i=1;i<=n;i++) if(ssp[i]==ssp[i+n]) return 0;
    return 1;
}
int main(){
    scanf("%d%d",&n,&m);
    while(m--){
        int x,phix,y,phiy,deltax,deltay;
        scanf("%d%d%d%d",&x,&phix,&y,&phiy);
        deltax=phix^1,deltay=phiy^1;
        addege(x+deltax*n,y+phiy*n);
        addege(y+deltay*n,x+phix*n);
    }
    if(Double_SAT()){
        printf("POSSIBLE\n");
        for(int i=1;i<=n;i++){
            printf("%d ",ssp[i]>ssp[i+n]);
        }
        printf("\n");
    }
    else{
        printf("IMPOSSIBLE");
    }
    return 0;
} 

TARJAN IS END

THE END

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