ACM算法总结 树上问题




简介

是一种联通无向非循环图,对于 n 个结点的树来说有 n-1 条边;如果不要求联通,我们称之为森林

在数据结构中有很多实用的树形结构,但是大多数都是基于二叉树的结构,这里更多地讨论一般树形结构。

关于树的一些名词定义:

  • :人为指定的一个结点;
  • 结点深度:从根结点到该结点的路径上的边数;
  • 树的高度:结点深度的最大值;
  • 叶结点:度数为 1 的非根结点;
  • 父亲、祖先、儿子、兄弟、后代:顾名思义即可

存储二叉树我们一般用数组存储,用 k<<1 和 k<<1|1 表示两个儿子,用 k>>1 表示父亲,对于一般树使用邻接表的存储方法(类似图),可以新开 f 数组记录父亲结点,或者在搜索时传递父亲以区分上下关系。




树的直径

从任意一个结点开始 bfs 找到最远结点,再从最远结点开始 bfs 找到最远结点,这个距离就是直径。




树的重心

定义:树上所有结点到重心的距离之和最小。 这等价于对于每一个点,取其所有子树中最大的结点数,这个数最小的就是重心。

求解方法就和定义类似,我们 dfs 的时候计算出每一个子树的结点数,然后取其中最大的那个,更新答案。

代码如下:

const int maxn=1e5+5;
vector<int> G[maxn];
int n,root,siz[maxn],max_siz=maxn;

void get_root(int u,int fa)
{
    int maxx=0; siz[u]=1;
    REP(i,0,G[u].size()-1)
    {
        int v=G[u][i];
        if(v==fa) continue;
        get_root(v,u);
        siz[u]+=siz[v];
        maxx=max(maxx,siz[v]);
    }
    maxx=max(maxx,n-siz[u]);
    if(maxx<max_siz || (maxx==max_siz && u<root)) max_siz=maxx,root=u;
}




树链剖分

下图为一棵树的树链剖分(重链剖分):

在这里插入图片描述

树链剖分把一棵树分为若干条不相交的重链,连接这些重链的树边称为轻链,一些名词定义如下:

  • 重儿子:所有儿子中子树最大的儿子结点(如有多个任取);
  • 轻儿子:除了重儿子之外的所有儿子;
  • 重边:父亲和重儿子的连边;
  • 轻边:父亲和轻儿子的连边;

注意单个结点我们也看成一条重链,这样整棵树就被划分为若干条不相交的重链,而每条重链有一个顶端结点(图中蓝色结点),表示这条重链的起始结点,也是这条重链中深度最小的结点。除此之外,重链的 dfn(dfs序,图中红色数字)一定是连续的。

树链剖分的过程就是两次 dfs 的过程,第一次处理 f、siz、d、son(父亲,子树大小,深度,重儿子),第二次处理 dfn、which、top(dfs序,dfn的逆(即dfs序为 i 的结点编号为which[i]),结点所在重链的起始结点)。注意第二次 dfs 要优先搜索重儿子。

代码如下:

const int maxn=1e6+5;
vector<int> G[maxn];
int n,m,root,cnt,a[maxn];
int siz[maxn],f[maxn],d[maxn],son[maxn],dfn[maxn],which[maxn],top[maxn];

void dfs1(int u,int fa,int depth)
{
    f[u]=fa; d[u]=depth; siz[u]=1; son[u]=0;
    REP(i,0,G[u].size()-1)
    {
        int v=G[u][i];
        if(v==fa) continue;
        dfs1(v,u,depth+1);
        siz[u]+=siz[v];
        if(siz[v]>siz[son[u]]) son[u]=v;
    }
}

void dfs2(int u,int tf)
{
    top[u]=tf; dfn[u]=++cnt; which[cnt]=u;
    if(!son[u]) return;
    dfs2(son[u],tf);
    REP(i,0,G[u].size()-1)
    {
        int v=G[u][i];
        if(v!=f[u] && v!=son[u]) dfs2(v,v);
    }
}

树链剖分本身只是一种对树形结构的操作,进行按重链划分,但它对于处理树上问题很有帮助。比如说对于 洛谷P3384重链剖分 ,要求维护结点 u 和 v 路径上的权值和以及某一结点的子树权值和。

由于同一条重链的 dfn 是连续的,所以我们可以用线段树维护 dfn 序列的权值和。对于两个结点路径权值和,我们将两个结点不停往上跳重链,每跳一次对线段树更新一次;对于某一结点子树权值和,发现子树的 dfn 也是连续的,我们通过子树根结点和其 siz 值计算出应该更新的 dfn 区间,然后更新线段树。




LCA

最近公共祖先(Least Common Ancestor),即两个结点 u 和 v 的所有祖先中最深的那个,该结点必然在 u 和 v 的路径上。

  • d i s t ( u , v ) = d ( u ) + d ( v ) 2 d ( L C A ( u , v ) ) dist(u,v)=d(u)+d(v)-2d(LCA(u,v)) ,其中 d 为深度,或者是某个结点到根的距离;

将树进行树链剖分之后,求解LCA就转变为对两个结点不停地往上跳重链(u=f[top[u]]),直到两个结点在同一条重链为止,这时比较浅的那个就是LCA。这里要注意往上跳重链的时候,要优先跳起始结点比较深的那个。

代码如下:

int LCA(int x,int y)
{
    while(top[x]!=top[y])
        d[top[x]]>d[top[y]]?(x=f[top[x]]):(y=f[top[y]]);
    return d[x]<d[y]?x:y;
}

还有一种倍增的方法也可以求解LCA。

  • 可换根的LCA:结点 u 和结点 v 的以 w 为根的LCA就是 LCA(u,v)、LCA(u,w)、LCA(v,w) 中最深的那个。




点分治

点分治用来处理树上路径问题。

对于某一个有根树,树上的路径分为两种:经过根的和不经过根的。但是不经过根的路径一定是某一棵子树上经过根的路径。所以树上的路径可以都看作是经过根的路径。要处理某一路径信息时,我们可以对子树选取一个根,然后枚举其儿子,统计儿子的路径信息,然后合并到总路径信息列表中,对下一个儿子处理时同时根据已有的总路径信息判断某一条件是否成立,这样的话就可以处理当前子树经过根的所有路径信息。处理完当前子树后对所有儿子所在子树递归处理即可。

对于某一子树,我们选取其重心作为树根,这样保证复杂度为 O(nlogn) 级别。

对于 洛谷P3806 点分治1 ,要求判断树上是否存在长度为 k 的路径,代码如下:

const int maxn=1e5+5;
struct edge {int v,w;};
vector<edge> G[maxn];
int n,m,root,siz[maxn],max_siz=maxn;
int e[maxn],dis[maxn],all_dis[maxn],tot,all_tot;
bool is[maxn*100],vis[maxn],ans[maxn];

void get_root(int u,int fa)
{
    int maxx=0; siz[u]=1;
    REP(i,0,G[u].size()-1)
    {
        int v=G[u][i].v;
        if(v==fa || vis[v]) continue;
        get_root(v,u);
        siz[u]+=siz[v];
        maxx=max(maxx,siz[v]);
    }
    maxx=max(maxx,n-siz[u]);
    if(maxx<max_siz || (maxx==max_siz && u<root)) max_siz=maxx,root=u;
}

void dfs(int u,int fa,int depth)
{
    dis[tot++]=depth;
    REP(i,0,G[u].size()-1)
    {
        int v=G[u][i].v,w=G[u][i].w;
        if(v==fa || vis[v]) continue;
        dfs(v,u,depth+w);
    }
}

void solve(int s)
{
    vis[s]=1; is[0]=1;
    all_tot=0;
    REP(i,0,G[s].size()-1)
    {
        int v=G[s][i].v,w=G[s][i].w;
        tot=0;
        dfs(v,s,w);
        REP(j,0,tot-1) REP(k,1,m)
            if(dis[j]<=e[k] && is[e[k]-dis[j]]) ans[k]=1;
        REP(j,0,tot-1) all_dis[all_tot++]=dis[j],is[dis[j]]=1;
    }
    REP(i,0,all_tot-1) is[all_dis[i]]=0;
    REP(i,0,G[s].size()-1)
    {
        int v=G[s][i].v;
        if(vis[v]) continue;
        root=0; max_siz=maxn;
        get_root(v,s);
        solve(root);
    }
}

int main()
{
    //freopen("input.txt","r",stdin);
    n=read(),m=read();
    REP(i,1,n-1)
    {
        int u=read(),v=read(),w=read();
        G[u].push_back((edge){v,w});
        G[v].push_back((edge){u,w});
    }
    REP(i,1,m) e[i]=read();
    get_root(1,0);
    solve(root);
    REP(i,1,m) puts(ans[i]?"AYE":"NAY");

    return 0;
}

以及对于 洛谷P4178 Tree ,要求统计长度小于等于 K 的路径数目,在统计某一子树的根的儿子的总路径信息时,我们用线段树维护长度区间路径数目即可。

猜你喜欢

转载自blog.csdn.net/dragonylee/article/details/104091056
今日推荐