一、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;
}
完