Kruskal重构树 - 归程

P4768 [NOI2018] 归程

核心思想:

以海拔为第一关键字对边进行从大到小的排序,然后修建kruskal重构树,这样就弄出了一颗以海拔为关键字的小根堆。然后对于每一棵子树,如果询问中的水位线是低于子树的根节点的,那么此时这棵子树中的所有叶子结点都是连通的。

将高海拔边建在靠近叶子节点的地方 , 使得回跳时如果一个节点满足海拔,则其整颗子树均满足海拔要求,即满足了回跳时海拔的单调性。放到题中就是说这颗子树中任选一个点出发,到子树中的其它点都不需要花费(路的海拔均高于该点,可以随便通行),即该子树中的节点对答案都不会做出贡献。

对于一个节点U,如果前往节点1的道路海拔低于水位线,则他必须在此地下车,一路步行回点1,距离即为U到点1的最短路。所以,我们只需要O(nlogn)跑一遍Dijkstra,便可在之后O(1)求得任意点U到1的最短距离,即在U点下车后走路的距离。

对于每一组询问,我们只需要找到一个离节点1最近(dis值最小)的最后可行节点(再往上走一层,海拔节点的海拔就不再满足要求的节点)该点到节点1的距离(即dis值)即为最小步行距离。

代码解析:

定义声明:
int n,m,lastans;
struct node
{
    int u,v,nex,len/*路径长度*/,hig/*海拔*/;
}e[N2]/*暂时存储边信息,后转移到长度节点上*/,tmp[N2]/*存储重构后的树*/,edge[N2]/*存储原图*/;
Dijkstra:

​ 初始化节点1到每个节点的距离,方便此后O(1)求距离。

int dis[N];
priority_queue <pair<int,int>,vector<pair<int,int> >,greater<pair<int,int> > > q;
inline void dijkstra()
{
    memset(dis,127,sizeof(dis));
    dis[1]=0;
    q.push(make_pair(0,1));
    while(!q.empty())
    {
        int u=q.top().second;
        if(dis[u]<q.top().first) {q.pop();continue;}//如果该点已经被更新过,则该点的dis值<q.top().second,退出(假如在加入一次过后又进行了一次加入,则后一次加入明显更优,此时len值也相应地进行了更新,等同于当前最优解的第二项的数值,而小于第一次第二项的数值,故再次运行到该点时就会满足条件直接退出)
        q.pop();
        for(int i=fir[u];i;i=edge[i].nex)
        {
            int v=edge[i].v;
            if(dis[v]>dis[u]+edge[i].len)
            {
                dis[v]=dis[u]+edge[i].len;
                q.push(make_pair(dis[v],v));
            }
        }
    }
}
Kruskal:

将海拔并着原图点一同建到新树中。

int fa[N];//并查集
int getf(int v)
{
    return fa[v]==v?v:fa[v]=getf(fa[v]);
}
inline bool cmp(node xx,node yy)
{
    return xx.hig>yy.hig;
}
int cnt;//节点编号
inline void Kruskal()
{
    fr(i,n)fa[i]=i;//重新初始化fa[]
    cnt=n;
    int num=0;
    
    sort(e+1,e+m+1,cmp);//按海拔将边从大到小排序(将高海拔边建在靠近叶子节点的地方,使得回跳时如果一个节点满足海拔,则其整颗子树均满足海拔要求,即满足了回跳时海拔的单调性)
    fr(i,m)//枚举所有边
    {
        int x=e[i].u,y=e[i].v;
        int fx=getf(x),fy=getf(y);
        if(fx^fy)//如果尚未联通
        {
            num++;//统计新建节点数
            tmp[++cnt].hig=e[i].hig;//将原本位于边上的信息转移到长度节点上
            fa[fx]=fa[fy]=fa[cnt]=cnt;//将两节点父亲(同时也将两节点)联通 (同时重置cnt的fa值为自己)
            Link(cnt,fx),Link(cnt,fy);//重构树连边
        }
        if(num==n-1)break;//优化:最多新建n-1个长度节点就可以把所有的点联通(因为最小生成树的末状态是所有点联通,故只需x-1个节点将它们相连)
    }
}
Dfs:

​ 求解新树中每个节点的深度,给Query()做准备。

int f[N][21]/*f[x][y]:x节点往上跳2^y步所在的位置*/,dep[N];
inline void Dfs(int x,int lst)//求dep[]
{
    dep[x]=dep[lst]+1,f[x][0]=lst/*标记往上跳一层的位置lst*/;
    fr(i,logN)
        f[x][i]=f[f[x][i-1]][i-1];//每个点往上跳2^i的长度的位置等同于往上2^i-1的位置再往上跳2^(i-1)步
    for(int i=fir[x];i;i=edge[i].nex)
    {
        int v=edge[i].v;
        Dfs(v,x);
        tmp[x].len=min(tmp[x].len,tmp[v].len);//寻求每天的最小路程,能更新就进行更新
    }
}
Query:

​ 从节点U出发往上跳,找到海拔高度大于p(水位线)的len值最小的点,即满足条件(高于水位线)中的最小海拔节点(亦即最短路径)。

inline int query(int x,int p)//从x(出发节点)出发往上跳,找到海拔大于p(水位线)的len值最小的点
{
    for(int i=logN;i>=0;--i)
    {
        if(dep[x]>(1<<i)/*如果深度大于2^i,即能跳的话*/ && tmp[f[x][i]].hig>p/*且海拔高于水位线*/)
        {
            x=f[x][i];//就往上跳
        }
    }
    return tmp[x].len;//即满足条件(高于水位线)中的最小长度节点(即最短路径)
}
Solve:
inline void solve()
{
    memset(fir,0,sizeof(fir));
    rot=0;
    Kruskal();
    Dfs(cnt,0);//为什么是cnt:cnt是新树中的最后一个节点,同时也是 最上面的 节点,即树根
    int q=rd(),k=rd(),s=rd();
    while(q--)
    {
        int x=(k*lastans+rd()-1)%n+1,p=(k*lastans+rd())%(s+1);
        cout<<(lastans=query(x,p))<<'\n';
    }
}
Main:
int main()
{
    int T=rd();
    while(T--)
    {
        init();
        n=rd(),m=rd();
        fr(i,m)
        {
            e[i].u=rd(),e[i].v=rd(),e[i].len=rd(),e[i].hig=rd();
            Link(e[i].u,e[i].v,e[i].len),Link(e[i].v,e[i].u,e[i].len);
        }
        dijkstra();//预处理出所有点到节点1的最短距离
        fr(i,n)tmp[i].len=dis[i];//并将其存储到新图中的原图节点上
        for(int i=n+1;i<=(n<<1);i++)tmp[i].len=Inf;//把长度节点len值设为 Inf,稍后查询走路距离最小值的时候就会把长度节点剔除掉,只考虑原图节点 
        solve();
    }
    return 0;
}

完整代码:

#include<bits/stdc++.h>
#define fr(i,r) for(int i=1;i<=r;++i)
using namespace std;
const int N=4e5+10,N2=8e5+10,Inf=0x7fffffff,logN=19;
char cch;
int res;
inline int rd()
{
    while((cch=getchar())<48);
    res=cch^48;
    while((cch=getchar())>=48)res=(res*10)+(cch^48);
    return res;
}

int n,m,lastans;
struct node
{
    int u,v,nex,len/*路径长度*/,hig/*海拔*/;
}e[N2]/*暂时存储边信息,后转移到长度节点上*/,tmp[N2]/*存储重构后的树*/,edge[N2]/*存储原图*/;

int fir[N],rot;
inline void Link(int u,int v,int w=0)
{
    edge[++rot].nex=fir[u];
    fir[u]=rot;
    edge[rot].v=v;
    edge[rot].len=w;
}

int dis[N];
priority_queue <pair<int,int>,vector<pair<int,int> >,greater<pair<int,int> > > q;
inline void dijkstra()
{
    memset(dis,127,sizeof(dis));
    dis[1]=0;
    q.push(make_pair(0,1));
    while(!q.empty())
    {
        int u=q.top().second;
        if(dis[u]<q.top().first) {q.pop();continue;}//如果该点已经被更新过,则该点的dis值<q.top().second,退出(假如在加入一次过后又进行了一次加入,则后一次加入明显更优,此时len值也相应地进行了更新,等同于当前最优解的第二项的数值,而小于第一次第二项的数值,故再次运行到该点时就会满足条件直接退出)
        q.pop();
        for(int i=fir[u];i;i=edge[i].nex)
        {
            int v=edge[i].v;
            if(dis[v]>dis[u]+edge[i].len)
            {
                dis[v]=dis[u]+edge[i].len;
                q.push(make_pair(dis[v],v));
            }
        }
    }
}

int fa[N];//并查集
int getf(int v)
{
    return fa[v]==v?v:fa[v]=getf(fa[v]);
}

inline bool cmp(node xx,node yy)
{
    return xx.hig>yy.hig;
}
int cnt;//节点编号
inline void Kruskal()
{
    fr(i,n)fa[i]=i;//重新初始化fa[]
    cnt=n;
    int num=0;
    
    sort(e+1,e+m+1,cmp);//按海拔将边从大到小排序(将高海拔边建在靠近叶子节点的地方,使得回跳时如果一个节点满足海拔,则其整颗子树均满足海拔要求,即满足了回跳时海拔的单调性)
    fr(i,m)//枚举所有边
    {
        int x=e[i].u,y=e[i].v;
        int fx=getf(x),fy=getf(y);
        if(fx^fy)//如果尚未联通
        {
            num++;//统计新建节点数
            tmp[++cnt].hig=e[i].hig;//将原本位于边上的信息转移到长度节点上
            fa[fx]=fa[fy]=fa[cnt]=cnt;//将两节点父亲(同时也将两节点)联通 (同时重置cnt的fa值为自己)
            Link(cnt,fx),Link(cnt,fy);//重构树连边
        }
        if(num==n-1)break;//优化:最多新建n-1个长度节点就可以把所有的点联通(因为最小生成树的末状态是所有点联通,故只需x-1个节点将它们相连)
    }
}

int f[N][21],dep[N];
inline void Dfs(int x,int lst)//求dep[]
{
    dep[x]=dep[lst]+1,f[x][0]=lst/*标记往上跳一层的位置lst*/;
    fr(i,logN)
        f[x][i]=f[f[x][i-1]][i-1];
    for(int i=fir[x];i;i=edge[i].nex)
    {
        int v=edge[i].v;
        Dfs(v,x);
        tmp[x].len=min(tmp[x].len,tmp[v].len);//寻求每天的最小路程,能更新就进行更新
    }
}

inline int query(int x,int y)//从x(出发节点)出发往上跳,找到海拔大于y(水位线)的len值最小的点
{
    for(int i=logN;i>=0;--i)
    {
        if(dep[x]>(1<<i)/*如果深度大于2^i,即能跳的话*/ && tmp[f[x][i]].hig>y/*且海拔高于水位线*/)
        {
            x=f[x][i];//就往上跳
        }
    }
    return tmp[x].len;//即满足条件(高于水位线)中的最小长度节点(即最短路径)
}

inline void solve()
{
    Kruskal();
    Dfs(cnt,0);
    int q=rd(),k=rd(),s=rd();
    while(q--)
    {
        int x=(k*lastans+rd()-1)%n+1,y=(k*lastans+rd())%(s+1);
        cout<<(lastans=query(x,y))<<'\n';
    }
}

inline void init()
{
    memset(fir,0,sizeof(fir));
    memset(fa,0,sizeof(fa));
    memset(f,0,sizeof(f));
    memset(tmp,0,sizeof(tmp));
    memset(edge,0,sizeof(edge));
    lastans=rot=0;
}
int main()
{
    int T=rd();
    while(T--)
    {
        init();
        n=rd(),m=rd();
        fr(i,m)
        {
            e[i].u=rd(),e[i].v=rd(),e[i].len=rd(),e[i].hig=rd();
            Link(e[i].u,e[i].v,e[i].len),Link(e[i].v,e[i].u,e[i].len);
        }
        dijkstra();//预处理出所有点到节点1的最短距离
        fr(i,n)tmp[i].len=dis[i];//并将其存储到新图中的原图节点上
        for(int i=n+1;i<=(n<<1);i++)tmp[i].len=Inf;//把长度节点len值设为 Inf,稍后查询走路距离最小值的时候就会把长度节点剔除掉,只考虑原图节点 
        memset(fir,0,sizeof(fir));
        rot=0;
        solve();
    }
    return 0;
}
posted @ 2022-03-18 21:32  penggeng  阅读(32)  评论(0编辑  收藏  举报