P1967 [NOIP2013提高组] 货车运输

解题思路

本题需要解决的是在带权无向图中,找到两点间路径的最小边权的最大值(即最大瓶颈路)。核心步骤包括:

  1. 构建最大生成树:使用Kruskal算法,确保路径上的最小边权尽可能大

  2. 预处理LCA和路径最小值:使用倍增法预处理每个节点的祖先信息和路径最小边权

  3. 查询处理:对于每个查询,通过LCA找到路径并确定最小边权

#include<bits/stdc++.h>
#define pii pair<int,int>
using namespace std;
const int N = 1e4 + 10, M = 5e4 + 10, inf = 0x3f3f3f3f;

// 边结构体
struct node {
    int x, y, z; // 起点、终点、限重
};

node a[M]; // 存储所有边
vector<pii> g[N]; // 最大生成树的邻接表
int n, m, q;
int dep[N]; // 节点深度
int fa[N][26]; // 倍增祖先表
int dp[N][26]; // 存储向上跳2^i步的路径最小边权
int f[N]; // 并查集父节点
int vis[N]; // 访问标记

// 按边权从大到小排序
bool cmp(node a, node b) {
    return a.z > b.z;
}

// 并查集查找
int find(int x) {
    if(f[x] != x) f[x] = find(f[x]);
    return f[x];
}

// 并查集合并
void merge(int x, int y) {
    int fx = find(x), fy = find(y);
    f[fy] = fx;
}

// Kruskal算法构建最大生成树
void kruskal() {
    sort(a + 1, a + 1 + m, cmp); // 按边权降序排序
    for(int i = 1; i <= n; i++) f[i] = i; // 初始化并查集
    
    for(int i = 1; i <= m; i++) {
        int x = a[i].x, y = a[i].y;
        if(find(x) != find(y)) { // 如果不在同一集合
            merge(x, y); // 合并集合
            g[x].push_back({y, a[i].z}); // 添加到生成树
            g[y].push_back({x, a[i].z});
        }
    }
}

// DFS预处理LCA和路径最小值
void dfs(int x, int rt, int d) {
    dep[x] = dep[rt] + 1; // 计算深度
    fa[x][0] = rt; // 直接父节点
    dp[x][0] = d; // 到父节点的边权
    vis[x] = 1; // 标记已访问
    
    // 预处理倍增表
    for(int i = 1; i <= 20; i++) {
        int y = fa[x][i - 1]; // 中间节点
        fa[x][i] = fa[y][i - 1]; // 祖先关系
        dp[x][i] = min(dp[x][i - 1], dp[y][i - 1]); // 路径最小值
    }
    
    // 遍历邻接节点
    for(int i = 0; i < g[x].size(); i++) {
        int y = g[x][i].first, z = g[x][i].second;
        if(y != rt && !vis[y]) { // 不是父节点且未访问
            dfs(y, x, z); // 递归处理
        }
    }
}

// LCA查询并计算路径最小边权
int lca(int x, int y) {
    int ans = inf; // 初始化最小边权
    
    // 确保x是较深的节点
    if(dep[x] < dep[y]) swap(x, y);
    
    // 将x提升到与y同一深度
    for(int i = 20; i >= 0; i--) {
        if(dep[fa[x][i]] >= dep[y]) {
            ans = min(ans, dp[x][i]); // 更新路径最小值
            x = fa[x][i]; // 跳跃
        }
    }
    
    if(x == y) return ans; // 如果已经是同一节点
    
    // 同时提升x和y
    for(int i = 20; i >= 0; i--) {
        if(fa[x][i] != fa[y][i]) { // 祖先不同才跳跃
            ans = min(ans, min(dp[x][i], dp[y][i])); // 更新最小值
            x = fa[x][i];
            y = fa[y][i];
        }
    }
    
    // 最后一步到LCA的边
    ans = min(ans, min(dp[x][0], dp[y][0]));
    return ans;
}

int main() {
    memset(dp, inf, sizeof(dp)); // 初始化DP表
    
    cin >> n >> m;
    for(int i = 1; i <= m; i++) 
        cin >> a[i].x >> a[i].y >> a[i].z;
    
    kruskal(); // 构建最大生成树
    
    // 预处理每个连通分量
    for(int i = 1; i <= n; i++)
        if(vis[i] == 0)
            dfs(i, 0, inf); // 根节点的父节点设为0
    
    cin >> q;
    while(q--) {
        int x, y;
        cin >> x >> y;
        if(find(x) != find(y)) // 不在同一连通块
            cout << -1 << endl;
        else
            cout << lca(x, y) << endl; // 输出路径最小边权
    }
    
    return 0;
}

算法分析

  1. 时间复杂度

    • Kruskal算法:O(MlogM) 排序边 + O(Mα(N)) 并查集操作

    • DFS预处理:O(NlogN) 每个节点处理20级祖先

    • 查询处理:每个查询O(logN)

  2. 空间复杂度

    • O(NlogN) 存储倍增表

    • O(N) 存储其他信息

  3. 关键点

    • 最大生成树确保路径最小边权最大化

    • 倍增法高效查询LCA和路径信息

    • 并查集处理连通性

posted @ 2025-04-29 19:52  CRt0729  阅读(29)  评论(0)    收藏  举报