1. 程式人生 > 其它 >Kruskal重構樹 - 歸程

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;
}