強連通分量題集

一本通OJ
靈動OJ

A.受歡迎的牛(popular)

題意描述
原題來自:USACO 2003 Fall

每一頭牛的願望就是變成一頭最受歡迎的牛。現在有 N 頭牛,給你 M 對整數 (A,B),表示牛 A 認爲牛 B 受歡迎。這種關係是具有傳遞性的,如果 A 認爲 B 受歡迎,B 認爲 C 受歡迎,那麼牛 A 也認爲牛 C 受歡迎。你的任務是求出有多少頭牛被除自己之外的所有牛認爲是受歡迎的。

解題思路
一頭牛是受歡迎的,當且僅當其它所有牛都是它的粉絲。那麼我們在這樣一個有向圖中,首先把強連通分量縮點,變成 DAG ,在新的圖上,出度爲 0 的點纔有可能是受歡迎的。如果這樣的點僅有一個,那麼該點所代表的牛就都是受歡迎的,如果有不止一個,那麼就不存在受歡迎的牛。

代碼示例

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
const int M = 2*N;
//建立有向圖的同時建立它的反圖
int head[N],ver[M],nex[M],edge[M],tot = 1;
int rhead[N],rver[M],rnex[M],redge[M],rtot = 1;
void addEdge(int x,int y){
	ver[++tot] = y; nex[tot] = head[x]; head[x] = tot;
	rver[++rtot] = x,rnex[rtot] = rhead[y]; rhead[y] = rtot;
}
/* 	co[x]:節點x所屬強連通的id,col則是用於計數
	ts[x]:結束時間爲x的節點編號,clk用於計數 */

int n,m,cnt[N],deg[N];
int co[N],col,ts[N],clk;

void dfs(int x){
	co[x] = 1;
	for(int i = head[x];i ;i = nex[i])
		if(!co[ver[i]]) dfs(ver[i]);
	ts[++clk] = x;	//第 clk 個結束的是x
}
void rdfs(int x,int col){
	co[x] = col; ++cnt[col];
	for(int i = rhead[x]; i ;i = rnex[i])
		if(!co[rver[i]]) rdfs(rver[i],col);
}
void Korasaju(){
	for(int i = 1;i <= n;i++) if(!co[i]) dfs(i);
	for(int i = 1;i <= n;i++) co[i] = 0;
	for(int i = n;i ;--i) 
		if(!co[ts[i]]) rdfs(ts[i],++col);
}
void solve(){
	Korasaju();
	for(int x = 1;x <= n;x++)
		for(int i = head[x];i ;i = nex[i])
			if(co[x] != co[ver[i]]) deg[co[x]]++;
	int ans = 0;
	for(int i = 1;i <= col;i++)
		if(!deg[i] && ans){
			puts("0"); return;
		}else if(!deg[i]) ans = cnt[i];
	printf("%d\n",ans);
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i = 1,x,y;i <= m;i++){
		scanf("%d%d",&x,&y);
		addEdge(x,y);
	}
	solve();
	return 0;
}

B.最大半連通子圖(semi)

題意描述
一個有向圖 G=( V , E ) 稱爲半連通的 (Semi-Connected),如果滿足對於圖中任意兩點 u,v , 存在一條 u 到 v 的有向路徑或者從 v 到 u 的有向路徑。

若 G’=(V’,E’) 滿足 V’屬於 V, E’ 是 E 中所有跟 V’ 有關的邊,則稱 G’ 是 G 的一個導出子圖。若 G’ 是 G 的導出子圖,且 G’ 半連通,則稱 G’ 爲 G 的半連通子圖。若 G’ 是 G 所有半連通子圖中包含節點數最多的,則稱 G’ 是 G 的最大半連通子圖。

給定一個有向圖 G,請求出 G 的最大半連通子圖擁有的節點數 K,以及不同的最大半連通子圖的數目 C。由於C可能比較大,僅要求輸出 C 對 X 的餘數。
解題思路
這道題首先要理解清楚半連通、半連通子圖的概念,然後才能進行求解。這道題有兩問,第一問求最大的半連通子圖(節點數最多的),第二問求有多少種不同的最大半連通子圖。

對於第一問可以通過先將原圖縮點,變成一張 DAG,然後再用拓撲排序來尋找‘最長鏈’,這個最長鏈就是最大半連通子圖,而在 topo 過程中我們還可以設置一個數組 f[i] 表示到節點 i 達到局部最長鏈的方案數。

代碼示例

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
const int M = 1e6+10;
int head[N],ver[M],nex[M],tot = 1;
void addEdge(int x,int y){
	ver[++tot] = y; nex[tot] = head[x]; head[x] = tot;
}
int co[N],col,dfn[N],clk,low[N];
int n,m,X;
int Stack[N],top,cnt[N];
void Tarjan(int x){
	dfn[x] = low[x] = ++clk;	//dfs序
	Stack[++top] = x;
	for(int i = head[x];i ;i = nex[i]){
		int y = ver[i];
		if(!dfn[y]) Tarjan(y), low[x] = min(low[x],low[y]);
		else if(!co[y]) low[x] = min(low[x],dfn[y]);
	}
	if(dfn[x] == low[x])
		for(++col;Stack[top+1] != x;top--)
			co[Stack[top]] = col,cnt[col]++;
}
int f[N]; //到 x 距離最長時不同的方案數
int deg[N],dis[N]; 
queue<int> que;
void topo(){
	for(int i = 1;i <= col;i++) if(!deg[i]) que.push(i),dis[i] = cnt[i];
	for(int i = 1;i <= col;i++) f[i] = 1;	//這兒置1,但是下面要置0
	while(que.size()){
		int x = que.front(); que.pop();
		for(int i = head[x];i ;i = nex[i]){
			int y = ver[i]; deg[y]--;
			if(!deg[y]) que.push(y);
			if(dis[y] < dis[x] + cnt[y])
				dis[y] = dis[x]+cnt[y], f[y] = 0;//要把 f[y] 置零
			if(dis[y] == dis[x] + cnt[y])	//任何情況下都要從 x 上轉移過來
				f[y] = (f[y]+f[x])%X;
		}
	}
}
pair<int,int> pa[M];
void solve(){
	for(int i = 1;i <= n;i++) if(!dfn[i]) Tarjan(i);
	//建立新圖
	for(int x = 1,tc = 0;x <= n;x++)
		for(int i = head[x];i ;i = nex[i])
			pa[++tc].first = co[x],pa[tc].second = co[ver[i]];
	sort(pa+1,pa+1+m);
	memset(head,0,sizeof head); tot = 1;
	for(int i = 1;i <= m;i++){
		int x = pa[i].first, y = pa[i].second;
		int xx = pa[i-1].first, yy = pa[i-1].second;
		if(x != y && (x != xx || y != yy))
			addEdge(x,y),deg[y]++;
	}
	//建圖後利用拓撲排序查找最長鏈以及不同個數
	topo();  int mx = 0,ty = 0;
	for(int i = 1;i <= col;i++) mx = max(mx,dis[i]);
	for(int i = 1;i <= col;i++) if(dis[i] == mx) ty = (ty+f[i])%X;
	printf("%d\n%d\n",mx,ty);
}
int main(){
	scanf("%d%d%d",&n,&m,&X);
	for(int i = 1,x,y;i <= m;i++){
		scanf("%d%d",&x,&y); addEdge(x,y);
	}
	solve();
	return 0;
}

C.網絡協議(net)

題意描述

出自 IOI 1996一些學校連接在一個計算機網絡上。學校之間存在軟件支援協議。每個學校都有它應支援的學校名單(學校 a 支援學校 b,並不表示學校 b 一定支援學校 a)。當某校獲得一個新軟件時,無論是直接得到還是網絡得到,該校都應立即將這個軟件通過網絡傳送給它應支援的學校。因此,一個新軟件若想讓所有連接在網絡上的學校都能使用,只需將其提供給一些學校即可。

任務a: 請編一個程序,根據學校間支援協議(各個學校的支援名單),計算最少需要將一個新軟件直接提供給多少個學校,才能使軟件通過網絡被傳送到所有學校;

任務b: 如果允許在原有支援協議上添加新的支援關係。則總可以形成一個新的協議,使得此時只需將一個新軟件提供給任何一個學校,其他所有學校就都可以通過網絡獲得該軟件。編程計算最少需要添加幾條新的支援關係。

解題思路
首先可以知道在一個強連通分量中,任意一個學校擁有資源,那麼其它所有學校就都會擁有資源,所以我們可以通過先縮點,將原圖變爲 DAG 。在此之後可以通過計算入度爲 0 的點的數目,這就是我們需要提供的學校數量。
第二問可以通過統計出度爲 0 的點的數目,因此來統計至少需要添加多少條邊才能使得 DAG 變爲強連通圖,當然要特判 DAG 僅有一個點的情況,此時無需添加額外的邊。

代碼示例

#include<bits/stdc++.h>
using namespace std;
const int N = 110;
int G[N][N];
int n;
int dfn[N],low[N],co[N],col,clk;
int Stack[N],top;
void Tarjan(int x){
	dfn[x] = low[x] = ++clk;
	Stack[++top] = x;
	for(int i = 1;i <= n;i++){
		if(!G[x][i]) continue;
		if(!dfn[i]) Tarjan(i),low[x] = min(low[x],low[i]);
		else if(!co[i]) low[x] = min(low[x],dfn[i]);
	}
	if(low[x] == dfn[x])
		for(++col;Stack[top+1] != x;--top)
			co[Stack[top]] = col;
}
int deg[N],deg2[N];
void solve(){
	for(int i = 1;i <= n;i++) if(!dfn[i]) Tarjan(i);
	for(int i = 1;i <= n;i++)
		for(int j = 1;j <= n;j++)
			if(G[i][j] && co[i] != co[j]) deg[co[j]]++,deg2[co[i]]++;
	int res = 0,res2 = 0;
	for(int i = 1;i <= col;i++) if(!deg[i]) res++;
	for(int i = 1;i <= col;i++) if(!deg2[i]) res2++;
	res2 = max(res,res2);
	if(col == 1) res2 = 0;
	cout << res << endl;
	cout << res2 << endl;
}
int main(){
	cin >> n;
	for(int i = 1,y;i <= n;i++){
		cin >> y;
		while(y)
			G[i][y] = 1, cin >> y;
	}
	solve();
	return 0;
}

D.消息的傳遞(message)

題意描述
我們的郭嘉大大在曹操這過得逍遙自在,但是有一天曹操給了他一個任務,在建鄴城內有N(<=1000)個袁紹的奸細 將他們從 1 到 N 進行編號,同時他們之間存在一種傳遞關係,即若C[i, j] = 1,則奸細 i 能將消息直接傳遞給奸細 j。
現在,曹操要發佈一個假消息,需要傳達給所有奸細,而我們的郭嘉大大則需要傳遞給儘量少的奸細使所有的奸細都知道這一個消息,問我們至少要傳給幾個奸細?

解題思路
和上一題有些類似,只需要縮點後統計入度爲 0 的點的數量即可。

代碼示例

#include<bits/stdc++.h>
using namespace std;
const int N = 1100;
int G[N][N],dfn[N],low[N],clk,co[N],col;
int n,Stack[N],top;
void Tarjan(int x){
	dfn[x] = low[x] = ++clk;
	Stack[++top] = x;
	for(int i = 1;i <= n;i++){
		if(!G[x][i]) continue;
		if(!dfn[i]) Tarjan(i),low[x] = min(low[x],low[i]);
		else if(!co[i]) low[x] = min(low[x],dfn[i]);
	}
	if(low[x] == dfn[x])
		for(++col;Stack[top+1] != x;top--)
			co[Stack[top]] = col;
}
int deg[N],ans;
int main(){
	scanf("%d",&n);
	for(int i = 1;i <= n;i++)
		for(int j = 1;j <= n;j++) scanf("%d",&G[i][j]);
	for(int i = 1;i <= n;i++) if(!dfn[i]) Tarjan(i);
	for(int i = 1;i <= n;i++)
		for(int j = 1;j <= n;j++)
			if(G[i][j] && co[i] != co[j]) deg[co[j]]++;
	for(int i = 1;i <= col;i++) if(!deg[i]) ans++;
	printf("%d\n",ans);
	return 0;
}

E.間諜網絡(spy)

題意描述
由於外國間諜的大量滲入,國家安全正處於高度的危機之中。如果A間諜手中掌握着關於B間諜的犯罪證據,則稱A可以揭發B。有些間諜收受賄賂,只要給他們一定數量的美元,他們就願意交出手中掌握的全部情報。所以,如果我們能夠收買一些間諜的話,我們就可能控制間諜網中的每一分子。因爲一旦我們逮捕了一個間諜,他手中掌握的情報都將歸我們所有,這樣就有可能逮捕新的間諜,掌握新的情報。

我們的反間諜機關提供了一份資料,包括所有已知的受賄的間諜,以及他們願意收受的具體數額。同時我們還知道哪些間諜手中具體掌握了哪些間諜的資料。假設總共有n個間諜(n不超過3000),每個間諜分別用1到3000的整數來標識。

請根據這份資料,判斷我們是否有可能控制全部的間諜,如果可以,求出我們所需要支付的最少資金。否則,輸出不能被控制的一個間諜。

解題思路
首先將原圖強連通分量縮點,變爲 DAG ,新點的權值爲原圖中強連通分量所有點權值的最小值;然後累加新圖中入度爲 0 的點的權值,如果有一個點入度爲 0 且不受賄,那麼就無解。

代碼示例

#include<bits/stdc++.h>
using namespace std;
const int N = 1e4+10;
const int M = 2e4+10;
const int INF = 0x3f3f3f3f;
int head[N],ver[M],nex[M],tot = 1;
void addEdge(int x,int y){
	ver[++tot] = y; nex[tot] = head[x]; head[x] = tot;
}
int dfn[N],clk,co[N],col,low[N];
int Stack[N],top;
int w[N],n,m,p;
int spy[N],ct[N];
void Tarjan(int x){
	dfn[x] = low[x] = ++clk;
	Stack[++top] = x;
	for(int i = head[x];i ;i = nex[i]){
		int y = ver[i];
		if(!dfn[y]) Tarjan(y), low[x] = min(low[x],low[y]);
		else if(!co[y]) low[x] = min(low[x],dfn[y]);
	}
	if(dfn[x] == low[x])
		for(++col;Stack[top+1] != x;top--){
			co[Stack[top]] = col;
			if(w[Stack[top]] != INF) ct[col] = min(ct[col],w[Stack[top]]);
			else spy[col] = min(spy[col],Stack[top]);
		}	
}
int deg[N],ans,ans2 = INF;
void solve(){
	for(int i = 1;i <= n;i++) if(!dfn[i]) Tarjan(i);
	for(int x = 1;x <= n;x++)
		for(int i = head[x];i ;i = nex[i])
			if(co[x] != co[ver[i]]) deg[co[ver[i]]]++;
	for(int i = 1;i <= col;i++)
		if(!deg[i] && ct[i] == INF){
			ans2 = min(ans2,spy[i]);
		}else if(!deg[i]) ans += ct[i];
	if(ans2 != INF) printf("NO\n%d\n",ans2);
	else printf("YES\n%d\n",ans);
}
int main(){
	for(int i = 1;i < N;i++) ct[i] = w[i] = spy[i] = INF;
	scanf("%d",&n); scanf("%d",&p);
	for(int i = 1,x,y;i <= p;i++)
		scanf("%d%d",&x,&y),w[x] = y;
	scanf("%d",&m);
	for(int i = 1,x,y;i <= m;i++) 
		scanf("%d%d",&x,&y),addEdge(x,y);
	solve();
	return 0;
}

F.搶掠計劃(grab)

題意描述

Siruseri 城中的道路都是單向的。不同的道路由路口連接。按照法律的規定, 在每個路口都設立了一個 Siruseri 銀行的 ATM 取款機。
令人奇怪的是,Siruseri 的酒吧也都設在路口,雖然並不是每個路口都設有酒吧。
Banditji 計劃實施 Siruseri 有史以來最驚天動地的 ATM 搶劫。他將從市中心出發,沿着單向道路行駛,搶劫所有他途徑的 ATM 機,最終他將在一個酒吧慶祝他的勝利。使用高超的黑客技術,他獲知了每個 ATM 機中可以掠取的現金數額。
他希望你幫助他計算從市中心出發最後到達某個酒吧時最多能搶劫的現金總數。他可以經過同一路口或道路任意多次。但只要他搶劫過某個ATM 機後,該ATM 機裏面就不會再有錢了。

解題思路
對於一個強連通分量,我們只要經過它就可以將其中所有的錢都給取走,所以我們可以對強連通分量縮點,縮點後得到 DAG 。
對新圖進行 SFPA 求從起點出發的單源最長路,然後再挑終點是酒吧的最長路即可。

代碼示例

#include<bits/stdc++.h>
using namespace std;
const int N = 5e5+10;
const int M = 2*N;
int head[N],ver[M],nex[M],tot = 1;
void addEdge(int x,int y){
	ver[++tot] = y; nex[tot] = head[x]; head[x] = tot;
}
int n,m,s,p;

int mon[N],w[N];
bool isBar[N],stop[N];
int dfn[N],clk,low[N],co[N],col;
int Stack[N],top;
void Tarjan(int x){
	dfn[x] = low[x] = ++clk;
	Stack[++top] = x;
	for(int i = head[x];i ;i = nex[i]){
		int y = ver[i];
		if(!dfn[y]) Tarjan(y),low[x] = min(low[x],low[y]);
		else if(!co[y]) low[x] = min(low[x],dfn[y]);
	}
	if(dfn[x] == low[x])
		for(++col;Stack[top+1] != x;top--){
			co[Stack[top]] = col, w[col] += mon[Stack[top]];
			stop[col] |= isBar[Stack[top]];
		}
}

pair<int,int> edges[N];
int cnt = 0;
int dis[N],vis[N];
queue<int> q;
void SPFA(int s){
	q.push(s); dis[s] = w[s];
	while(q.size()){
		int x = q.front(); q.pop();
		vis[x] = false;
		for(int i = head[x];i ;i = nex[i]){
			int y = ver[i];
			if(dis[y] < dis[x] + w[y]){
				dis[y] = dis[x] + w[y];
				if(!vis[y]) q.push(y), vis[y] = true;
			}
		}
	}
}
void solve(){
	for(int i = 1;i <= n;i++) if(!dfn[i]) Tarjan(i);
	for(int x = 1;x <= n;x++)
		for(int i = head[x];i ;i = nex[i])
			edges[++cnt].first = co[x],edges[cnt].second = co[ver[i]];
	sort(edges+1,edges+1+cnt);
	memset(head,0,sizeof head); tot = 1;
	for(int i = 1;i <= cnt;i++){
		int x = edges[i].first, y = edges[i].second;
		int xx = edges[i-1].first, yy = edges[i-1].second;
		if(x != y && (x != xx || y != yy)) addEdge(x,y);
	}
	SPFA(co[s]);
	int ans = 0;
	for(int i = 1;i <= col;i++) if(stop[i]) ans = max(ans,dis[i]);
	printf("%d\n",ans);
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i = 1,x,y;i <= m;i++){
		scanf("%d%d",&x,&y); addEdge(x,y);
	}
	for(int i = 1;i <= n;i++) scanf("%d",mon+i);
	scanf("%d%d",&s,&p);
	for(int i = 1,x;i <= p;i++) scanf("%d",&x),isBar[x] = true;
	solve();
	return 0;
}

G.和平委員會(peace)

題意描述
原題來自:POI 2001

根據憲法,Byteland 民主共和國的公衆和平委員會應該在國會中通過立法程序來創立。 不幸的是,由於某些黨派代表之間的不和睦而使得這件事存在障礙。此委員會必須滿足下列條件:

  • 每個黨派都在委員會中恰有1個代表,如果2個代表彼此厭惡,則他們不能都屬於委員會。
  • 每個黨在議會中有2個代表。代表從1編號到2n。 編號爲 2i-1和 2i的代表屬於第 i個黨派。

任務:寫一程序讀入黨派的數量和關係不友好的代表對,計算決定建立和平委員會是否可能,若行,則列出委員會的成員表。

解題思路
這道題屬於 2-SAT 問題,按照 2-SAT 問題的一般策略,對於兩對黨派(a, b) 、(x , y),若 a 與 x 有矛盾,那麼可以確定的是若 a 出席,則必定 y 出席,若 x 出席,則必定 b 出席。這對應着原命題與逆否命題,而其它命題雖然可能成立但並不能用“必須”修飾。
因此我們再利用 Tarjan 求強連通分量,即可解決2-SAT問題。

需要注意的是,邊是有向的,也就是說添加的方向很重要,不可以隨意添加。同時不能添加反向邊,因爲逆命題不一定成立。

由於 Tarjan 算法中不同連通塊的顏色是按照拓撲排序來賦值的,也就是說顏色值越大的點越靠前(在拓撲排序中);那麼在本題中自然是優先選擇同一個強連通分量的點,因此我們可以考慮優先輸出顏色值大的點,這樣我們就會按照拓撲排序的順序挑選節點。

代碼示例

#include<bits/stdc++.h>
using namespace std;
const int N = 2*8100;
const int M = 1e5+10;
int n,m;
int head[N],ver[M],nex[M],tot = 1;
void addEdge(int x,int y){
	ver[++tot] = y; nex[tot] = head[x]; head[x] = tot;
}
int dfn[N],clk,co[N],col,low[N];
int Stack[N],top;
void Tarjan(int x){
	dfn[x] = low[x] = ++clk;
	Stack[++top] = x;
	for(int i = head[x];i ;i = nex[i]){
		int y = ver[i];
		if(!dfn[y]) Tarjan(y),low[x] = min(low[x],low[y]);
		else if(!co[y]) low[x] = min(low[x],dfn[y]);
	}
	if(dfn[x] == low[x])
		for(++col;Stack[top+1] != x;top--)
			co[Stack[top]] = col;
}
void solve(){
	for(int i = 1;i <= n;i++) if(!dfn[i]) Tarjan(i);
	for(int i = 1;i <= n;i += 2) 
		if(co[i] == co[i+1]){puts("NIE"); return;}
	for(int i = 1;i <= n;i += 2)
		if(co[i] < co[i+1]) printf("%d\n",i+1);//star
		else printf("%d\n",i);
}
int main(){
	scanf("%d%d",&n,&m), n <<= 1;
	for(int i = 1,x,y;i <= m;i++){
		scanf("%d%d",&x,&y);
		int u = x%2 == 0?x-1:x+1;
		int v = y%2 == 0?y-1:y+1;
		addEdge(u,y); addEdge(v,x); //star
	}
	solve();
	return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章