ACM春季集训树上问题

 感觉树上问题的难度真的好大啊

LCA,倍增,生成树,树链剖分,唉好难

下面这道题写了好长时间,就是树链剖分+LCA,我写了两种方法不知道为什么有一种是RE和MLE了

真的好痛苦,调了好久的bug还没有调出来,emmmm我寻思问问大佬们了,希望斧正一下我的代码

在给出代码之前看了网课(洛谷的啦),emmm寻思总结一下喽:

先来回顾两个问题:

1,将树从x到y结点最短路径上所有节点的值都加上z

这也是个模板题了吧

我们很容易想到,树上差分可以以O(n+m)的优秀复杂度解决这个问题

2,求树从x到y结点最短路径上所有节点的值之和

lca大水题,我们又很容易地想到,dfs O(n)预处理每个节点的dis(即到根节点的最短路径长度)

然后对于每个询问,求出x,y两点的lca,利用lca的性质 distance (x,y)=dis(x)+dis(y)-2*dis(lca) 求出结果

时间复杂度O(mlogn+n)

现在来思考一个bug:

如果刚才的两个问题结合起来,成为一道题的两种操作呢?

刚才的方法显然就不够优秀了(每次询问之前要跑dfs更新dis)


树剖是通过轻重边剖分将树分割成多条链,然后利用数据结构来维护这些链(本质上是一种优化暴力)

首先明确概念:

  • 重儿子:父亲节点的所有儿子中子树结点数目最多(size最大)的结点;
  • 轻儿子:父亲节点中除了重儿子以外的儿子;
  • 重边:父亲结点和重儿子连成的边;
  • 轻边:父亲节点和轻儿子连成的边;
  • 重链:由多条重边连接而成的路径;
  • 轻链:由多条轻边连接而成的路径;

变量声明

const int maxn=1e5+10;
struct edge{
    int next,to;
}e[2*maxn];
struct Node{
    int sum,lazy,l,r,ls,rs;
}node[2*maxn];
int rt,n,m,r,a[maxn],cnt,head[maxn],f[maxn],d[maxn],size[maxn],son[maxn],rk[maxn],top[maxn],id[maxn];

名称解释f[u]保存结点u的父亲节点d[u]保存结点u的深度值size[u]保存以u为根的子树节点个数son[u]保存重儿子

rk[u]保存当前dfs标号在树中所对应的节点top[u]保存当前节点所在链的顶端节点id[u]保存树中每个节点剖分以后的新编号(DFS的执行顺序)

1,对于一个点我们首先求出它所在的子树大小,找到它的重儿子(即处理出size,son数组)

2,在dfs过程中顺便记录其父亲以及深度(即处理出f,d数组),操作1,2可以通过一遍dfs完成

void dfs1(int u,int fa,int depth)    //当前节点、父节点、层次深度
{
    f[u]=fa;
    d[u]=depth;
    size[u]=1;  //这个点本身size=1
    for(int i=head[u];i;i=e[i].next)
    {
        int v=e[i].to;
        if(v==fa)
            continue;
        dfs1(v,u,depth+1);  //层次深度+1
        size[u]+=size[v];   //子节点的size已被处理,用它来更新父节点的size
        if(size[v]>size[son[u]])
            son[u]=v;   //选取size最大的作为重儿子
    }
}
//进入
dfs1(root,0,1);

3,第二遍dfs,然后连接重链,同时标记每一个节点的dfs序,并且为了用数据结构来维护重链,

我们在dfs时保证一条重链上各个节点dfs序连续(即处理出数组top,id,rk)

void dfs2(int u,int t)    //当前节点、重链顶端
{
    top[u]=t;
    id[u]=++cnt;    //标记dfs序
    rk[cnt]=u;  //序号cnt对应节点u
    if(!son[u])
        return;
    dfs2(son[u],t);
/*我们选择优先进入重儿子来保证一条重链上各个节点dfs序连续,
一个点和它的重儿子处于同一条重链,所以重儿子所在重链的顶端还是t*/
    for(int i=head[u];i;i=e[i].next)
    {
        int v=e[i].to;
        if(v!=son[u]&&v!=f[u])
            dfs2(v,v);  //一个点位于轻链底端,那么它的top必然是它本身
    }
}

4,两遍dfs就是树链剖分的主要处理,通过dfs我们已经保证一条重链上各个节点dfs序连续,

那么可以想到,我们可以通过数据结构(以线段树为例)来维护一条重链的信息

回顾上文的那个题目,修改和查询操作原理是类似的,以查询操作为例,其实就是个LCA,不过这里使用了top来进行加速,

因为top可以直接跳转到该重链的起始结点,轻链没有起始结点之说,他们的top就是自己。需要注意的是,每次循环只能跳

一次,并且让结点深的那个来跳到top的位置,避免两个一起跳从而插肩而过。

int sum(int x,int y)
{
    int ans=0,fx=top[x],fy=top[y];
    while(fx!=fy)   //两点不在同一条重链
    {
        if(d[fx]>=d[fy])
        {
            ans+=query(id[fx],id[x],rt);    //线段树区间求和,处理这条重链的贡献
            x=f[fx],fx=top[x];  //将x设置成原链头的父亲结点,走轻边,继续循环
        }
        else
        {
            ans+=query(id[fy],id[y],rt);
            y=f[fy],fy=top[y];
        }
    }
    //循环结束,两点位于同一重链上,但两点不一定为同一点,所以我们还要统计这两点之间的贡献
    if(id[x]<=id[y])
        ans+=query(id[x],id[y],rt);
    else
        ans+=query(id[y],id[x],rt);
    return ans;
}

题目传送门:https://www.luogu.com.cn/problem/P4281

看完上面的解释,这道题的思路就有了:

首先,如果每次询问都只有两个点,这个问题就很简单,只要是树上的路径上的点就可以,寻找树上的路径其实就是寻找LCALCA的过程。

这可以启发我们对于三个点的情况的思考。

如果这里有三个点,我们来认真的思考一下。经过上一问的启发,我们来思考一下能不能运用LCALCA来解决这道题。

我们可以发现,树上三个点的三对LCALCA一定有两个是相同的。这是一件想想的话比较显然的事情。必然能够找到某个节点,让三个点中

的两个在一侧,一个在另一侧。而这个点就是两个公共的LCALCA。思考的再深入些(并且结合瞎蒙),我们会发现这个相同的LCALCA肯

定是深度最小的一个LCALCA。

这里,我们首先可以显而易见的发现,这个点必须在三个点互相通达的路径上。

我们再思考一下LCALCA与路径和的关系。假设我们知道aa和bb的LCALCA是xx,而且xx是上述的3个LCALCA中深度最大的那个,那么可

以发现从xx到aa的距离加上从xx到bb的距离一定是最小的。根据上面的结论,我们知道aa,cc和bb,cc的LCALCA点yy一定在一个点上,

而且这个yy一定比xx深度小。

那么这个时候,我们会发现此时aa,bb,cc到xx的距离和是最小的。证明的话可以这么想:如果x'x′比xx高,那么虽然cc到xx的距离减小了

ww,但是aa,bb到x'x′的距离均增大了ww,显然距离和增大。如果x'x′比xx低,有一个节点到x'x′的距离减小了ww,剩下两个节点到x'x′的距

离均增大了ww,显然距离和也增大。

所以我们就找到了到三个点距离和最小的点:这三个点的三对LCALCA中,深度大的那两个LCA就是答案。

我们在求LCALCA之前,可以先预处理出深度depdep,那么从节点uu到vv的路径长度就是dis = dep[u] + dep[v] - 2*dep[lca(u,v)]dis=dep[u]+dep[v]2dep[lca(u,v)]。

运用这个式子分别算出aa,bb,cc到a1a1,b1b1,c1c1(三个LCALCA)的距离,最后发现总的disdis居然是轮换式:

ans = dep[a]+dep[b]+dep[c]-dep[a1]-dep[b1]-dep[c1]ans=dep[a]+dep[b]+dep[c]dep[a1]dep[b1]dep[c1]

AC代码:

#include<cstdio>
#include<algorithm>
#define N 500010
#define rg register
using namespace std;
int n,m,tot,last[N],dep[N],son[N],size[N],fa[N],top[N];
struct edge{
    int to,pre;
}e[N<<1];

inline int read(){
    int k=0,f=1; char c=getchar();
    while(c<'0'||c>'9')c=='-'&&(f=-1),c=getchar();
    while('0'<=c&&c<='9')k=k*10+c-'0',c=getchar();
    return k*f;
}

inline void add(int x,int y){
    e[++tot]=(edge){y,last[x]}; last[x]=tot;
}

void dfs1(int x){
    size[x]=1; dep[x]=dep[fa[x]]+1;
    for(rg int i=last[x],to;i;i=e[i].pre)if((to=e[i].to)!=fa[x]){
        fa[to]=x; dfs1(to);
        size[x]+=size[to];
        if(size[to]>size[son[x]]) son[x]=to;
    }
}

void dfs2(int x,int tp){
    top[x]=tp;
    if(son[x]) dfs2(son[x],tp);
    for(rg int i=last[x],to;i;i=e[i].pre)
    if((to=e[i].to)!=fa[x]&&to!=son[x]) dfs2(to,to);
}

inline int lca(int x,int y){
    int f1=top[x],f2=top[y];
    while(f1!=f2){
        if(dep[f1]<dep[f2]) swap(f1,f2),swap(x,y);
        x=fa[f1]; f1=top[x];
    }
    return dep[x]<dep[y]?x:y;
}

int main(){
    n=read(); m=read();
    for(rg int i=1;i<n;i++){
        int u=read(),v=read();
        add(u,v); add(v,u);
    }
    dfs1(1); dfs2(1,1);
    for(rg int i=1;i<=m;i++){
        int a=read(),b=read(),c=read();
        int l1=lca(a,b),l2=lca(a,c),l3=lca(b,c);
        if(dep[l1]>=dep[l2]&&dep[l1]>=dep[l3]){
            printf("%d ",l1);
            printf("%d\n",dep[a]+dep[b]+dep[c]-dep[l1]-(dep[lca(l1,c)]<<1));
            continue;
        }
        if(dep[l2]>=dep[l1]&&dep[l2]>=dep[l3]){
            printf("%d ",l2);
            printf("%d\n",dep[a]+dep[b]+dep[c]-dep[l2]-(dep[lca(l2,b)]<<1));
            continue;
            
        }
        if(dep[l3]>=dep[l1]&&dep[l3]>=dep[l2]){
            printf("%d ",l3);
            printf("%d\n",dep[a]+dep[b]+dep[c]-dep[l3]-(dep[lca(l3,a)]<<1));
            continue;
        }
    }
    return 0;
}

猜你喜欢

转载自www.cnblogs.com/ssxzwwsjz/p/12374800.html