树上各种操作简单整理

树的定义:
树是一种数据结构,包含N个结点和N-1条边。树形结构的特点是一个数据元素可以有很多个直接后继,但只有一个直接前驱。而这里面最常见的就是二叉树。当树边的长度未给出时默认是1。
一般建图模板(链式前向星)

*基环树:*就是n个点n条边的连通图,可以发现只有一个环,并且删掉环上任意一个边可以变成一棵树。

int head[maxn];
struct edge
{
    int to,next,l;
}e[maxn*2];
void add(int x,int y,int l)
{
    e[cnt].to=y;
    e[cnt].l=l;
    e[cnt].next=head[x];
    head[x]=cnt++;
}

树的一些名词解释:
重儿子:每个点的子树中,子树大小(即节点数)最大的子节点
轻儿子:除重儿子外的其他子节点
重边:每个节点与其重儿子间的边
轻边:每个节点与其轻儿子间的边
重链:重边连成的链
轻链:轻边连成的链
树的直径:
树的直径就是树上存在的最长路径。求树的直径一般有两种方法。两次DFS或两次BFS和树形DP。
1.双DFS:两次DFS一般除了求出树的直径长度外还能求出路径等一些奇奇怪怪的操作,但是不能处理存在负权边的树。第一次DFS先随便取一个点作为根结点求出最深点p,p就是树的直径的一个端点,再以p为根结点进行第二次DFS,找到最深点q,p和q即为树的直径的两个端点。下面给出的代码中tot记录了树的直径的长度。

void dfs1(int u,int fa)
{
    if(dis[u]>=tot)
    {
        p=u;
        tot=dis[u];
    }
    for(int i=head[u];i!=-1;i=e[i].next)
    {
        int v=e[i].to;
        if(v==fa)continue;
        dis[v]=dis[u]+e[i].l;
        dfs1(v,u);
    }
}
void dfs2(int u,int fa)
{
    if(dis[u]>tot)
    {
        p=u;
        tot=dis[u];
    }
    for(int i=head[u];i!=-1;i=e[i].next)
    {
        int v=e[i].to;
        if(v==fa)continue;
        dis[v]=dis[u]+e[i].l;
        f[v]=u;//记录直径上的点
        b[v]=i;//记录直径上的边
        dfs2(v,u);
    }
}

2.树形DP求树的直径:树形DP求树的直径只能求出直径长度,不能记录路径

void dfsdp(int u,int fa)
{
    for(int i=head[u];i!=-1;i=e[i].next)
    {
        int v=e[i].to;
        if(v==fa)continue;
        dfsdp(v,u);
        tot=max(tot,dis[u]+dis[v]+e[i].l);
        //先更新直径长度再更新最长点到当前点的距离,避免最长边被记录两次
        //可联想当前子树是链的情况
        dis[u]=max(dis[u],dis[v]+e[i].l);
    }
}

树上(最近公共祖先)LCA问题:
模板题 洛谷P3379
定义:两个点的最近公共祖先,即两个点的所有公共祖先中,离根节点最远的一个节点。
求解最近公共祖先,常用的方法是树上倍增或者树链剖分。
*倍增求LCA:*倍增比起树链剖分,代码短,容易查错,时空复杂度也优很多(nlogn),只是功能有些欠缺。
倍增的思想是二进制。首先开一个n×logn的数组,比如fa[n][logn],其中fa[i][j]表示i节点的第2^j个父亲是谁。
然后,我们会发现有这么一个性质:

                   fa[i][j]=fa[fa[i][j-1]][j-1]

    用文字叙述为:i的第2^j个父亲 是i的第2^(j-1)个父亲的第2^(j-1)个父亲。

我们知道,一个数的二进制形式中,如果右边数第i位上是1,表示这个数如果分解为若干个2的次幂的和的形式,其中有一项一定是2^(i-1)。
举个例子:10的二进制表示为1010,它的第2位和第4位上是1,所以10=2^1
+2^3。
我们可以通过一次dfs处理出fa数组:(dep[i]表示i的深度,这个可以一起处理出来,以后要用)如果待处理的树有n个节点,那么最多有一个节点会有2^(logn)个父亲,所以我们的fa数组第二维开logn就够了。
对于求u、v的LCA,我们可以先把u、v用倍增法把深度大的提到和另一个深度相同。如果此时u、v已经相等了,表示原来u、v就在一条树链上,直接返回此时的结果。对于求u、v的LCA,我们可以先把u、v用倍增法把深度大的提到和另一个深度相同。如果此时u、v已经相等了,表示原来u、v就在一条树链上,直接返回此时的结果。
如果此时u、v深度相同但不等,则证明他们的lca在更“浅”的地方,此时需要把u、v一起用倍增法上提到他们的父亲相等。为啥是提到父亲相等呢?因为倍增法是一次上提很多,所以有可能提“过”了,如果是判断他们本身作为循环终止条件,就无法判断是否提得过多了,所以要判断他们父亲是否相等。

void dfs(int u,int fa,int d)  //求出每个点的深度以及父亲结点
{
    dep[u]=d;
    p[u][0]=fa;
    for(int i=head[u];i!=-1;i=e[i].next)
    {
        int v=e[i].to;
        if(v==fa)continue;
        dis[v]=dis[u]+1;
        dfs(v,u,d+1);
    }
}
void intit()   //倍增操作
{
    for(int i=1;(1<<i)<=n;i++)
    {
        for(int u=1;u<=n;u++)
        {
            p[u][i]=p[p[u][i-1]][i-1];
        }
    }
}
int LCA(int u,int v)  //查询操作
{
    if(dep[u]<dep[v])swap(u,v);
    int d=dep[u]-dep[v];
    for(int j=20;j>=0;j--)
    {
        if((1<<j)&d)u=p[u][j];
    }
    if(u==v)return u;
    for(int i=20;i>=0;i--)
    {
        if(p[u][i]!=p[v][i])
        {
            u=p[u][i];
            v=p[v][i];
        }
    }
    return p[u][0];
}

树上倍增:
树上倍增操常见的都是向上倍增即求LCA,具体看上面求LCA的。还有一种操作是向下倍增,可用于快速求出子树的重心。向上倍增操作基于当前结点的父亲结点,向下倍增操作一般基于当前结点的重儿子即子树最大的结点。向下倍增操作实例见下方树的重心的例题。

树的重心:
定义:设f(x)表示节点x为根时,所有的子树节点个数中的最大值。f(x)最小的节点即这颗无根树的重心。
性质:
1.以树的重心为根时,所有子树的大小都不超过整棵树大小的一半。
2.树中所有点到某个点的距离和中,到重心的距离和是最小的;如果有两个重心,那么到它们的距离和一样。
3.把两棵树通过一条边相连得到一棵新的树,那么新的树的重心在连接原来两棵树的重心的路径上。
4.在一棵树上添加或删除一个叶子,那么它的重心最多只移动一条边的距离。
5.记g(node)表示node为根的子树的重心。假设son是node的子树中size最大的儿子(重儿子),那么g(node)一定是g(son)的祖先,因此让g(son)向上跳即可。这个很显然。g(node)一定在node所在的重链上。

求树的重心:1.利用树形DP(两次DFS,树形DP也是DFS实现的感觉没什么区别),第一次DFS求出以每个节点为根的子树大小以及所有结点到1号点的距离和(默认以1号点为根),第二次DFS求出每一个点到其他所有点的距离和。转移方程解释看模板。模板题洛谷P1395。

void dfs1(int u,int fa)
{
    siz[u]=1;
    f[u]=0;
    for(int i=head[u];i!=-1;i=e[i].next)
    {
        int v=e[i].to;
        if(v==fa)continue;
        dfs1(v,u);
        siz[u]+=siz[v];
        f[u]+=f[v];
    }
    f[u]+=siz[u]-1;
}
void dfs2(int u,int fa)
{
//fa是u的父亲结点,f代表的是每一个点到其他所有点的距离总和
//父亲结点向子节点推移时,距离和要减去以子节点为根的子树大小再加上剩下结点(树结点总数减去以u为根的子树的数量)
    if(u!=fa)f[u]=f[fa]-siz[u]+n-siz[u];
    for(int i=head[u];i!=-1;i=e[i].next)
    {
        int v=e[i].to;
        if(v==fa)continue;
        dfs2(v,u);
    }
}

一些拓展操作:
1.快速求出每颗子树的重心,利用基于重链上的向下倍增。
重链就是 例题 洛谷P5666 把每一条边都分裂一次,求出每次分裂两颗子树的重心和。即快速求出每颗子树的重心。记g(node)表示node为根的子树的重心。假设son是node的子树中size最大的儿子(重儿子),那么g(node)一定是g(son)的祖先,因此让g(son)向上跳即可。这个很显然。g(node)一定在node所在的重链上。
对于一个点,若x不是重心,重心要么在重儿子子树里,要么在父亲节点上
第一次dfs求出son[x],s[x]。p[x][i]表示x沿着重儿子走2^i步
第二次dfs换根,把上面数组的定义从以1为根改为以x为根
对于一条边,以下方的重心,可以直接倍增,可以跳的条件为上方的size<=sum/2
对于上方的重心,换根时记录信息,倍增方法一样

#include<bits/stdc++.h>
using namespace std;
const int maxn=3e5+10;
typedef long long int ll;
int n,cnt;
struct edge
{
    int to,next;
}e[maxn*2];
int head[maxn],p[maxn][20],siz[maxn],son[maxn];
int son1[maxn],son2[maxn],f[maxn],siz2[maxn],ff[maxn];
//son记录最大子树,son2记录次大子树,son1记录临时最大儿子
void add(int x,int y)
{
    e[cnt].to=y;
    e[cnt].next=head[x];
    head[x]=cnt++;
}
void dfs1(int u,int fa)  //搜索最大儿子与次大儿子,并向下建立倍增
{
    siz[u]=1;  //记录子树大小
    f[u]=fa;
    for(int i=head[u];i!=-1;i=e[i].next)
    {
        int v=e[i].to;
        if(v==fa)continue;
        dfs1(v,u);
        if(siz[v]>siz[son[u]])son2[u]=son[u],son[u]=v;
        else if(siz[v]>siz[son2[u]])son2[u]=v;
        siz[u]+=siz[v];
    }
    p[u][0]=son[u];
    for(int i=1;i<=17;i++)p[u][i]=p[p[u][i-1]][i-1];
}
ll ans;
int check(int x,int sum) //检查该点是否是树的重心
{
    return x*(max(siz2[son1[x]],sum-siz2[x])<=sum/2);
}
void dfs2(int u,int fa)
{
    for(int i=head[u];i!=-1;i=e[i].next)
    {
        int v=e[i].to;
        if(v==fa)continue;
        siz2[u]=siz[1]-siz[v];ff[v]=ff[u]=0;
        if(son[u]==v)son1[u]=son2[u];
        else son1[u]=son[u];
        if(siz2[fa]>siz2[son1[u]])son1[u]=fa;//因为换根后当前根结点是u,所以非u的子树上的点可以看出u的以u的fa结点为根节点的子树。因为也是u的一颗子树了,所以要判断是不是最大子树
        p[u][0]=son1[u];
        for(int j=1;j<=17;j++)p[u][j]=p[p[u][j-1]][j-1];//断边换根结束
        int b=u;
        for(int j=17;j>=0;j--)//向下倍增寻找树的重心
        {
            if(siz2[u]-siz2[p[b][j]]<=siz2[u]/2)b=p[b][j];
        }
        ans+=check(son1[b],siz2[u])+check(b,siz2[u])+check(ff[b],siz2[u]);
        //记录答案,子树重心最多只有两个,只可能落在b点以及b点父亲结点或者最大儿子结点
        b=v;
        for(int j=17;j>=0;j--)//向下倍增寻找树的重心
        {
            if(siz2[v]-siz2[p[b][j]]<=siz2[v]/2)b=p[b][j];
        }
        ans+=check(son1[b],siz2[v])+check(b,siz2[v])+check(ff[b],siz2[v]);
        ff[u]=v;//下一次换根时v为根,所以v会变成u的父亲结点
        dfs2(v,u);
    }
    son1[u]=p[u][0]=son[u];ff[u]=f[u];
    for(int j=1;j<=17;j++)p[u][j]=p[p[u][j-1]][j-1];
    siz2[u]=siz[u];
}
int main()
{
    int T;
    scanf("%d",&T); 
    while(T--)
    {
        memset(head,-1,sizeof(head));
        memset(siz,0,sizeof(siz));
        memset(son,0,sizeof(son));
        memset(f,0,sizeof(f));
        memset(son2,0,sizeof(son2));
        ans=cnt=0;
        scanf("%d",&n);
        int x,y;
        for(int i=1;i<n;i++)
        {
            scanf("%d%d",&x,&y);
            add(x,y);
            add(y,x);
        }
        dfs1(1,0);
        memcpy(siz2,siz,sizeof(siz2));
        memcpy(son1,son,sizeof(son1));
        memcpy(ff,f,sizeof(ff));
        dfs2(1,0);
        printf("%lld\n",ans);
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值