You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

164 lines
5.1 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

## [$AcWing$ $397$. 逃不掉的路
[题目传送门](https://www.acwing.com/problem/content/description/399/)
### 一、题目描述
现代社会,路是必不可少的。任意两个城镇都有路相连,而且往往不止一条。但有些路连年被各种$XXOO$,走着很不爽。按理说条条大路通罗马,大不了绕行其他路呗——可小撸却发现:从$a$城到$b$城不管怎么走,总有一些逃不掉的必经之路。
他想请你计算一下,$a$到$b$的所有路径中,有几条路是逃不掉的?
**输入格式**
第一行是$n$和$m$,用空格隔开。
接下来$m$行,每行两个整数$x$和$y$,用空格隔开,表示$x$城和$y$城之间有一条长为$1$的 **双向路**。
第$m+2$行是$q$。接下来$q$行,每行两个整数$a$和$b$,用空格隔开,表示一次询问。
**输出格式**
对于每次询问,输出一个正整数,表示$a$城到$b$城必须经过几条路。
### 二、题目简析
时间复杂度:$O(nlogn)$
求 $a$ 到 $b$ 的所有路径中的必经之路,那我们来想一想,为什么会有多条路径呢?
当然就是下面这种情况了
<center><img src='https://ftp.bmp.ovh/imgs/2020/11/5ee89b418317dd3c.png'></center>
那为什么会出现下面这种情况呢?
**当然是因为有环出现了**,才会出现多条路径
也就是说,环内的点,**一定不会有必经之路,因为至少也有顺时针逆时针两条路**
那么什么边才是必经之路呢?
就是 **环和环之间的路** 肯定就是 **必经之路** 了
好,那就把每个环缩成一个点,新图中的边就必然是必经之路了
而这个没有环的新图就成了一棵树
那怎么求两点经过的边有多少条呢?
这不就是 **树上最短路** 吗
我用了$lca$来求
```c++
#include <bits/stdc++.h>
using namespace std;
const int N = 4e5 + 10, M = N << 2; //因为需要装无向图,所以N=2e5,又因为有新图有旧图都保存到一个数组中所以需要再乘以2
//链式前向星
int e[M], h1[N], h2[N], idx, w[M], ne[M];
void add(int h[], int a, int b, int c = 0) {
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
int f[N][16];
int depth[N], dist[N];
void bfs() {
// 1号点是源点
depth[1] = 1;
queue<int> q;
q.push(1);
while (q.size()) {
int u = q.front();
q.pop();
for (int i = h2[u]; ~i; i = ne[i]) {
int j = e[i];
if (!depth[j]) {
q.push(j);
depth[j] = depth[u] + 1;
dist[j] = dist[u] + w[i];
f[j][0] = u;
for (int k = 1; k <= 15; k++) f[j][k] = f[f[j][k - 1]][k - 1];
}
}
}
}
//最近公共祖先
int lca(int a, int b) {
if (depth[a] < depth[b]) swap(a, b);
for (int k = 15; k >= 0; k--)
if (depth[f[a][k]] >= depth[b]) a = f[a][k];
if (a == b) return a;
for (int k = 15; k >= 0; k--)
if (f[a][k] != f[b][k]) a = f[a][k], b = f[b][k];
return f[a][0];
}
//边双连通分量
int dfn[N], low[N], ts, stk[N], top;
vector<int> dcc[N]; //边双中有哪些原始点
int id[N], dcc_cnt; //原始点x属于哪个边双连通分量dcc_cnt指边双连通分量个数
int is_bridge[M]; //记录哪些边是割边
void tarjan(int u, int fa) {
dfn[u] = low[u] = ++ts;
stk[++top] = u;
for (int i = h1[u]; ~i; i = ne[i]) {
int j = e[i];
if (!dfn[j]) {
tarjan(j, i);
low[u] = min(low[u], low[j]);
if (dfn[u] < low[j]) is_bridge[i] = is_bridge[i ^ 1] = true; //记录割边
} else if (i != (fa ^ 1))
low[u] = min(low[u], dfn[j]);
}
if (dfn[u] == low[u]) {
++dcc_cnt; //边双数量+1
int x;
do {
x = stk[top--];
id[x] = dcc_cnt; // 记录点与边双关系
dcc[dcc_cnt].push_back(x); // 记录边双中有哪些点
} while (x != u);
}
}
int n, m, q;
int main() {
memset(h1, -1, sizeof h1); // h1是原图的表头
memset(h2, -1, sizeof h2); // h2是新生成的缩完点的图表头
scanf("%d %d", &n, &m);
//原图
for (int i = 1; i <= m; i++) {
int a, b;
scanf("%d %d", &a, &b);
add(h1, a, b), add(h1, b, a);
}
//用tarjan来缩点,将边双连通分量缩点
for (int i = 1; i <= n; i++)
if (!dfn[i]) tarjan(i, -1);
//将新图建出来
for (int u = 1; u <= n; u++)
for (int i = h1[u]; ~i; i = ne[i]) {
int j = e[i];
if (id[u] != id[j])
add(h2, id[u], id[j]), add(h2, id[j], id[u]);
}
//随便找个存在的号作为根节点,预处理出每个点到根节点的距离
bfs();
scanf("%d", &q); // q次询问
for (int i = 1; i <= q; i++) {
int a, b;
scanf("%d %d", &a, &b);
a = id[a], b = id[b];
printf("%d\n", depth[a] + depth[b] - depth[lca(a, b)] * 2); //这就很显然了
}
return 0;
}
```