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.

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. 逃不掉的路

题目传送门

一、题目描述

现代社会,路是必不可少的。任意两个城镇都有路相连,而且往往不止一条。但有些路连年被各种XXOO,走着很不爽。按理说条条大路通罗马,大不了绕行其他路呗——可小撸却发现:从a城到b城不管怎么走,总有一些逃不掉的必经之路。

他想请你计算一下,ab的所有路径中,有几条路是逃不掉的?

输入格式

第一行是nm,用空格隔开。

接下来m行,每行两个整数xy,用空格隔开,表示x城和y城之间有一条长为1双向路

m+2行是q。接下来q行,每行两个整数ab,用空格隔开,表示一次询问。

输出格式

对于每次询问,输出一个正整数,表示a城到b城必须经过几条路。

二、题目简析

时间复杂度:O(nlogn)ab 的所有路径中的必经之路,那我们来想一想,为什么会有多条路径呢? 当然就是下面这种情况了

那为什么会出现下面这种情况呢?

当然是因为有环出现了,才会出现多条路径

也就是说,环内的点,一定不会有必经之路,因为至少也有顺时针逆时针两条路

那么什么边才是必经之路呢?

就是 环和环之间的路 肯定就是 必经之路

好,那就把每个环缩成一个点,新图中的边就必然是必经之路了

而这个没有环的新图就成了一棵树

那怎么求两点经过的边有多少条呢?

这不就是 树上最短路

我用了lca来求

#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;
}