「NOIP2013」货车运输 题解

发布时间 2023-08-21 21:42:34作者: LYXOfficial

「NOIP2013」货车运输

前言

这道题算是一个稍有思维难度的 MST+LCA 题目了。

稍微卡了一会(0-88-88-88-100(打表)-100(打表)-100(正解)),开始是打了表过了,后面在 DCZ 的帮助下正解通过(下面注释提到的一个坑)。

题目大意

给出一张无向图 \(G\),有 \(n\) 个点和 \(m\) 个边 \((x,y)=z\) ,找到一条路径使 \((u \to v)\) 中最小的 \(w\) 最大并输出 \(w\),如果找不到这样的路径,输出 -1

题目解析

考虑最大生成树,因为最大生成树可以保证图的每个点之间依然联通且可以拿到尽可能大的边权,可以保证接下来能操作的路径之间权值更大。

这里使用 Kruskal 算法,在执行时以每条当前极大边建无向图就行了。

得到这个树形结构的图之后,为了获取两点之间的通路和最小的 \(w\) ,就需要求解 LCA(最近公共祖先)了,下面采用倍增的方式。

如果直接采用普通的 ST LCA,会发现一个问题:在求解时最小的 \(w\) 怎么获取呢?

我们可以考虑在执行预处理 pre() 时和 f[u][k] 同时维护一个数组 wi[u][k] 表示点 \(u\) 向上走 \(2^k\) 步时最小的 \(w\)

\(f(u,\ i+1)=f(f(u,\ i),\ i)\) 这个式子一样,易得 \(wi(u,\ i+1)=\min(wi(u,\ i),wi(f(u,\ i),\ i))\)

为了在 pre() 中能够处理 wi[][],我们需要在 pre(u,fa) 新增一个参数 w,表示 \((fa,u)\) 的权值,以此来设置边界条件,即:\(wi(u,\ 0)=w\)

最后在 lca() 中维护一个 \(ans\) 表示 \((u \to v)\) 中最小的 \(w\),并在每次上移时更新 \(ans\),返回该值即可。

还有一个问题:如何判定两点不通?其实在运行 Kruskal 时留下的并查集就可以发挥作用,如果两点不通,肯定不是在一个集合里面的,所以只需要在 lca() 中的第一行这样子写:

if(find(x)!=find(y)) return -1;

代码

下面的代码注释会提出几个小坑,如果您自己码题时不注意您一定无法通过此题(打表除外)!

注释版

#include <bits/stdc++.h> //万能头偷懒(
using namespace std;
const int maxn=5e5+5; //注意数据范围
struct r {int u,v,w;}; //结构体,存储Kruskal时需要的边
int n,m,cnt,dep[maxn],fa[maxn],f[maxn][24],wi[maxn][24],vis[maxn];
vector<pair<int,int>> g[maxn]; //邻接表vector建图
vector<r> edge; //Kruskal用
inline void add(int u,int v,int w) {g[u].push_back(make_pair(v,w));}
inline int find(int x) {return x==fa[x]?x:fa[x]=find(fa[x]);}
void pre(int u,int fa,int w){ //预处理
    dep[u]=dep[fa]+1; //深度处理
    f[u][0]=fa,wi[u][0]=w,vis[u]=1; //vis和递推の边界条件
    for(int i=0;i<20;i++) //倍增递推
        f[u][i+1]=f[f[u][i]][i],
        wi[u][i+1]=min(wi[u][i],wi[f[u][i]][i]);
    for(auto [v,w]:g[u]) //遍历当前点扩展节点
        //auto [v,w]:g[u] 为 C++17语法,可以把结构体的两个值解包到 v,w 中(包括pair)
        if(!vis[v]) pre(v,u,w);
}
int lca(int x,int y){
    int ans=1e9; //维护的最小的w ans
    if(find(x)!=find(y)) return -1; // 不是通路
    if(dep[x]<dep[y]) swap(x,y); //交换,方便
    for(int i=20;i>=0;i--){
        if(dep[f[x][i]]>=dep[y]) //同步x和y的层级
            ans=min(ans,wi[x][i]),x=f[x][i]; //更新ans
        if(x==y) return ans; //共线,找到LCA,返回
    }
    for(int i=20;i>=0;i--) //向上试探
        if(f[x][i]!=f[y][i]) //直到LCA的下一层
            ans=min(ans,min(wi[x][i],wi[y][i])), //更新ans
            x=f[x][i],y=f[y][i]; //向上更新x,y
    return min(ans,min(wi[x][0],wi[y][0])); //再次更新最后一级,返回ans
}
inline void kruskal(){ //kruskal部分
    sort(edge.begin(),edge.end(),[](r a,r b){
        return a.w>b.w; //注意是最大生成树
    });
    for(auto [u,v,w]:edge){ //结构体解包
        int x=find(u),y=find(v); //正常部分
        if(x==y) continue;
        fa[x]=y,cnt++;
        add(u,v,w),add(v,u,w); //建无向树形图
        if(cnt==n-1) break; //已经生成一棵树了
    }
}
int main(){
    ios::sync_with_stdio(0); //关同步流
    cin>>n>>m;
    for(int i=1,a,b,w;i<=m;i++)
        cin>>a>>b>>w,edge.push_back({a,b,w}); //读入边并存入边集
    iota(fa+1,fa+1+n,1),kruskal();
    //iota在stl中稍冷门,可以在迭代器范围内从第三个参数起自增1赋值,其实很适合拿来初始化并查集
    pre(1,0,0);int x,y,q;cin>>q; //预处理,初始化部分变量
    while(q--){
        cin>>x>>y;
        if(!vis[x]) pre(x,0,0); 
        //坑:多次预处理,以防第一次从1开始的图部分点不联通导致两个不联通于1的点未成功初始化出错,不加这句只有88分qwq
		cout<<lca(x,y)<<endl;
    }
    return 0;
}
//Endl QwQ

极速版

可以CTJ的版本

#include <bits/stdc++.h>
using namespace std;
const int maxn=5e5+5;
struct r {int u,v,w;};
int n,m,cnt,dep[maxn],fa[maxn],f[maxn][24],wi[maxn][24],vis[maxn];
vector<pair<int,int>> g[maxn];
vector<r> edge;
inline void add(int u,int v,int w) {g[u].push_back(make_pair(v,w));}
inline int find(int x) {return x==fa[x]?x:fa[x]=find(fa[x]);}
void pre(int u,int fa,int w){
    dep[u]=dep[fa]+1;
    f[u][0]=fa,wi[u][0]=w,vis[u]=1;
    for(int i=0;i<20;i++)
        f[u][i+1]=f[f[u][i]][i],
        wi[u][i+1]=min(wi[u][i],wi[f[u][i]][i]);
    for(auto [v,w]:g[u])
        if(!vis[v]) pre(v,u,w);
}
int lca(int x,int y){
    int ans=1e9;
    if(find(x)!=find(y)) return -1;
    if(dep[x]<dep[y]) swap(x,y);
    for(int i=20;i>=0;i--){
        if(dep[f[x][i]]>=dep[y]) 
            ans=min(ans,wi[x][i]),x=f[x][i];
        if(x==y) return ans;
    }
    for(int i=20;i>=0;i--)
        if(f[x][i]!=f[y][i])
            ans=min(ans,min(wi[x][i],wi[y][i])),
            x=f[x][i],y=f[y][i];
    return min(ans,min(wi[x][0],wi[y][0]));
}
inline void kruskal(){
    sort(edge.begin(),edge.end(),[](r a,r b){
        return a.w>b.w;
    });
    for(auto [u,v,w]:edge){
        int x=find(u),y=find(v);
        if(x==y) continue;
        fa[x]=y,cnt++;
        add(u,v,w),add(v,u,w);
        if(cnt==n-1) break;
    }
}
int main(){
    ios::sync_with_stdio(0);
    cin>>n>>m;
    for(int i=1,a,b,w;i<=m;i++)
        cin>>a>>b>>w,edge.push_back({a,b,w});
    iota(fa+1,fa+1+n,1),kruskal();
    pre(1,0,0);int x,y,q;cin>>q;
    while(q--){
        cin>>x>>y;
        if(!vis[x]) pre(x,0,0);
		cout<<lca(x,y)<<endl;
    }
    return 0;
}