树的直径,LCA复习笔记
前言
复习笔记第6篇。
求直径的两种方法
树形DP:
dfs(y); ans=max( ans,d[x]+d[y]+w[i] ); d[x]=max( d[x],d[y]+w[i] );
int dis=dfs( v,u )+1; if ( f[u]<dis ) g[u]=f[u],f[u]=dis; else if ( g[u]<dis ) g[u]=dis; ans=max( ans,f[u]+g[u]+1 ); return f[u];
两次 bfs/dfs:
从任意点出发,找到最远点l;
从 l 出发,找到最远点 r. \(l\to r\) 即为所求。
0——P4408 [NOI2003]逃学的小孩
题意
\(n\) 点 \(m\) 边的带权无向图,任意两点之间有且仅有一条通路。有三个点:A,B,C,从 C 出发,先去 AB 中较近的一个,如果没找到,再去另一个,不给出具体的 ABC,问最坏情况下要多长时间。
思路
题目要求就是在一棵树上找到3个点 \(A\) 、\(B\)、\(C\) 令 \(AB+BC\) 最大,同时要满足 \(AC>AB\)。
由于要最大化这个距离,一个很明显的想法就是让其中一条成为直径,设为 \(AB.\) 然后再找另一条 \(BC\) 即可。但是要满足 \(AC>AB\) ,所以就是:先找直径 \(AB\) ,然后找一点 \(C\) 使得 \(min(AC,BC)\) 取得最大值,\(ans=\) 直径 \(+min(AC,BC)=\) 直径 \(+BC.\)
代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N=2e5+10;
struct edge
{
int to,nxt;ll val;
}e[N*10];
int n,m,head[N],tot=0,vis[N];
ll dis[N],dis2[N];
void add( int u,int v,ll w )
{
tot++; e[tot].to=v; e[tot].nxt=head[u]; e[tot].val=w; head[u]=tot;
}
int bfs( int S )
{
memset( dis,0,sizeof(dis) ); memset( vis,0,sizeof(vis) );
queue<int> q;
while ( !q.empty() ) q.pop();
q.push( S ); vis[S]=1; int res=0,num=0;
while ( q.size() )
{
int u=q.front(); q.pop();
for ( int i=head[u]; i; i=e[i].nxt )
{
int v=e[i].to;
if ( vis[v] ) continue;
vis[v]=1; dis[v]=dis[u]+e[i].val; q.push( v );
if ( dis[v]>res ) res=dis[v],num=v;
}
}
return num;
}
int main()
{
scanf( "%d%d",&n,&m );
for ( int i=1; i<=m; i++ )
{
int u,v; ll w; scanf( "%d%d%lld",&u,&v,&w );
add( u,v,w ); add( v,u,w );
}
int l=bfs(1),r=bfs(l); ll ans=dis[r],res=0;
for ( int i=1; i<=n; i++ )
dis2[i]=dis[i];
bfs( r );
for ( int i=1; i<=n; i++ )
res=max( res,min(dis[i],dis2[i]) );
printf( "%lld",ans+res );
}
1——P2491 [SDOI2011]消防
题意
\(n\) 个城市,任意两个都连通且有唯一路径,每条连通两个城市的道路的长度为 \(z_i\). 在一条边长度和不超过 \(s\) 的路径(两端都是城市)上建立消防枢纽,要求其他所有城市到这条路径的距离的最大值最小。求枢纽位置。
思路
每一个点到树上最远的点一定在直径上.所以可以直接枚举直径上的点,然后在找直径上的点到其他点的距离最大是多少.一个很显然的想法是,在直径上取的距离越大越优,那么可以直接枚举起点。
但是 \(N^2\) 复杂度还是不够。显然,答案具有单调性(不超过 s 的情况下越长越好),那么可以通过双指针,单调队列优化,\(O(n)\) 实现这个过程。当然也可以二分不过要带一个 \(\log.\)
代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const ll N=3e5+10;
const ll inf=1e15+10;
struct edge
{
ll fro,to,nxt; ll val;
}e[N<<1];
ll n,s,tot,head[N],dis[N],pre[N],bet[N],l,r;
ll ans=inf,sum[N],dis1[N],que[N]={inf},t=0,h=0;
void add( ll u,ll v,ll w )
{
e[++tot].fro=u; e[tot].to=v; e[tot].val=w; e[tot].nxt=head[u]; head[u]=tot;
}
void dfs( ll u,ll fa,ll sum,bool sav )
{
if ( sav ) pre[u]=fa,bet[u]=sum;
dis[u]=dis[fa]+sum;
for ( ll i=head[u]; i; i=e[i].nxt )
if ( e[i].to!=fa ) dfs( e[i].to,u,e[i].val,sav );
}
void get_path()
{
dfs( 1,0,0ll,0 ); ll mx=0;
for ( ll i=1; i<=n; i++ )
if ( dis[i]>mx ) l=i,mx=dis[i];
dfs( l,0,0ll,1 ); mx=0;
for ( ll i=1; i<=n; i++ )
if ( dis[i]>mx ) r=i,mx=dis[i];
}
void bfs()
{
memset( dis,63,sizeof(dis) ); queue<ll> q,fro;
for ( ll i=r; i; i=pre[i] )
q.push(i),fro.push(i),dis[i]=0;
while ( !q.empty() )
{
ll v=q.front(),u=fro.front(); q.pop(); fro.pop();
for ( ll i=head[v]; i; i=e[i].nxt )
if ( dis[e[i].to]>=inf )
{
dis[e[i].to]=dis[v]+e[i].val;
dis1[u]=max( dis1[u],dis[e[i].to] );
q.push( e[i].to ); fro.push( u );
}
}
}
int main()
{
scanf( "%lld%lld",&n,&s );
for ( ll i=1,u,v,w; i<n; i++ )
scanf( "%lld%lld%lld",&u,&v,&w ),add( u,v,w ),add( v,u,w );
get_path(); bfs();
pre[n+1]=r;
for ( ll i=n+1; i; i=pre[i] )
sum[pre[i]]=sum[i]+bet[i];
for ( ll L=r,R=r; L && R!=l; L=pre[L] )
{
ll las=R; h++;
while ( sum[R]-sum[L]<=s && R )
{
las=R; R=pre[R];
if ( R && sum[R]-sum[L]<=s )
{
while ( dis1[R]>=que[t] && t>=h ) t--;
que[++t]=dis1[R];
}
}
if ( R==0 || sum[R]-sum[L]>s ) R=las;
ll tmp=max( sum[L],sum[l]-sum[R] ); tmp=max( tmp,que[h] );
ans=min( tmp,ans );
}
printf( "%lld",ans );
return 0;
}
2——P2610 [ZJOI2012]旅游
题意
T国的国土可以用一个凸N边形来表示,包含 \(N-2\) 个城市,每个城市都是顶点为 \(N\) 边形顶点的三角形,两人的旅游路线可以看做是连接N个顶点中不相邻两点的线段。问一路能经过最多多少城市。
一个城市被当做经过当且仅当其与线路有至少两个公共点。
思路
很巧妙的一道题。(不愧是ZJOI)
三角剖分是个很有意思的信息。不是让你想递推啊喂
考虑什么样的城市是不满足三角剖分的。会发现,不可能存在一些城市围成一圈(这样有一个点就会在内部而不是端点),所以如果将相邻的城市连边,是不可能存在环的。没有环,又是连通的,那么就是树了。
问题就转化成了求树的直径,裸题。
然而事实上,建图有亿点麻烦...但是非常幸运的是,由于这道题是三角形,所以肯定是二叉树,那么我们有pair!我们有 map!我们有STL!重点是我们有O2!
于是这道题就做完了。
代码
#include <bits/stdc++.h>
using namespace std;
const int N=2e5+10;
struct edge
{
int to,nxt;
}e[N<<1];
int head[N],tot=0,n,s[N][3],ans,g[N],f[N];
map<pair<int,int>,int> mp;
void add( int u,int v )
{
e[++tot].to=v; e[tot].nxt=head[u]; head[u]=tot;
e[++tot].to=u; e[tot].nxt=head[v]; head[v]=tot;
}
int dfs( int u,int fa )
{
for ( int i=head[u]; i; i=e[i].nxt )
{
int v=e[i].to;
if ( v==fa ) continue;
int dis=dfs( v,u )+1;
if ( f[u]<dis ) g[u]=f[u],f[u]=dis;
else if ( g[u]<dis ) g[u]=dis;
}
ans=max( ans,f[u]+g[u]+1 );
return f[u];
}
void build( int i,int j,int u )
{
pair<int,int> pr=make_pair( i,j );
if ( mp[pr] ) add( u,mp[pr] );
else mp[pr]=u;
}
int main()
{
scanf( "%d",&n );
for ( int i=1; i<=n-2; i++ )
{
scanf( "%d%d%d",&s[i][0],&s[i][1],&s[i][2] ); sort( s[i],s[i]+3 );
build( s[i][0],s[i][1],i ); build( s[i][1],s[i][2],i ); build( s[i][0],s[i][2],i );
}
dfs( 1,0 );
printf( "%d",ans );
return 0;
}
3——P3629 [APIO2010]巡逻
题意
有 \(n\) 个村庄,编号为 \(1, 2, ..., n\) 。有 \(n – 1\) 条道路连接着这些村 庄,从任何一个村庄都可以到达其他任一个村庄。道路长度均为 1。 巡警车每天要到所有的道路上巡逻。警察局设在编号为 \(1\) 的村庄里,每天巡警车总是从警察局出发又回到警察局。
在这些村庄之间建 \(K\) 条新的道路, 可以连接任意两个村庄。每天巡警车必须 经过新建的道路正好一次. 求最小的巡逻距离。
思路
考虑逐条加边。
如果不加边,那么答案显然是 \(2(n-1)\).
如果加一条边,由于必须经过恰好一次,所以在沿着新的道路 \((u,v)\) 走了一次之后,要返回 \(u\) ,必须沿着树上的环的另一半再走一遍,那么这时候 \(u\to v\) 的路径只需要走一次,所以 \(ans=2(n-1)-L-+1.\)
再加一条边,如果环没有重叠,那么按照一条的情况处理即可。否则,重叠部分不会被走过,所以还要走一次,又变成了需要走两次的边。
总结两种情况,得到算法:
- 找一遍直径,边权取反,长度为 \(L_1\)
- 再求直径,得到 \(L_2\)
- \(ans=2(n-1)-(L_1-1)-(L_2-1)\)
代码
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+10;
struct edge
{
int to,nxt,val;
}e[N<<1];
int n,k,tot=0,mx,head[N],dis[N],pre[N],f[N];
bool vis[N];
queue<int> q;
void add( int u,int v,int w )
{
e[++tot].to=v; e[tot].val=w; e[tot].nxt=head[u]; head[u]=tot;
}
int bfs( int s )
{
memset( dis,0x3f,sizeof(dis) );
q.push( s ); dis[s]=pre[s]=0;
while ( q.size() )
{
int t=q.front(); q.pop();
for ( int i=head[t]; i; i=e[i].nxt )
if ( dis[e[i].to]==0x3f3f3f3f )
dis[e[i].to]=dis[t]+e[i].val,pre[e[i].to]=i,q.push( e[i].to );
}
int res=1;
for ( int x=1; x<=n; x++ )
if ( dis[x]>dis[res] ) res=x;
return res;
}
void dp( int x )
{
vis[x]=1;
for ( int i=head[x]; i; i=e[i].nxt )
if ( !vis[e[i].to] )
{
dp( e[i].to );
mx=max( mx,f[e[i].to]+f[x]+e[i].val );
f[x]=max( f[x],f[e[i].to]+e[i].val );
}
}
int main()
{
memset( head,0,sizeof(head) ); tot=1;
scanf( "%d%d",&n,&k );
for ( int i=1,u,v; i<n; i++ )
scanf( "%d%d",&u,&v ),add( u,v,1 ),add( v,u,1 );
int l=bfs( 1 ); l=bfs(l);
int L1=dis[l],fl=1; mx=0;
if ( k==2 )
{
for ( ; pre[l]; l=e[pre[l]^1].to )
e[pre[l]].val=e[pre[l]^1].val=-1;
dp( 1 ); fl=2;
}
printf( "%d",2*(n-1)-L1-mx+fl );
return 0;
}
4——P4381 [IOI2008]Island
题意
\(N\) 个岛屿组成,从每个岛屿 \(i\) 出发向另外一个岛屿建了一座长度为 \(L_i\) 的桥,可以双向行走。同时,每对岛屿之间都有一艘专用的往来两岛之间的渡船。你希望经过的桥的总长度尽可能长,但受到以下的限制:
- 可以自行挑选一个岛开始游览。
- 任何一个岛都不能游览一次以上。
- 任何时间都可以由当前所在的岛 \(S\) 去另一个从未到过的岛 \(D\)。从 \(S\) 到 \(D\) 有如下方法:
- 步行:仅当两个岛之间有一座桥,桥长会累加到步行总距离中。
- 渡船:仅当没有任何桥和以前使用过的渡船的组合可以由 \(S\) 走到 \(D\) (检查是否可到达时应该考虑所有路径,包括经过曾游览过的岛)。
注意,你不必游览所有的岛,也可能无法走完所有的桥。
给定 \(N\) 座桥以及它们的长度,按照上述的规则,计算你可以走过的桥的长度之和的最大值。
思路
每个岛屿一座桥,\(n\) 个岛屿 \(n\) 座桥……诶,不是树?出大问题
于是你不幸地发现,这是基环树,而且还是基环树森林!不过没有关系。由题意可知,如果乘船离开一棵基环树就不能再回来了。(因为回来要坐船,然而你坐过了,所以可达性成立,你就不能坐船了)
于是题目就变成了,求所有基环树的直径之和。
首先,找出基环树的环。然后,对于直径,显然分成两部分,要么在去掉环的某棵子树内,要么是环上一段距离加上两棵不同子树内的距离。
于是可以先预处理出环上每个点在去掉环的前提下,子树内的直径。然后把环展开,分顺时针和逆时针两种情况讨论,每次的 \(res=dis(i,j)+d[i]+d[j]\) ,取max 即可。
代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N=1e6+10;
int n,m,cnt=0,dfn[N],fa[N],pre[N],a[N<<1];
int head[N],tot=1,q[N<<1];
ll ans=0,d[N<<1],f[N],b[N<<1],res;
bool vis[N];
struct edge
{
int nxt,to,val;
}e[N<<1];
void add( int u,int v,int w )
{
e[++tot]=(edge){head[u],v,w}; head[u]=tot;
}
void bfs1( int u ) //找环
{
int tail=0; q[++tail]=u;
while ( tail )
{
int he=q[tail--]; dfn[he]=++cnt;
for ( int i=head[he]; i; i=e[i].nxt )
{
int v=e[i].to;
if ( i==(pre[he]^1) ) continue;
if ( !dfn[v] ) { fa[v]=he,pre[v]=i,q[++tail]=v; }
else if ( !m )
{
int p=he;
for ( ; p!=v; p=fa[p] )
a[++m]=p,d[m]=e[pre[p]].val,vis[p]=1;
a[++m]=v; d[m]=e[i].val; vis[v]=1;
}
}
}
}
ll bfs( int x )
{
int h=1,t=0; q[++t]=x;
while ( h<=t )
{
int u=q[h++];
for ( int i=head[u]; i; i=e[i].nxt )
{
int v=e[i].to;
if ( vis[v] ) continue;
fa[v]=u; vis[v]=1; q[++t]=v; pre[v]=e[i].val;
}
}
for ( int i=t; i>1; i-- )
{
int v=q[i],u=fa[v],w=pre[v];
res=max( res,f[u]+f[v]+w );
f[u]=max( f[u],f[v]+w );
}
return f[x];
}
void solve( int x )
{
m=res=0; bfs1( x );
if ( !m ) { bfs( x ); ans+=res; return; }
reverse( a+1,a+1+m ); reverse( d+1,d+1+m ); //反转
for ( int i=1; i<=m; i++ )
b[i]=bfs( a[i] );
for ( int i=m+1; i<=m*2; i++ ) //复制,展环成链
a[i]=a[i-m],b[i]=b[i-m],d[i]=d[i-m];
for ( int i=1; i<=m*2; i++ )
d[i]+=d[i-1];
int h=0,t=0; q[0]=0;
for ( int i=1; i<=m*2; i++ )
{
while ( h<=t && i-q[h]+1>m ) h++;
if ( h<=t ) res=max( res,b[i]+b[q[h]]+d[i]-d[q[h]] );
while ( h<=t && b[q[t]]-d[q[t]]<=b[i]-d[i] ) t--;
q[++t]=i;
}
ans+=res;
}
int main()
{
scanf( "%d",&n );
for ( int i=1,v,w; i<=n; i++ )
scanf( "%d%d",&v,&w ),add( v,i,w ),add( i,v,w );
for ( int i=1; i<=n; i++ )
if ( !dfn[i] ) solve( i );
printf( "%lld\n",ans );
}
以上是树的直径部分。
5——P5021 赛道修建
题意
有一棵带边权的树,在树上选出 \(m\) 条互不相交的链(点可以重合,但是边不能重合),使得 \(m\) 条链中最短的链最长。
思路
看到这个很容易想到总体思路是二分。
考虑如何判定。然后发现这道题其实藏了一个贪心:对于一棵子树内的所有链,在有最多的儿子对答案做出贡献的前提下,最大化 \(f_i\) 的值。由于 一个点的 \(f\) 最多对答案产生 \(1\) 的贡献,所以让儿子更少贡献,转移更大的 \(f_i\) 不会变优。
那么正解就来了。首先二分出一个 \(mid\) 。然后对于一棵子树,令 \(f_i\) 为以 \(i\) 为根的子树中,最优的不完整的链长(完整的根据上面的贪心分析,已经 \(\ge mid\) ,贡献到答案里去了;不完整就是还要和别的链拼接的)。
暂时不考虑根节点 \(i\) ,对于 \(i\) 所有的儿子,如果能单独贡献则直接计入答案。否则,尝试两两合并这些子链(见题意,点是可以重合的,在根节点合并多少都没有关系),如果长度 \(\ge mid\) 就计入答案。如果都不行,那就在这些剩余的链中选取最长的一条,计入 \(f_i\) ,尝试往上走。于是就可以把所有子节点的 \(f_j\) 排序,贪心找最大的匹配数(能匹配的几条中最小的一个),然后把剩下的转移给 \(f_i\) 即可。
代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N=5e4+10;
struct edge
{
int to,nxt; ll val;
}e[N<<1];
int head[N],tot=0,n,m;
ll f[N],subans[N];
vector<int> son[N];
void add( int u,int v,ll w )
{
e[++tot].to=v; e[tot].nxt=head[u]; e[tot].val=w; head[u]=tot;
}
int get_ans( int u,int pos,int cnt,ll x )
{
int res=0,l=0;
for ( int r=cnt-1; r; r-- )
{
r-=(r==pos);
while ( l<r && son[u][l]+son[u][r]<x ) ++l;
l+=(l==pos);
if ( l>=r ) break;
res++; l++;
}
return res;
}
void dfs( int u,int fa,ll x )
{
f[u]=subans[u]=0; son[u].clear();
for ( int i=head[u]; i; i=e[i].nxt )
{
int v=e[i].to;
if ( v==fa ) continue;
dfs( v,u,x ); f[v]+=e[i].val;
if ( f[v]>=x ) subans[u]++;
else son[u].push_back( f[v] );
}
int cnt=son[u].size(); sort( son[u].begin(),son[u].end() );
int l=0,r=cnt,sub=0,res;
for ( int r=cnt-1; r; r-- ) //配对
{
while ( l<r && son[u][l]+son[u][r]<x ) l++;
if ( l>=r ) break;
sub++; l++;
}
subans[u]+=sub;
if ( sub*2==cnt ) return;
l=0; r=cnt-1;
while ( l<=r ) //二分找最大的一个mid,使得首先满足子树要求
{
int mid=(l+r)>>1;
int tmp=get_ans( u,mid,cnt,x );
if ( tmp==sub ) res=mid,l=mid+1;
else r=mid-1;
}
f[u]=son[u][res]; //记入f[i]
}
bool check( ll x )
{
int res=0; dfs( 1,0,x );
for ( int i=1; i<=n; i++ ) //每个点的贡献
res+=subans[i];
return res>=m;
}
int main()
{
ll l=0,r=0;
scanf( "%d%d",&n,&m );
for ( int i=1; i<n; i++ )
{
int u,v; ll w; scanf( "%d%d%lld",&u,&v,&w );
add( u,v,w ); add( v,u,w ); r+=w;
}
r/=(ll)m; ll ans=0;
while ( l<=r )
{
ll mid=(l+r)>>1;
if ( check(mid) ) ans=mid,l=mid+1;
else r=mid-1;
}
printf( "%lld",ans );
return 0;
}
6——P1852 跳跳棋
题意
跳跳棋是在一条数轴上进行的。棋子只能摆在整点上。每个点不能摆超过一个棋子。棋盘上有3颗棋子,分别在 \(a,b,c\) 这三个位置。我们要通过最少的跳动把他们的位置移动成 \(x,y,z\) 。(棋子是没有区别的)
跳动的规则很简单,任意选一颗棋子,对一颗中轴棋子跳动。跳动后两颗棋子距离不变。一次只允许跳过1颗棋子。
判断是否可以完成任务。如果可以,输出最少需要的跳动次数。
思路
神仙题……非常巧妙地建模。只能说:女少口阿
首先,对于中轴棋子为 \(b\) (中间那个)的情况,显然一直往中间跳可以一直减小范围,直到不能跳为止。这时候就得到了一个非常有用的“Basic” 状态,也就是“根状态”(这怎么跟某道字符串手玩题这么像啊)
然后把 \(b\) 往左右跳的情况看成左右节点状态,那么所有状态构成了一棵二叉树。对于棋盘上所有的 \(a,b,c\) ,状态构成了一个森林。
那么,如果 \((a,b,c)\to (x,y,z)\) ,首要条件是在同一棵树上。这样第一问就解决了。
考虑状态怎么去树根。利用 LCA 的思想,把两个状态到根的距离调整到一样,然后二分向上的步数,最后找到一个 \(L\) 使得两个状态向上 \(L\) 步相遇,那么总答案就是 高度差加上二分答案的两倍。
代码
#include <bits/stdc++.h>
using namespace std;
const int inf=1e9+7;
int sx,sy,sz,dep,mx;
void init( int &x,int &y,int &z )
{
x+=inf; y+=inf; z+=inf;
if ( y>z ) swap( y,z );
if ( x>y ) swap( x,y );
if ( y>z ) swap( y,z );
}
void dfs( int x,int y,int z,int step )
{
int del1=y-x,del2=z-y;
if ( step==mx || del1==del2 ) { sx=x,sy=y,sz=z; dep=step; return; }
if ( del1>del2 )
{
swap( del1,del2 ); int del=del2/del1;
if ( del2%del1==0 ) del--;
if ( step+del<=mx ) dfs( x,y-del*del1,z-del*del1,step+del );
else dfs( x,y-(mx-step)*del1,z-(mx-step)*del1,mx );
}
else
{
int del=del2/del1; del-=(del2%del1==0);
if ( step+del<=mx ) dfs( x+del*del1,y+del*del1,z,step+del );
else dfs( x+(mx-step)*del1,y+(mx-step)*del1,z,mx );
}
}
int main()
{
int x,y,z,a,b,c;
scanf( "%d%d%d",&a,&b,&c ); init( a,b,c );
scanf( "%d%d%d",&x,&y,&z ); init( x,y,z );
mx=inf;
dfs( a,b,c,0 ); int sa=sx,sb=sy,sc=sz,sd=dep;
dfs( x,y,z,0 );
if ( sx!=sa || sy!=sb || sz!=sc ) { printf( "NO" ); return 0; }
printf( "YES\n" );
//------------query1-------------------
int ans=0;
if ( sd>dep )
{
ans=sd-dep; mx=sd-dep;
dfs( a,b,c,0 ); a=sx; b=sy; c=sz;
}
if ( sd<dep )
{
ans=dep-sd; mx=dep-sd;
dfs( x,y,z,0 ); x=sx,y=sy,z=sz;
}
int l=0,r=inf;
while ( l<=r )
{
mx=(l+r)>>1;
dfs( a,b,c,0 ); sa=sx,sb=sy,sc=sz;
dfs( x,y,z,0 );
if ( sa!=sx || sb!=sy || sc!=sz ) l=mx+1;
else r=mx-1;
}
printf( "%d",(l<<1)+ans );
}
Last
To be continue...