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