割点
\(\text{luogu-3388}\)
给出一个 \(n\) 个点,\(m\) 条边的无向图,求图的割点。
\(1\leq n \le 2\times 10^4\),\(1\leq m \le 1 \times 10^5\)。
模板题。首先明确割点的定义:
对于一个无向图,如果把一个点删除后这个图的极大连通分量数增加了,那么这个点就是这个图的割点(又称割顶).
我们尝试用 Tarjan 算法解决这个问题。
在 Tarjan 的过程中,我们可以处理出一张图每个节点的 \(dfn_i\) 序,其实就是时间戳。
其次,同时处理一个 \(low_i\),表示不经过与其父亲的连边,所能到达的最小时间戳。
有了这两个东西,显然一个点 \(u\) 是割点当且仅当,存在 \(v\) 是 \(u\) 的一个儿子,满足 \(low_v \ge dfn_u\)。
这个判断条件也可以写为 \(low_v = dfn_u \vee low_v = dfn_v\),因为实际上只有这两种情况。
其实很好理解,就是 \(v\) 无法到达 \(u\) 及其祖先节点,于是如果我们把 \(u\) 删除掉,则 \(v\) 与 \(u\) 的祖先节点一定是不同联通分量中的,因此 \(u\) 为割点。
但此结论不适用于搜索的起点,对于搜索起点 \(s\),当且仅当 \(s\) 有大于一个儿子节点,此时删除 \(s\) 则 \(s\) 的儿子节点一定不连通。原因是因为无向图中用 Tarjan 构建的搜索树一定不存在横插边,可以思考一下。
#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;
#define MAXN 20005
long long read() {
long long x = 0, f = 1;
char c = getchar();
while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
return x * f;
}
long long n, m, dfn[MAXN], low[MAXN], dn, res;
vector<long long> v[MAXN];
bool vis[MAXN], f[MAXN];
void tarjan(long long x, long long fa) {
dfn[x] = low[x] = ++ dn, vis[x] = 1;
long long son = 0;
for(auto y : v[x])
if(!vis[y]) {
son ++, tarjan(y, x);
low[x] = min(low[x], low[y]);
if(x != fa && low[y] >= dfn[x] && !f[x])
f[x] = 1, res ++;
}
else if(y != fa) low[x] = min(low[x], dfn[y]);
if(x == fa && son >= 2 && !f[x]) f[x] = 1, res ++;
return;
}
int main() {
n = read(), m = read();
for(int i = 1; i <= m; i ++) {
long long x = read(), y = read();
v[x].push_back(y), v[y].push_back(x);
}
for(int i = 1; i <= n; i ++)
if(!vis[i]) dn = 0, tarjan(i, i);
cout << res << "\n";
for(int i = 1; i <= n; i ++)
if(f[i]) cout << i << " ";
cout << "\n";
return 0;
}
\(\text{luogu-3469}\)
B 城有 \(n\) 个城镇(从 \(1\) 到 \(n\) 标号)和 \(m\) 条双向道路。
每条道路连结两个不同的城镇,没有重复的道路,所有城镇连通。
把城镇看作节点,把道路看作边,容易发现,整个城市构成了一个无向图。
请你对于每个节点 \(i\) 求出,把与节点 \(i\) 关联的所有边去掉以后(不去掉节点 \(i\) 本身),无向图有多少个有序点 \((x,y)\),满足 \(x\) 和 \(y\) 不连通。
\(1 \le n \le 10^5\),\(1 \le m \le 5 \times 10^5\)。
其实答案很容易想出来,对于每个割点的贡献,就是下面的式子,假设 \(u\) 有 \(p\) 个儿子节点:
我们记 \(S = \sum\limits_{v} sz_v + 1\)。
三部分分别是,\(u\) 的儿子节点子树的贡献;\(u\) 的祖先构成子树的贡献;\(u\) 作为割点自己的贡献。
于是我们在 Tarjan 过程中计算这个式子就好了。
但是上面的板子就不太好用了,于是我找到了一种更简便的方式求割点。
#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;
#define MAXN 100005
long long read() {
long long x = 0, f = 1;
char c = getchar();
while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
return x * f;
}
long long n, m, dfn[MAXN], low[MAXN], dn, sz[MAXN], ans[MAXN];
vector<long long> v[MAXN];
bool vis[MAXN], f[MAXN];
void tarjan(long long x) {
dfn[x] = low[x] = ++ dn, vis[x] = 1;
long long son = 0, res = 0; sz[x] = 1;
for(auto y : v[x])
if(!vis[y]) {
tarjan(y), sz[x] += sz[y];
low[x] = min(low[x], low[y]);
if(low[y] >= dfn[x]) {
ans[x] += sz[y] * (n - sz[y]);
res += sz[y], son ++;
if(x != 1 || son >= 2) f[x] = 1;
}
}
else low[x] = min(low[x], dfn[y]);
if(f[x]) ans[x] += (n - res - 1) * (res + 1) + n - 1;
else ans[x] = 2 * (n - 1);
return;
}
int main() {
n = read(), m = read();
for(int i = 1; i <= m; i ++) {
long long x = read(), y = read();
v[x].push_back(y), v[y].push_back(x);
}
tarjan(1);
for(int i = 1; i <= n; i ++) cout << ans[i] << "\n";
return 0;
}
\(\text{loj-10099 / luogu-3225}\)
煤矿工地可以看成是由隧道连接挖煤点组成的无向图。为安全起见,希望在工地发生事故时所有挖煤点的工人都能有一条出路逃到救援出口处。于是矿主决定在某些挖煤点设立救援出口,使得无论哪一个挖煤点坍塌之后,其他挖煤点的工人都有一条道路通向救援出口。
计算至少需要设置几个救援出口,以及不同最少救援出口的设置方案总数。
\(1 \le n \le 1000\),\(1 \le m \le 500\)。注意与正常不同的输入方式。
挖煤点坍塌相当于把该点和与其相连的边在图上删掉。
我们定义“叶子连通块”为“只包含 \(1\) 个割点的点双连通分量”,“非叶子连通块”为“包含 \(\ge 2\) 个割点的点双连通分量”。
如下图,橙色点是割点,红色框圈出的是点双,加粗的是叶子连通块。

叶子连通块只有 \(1\) 个割点,所以必须保证该连通块内部存在逃生出口,否则割点塌陷,里面的人就逃不出去了。显然逃生出口只要设置在此连通块的非割点处即可。
由于非叶子连通块有 \(\ge 2\) 个割点,所以就算其中一个割点塌陷,该连通块的人仍然可以通过其他未塌陷的割点跑到叶子连通块或者其他连通块去。既然任何一个直接可达的叶子连通块都已经存在逃生出口了,那就不必额外耗费资源去建逃生出口了。
所以这道题第一问是叶子连通块的个数,第二问是叶子连通块中非割点个数的乘积。
注意特判不存在叶子连通块(也就是整张图不存在割点)的情况,此时需要建 \(2\) 个逃生出口,以防其中一个塌陷。答案是 \(\tbinom{n}{2} = \frac{n(n-1)}{2}\)。
时间复杂度 \(O(Tn)\)。
注意:多测情况时注意特判里也要清!
#include<iostream>
#include<cstdio>
#include<vector>
#include<cstring>
#include<stack>
using namespace std;
#define MAXN 10005
long long read() {
long long x = 0, f = 1;
char c = getchar();
while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
return x * f;
}
long long n, m, dfn[MAXN], low[MAXN], dn, cnt, c1, c2;
vector<long long> v[MAXN];
bool f[MAXN], vis[MAXN];
stack<long long> s;
void tarjan(long long x) {
dfn[x] = low[x] = ++ dn;
long long son = 0;
for(auto y : v[x])
if(!dfn[y]) {
tarjan(y), son ++;
low[x] = min(low[x], low[y]);
if(x != 1 && low[y] >= dfn[x]) f[x] = 1;
}
else low[x] = min(low[x], dfn[y]);
if(x == 1 && son >= 2) f[x] = 1;
return;
}
void dfs(long long x) {
if(vis[x]) return;
vis[x] = 1, c2 ++;
if(f[x]) { s.push(x), c1 ++; return; }
for(auto y : v[x]) dfs(y);
return;
}
int main() {
while(m = read()) {
memset(dfn, 0, sizeof dfn);
memset(low, 0, sizeof low);
memset(f, 0, sizeof f);
memset(vis, 0, sizeof vis);
n = dn = 0; while(!s.empty()) s.pop();
for(int i = 1; i <= m; i ++) {
long long x = read(), y = read();
v[x].push_back(y), v[y].push_back(x);
n = max(n, max(x, y));
}
tarjan(1); long long ans1 = 0, ans2 = 1, res = 0;
for(int i = 1; i <= n; i ++) if(f[i]) res ++;
if(!res) {
cout << "Case " << (++ cnt) << ": 2 " << (n - 1) * n / 2 << "\n";
for(int i = 1; i <= n; i ++) v[i].clear();
continue;
}
for(int i = 1; i <= n; i ++) {
if(f[i]) continue;
c1 = c2 = 0, dfs(i);
while(!s.empty()) vis[s.top()] = 0, s.pop();
if(c1 == 1) ans1 ++, ans2 *= (c2 - 1);
}
cout << "Case " << (++ cnt) << ": " << ans1 << " " << ans2 << "\n";
for(int i = 1; i <= n; i ++) v[i].clear();
}
return 0;
}
本文来自博客园,作者:So_noSlack,转载请注明原文链接:https://www.cnblogs.com/So-noSlack/p/19474188

浙公网安备 33010602011771号