20240309 專項訓練

hayzxjr發表於2024-03-10

圖論(拓撲、強連通分量)專項訓練

以下演算法若無特殊提及,複雜度一般都為 \(\mathcal{O}(n + m)\) 水平。

study

link
\(n\) 個專案,對於某些專案 \(x\)\(y\) ,必須先學完 \(x\) 再開始學 \(y\)。請問能否完成所有專案的學習。

對於 \(30\%\) 的資料,保證 \(1 \le n, m \le 15\)

對於 \(70\%\) 的資料,保證 \(1 \le n, m \le 1000\)

對於 \(100\%\) 的資料,保證 \(1 \le n, m \le 10^5, 1 \le T \le10\)

100 pts 做法

建圖後,跑拓撲排序,確定圖為 DAG 即可。

#include<bits/stdc++.h>
#define ll long long
#define N 100005
using namespace std;
ll read(){
	ll x = 0, f = 1; char ch = getchar();
	while(!isdigit(ch)) {if(ch == '-') f = -1; ch = getchar();}
	while(isdigit(ch)) {x = (x << 3) + (x << 1) + ch - 48; ch = getchar();}
	return x * f;
}
int T, n, m, d[N];
vector < int > G[N];
void work(){
	queue < int > q;
	n = read(), m = read();
	memset(d, 0, sizeof d);
	vector < int > t;
	for(int i = 1; i <= n; i++) G[i] = t;
	for(int i = 1; i <= m; i++){
		int u = read(), v = read();
		G[u].push_back(v); d[v]++;
	}
	int cnt = 0;
	for(int i = 1; i <= n; i++)
	  if(d[i] == 0) q.push(i);
	while(!q.empty()){
		int u = q.front(); q.pop(); cnt++;
		for(auto v : G[u]) if(--d[v] == 0) q.push(v);
	}
	if(cnt != n) printf("no\n");
	else printf("yes\n");
}
int main(){
	freopen("study.in", "r", stdin);
	freopen("study.out", "w", stdout);
	T = read();
	while(T--) work();
	return 0;
}

star

link
牛欄裡有 \(n\) 頭奶牛,給定若干組 \((u, v)\) 表示奶牛 \(u\) 喜歡 \(v\),並且如果 A 喜歡 B,B 喜歡 C,那麼 A 也喜歡 C,請你算出有多少頭奶牛被所有奶牛喜歡。

對於 \(30\%\) 的資料,保證 \(n \le 20, m \le 50\)

對於 \(70\%\) 的資料,保證 \(n \le 1000, m \le 10^4\)

對於 \(100\%\) 的資料,保證 \(n \le 10^5, m \le 5 \times 10^5\)

100 pts 做法

經典奶牛題。tarjan 縮點後形成一張有向無環圖。

如果存在兩個以上出度為 \(0\) 的點就不存在“明星奶牛”。

否則唯一出度為 \(0\) 的點中的點數就是答案。

#include<bits/stdc++.h>
#define ll long long
#define N 100005
using namespace std;
int read(){
	int x = 0, f = 1; char ch = getchar();
	while(!isdigit(ch)) {if(ch == '-') f = -1; ch = getchar();}
	while(isdigit(ch)) {x = (x << 3) + (x << 1) + ch - 48; ch = getchar();}
	return x * f;
}
int n, m, belong[N], dfn[N], low[N];
int cnt, k, num[N], d[N];
bool instack[N];
vector < int > H[N];
stack < int > st;
void tarjan(int u){
	dfn[u] = low[u] = ++cnt;
	st.push(u); instack[u] = true;
	for(auto v : H[u]){
		if(!dfn[v]){
			tarjan(v); low[u] = min(low[u], low[v]);
		} else if(instack[v]) low[u] = min(low[u], dfn[v]);
	}
	if(dfn[u] == low[u]){
		k++;
		while(st.top() != u){
			int v = st.top(); st.pop();
			belong[v] = k; num[k]++;
			instack[v] = false;
		}
		int v = st.top(); st.pop();
		belong[v] = k; num[k]++;
		instack[v] = false;
	}
	return;
}
int main(){
	freopen("star.in", "r", stdin);
	freopen("star.out", "w", stdout);
	n = read(), m = read();
	for(int i = 1; i <= m; i++){
		int u = read(), v = read();
		H[u].push_back(v);
	}
	for(int i = 1; i <= n; i++)
	  if(!dfn[i]) tarjan(i);
	for(int u = 1; u <= n; u++){
		for(auto v : H[u])
		  if(belong[u] != belong[v]) d[belong[u]]++;
	}
	int ans = 0, ans1 = 0;
	for(int i = 1; i <= k; i++){
		if(d[i] == 0) ans += num[i], ans1++;
	}
	if(ans1 == 1) printf("%d\n", ans);
	else printf("0\n");
	return 0;
}

t3

link
在一個有向圖中,有 \(n\) 個頂點,給出 \(m\) 對數字 \((u, v)\) 表示需要存在一條從頂點 \(u\) 走到頂點 \(v\) 的路徑。讓你構造一個這樣的圖,輸出最少需要多少條邊。

對於 \(30\%\) 的資料,保證 \(1 \le n \le 5\)

對於 \(70\%\) 的資料,保證 \(1 \le n \le 200\)

對於 \(100\%\) 的資料,保證 \(1 \le n, m \le 10^5\)

30 pts 做法

列舉 \(n\) 個點所有連邊情況。複雜度 \(\mathcal{O}(2^m)\)\(m = n(n-1)/2\)

100 pts 做法

path

link
給定一張 \(n\) 個點, \(m\) 條邊的有向圖,你需要判斷是否存在兩個點 \(u,v\),使得不存在從 \(u\)\(v\) 的路徑,也不存在從 \(v\)\(u\) 的路徑。

對於 \(30\%\) 的資料,保證 \(n \le 100, m \le 300\)

對於 \(70\%\) 的資料,保證 \(n \le 1000, m \le 5000\)

對於 \(100\%\) 的資料,保證 \(n \le 5 \times 10^4, m \le 10^5, T \le 10\)

100 pts 做法 1

一眼縮點。然後先考慮一種暴力做法。

\(\texttt{bitset}\) 記錄每個點能否走到當前點。正反圖各跑一遍,如果兩次跑出來結果之和不為縮點後的點數的話,那麼就說明一定存在一個點不能到達另一個點。

實測該演算法在 luogu \(\texttt{C++14} + \texttt{O2}\) 情況下可透過,學校 OJ 被卡。

注意多測清空

#include<bits/stdc++.h>
#define mp make_pair
#define ll long long
#define N 50005
using namespace std;
ll read(){
	ll x = 0, f = 1; char ch = getchar();
	while(!isdigit(ch)) {if(ch == '-') f = -1; ch = getchar();}
	while(isdigit(ch)) {x = (x << 3) + (x << 1) + ch - 48; ch = getchar();}
	return x * f;
}
int n, m, belong[N], cnt;
int k, dfn[N], low[N], d[N];
int d1[N], f11[N];
bool instack[N];
bitset < N > f1[N];
vector < int > G[N], H[N], G1[N];
stack < int > st;
void tarjan(int u){
	dfn[u] = low[u] = ++cnt;
	st.push(u); instack[u] = true;
	for(auto v : H[u]){
		if(!dfn[v]){
			tarjan(v); low[u] = min(low[u], low[v]);
		} else if(instack[v]) low[u] = min(low[u], dfn[v]);
	}
	if(dfn[u] == low[u]){
		k++;
		while(st.top() != u){
			int v = st.top(); st.pop();
			belong[v] = k;
			instack[v] = false;
		}
		int v = st.top(); st.pop();
		belong[v] = k;
		instack[v] = false;
	}
	return;
}
void work(){
	n = read(), m = read();
	memset(dfn, 0, sizeof dfn);
	memset(low, 0, sizeof low);
	vector < int > t; k = cnt = 0;
	for(int i = 1; i <= n; i++) G[i] = t, H[i] = t, G1[i] = t;
	for(int i = 1; i <= m; i++){
		int u = read(), v = read();
		H[u].push_back(v);
	}
	for(int i = 1; i <= n; i++) if(!dfn[i]) tarjan(i);
	for(int u = 1; u <= n; u++){
		for(auto v : H[u])
		  if(belong[u] != belong[v]){
		  	G[belong[u]].push_back(belong[v]);
		  	G1[belong[v]].push_back(belong[u]);
		  	d[belong[v]]++; d1[belong[u]]++;
		  }
	}
	queue < int > q;
	for(int i = 1; i <= k; i++) f1[i].reset(), f1[i][i] = true;
	for(int i = 1; i <= k; i++)
	  if(d[i] == 0) q.push(i);
	while(!q.empty()){
		int u = q.front(); q.pop();
		for(auto v : G[u]){
			f1[v] |= f1[u];
			if((--d[v]) == 0) q.push(v);
		}
	}
	for(int i = 1; i <= k; i++) f11[i] = f1[i].count();
	for(int i = 1; i <= k; i++) f1[i].reset(), f1[i][i] = true;
	for(int i = 1; i <= k; i++) if(d1[i] == 0) q.push(i);
	while(!q.empty()){
		int u = q.front(); q.pop();
		for(auto v : G1[u]){
			f1[v] |= f1[u];
			if((--d1[v]) == 0) q.push(v);
		}
	}
	bool flag = false;
	for(int i = 1; i <= k; i++)
	  if(f11[i] + f1[i].count() != k + 1) flag = true;
	if(k == 1) flag = false;
	if(flag) printf("yes\n");
	else printf("no\n");
}
int main(){
	freopen("path.in", "r", stdin);
	freopen("path.out", "w", stdout);
	int T = read();
	while(T--) work();
	return 0;
}

100 pts 做法 2

考慮更加簡潔的做法。

如果存在兩個點同時稱為入度為 \(0\) 的點的話,那麼就一定存在。

注:關鍵在 if(!q.empty()) return false;

#include<bits/stdc++.h>
#define mp make_pair
#define ll long long
#define N 50005
using namespace std;
ll read(){
	ll x = 0, f = 1; char ch = getchar();
	while(!isdigit(ch)) {if(ch == '-') f = -1; ch = getchar();}
	while(isdigit(ch)) {x = (x << 3) + (x << 1) + ch - 48; ch = getchar();}
	return x * f;
}
int n, m, belong[N], cnt;
int k, dfn[N], low[N], d[N];
bool instack[N];
vector < int > G[N], H[N];
stack < int > st;
void tarjan(int u){
	dfn[u] = low[u] = ++cnt;
	st.push(u); instack[u] = true;
	for(auto v : H[u]){
		if(!dfn[v]){
			tarjan(v); low[u] = min(low[u], low[v]);
		} else if(instack[v]) low[u] = min(low[u], dfn[v]);
	}
	if(dfn[u] == low[u]){
		k++;
		while(st.top() != u){
			int v = st.top(); st.pop();
			belong[v] = k;
			instack[v] = false;
		}
		int v = st.top(); st.pop();
		belong[v] = k;
		instack[v] = false;
	}
	return;
}
bool topo(){
	queue < int > q;
	int tot = 0;
	for(int i = 1; i <= k; i++)
	  if(!d[i]) q.push(i);
	while(!q.empty()){
		int u = q.front(); q.pop();
		if(!q.empty()) return false;
		tot++;
		for(auto v : G[u])
		  if(!(--d[v])) q.push(v);
	}
	if(tot == k) return true;
	return false;
}
void work(){
	n = read(), m = read();
	memset(dfn, 0, sizeof dfn);
	memset(low, 0, sizeof low);
	memset(d, 0, sizeof d);
	vector < int > t; k = cnt = 0;
	for(int i = 1; i <= n; i++)
	  G[i] = t, H[i] = t;
	for(int i = 1; i <= m; i++){
		int u = read(), v = read();
		H[u].push_back(v);
	}
	for(int i = 1; i <= n; i++)
	  if(!dfn[i]) tarjan(i);
	for(int u = 1; u <= n; u++){
		for(auto v : H[u])
		  if(belong[u] != belong[v]){
		  	G[belong[u]].push_back(belong[v]);
		  	d[belong[v]]++;
		  }
	}
	if(topo()) printf("no\n");
	else printf("yes\n");
}
int main(){
	freopen("path.in", "r", stdin);
	freopen("path.out", "w", stdout);
	int T = read();
	while(T--) work();
	return 0;
}

相關文章