BZOJ4379 POI2015 Modernizacja autostrady

这篇博客探讨了在无根树中如何通过移除一条边并添加一条新边来改变树的直径,旨在最小化和最大化新树的直径。博主首先介绍了问题背景和输入输出格式,并详细阐述了求解最小直径和最大直径的思路。最小直径的求解关键是断开直径并在两部分中寻找最短直径;最大直径的求解则需要考虑断开非直径边可能导致的影响。博客中提到,最小直径可通过预处理树形DP解决,最大直径则需要枚举断点。文章最后提到了寻找距离中点最近点的方法,总结了树的直径问题的各种性质。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


Description

给定一棵无根树,边权都是1,请去掉一条边并加上一条新边,定义直径为最远的两个点的距离,请输出所有可能的新树的直径的最小值和最大值。

Input

第一行包含一个正整数n(3<=n<=500000),表示这棵树的点数。

接下来n-1行,每行包含两个正整数u,v(1<=u,v<=n),表示u与v之间有一条边。

Output

第一行输出五个正整数k,x1,y1,x2,y2,其中k表示新树直径的最小值,x1,y1表示这种情况下要去掉的边的两端点,x2,y2表示这种情况下要加上的边的两端点。

第二行输出五个正整数k,x1,y1,x2,y2,其中k表示新树直径的最大值,x1,y1表示这种情况下要去掉的边的两端点,x2,y2表示这种情况下要加上的边的两端点。

若有多组最优解,输出任意一组。

Sample Input

6
1 2
2 3
2 4
4 5
6 5

Sample Output

3 4 2 2 5
5 2 1 1 6


Solution:

一道树的直径裸题。

首先要抛开打印解的部分,因为求得应该断哪个点之后,我们可以直接在两个连通块内找到直径端点或者最靠近直径中点的点,时间复杂度是 O(n) 的。结果我一开始并没有这么考虑,反而想着一边找最优解一边求方案,结果复杂度就无故增高,还增加了代码难度。

显然这个修改的边一定和这棵树的原直径,于是先将将树的直径抽离出来。接下来分别讨论两种最值的情况。

对于最小值,显然我们应该断在直径上。因为断在其他边不会改变直径的长度,而我们的目的又是让直径尽可能小。接下来对于断出的两个连通块,最后得到的连通块最小直径取决于以下三种情况:

  • 在连通块A内。
  • 在连通块B内。
  • 同时经过连通块A、B。

对于第三种情况,肯定是每个连通块最靠近中点的点——我们定义中点指的是这棵树直径的半径点位置,显然这个中点不一定在顶点上,也可能在边上,那么这个最靠近中点的点有这样一个性质:

  • 树的直径两端点到该点的最大值最小。

这个结论在IOI2013_dreaming也有体现。那么新的直径的最小值即为:

Dmin=max{DA,DB,DA2+DB2+1A,BV}

显然我们不能 O(n) 地去找某个连通块的直径。回到原树,我们可以看做一条直径上挂了许多子树,显然这些链一定都不大于两侧的直径长度。所以,对于当前的连通块,它的新直径一定是原直径或者部分原直径加上一条链(被这个东西小小地坑了一发),可以通过树形dp预处理出来。时间复杂度为 O(n)

  • (16/11/17更新)注意,上述这条性质也可以表述成:从任意一点出发,一定会走到某一条直径的某个端点上,所以下面我顺带把求直径的朴素算法给证明了。

当然可能还有不经过原直径的情况,我们可以进行简要证明这是不可能的:显然这只可能是在抽离直径后的某个点的子树内才会出现。由于原树直径的性质,导致子树内的最深深度一定不超过根节点到树直径端点的长度。假设这条直径由 {u,lca,v} 构成,根节点为 root ,在这个连通块的直径端点为 D ,则一定有:

{disulcadisDrootdisvlcadisvrootdisuvdisvD

即无论如何,取原树直径端点一定不会比不取差。命题得证。

对于最大值,就不一定断在直径上了(又是一个深坑),但是可以证明的是,除了断在直径上,就是断在每条链和直径相连的部分。这个也可以意识流证明:

  • 如果断在下面的某条边,那么这棵子树没有独立出来的部分就没有卵用了,它显然不会改变原直径的长度。而对于剩下的子树,显然如果截的越高,答案可能会越大不是?所以只需要枚举断根节点到儿子的边即可。

此时我们可以暴力跑这个小子树的直径,平摊复杂度仍然为 O(n) ,最后最大值即为:

Dmax=max{DA+DB+1A,BV}

上述内容依次按序统计即可。

在打印解的部分,如何找那个距中点最近的点?这个按照定义,从两个直径端点开始都查一遍到某个点的距离,最大值最小的那个(些)就是了。

所以这道题目大概把所有的树的直径的基本性质都来了一遍(难得自己能不看题解写出来qvq)。

代码里写了很多类似的dfs,所以比较丑。

#include <bits/stdc++.h>
#define clear(x,val) memset(x,val,sizeof(x))
#define M 500005
using namespace std;
template <class temp>
inline void Rd(temp &res){
    res=0;char c;
    while(c=getchar(),c<48);
    do res=(res<<3)+(res<<1)+(c^48);
    while(c=getchar(),c>47);
}
template <class temp>
inline bool check_max(temp &a,temp b){
    if(a>b)return false;
    a=b;return true;
}
template <class temp>
inline bool check_min(temp &a,temp b){
    if(a<b)return false;
    a=b;return true;
}
int n,head[M];
struct edge{int v,nxt;}Edge[M<<1];
void add_edge(int u,int v,int &top){
    Edge[++top]=(edge){v,head[u]};head[u]=top;
    Edge[++top]=(edge){u,head[v]};head[v]=top;
}
//solve_diameter--------------
int fa[M],root=0,endroot=0;
int mx_diam,small_diam,diam[M],dtop=0,dp[M];
int sumL[M],sumR[M];
bool mark[M];

void dfs_pre(int u,int pre,int dep,int &res){
    if(check_max(res,dep))root=u;
    for(int j=head[u];j;j=Edge[j].nxt){
        int &v=Edge[j].v;
        if(v!=pre&&!mark[v])dfs_pre(v,u,dep+1,res);
    }
}//直径处理step 1
void dfs_diam(int u,int pre,int dep){
    fa[u]=pre;
    if(check_max(mx_diam,dep))endroot=u;
    for(int j=head[u];j;j=Edge[j].nxt){
        int &v=Edge[j].v;
        if(v!=pre)dfs_diam(v,u,dep+1);
    }
}//直径处理step 2

int stk[M],stop=0;
void up(int u){
    mark[u]=true;
    while(u!=root){
        stk[stop++]=u;
        u=fa[u];
        mark[u]=true;
    }
    diam[++dtop]=u;
    while(stop){
        ++dtop,--stop;
        diam[dtop]=stk[stop];
    }
}//将直径抽出 

int dfs_treedp(int u,int pre){
    int mx=0;
    for(int j=head[u];j;j=Edge[j].nxt){
        int &v=Edge[j].v;
        if(v!=pre&&!mark[v])
            check_max(mx,dfs_treedp(v,u)+1);
    }
    return mx;
}

void build_diameter(int st){
    mx_diam=0,dfs_pre(st,0,0,mx_diam);
    mx_diam=0,dfs_diam(root,0,0);
    up(endroot);
    for(int i=1;i<=dtop;i++)dp[i]=dfs_treedp(diam[i],0);
    for(int i=1;i<=dtop;i++)
        sumL[i]=max(sumL[i-1],i-1+dp[i]);
    for(int i=dtop;i>=1;i--)
        sumR[i]=max(sumR[i+1],dtop-i+dp[i]);
}
//----------------------------

int dis[M];
void dfs_once(int u,int pre,int dep,int &res){
    dis[u]=dep;
    if(check_max(res,dep))root=u;
    for(int j=head[u];j;j=Edge[j].nxt){
        int v=Edge[j].v;
        if(v!=pre&&!mark[v])dfs_once(v,u,dep+1,res);
    }
}
int pos;
int dfs_twice(int u,int pre,int dep,int &res){
    check_max(dis[u],dep);
    if(check_min(res,dis[u]))pos=u;
    for(int j=head[u];j;j=Edge[j].nxt){
        int v=Edge[j].v;
        if(v!=pre&&!mark[v])dfs_twice(v,u,dep+1,res);
    }
}

struct Ans{int val,u,v;}mx,mi;
int main(){
    Rd(n);
    for(int i=1,top=0,u,v;i<n;i++)
        Rd(u),Rd(v),add_edge(u,v,top);
    build_diameter(1);

    //最小值----------------------
    mi.val=n;
    for(int d=1;d<dtop;d++){
        int val1=sumL[d],val2=sumR[d+1];
        int val=max((val1+1)/2+(val2+1)/2+1,max(val1,val2));
        if(check_min(mi.val,val))mi.u=diam[d],mi.v=diam[d+1];
    }

    //最大值----------------------
    mx.val=0;
    for(int d=1;d<dtop;d++)//断直径的情况 
        if(check_max(mx.val,sumL[d]+sumR[d+1]+1))
            mx.u=diam[d],mx.v=diam[d+1];

    for(int d=1;d<=dtop;d++)//断与直径连边的情况 
        for(int j=head[diam[d]];j;j=Edge[j].nxt){
            int v=Edge[j].v;
            if(mark[v])continue;
            small_diam=0,dfs_pre(v,diam[d],0,small_diam);
            small_diam=0,dfs_pre(root,0,0,small_diam);
            if(check_max(mx.val,mx_diam+small_diam+1))
                mx.u=diam[d],mx.v=v;
        }

    clear(mark,0);
    //最小值---------------------- 
    printf("%d %d %d ",mi.val,mi.u,mi.v);

    mark[mi.v]=true;

    small_diam=0;
    dfs_pre(mi.u,mi.v,0,small_diam);
    small_diam=0,dfs_once(root,0,0,small_diam);
    small_diam=n,dfs_twice(root,0,0,small_diam);
    printf("%d ",pos);

    mark[mi.u]=true,mark[mi.v]=false;

    small_diam=0;
    dfs_pre(mi.v,mi.u,0,small_diam);
    small_diam=0,dfs_once(root,0,0,small_diam);
    small_diam=n,dfs_twice(root,0,0,small_diam);
    printf("%d\n",pos);

    mark[mi.u]=false;

    //最大值--------------------- 
    printf("%d %d %d ",mx.val,mx.u,mx.v);

    mark[mx.u]=mark[mx.v]=true;

    small_diam=0;
    dfs_pre(mx.u,mx.v,0,small_diam);
    printf("%d ",root);

    small_diam=0;
    dfs_pre(mx.v,mx.u,0,small_diam);
    printf("%d\n",root);

    mark[mx.u]=mark[mx.v]=false;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值