SCC縮點

ltign發表於2024-05-08

一、P2812 校園網路【[USACO]Network of Schools加強版】

P2812 校園網路【[USACO]Network of Schools加強版】

1、演算法簡析

首先,建立一張有向圖——學校是節點,學校間的單向線路是有向邊。\(Q_1\):選出若干個節點,從這些節點出發可以到達其它的任意節點(即不考慮選出的節點),求這些節點數量的最小值。\(Q_2\):新增若干條有向邊,使有向圖中任意兩點可以相互到達,求這些邊數量的最小值。

我們知道,在強連通分量中,任意兩點可以相互到達,所以對於一個強連通分量,只要有一個節點作為代表即可。因此,我們可以透過 \(Tarjan\) 演算法求 \(SCC\),用 \(SCC\) 的編號建立新的節點,代替相應的強連通分量。然後建立新圖,對原圖進行了簡化。這就是 \(SCC\) 縮點問題。

接下來,在縮點後的新圖(無環圖 \(DAG\))上討論問題。

對於 \(Q_1\),若一個節點的入度為 \(0\),則其它節點不可能到達它,所以該點必須作為起點。因此,入度為 \(0\) 的節點個數即所求。

對於 \(Q_2\),要使任意兩點可以相互到達,那麼任意節點的出入度都不能為 \(0\)。又要使新增的邊數最少,可以令出度為 \(0\) 的節點指向入度為 \(0\) 的節點。若兩種節點的數量相等,則該數即所求。若數量不等,則取二者的最大值。因此出入度為 \(0\) 的節點數的最大值即所求。

2、Code

#include <bits/stdc++.h>

using namespace std;
typedef long long ll;

const int MAX = 1e4 + 3;
int N;
vector<int> G[MAX];
int dfn[MAX], low[MAX], tot;
stack<int> S;
bool instk[MAX];
int scc[MAX], cnt; 
int din[MAX], dout[MAX];      // din[x] -- 縮點i的入度; dout[x] -- 縮點i的出度

void tarjan(int u)
{
	dfn[u] = low[u] = ++tot;
	S.push(u), instk[u] = true;
	
	for (int i = 0; i < G[u].size(); ++i)
	{
		int v = G[u][i];
		if (!dfn[v])
		{
			tarjan(v);
			low[u] = min(low[u], low[v]);
		}
		else if (instk[v])
			low[u] = min(low[u], dfn[v]);
	}
	
	if (dfn[u] == low[u])
	{
		int t;
		++cnt;
		do
		{
			t = S.top();
			S.pop(), instk[t] = false;
			scc[t] = cnt;
		} while (t != u);
	}
}

int main()
{
	#ifdef LOCAL
	freopen("test.in", "r", stdin);
	#endif
	
	cin >> N;
	for (int i = 1; i <= N; ++i)
	{
		int a;
		while (cin >> a && a)
			G[i].push_back(a);
	}
	
	// scc縮點處理 
	for (int i = 1; i <= N; ++i)
		if (!dfn[i])
			tarjan(i);
			
	// 計算出入度
	for (int i = 1; i <= N; ++i)
	{
		for (int j = 0; j < G[i].size(); ++j)
		{
			int v = G[i][j];
			if (scc[i] != scc[v])
			{
				++din[scc[v]];
				++dout[scc[i]];	
			}	
		}	
	}
	
	// 計算出入度為零的節點個數
	int a = 0, b = 0;
	for (int i = 1; i <= cnt; ++i)
	{
		if (!din[i])    ++a;
		if (!dout[i])    ++b;
	}
	
	// 問1:入度為0的節點即所求
	cout << a << endl;
	// 問2:要滿足題意,則所有節點的出入度都不為0。
	// 要使使用的邊最少,則將出度為0的節點指向入度為0的節點。
	// 兩種節點的個數相等的情況下,兩者的個數即所求。
	// 若不相等,取二者的max。
	if (cnt == 1)    cout << 0 << endl;    // 特判,此時原圖就是連通的,不需要添邊 
	else    cout << max(a, b) << endl; 
	
	return 0;
}

二、P2341 [USACO03FALL / HAOI2006] 受歡迎的牛 G

P2341 [USACO03FALL / HAOI2006] 受歡迎的牛 G

1、演算法簡析

建立有向圖模型:奶牛是節點,“喜歡”是有向邊。因為強連通分量的定義,所以一個強連通分量中的奶牛都可能是“明星”。因此,我們進行 \(SCC\) 縮點處理,使原圖變成 \(DAG\) 圖。此時,若我們選擇一個入度為 \(0\) 的節點作為根節點,向上拉直,就形成了一棵樹(當然,原圖可能是不連通的,所以也可能形成森林)。顯然,樹(森林)的葉子節點就是所求。換句話說,也就是出度為 \(0\)\(SCC\) 中的節點即所求。

需要注意的是,若出度為 \(0\)\(SCC\) 的個數大於 \(1\),則滿足條件的節點個數為 \(0\)。因為“明星奶牛”需要能被所有節點訪問,若存在其它節點的出度為 \(0\),顯然就不能滿足要求。也就是說,形成的樹(森林)的葉子節點只能有一個。或者,出度為 \(0\)\(SCC\) 的個數只能為 \(1\)

2、Code

#include <bits/stdc++.h>

using namespace std;
typedef long long ll;

ll quickin(void)
{
	ll ret = 0;
	bool flag = false;
	char ch = getchar();
	while (ch < '0' || ch > '9')
	{
		if (ch == '-')    flag = true;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9' && ch != EOF)
	{
		ret = ret * 10 + ch - '0';
		ch = getchar();
	}
	if (flag)    ret = -ret;
	return ret;
}

const int MAX = 1e4 + 3;
int N, M;
vector<int> G[MAX];
int dfn[MAX], low[MAX], tot;
stack<int> S;
bool instk[MAX];
int scc[MAX], siz[MAX], cnt;
int dout[MAX];

void tarjan(int u)
{
	dfn[u] = low[u] = ++tot;
	S.push(u), instk[u] = true;
	
	for (int i = 0; i < G[u].size(); ++i)
	{
		int v = G[u][i];
		if (!dfn[v])
		{
			tarjan(v);
			low[u] = min(low[u], low[v]);
		}
		else if (instk[v])
			low[u] = min(low[u], dfn[v]);
	}
	
	if (dfn[u] == low[u])
	{
		int t;
		++cnt;
		do
		{
			t = S.top();
			S.pop(), instk[t] = false;
			scc[t] = cnt;
			++siz[cnt];
		} while (t != u);
	}
}

int main()
{
	#ifdef LOCAL
	freopen("test.in", "r", stdin);
	#endif
	
	N = quickin(), M = quickin();
	for (int i = 0; i < M; ++i)
	{
		int a, b;
		a = quickin(), b = quickin();
		G[a].push_back(b);
	}
	
	for (int i = 1; i <= N; ++i)
		if (!dfn[i])
			tarjan(i);
			
	for (int i = 1; i <= N; ++i)
	{
		for (int j = 0; j < G[i].size(); ++j)
		{
			int v = G[i][j];
			if (scc[i] != scc[v])
			{
				++dout[scc[i]];
			}
		}
	}
	
	int sum = 0, zeros = 0; // sum -- 出度為0的SCC包含的節點個數; zeors -- 出度為0的SCC個數
	for (int i = 1; i <= cnt; ++i)
	{
		if (!dout[i])
		{
			sum += siz[i];
			++zeros;
		}
	}
	
	if (zeros > 1)    sum = 0; // 特判:若存在兩個及以上的SCC出度為0,它們肯定無法相互訪問,都不滿足要求 
	cout << sum << endl;
	
	return 0;
}

三、P3387 【模板】縮點

P3387 【模板】縮點

1、演算法簡析

對於本題,我們可以先進行縮點,建立一張新的 \(DAG\) 圖,然後在新圖上尋找最大的權值之和。

注意,\(SCC\) 縮點後,新節點的編號滿足拓撲序列。因此,可以用動態規劃求最大值。

2、Code

#include <bits/stdc++.h>

using namespace std;
typedef long long ll;

ll quickin(void)
{
	ll ret = 0;
	bool flag = false;
	char ch = getchar();
	while (ch < '0' || ch > '9')
	{
		if (ch == '-')    flag = true;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9' && ch != EOF)
	{
		ret = ret * 10 + ch - '0';
		ch = getchar();
	}
	if (flag)    ret = -ret;
	return ret;
}

const int MAX = 1e4 + 3;
int N, M, worth[MAX];
vector<int> G[MAX];
int dfn[MAX], low[MAX], tot;
stack<int> S;
bool instk[MAX];
int scc[MAX], cnt;
int nworth[MAX];         // 新圖中各節點的權值 
vector<int> nG[MAX];     // 新圖中的邊 
int dp[MAX];             // dp[u] -- 以u為起點的路徑的最大值 

void tarjan(int u)
{
	dfn[u] = low[u] = ++tot;
	S.push(u), instk[u] = true;
	
	for (int i = 0; i < G[u].size(); ++i)
	{
		int v = G[u][i];
		if (!dfn[v])
		{
			tarjan(v);
			low[u] = min(low[u], low[v]);
		}
		else if (instk[v])
			low[u] = min(low[u], dfn[v]);
	}
	
	if (dfn[u] == low[u])
	{
		int t;
		++cnt;
		do
		{
			t = S.top();
			S.pop(), instk[t] = false;
			scc[t] = cnt;
		} while (t != u);
	}
}

int main()
{
	#ifdef LOCAL
	freopen("test.in", "r", stdin);
	#endif
	
	N = quickin(), M = quickin();
	for (int i = 1; i <= N; ++i)
		worth[i] = quickin();
	for (int i = 0; i < M; ++i)
	{
		int a, b;
		a = quickin(), b = quickin();
		G[a].push_back(b);
	}
	
	for (int i = 1; i <= N; ++i)
		if (!dfn[i])
			tarjan(i);
			
	for (int u = 1; u <= N; ++u)
	{
		nworth[scc[u]] += worth[u];              // 新節點的權值 
		for (int i = 0; i < G[u].size(); ++i)
		{
			int v = G[u][i];
			if (scc[u] != scc[v])                // 存新邊 
				nG[scc[u]].push_back(scc[v]);
		}
	}
	
	// SCC縮點後的新節點的編號按降序滿足拓撲序列
	for (int u = 1; u <= N; ++u)
	{
		if (dp[u] == 0)    dp[u] = nworth[u];

		for (int i = 0; i < nG[u].size(); ++i)
		{
			int v = nG[u][i];
			dp[u] = max(dp[u], dp[v] + nworth[u]);
		}
	}
	
	int ans = 0;
	for (int i = 1; i <= cnt; ++i)
		ans = max(ans, dp[i]);
		
	cout << ans << endl; 
	
	return 0;
}

相關文章