圖論(拓撲、強連通分量)專項訓練
以下演算法若無特殊提及,複雜度一般都為 \(\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;
}