P5304 [GXOI/GZOI2019] 旅行者 题解

题目传送门

前置知识

  1. 最短路

题目大意

给定 $ n $ 个城市, $ m $ 条有向边, $ k $ 个关键城市,求这 $ k $ 个关键城市中任意两个不同城市的距离的最小值。

整体思路

如果直接去暴力进行全源最短路,时间复杂度为 $ O(km log n) $ ,在 $ n,k \le 10^5 $ , $ m \le 5 \times 10^5 $的高压下,吸氧也过不了。

我们把这些关键点放在一个图里不好处理,那我们可不可以把它分成两个图呢?

如果是两个图,我们建一个超级源点( $ 0 $ 号点),与超级终点( $ n+1 $ 号点)。让超级源点依次连接左边部分的点,边权为 $ 0 $ ,再让右边点依次连接超级终点,边权也为 $ 0 $ 。那么再从超级源点跑一次最短路,那最后到终点的最短距离就是左右两部分关键点之间的最小距离。

(不会最短路的同学详见P4779 【模板】单源最短路径(标准版)

问题是,怎么把这 $ k $ 个关键点分成左右两部分呢?

这才是这道题的关键之处。

因为我们随机分配,很有可能把最终答案对应的两个点分在同一部分,导致最终求的最短路并不包含这两个点之间的距离。

有人就要问了:我们难道可以就不可以多随机几次吗?

确实,这的确是一个思路,随机 $ 20 $ 几次就有近 $ 99.7% $ 的概率随机出正确答案。

有人又要问了:那有没有靠谱的做法呢?

别说,还真有!

我们可以将这些数按二进制进行分组,如果二进制下这一位是 $ 0 $ 那么就分配到左部分,反之就在右部分。然后再将二进制下这一位是 $ 0 $ 的数放在右部分,剩下的就分配到左部分,再跑一遍最短路。

这样的话时间复杂度就是 $ O(logn \times mlogn) $ ,勉强能过。

那为什么这样就能保证一定会让最终答案的两个点分别会被分到两个部分至少一次呢?

任意两个不同的数,它们的二进制至少有一位不同,因此总有一位会将它们分到两个部分。

然后本人不是特别懂链式前向星,所以干脆就不用超级终点了,枚举右部分的点,取最小值,那么初始化就只需要将 $ head[0] $ (记录 $ 0 $ 号节点的第一条边的编号)初始化为 $ 0 $ 就行了。

然后不知道为什么,我的代码有点缺氧,不开 $ O2 $ 只有 $ 36 $ 分,开了就能过。( $ O2 $ 是怎么做到优化掉 $ 4 $ 秒的。。。),你看代码长度( $ 1.76 $ KB )就知道这两份提交记录是同一份代码的。

缺氧证明

治疗结果

代码

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=100005,M=700005;
int n,s,t,f[31],head[M],cnt,b[N],m,k,dis[N];
struct node
{
    int v,nxt,w;
}a[M];
int read()//快读 
{
    int x=0,f=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
    return x*f;
}
void add(int u,int v,int w)//链式前向星 
{
    a[++cnt].v=v;
    a[cnt].w=w;
    a[cnt].nxt=head[u];
    head[u]=cnt;
}
priority_queue<pair<int,int>,vector<pair<int,int> >,greater<pair<int,int> > >q;
void dijkstra(int s)//dijkstra求最短路 
{
    for(int i=1;i<=n+1;i++) dis[i]=1e18;
    dis[s]=0;
    q.push(make_pair(0,s));
    while(!q.empty())
    {
        int u=q.top().second,d=q.top().first;
        q.pop();
        if(d!=dis[u]) continue;
        for(int i=head[u];i!=0;i=a[i].nxt)
        {
            int v=a[i].v,w=a[i].w;
            if(dis[v]>dis[u]+w) dis[v]=dis[u]+w,q.push(make_pair(dis[v],v));
         } 
    }
}
signed main()
{
    f[0]=1;
    for(int i=1;i<=30;i++) f[i]=f[i-1]*2;//预处理出2的i次方 
    t=read();
    while(t--)
    {
        for(int i=0;i<=cnt;i++) head[i]=0;//初始化链式前向星 
        cnt=0;
        n=read(),m=read(),k=read();
        int ans=1e18;
        for(int i=1;i<=m;i++)
        {
            int u=read(),v=read(),w=read();
            add(u,v,w);//单向边 
        }
        for(int i=1;i<=k;i++) b[i]=read();//读入关键点 
        int l=log2(n)+1;//二进制的范围 
        for(int p=0;p<=l;p++)//二进制分解 
        {
            head[0]=0;//初始化超级源点 
            cnt=m;
            for(int i=1;i<=k;i++)
            {
                if(b[i]&f[p]) add(0,b[i],0);//如果二进制下这一位为1 
            }
            dijkstra(0);//最短路 
            for(int i=1;i<=k;i++)
            {
                if(!(b[i]&f[p])) ans=min(ans,dis[b[i]]); //求最小值 
            }
            head[0]=0;//初始化 
            cnt=m;
            for(int i=1;i<=k;i++)
            {
                if(!(b[i]&f[p])) add(0,b[i],0);//再把另一种情况跑一遍
            }
            dijkstra(0);
            for(int i=1;i<=k;i++)
            {
                if((b[i]&f[p])) ans=min(ans,dis[b[i]]);//求最小值 
            }
        }
        printf("%lld\n",ans);
    }
    return 0;
}

posted @ 2024-01-10 21:34  Twilight_star  阅读(18)  评论(0)    收藏  举报