题解:P10932 Freda的传呼机

· · 题解

Update

写圆方树板子,纠结了好久,用了一下午才把它弄懂,写篇题解回顾一下。

原题,双倍经验。

题意

给一棵仙人掌,每次询问两点之间的最短距离。

题解

我们讲讲如何发明圆方树的人怎么想到的。

首先,如果不考虑环的边权,那么缩点双连通分量,LCA 即可。

那么现在有环的边权,我们不可能像基环树那样对每个点进行操作,因为一条路径上如果全是环,那么复杂度就爆炸了。

于是,我们考虑越过一个环如何处理。

越过一个环,有两种情况,我们分别考虑。

first

注:我们假设 dep_x<dep_1<dep_ydep_a 代表 a 在搜索树的深度。

这种情况我们只需要知道环上的点到环上深度最小的点的距离,我们设深度最小的点是 p

那么我们可以建一个虚点(也就是方点,设其为 s),边权有如下设定:

这样我们就可以实现任意 i \ne pi \to p 的路径最小值等于 ip 树上两点之间的距离了。

second

注:我们设 dep_1 < dep_xdep_1 <dep_y

这种情况对于刚才的建树就失效了,因为刚才只统计了每个节点到深度最小的节点的距离。

但是,我们会发现一个很好的性质:这种情况只会在他们的最近公共祖先处出现一次

于是我们就可以找到它们最短路径上最近公共祖先“下面”的两个点,我们设它们为 x_0,y_0x \leftrightarrow x_0y \leftrightarrow y_0 的距离可以 O(1) 求,接下来转化为求 x_0 \leftrightarrow y_0 的距离。

很简单,预处理每个环的前缀和,找到他们取个最小值即可。

总结

Tarjan 算法缩点是 O(n) 的,建圆方树是 O(n) 的。

LCA 我写的树剖,预处理 O(n),查询 O(\log n)

综上,时间复杂度为 O(n+m+q\log n)

如果使用离线 Tarjan 求 LCA,可以做到优秀的线性复杂度。

关于代码实现

我们可以在 Tarjan 是顺便把圆方树建出来,但是有些复杂,我们结合重要代码(对于一个环建圆方树)看一看。(可以先看后面的代码部分,这里只是突出重点部分)

void help(int x,int y,int val) //val 是非树边边权 
{
    int cur=y,lst=val;
    while(cur!=x)
    {
        sum[cur]+=lst;
        sum[fa[cur]]+=sum[cur];
        lst=vval[cur];
        cur=fa[cur];
    }
    int sq=++ext;
    sum[sq]=sum[x]+lst; // 将所有边权放在方点上 
    sum[x]=0; // 方便后面处理 
    cur=y;
    while(cur!=fa[x])
    {
        int val=min(sum[cur],sum[sq]-sum[cur]); //再来一遍记录边权 
        Tree::add(cur,sq,val);
        cur=fa[cur];
    }
    return ;
}

我们注意到有一行 _tree.sum[x]=0,我们解读一下。

后面代码的 $\min$ 实际上要减一个 `_tree.sum[x]`,由于它是 $0$,所以忽略不计。 询问如果最近公共祖先在环内,那么上文中 $x_0,y_0$ 一定不是 $x$,因为 $x$ 的深度比方点小。 **如果两个环有交点,那么交点在搜索树上必然会是两个环深度最小的节点之一**。 证明比较简单,因为我们是按 DFS 序来确定深度的,访问到的第一个节点就是深度最小的,其余的点深度都比他大,因为其余点后遍历到。 **同时,我们会优先处理最小深度较大的环**,所以,两个环相交,**交点只在一个环中有意义**,对于这个交点在深度最小的那一个环,我们会先将它赋值成没有意义的 $0$,保证后面前缀和的累加不会出问题。 ### 代码 码风真的丑。 ```cpp #include<bits/stdc++.h> using namespace std; #define int long long const int N=1e5+5; int n,m,q,ext,fa[N]; class eg { public: int to,w; }; namespace Tree { vector<eg>nbr[N]; int dis[N],sum[N],siz[N],hvy[N],sg[N],dep[N],fat[N]; void add(int x,int y,int w) { nbr[x].push_back({y,w}); nbr[y].push_back({x,w}); return ; } void init(int cur,int fa) { dep[cur]=dep[fa]+1; fat[cur]=fa; siz[cur]=1; for(auto dat:nbr[cur]) { int nxt=dat.to,w=dat.w; if(nxt==fa) continue; dis[nxt]=dis[cur]+w; init(nxt,cur); siz[cur]+=siz[nxt]; if(siz[nxt]>siz[hvy[cur]]) hvy[cur]=nxt; } return ; } void dfs(int cur) { if(hvy[fat[cur]]==cur) sg[cur]=sg[fat[cur]]; else sg[cur]=cur; for(auto dat:nbr[cur]) if(dat.to!=fat[cur]) dfs(dat.to); return ; } int lca(int x,int y) { while(sg[x]!=sg[y]) { if(dep[sg[x]]<dep[sg[y]]) x^=y^=x^=y; x=fat[sg[x]]; } if(dep[x]>dep[y]) x^=y^=x^=y; return x; } int findson(int x,int zx) { while(dep[x]-1>dep[zx]) { x=sg[x]; if(fat[x]==zx) return x; x=fat[x]; } if(fat[x]==zx) return x; return hvy[zx]; } int query(int x,int y) { int pos=lca(x,y); if(pos<=n) return dis[x]+dis[y]-2*dis[pos]; else //方点 { int sx=findson(x,pos),sy=findson(y,pos); int now=dis[x]+dis[y]-dis[sx]-dis[sy]; if(sum[sx]>sum[sy]) sx^=sy^=sx^=sy; int val=sum[sy]-sum[sx]; return now+min(val,sum[pos]-val); } }} namespace Graph { using Tree::sum; int dfn[N],low[N],tot,dep[N],vval[N]; vector<eg>nbr[N]; void build() { for(int i=1;i<=m;i++) { int x,y,w; cin>>x>>y>>w; nbr[x].push_back({y,w}); nbr[y].push_back({x,w}); } return ; } void help(int x,int y,int val) //val 是非树边边权 { int cur=y,lst=val; while(cur!=x) { sum[cur]+=lst; sum[fa[cur]]+=sum[cur]; lst=vval[cur]; cur=fa[cur]; } int sq=++ext; sum[sq]=sum[x]+lst; // 将所有边权放在方点上 sum[x]=0; // 方便后面处理 cur=y; while(cur!=fa[x]) { int val=min(sum[cur],sum[sq]-sum[cur]); //再来一遍记录边权 Tree::add(cur,sq,val); cur=fa[cur]; } return ; } void Tarjan(int cur,int fa) { ::fa[cur]=fa; dep[cur]=dep[fa]+1; dfn[cur]=low[cur]=++tot; for(auto dat:nbr[cur]) { int nxt=dat.to,w=dat.w; if(nxt==fa) continue; if(dfn[nxt]==0) { Tarjan(nxt,cur); vval[nxt]=w; //把边权放到深度较深的点上 low[cur]=min(low[cur],low[nxt]); } else low[cur]=min(low[cur],dfn[nxt]); if(low[nxt]>dfn[cur]) //点双连通可以有等于,这里不行,因为是圆点的连边,nxt 有反祖边到 cur 同样形成环 Tree::add(cur,nxt,w); } for(auto dat:nbr[cur]) { int nxt=dat.to,w=dat.w; if(nxt==fa) continue; if(::fa[nxt]!=cur&&dep[cur]<dep[nxt]) //找到非树边 help(cur,nxt,w); } return ; }} signed main() { ios::sync_with_stdio(false); cin.tie(0); cin>>n>>m>>q; ext=n; Graph::build(); Graph::Tarjan(1,0); //建圆方树 Tree::init(1,0); Tree::dfs(1); //Tree chain split while(q--) { int x,y; cin>>x>>y; cout<<Tree::query(x,y)<<"\n"; } return 0; } ``` ---- 欢迎提问。