UVa 1464 Traffic Real Time Query System

问题描述

给定一个无向图,包含 nnn 个节点和 mmm 条边。有 qqq 个查询,每个查询给出两条边 SSS 和 TTT,要求找出从边 SSS 到边 TTT 的所有路径必须经过的节点数量。

数据范围

  • 0<n≤100000 < n \leq 100000<n≤10000
  • 0<m≤1000000 < m \leq 1000000<m≤100000
  • 0<q≤100000 < q \leq 100000<q≤10000

问题建模与分析

直观理解

想象一个交通网络,每条边代表一条道路,每个查询问:从道路 SSS 到道路 TTT,无论走哪条路线都必然经过的十字路口有哪些?

关键观察

  1. 必经节点一定是割点:如果某个节点不是割点,那么删除它后图仍然连通,意味着存在不经过该节点的替代路径。

  2. 点双连通分量内部无必经点:在同一个点双连通分量内,任意两点间都有至少两条点不交的路径,因此分量内部没有必经点。

  3. 必经点出现在连接不同分量的路径上:必经点实际上是连接不同点双连通分量的"桥梁"。

算法框架

我们的解决方案分为四个主要步骤:

  1. Tarjan\texttt{Tarjan}Tarjan 算法求点双连通分量
  2. 构建圆方树
  3. 树链剖分预处理
  4. 查询处理

下面我们详细讲解每个步骤。

步骤一:Tarjan\texttt{Tarjan}Tarjan 算法求点双连通分量

点双连通分量定义

点双连通图:一个无向图,如果删除任意一个顶点后,图仍然保持连通。

点双连通分量:极大的点双连通子图。

Tarjan\texttt{Tarjan}Tarjan 算法原理

Tarjan\texttt{Tarjan}Tarjan 算法通过深度优先遍历(DFS\texttt{DFS}DFS)来寻找点双连通分量,核心是维护两个数组:

  • dfn[u]dfn[u]dfn[u]:节点 uuu 的深度优先遍历序号(时间戳)
  • low[u]low[u]low[u]:从 uuu 出发能访问到的最早的祖先节点的时间戳
算法流程
cpp 复制代码
void tarjan(int u) {
    dfn[u] = low[u] = ++dfsClock;
    stack.push(u);
    
    for (每个邻接边 (u, v)) {
        if (边已访问) continue;
        标记边为已访问;
        edgeStack.push(边ID);
        
        if (!dfn[v]) {
            tarjan(v);
            low[u] = min(low[u], low[v]);
            
            if (low[v] >= dfn[u]) {
                // 发现点双分量
                创建新方点;
                弹出节点直到 v,连接方点;
                弹出边直到当前边,记录边到方点映射;
            }
        } else {
            low[u] = min(low[u], dfn[v]);
        }
    }
}
正确性分析

当 low[v]≥dfn[u]low[v] \geq dfn[u]low[v]≥dfn[u] 时,说明从 vvv 出发无法绕过 uuu 到达 uuu 的祖先,因此 uuu 是一个割点,我们找到了一个以 uuu 为"顶部"的点双分量。

步骤二:构建圆方树

圆方树概念

圆方树是一种将无向图转化为树结构的方法:

  • 圆点:原图中的节点
  • 方点 :每个点双连通分量(编号从 n+1n+1n+1 开始)
  • 连接规则:方点连接它包含的所有圆点

构建过程

对于每个点双分量 BBB:

  1. 创建一个新的方点 squaresquaresquare
  2. 对于 BBB 中的每个圆点 uuu:
    • 添加边 (u,square)(u, square)(u,square)
    • 添加边 (square,u)(square, u)(square,u)

示例

考虑以下图结构:

复制代码
1---2
| / |
3---4

点双分量:

  • B1={1,2,3}B_1 = \{1,2,3\}B1={1,2,3}(三角形)
  • B2={2,3,4}B_2 = \{2,3,4\}B2={2,3,4}(另一个三角形)

圆方树:

复制代码
    1
    |
   B1
  / | \
 1  2  3
    | \
    B2 \
   / | \ \
  2  3  4 4

注意 :节点 222 和 333 同时属于两个点双分量,在圆方树中它们会与两个方点相连。

圆方树的性质

  1. 原图节点间的路径圆方树中对应圆点间的路径
  2. 必经点圆方树路径上的圆点
  3. 点双内部无必经点方点内部没有关键信息

步骤三:树链剖分求 LCA\texttt{LCA}LCA

为什么需要 LCA\texttt{LCA}LCA?

在圆方树中,我们需要快速计算两个方点之间路径上的圆点数量。由于树中任意两点间只有唯一简单路径,我们可以通过 LCA\texttt{LCA}LCA(最近公共祖先)来高效计算路径属性。

树链剖分概念

树链剖分将树分解为若干条链,支持高效路径查询:

  • 重儿子 :节点 uuu 的所有儿子中子树大小最大的那个
  • 轻儿子:除重儿子外的其他儿子
  • 重链:由重儿子连接形成的链
  • 轻边:连接不同重链的边

树链剖分过程

第一次 DFS\texttt{DFS}DFS:计算基本信息
cpp 复制代码
void dfs1(int u, int fa) {
    parent[u] = fa;
    depth[u] = depth[fa] + 1;
    size[u] = 1;
    heavySon[u] = 0;
    
    for (v in tree[u]) {
        if (v == fa) continue;
        dfs1(v, u);
        size[u] += size[v];
        if (size[heavySon[u]] < size[v]) {
            heavySon[u] = v;
        }
    }
}
第二次 DFS\texttt{DFS}DFS:划分重链
cpp 复制代码
void dfs2(int u, int chainTop) {
    top[u] = chainTop;
    if (heavySon[u]) {
        dfs2(heavySon[u], chainTop);  // 重儿子延续当前链
    }
    for (v in tree[u]) {
        if (v != parent[u] && v != heavySon[u]) {
            dfs2(v, v);  // 轻儿子开始新链
        }
    }
}
LCA\texttt{LCA}LCA 查询
cpp 复制代码
int lca(int u, int v) {
    while (top[u] != top[v]) {  // 在不同链上
        if (depth[top[u]] < depth[top[v]]) swap(u, v);
        u = parent[top[u]];  // 跳转到链顶的父亲
    }
    return depth[u] < depth[v] ? u : v;  // 在同一条链上
}

复杂度分析

  • 预处理 :O(n)O(n)O(n)
  • 每次查询 :O(log⁡n)O(\log n)O(logn)

步骤四:查询处理

查询公式

对于查询边 SSS 到边 TTT:

  1. 找到边 SSS 和 TTT 对应的方点 uuu 和 vvv
  2. 计算 lca=LCA(u,v)lca = \texttt{LCA}(u, v)lca=LCA(u,v)
  3. 计算路径上的圆点数量:
cpp 复制代码
int result = roundNodeCount[u] + roundNodeCount[v] 
           - 2 * roundNodeCount[lca] + (lca <= n ? 1 : 0);

公式正确性证明

设 roundNodeCount[x]roundNodeCount[x]roundNodeCount[x] 表示从根节点到 xxx 路径上的圆点数量。

  • roundNodeCount[u]roundNodeCount[u]roundNodeCount[u]:根到 uuu 的圆点数量
  • roundNodeCount[v]roundNodeCount[v]roundNodeCount[v]:根到 vvv 的圆点数量
  • roundNodeCount[lca]roundNodeCount[lca]roundNodeCount[lca]:根到 lcalcalca 的圆点数量

路径 u→vu \to vu→v 的圆点数量 = 路径 u→lcau \to lcau→lca 的圆点 + 路径 lca→vlca \to vlca→v 的圆点

由于:

  • 路径 u→lcau \to lcau→lca 的圆点 = roundNodeCount[u]−roundNodeCount[lca]roundNodeCount[u] - roundNodeCount[lca]roundNodeCount[u]−roundNodeCount[lca]
  • 路径 lca→vlca \to vlca→v 的圆点 = roundNodeCount[v]−roundNodeCount[lca]roundNodeCount[v] - roundNodeCount[lca]roundNodeCount[v]−roundNodeCount[lca]
  • lcalcalca 本身被减了两次,需要加回一次(如果 lcalcalca 是圆点)

因此:
answer=roundNodeCount[u]+roundNodeCount[v]−2×roundNodeCount[lca]+[lca≤n] answer = roundNodeCount[u] + roundNodeCount[v] - 2 \times roundNodeCount[lca] + [lca \leq n] answer=roundNodeCount[u]+roundNodeCount[v]−2×roundNodeCount[lca]+[lca≤n]

其中 [lca≤n][lca \leq n][lca≤n] 是指示函数,当 lcalcalca 是圆点时值为 111,否则为 000。

完整算法复杂度

  • Tarjan\texttt{Tarjan}Tarjan 求点双 :O(n+m)O(n + m)O(n+m)
  • 构建圆方树 :O(n+m)O(n + m)O(n+m)
  • 树链剖分预处理 :O(n)O(n)O(n)
  • 每个查询 :O(log⁡n)O(\log n)O(logn)
  • 总复杂度 :O(n+m+qlog⁡n)O(n + m + q \log n)O(n+m+qlogn)

总结

本问题通过以下关键步骤解决:

  1. 问题转化:将边到边的必经点问题转化为点双分量间的路径问题
  2. 图分解:使用 Tarjan 算法找到点双连通分量
  3. 结构转换:构建圆方树,将图结构转化为树结构
  4. 高效查询 :使用树链剖分支持快速的 LCA\texttt{LCA}LCA 查询和路径计算

这种解法充分利用了点双连通分量的性质和树结构的优势,将复杂的图论问题转化为高效的树上操作,是一个经典的图论问题解决思路。

参考代码

cpp 复制代码
// Traffic Real Time Query System
// UVa ID: 1464
// Verdict: Accepted
// Submission Date: 2025-10-30
// UVa Run Time: 0.080s
//
// 版权所有(C)2025,邱秋。metaphysis # yeah dot net

#include <bits/stdc++.h>

using namespace std;

const int MAXN = 20010;  // 扩大范围,包含圆方树节点
const int MAXM = 200010;

int n, m, totalNodes;
int edgeToBlock[MAXM];  // 边对应的点双分量ID

struct Graph {
    struct Edge {
        int to, next;
    } edges[MAXM * 2];
    int head[MAXN], edgeCount;
    
    void init() {
        memset(head, 0, sizeof(head));
        edgeCount = 1;  // 从1开始,方便异或操作
    }
    
    void addEdge(int u, int v) {
        edges[++edgeCount] = {v, head[u]};
        head[u] = edgeCount;
    }
    
    void addUndirected(int u, int v) {
        addEdge(u, v);
        addEdge(v, u);
    }
};

Graph originalGraph, blockCutTree;

// Tarjan相关
int dfn[MAXN], low[MAXN], dfsClock;
stack<int> nodeStack;
stack<int> edgeStack;
bool visited[MAXM * 2];

void tarjan(int u) {
    dfn[u] = low[u] = ++dfsClock;
    nodeStack.push(u);
    
    for (int i = originalGraph.head[u]; i; i = originalGraph.edges[i].next) {
        if (visited[i]) continue;
        visited[i] = visited[i ^ 1] = true;
        
        int edgeId = i >> 1;
        edgeStack.push(edgeId);
        
        int v = originalGraph.edges[i].to;
        if (!dfn[v]) {
            tarjan(v);
            low[u] = min(low[u], low[v]);
            
            if (low[v] >= dfn[u]) {
                // 找到点双分量,创建方点
                int blockId = ++totalNodes;
                blockCutTree.addUndirected(u, blockId);
                
                // 弹出节点直到v
                while (true) {
                    int x = nodeStack.top(); nodeStack.pop();
                    blockCutTree.addUndirected(x, blockId);
                    if (x == v) break;
                }
                
                // 弹出边直到当前边
                while (!edgeStack.empty()) {
                    int e = edgeStack.top(); edgeStack.pop();
                    edgeToBlock[e] = blockId;
                    if (e == edgeId) break;
                }
            }
        } else {
            low[u] = min(low[u], dfn[v]);
        }
    }
}

// 树链剖分LCA
int depth[MAXN], parent[MAXN], size[MAXN], heavySon[MAXN], top[MAXN];
int roundNodeCount[MAXN];

void dfs1(int u, int fa) {
    parent[u] = fa;
    depth[u] = depth[fa] + 1;
    size[u] = 1;
    roundNodeCount[u] = roundNodeCount[fa] + (u <= n ? 1 : 0);
    heavySon[u] = 0;
    
    for (int i = blockCutTree.head[u]; i; i = blockCutTree.edges[i].next) {
        int v = blockCutTree.edges[i].to;
        if (v == fa) continue;
        dfs1(v, u);
        size[u] += size[v];
        if (size[heavySon[u]] < size[v]) {
            heavySon[u] = v;
        }
    }
}

void dfs2(int u, int chainTop) {
    top[u] = chainTop;
    if (!heavySon[u]) return;
    dfs2(heavySon[u], chainTop);
    
    for (int i = blockCutTree.head[u]; i; i = blockCutTree.edges[i].next) {
        int v = blockCutTree.edges[i].to;
        if (v != parent[u] && v != heavySon[u]) {
            dfs2(v, v);
        }
    }
}

int lca(int u, int v) {
    while (top[u] != top[v]) {
        if (depth[top[u]] < depth[top[v]]) swap(u, v);
        u = parent[top[u]];
    }
    return depth[u] < depth[v] ? u : v;
}

int query(int u, int v) {
    int x = lca(u, v);
    return roundNodeCount[u] + roundNodeCount[v] - 2 * roundNodeCount[x] + (x <= n ? 1 : 0);
}

int main() {
    cin.tie(0), cout.tie(0), ios::sync_with_stdio(false);
    
    while (cin >> n >> m) {
        if (n == 0 && m == 0) break;
        
        // 初始化
        totalNodes = n;
        originalGraph.init();
        blockCutTree.init();
        memset(dfn, 0, sizeof(dfn));
        memset(low, 0, sizeof(low));
        memset(visited, 0, sizeof(visited));
        memset(edgeToBlock, 0, sizeof(edgeToBlock));
        dfsClock = 0;
        while (!nodeStack.empty()) nodeStack.pop();
        while (!edgeStack.empty()) edgeStack.pop();
        
        // 读入图
        for (int i = 1; i <= m; i++) {
            int u, v;
            cin >> u >> v;
            originalGraph.addUndirected(u, v);
        }
        
        // Tarjan算法求点双
        for (int i = 1; i <= n; i++) {
            if (!dfn[i]) {
                tarjan(i);
                // 处理栈中剩余节点
                if (!nodeStack.empty()) {
                    int blockId = ++totalNodes;
                    while (!nodeStack.empty()) {
                        int x = nodeStack.top(); nodeStack.pop();
                        blockCutTree.addUndirected(x, blockId);
                    }
                }
                // 处理栈中剩余边
                while (!edgeStack.empty()) {
                    int e = edgeStack.top(); edgeStack.pop();
                    if (edgeToBlock[e] == 0) {
                        edgeToBlock[e] = totalNodes;
                    }
                }
            }
        }
        
        // 构建树链剖分
        memset(depth, 0, sizeof(depth));
        memset(parent, 0, sizeof(parent));
        memset(size, 0, sizeof(size));
        memset(heavySon, 0, sizeof(heavySon));
        memset(top, 0, sizeof(top));
        memset(roundNodeCount, 0, sizeof(roundNodeCount));
        
        for (int i = 1; i <= totalNodes; i++) {
            if (depth[i] == 0) {
                dfs1(i, 0);
                dfs2(i, i);
            }
        }
        
        int q;
        cin >> q;
        while (q--) {
            int s, t;
            cin >> s >> t;
            cout << query(edgeToBlock[s], edgeToBlock[t]) << endl;
        }
    }
    
    return 0;
}
相关推荐
laocooon5238578863 小时前
寻找使a×b=c成立的最小进制数(2-16进制)
数据结构·算法
YY_TJJ3 小时前
算法题——图论
算法·深度优先·图论
默默的流星雨3 小时前
TARJAN相关
c++·算法·深度优先·图论
JJJJ_iii4 小时前
【机器学习11】决策树进阶、随机森林、XGBoost、模型对比
人工智能·python·神经网络·算法·决策树·随机森林·机器学习
loong_XL5 小时前
AC自动机算法-字符串搜索算法:敏感词检测
开发语言·算法·c#
Xの哲學5 小时前
Linux Netlink全面解析:从原理到实践
linux·网络·算法·架构·边缘计算
Tisfy5 小时前
LeetCode 3289.数字小镇中的捣蛋鬼:哈希表O(n)空间 / 位运算O(1)空间
算法·leetcode·散列表·题解·位运算·哈希表
2501_938963965 小时前
基于音乐推荐数据的逻辑回归实验报告:曲风特征与用户收听意愿预测
算法·机器学习·逻辑回归
2501_938791225 小时前
逻辑回归正则化解释性实验报告:L2 正则对模型系数收缩的可视化分析
算法·机器学习·逻辑回归