学习笔记-缩点
前言
缩点是一种处理技巧,利用 Kosaraju 或 Tarjan 求出SCC,把每个 SCC 当做一个点建立 DAG 来解决原图上不易直接解决的问题. 通过下面的例题来学习 Tarjan 和缩点的建图方法
P3387 [模板]缩点
探究
给定一个 DAG (有向无环图),可以用记忆化搜索的方式找到以图上任意点出发的权最大路,但是该题给出的图可能有环路(即强连通分量 SCC),思考一下,现在走到了其中一个 SCC 上的一个点,那么最佳方案一定是把整个SCC上的点都走一遍,这种方法不会影响接下来的决策(因为 SCC 上的点可以互相到达),因此可以把每一个SCC看做一个点,利用缩点技术,在新生成的 DAG 上跑记忆化搜索即可.
实现
如何找到一个图上的 SCC?我们引入 Tarjan 算法
我们在图上做 DFS,现在假如查找到了第 \(i\) 条边,它的起点是 \(u\),终点是 \(v\),如果 \(v\) 我们已经遍历过了,它要么和 \(u\) 不存在父子关系,要么和 \(u\) 存在父子关系. 如果不存在父子关系,那么这条边对 SCC 没有贡献,如果存在父子关系,那么这条边是一条回退边(指向它的祖先),显然这条回退边在一个 SCC 上. 我们用 \(dfn_i\) 来表示一个点的 DFS序,用 \(low_i\) 表示它及其后代所能连接的最高的祖先的 DFS序. 经过推理,一个 SCC 上的所有点的 \(low\) 值都等于 DFS 第一次进入该 SCC 的点的 DFS序,统计有多少不同的 \(low\) 值就能找出原图上有多少 SCC.
其实存在更好的方法,可以在 DFS 的过程中就把每一个 SCC 分开. 原理如下:DFS 每次访问到一个点就把它加入到栈中,根据上文我们知道,如果我们访问了一个 SCC,访问的第一个节点 \(i\) 一定在 SCC 其他节点的底部,且其他节点的 \(low\) 都等于 \(dfn_i\),立刻推出 SCC 的其他任一节点 \(j\) 都满足 \(dfn_j > low_j\),我们每次把栈顶取出来加入数组 \(num\),直到有一个点满足 \(dfn_j == low_j\),这个点是SCC的第一个点,\(num\) 中的点一定都是 SCC 里的点
该题的缩点存在点权,只需要把每一个 SCC 的所有点点权相加. 如何建新图?遍历所有原边,如果起点和终点不属于同一个 SCC 就建边,这样就构造出了一个新图,记忆化搜索即可.
Code
#include <bits/stdc++.h>
using namespace std;
struct Edge {
int from, to;
};
const int N = 10050, M = 100050;
int cnt; // 强连通分量的个数
int low[N], num[N], dfn;
int n, m, a[N], na[N], sccno[N], dp[N], ans = 0;
vector<int> G[N], NG[N];
stack<int> s;
Edge e[M];
void dfs(int u) {
s.push(u);
low[u] = num[u] = ++dfn;
for(auto v : G[u]) {
if(!num[v]){
dfs(v);
low[u] = min(low[v], low[u]);
}
else if(!sccno[v]) {
low[u] = min(low[u], num[v]);
}
}
if(low[u] == num[u]) {
cnt++;
while(true) {
int v = s.top();
s.pop();
sccno[v] = cnt;
na[sccno[v]] += a[v];
if(u == v) break;
}
}
}
void tarjan(int x) {
for (int i = 1; i <= x; i++) {
if (!num[i]) dfs(i);
}
}
void search(int x) {
if(dp[x]) return;
dp[x] = na[x];
int maxSum = 0;
for(auto v : NG[x]) {
if(!dp[v]) search(v);
maxSum = max(maxSum, dp[v]);
}
dp[x] += maxSum;
}
int main(int argc, char** argv) {
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i <= m; i++) {
cin >> e[i].from >> e[i].to;
G[e[i].from].push_back(e[i].to);
}
tarjan(n);
for(int i = 1; i <= m; i++) {
if(sccno[e[i].from] != sccno[e[i].to]) {
NG[sccno[e[i].from]].push_back(sccno[e[i].to]);
}
}
for(int i = 1; i <= cnt; i++) {
if(!dp[i]) {
search(i);
ans = max(ans, dp[i]);
}
}
cout << ans;
return 0;
}
P2341 受欢迎的牛
探究
几乎就是模板,采取的方法是建反向边 DAG,能到达其他所有缩点的缩点的点权(定义为缩点内牛的数量)就是明星牛的数量,在图上 DFS 统计可以到达的点的数量(包括它自己),如果等于所有缩点的数量就让答案增加这个缩点的点权
进一步思考,这样的缩点有且只有一个,找到了这个缩点那么它的点权就是答案,所以找到了之后直接输出并结束程序可以减少一部分计算量(实测有的测试点从400ms降低到了6ms,效果显著).
Code
#include <bits/stdc++.h>
using namespace std;
struct Edge {
int from, to;
};
const int N = 10050, M = 50050;
int n, m, dfn[N], low[N], sccno[N], a[N], cnt, ord;
int vis[N], jud = 0, ans = 0;
vector<int> G[N], NG[N];
Edge e[M];
stack<int> s;
void dfs(int u) {
s.push(u);
low[u] = dfn[u] = ++ord;
for (auto v : G[u]) {
if (!dfn[v]) {
dfs(v);
low[u] = min(low[v], low[u]);
} else if (!sccno[v]) {
low[u] = min(low[u], dfn[v]);
}
}
if (low[u] == dfn[u]) {
cnt++;
while (true) {
int v = s.top();
s.pop();
sccno[v] = cnt;
a[cnt]++;
if (u == v) break;
}
}
}
void tarjan(int x) {
for (int i = 1; i <= x; i++) {
if (!dfn[i]) dfs(i);
}
}
void search(int x) {
vis[x] = 1;
jud++;
for (auto v : NG[x]) {
if (!vis[v]) search(v);
}
}
int main(int argc, char** argv) {
cin >> n >> m;
for (int i = 1; i <= m; i++) {
cin >> e[i].from >> e[i].to;
G[e[i].from].push_back(e[i].to);
}
tarjan(n);
for (int i = 1; i <= m; i++) {
if (sccno[e[i].from] != sccno[e[i].to]) {
NG[sccno[e[i].to]].push_back(sccno[e[i].from]);
}
}
for (int i = 1; i <= cnt; i++) {
memset(vis, 0, sizeof vis);
jud = 0;
search(i);
if (jud == cnt) {
ans = a[i];
cout << ans;
return 0; // 找到了直接返回
}
}
cout << ans; // 没找到输出 ans = 0
return 0;
}

浙公网安备 33010602011771号