P3884 二叉树问题

题目描述

如下图所示的一棵二叉树的深度、宽度及结点间距离分别为:

深度:4 宽度:4(同一层最多结点个数)

结点间距离: ⑧→⑥为8 (3×2+2=8)

⑥→⑦为3 (1×2+1=3)

注:结点间距离的定义:由结点向根方向(上行方向)时的边数×2,

与由根向叶结点方向(下行方向)时的边数之和。

输入格式

输入文件第一行为一个整数n(1≤n≤100),表示二叉树结点个数。接下来的n-1行,表示从结点x到结点y(约定根结点为1),最后一行两个整数u、v,表示求从结点u到结点v的距离。

输出格式

三个数,每个数占一行,依次表示给定二叉树的深度、宽度及结点u到结点v间距离。


这道题有明显的板子气息。如果只看节点间距离的话,明显是求两个点的LCA,即最近公共祖先。

最近公共祖先(Lowest Common Ancestors),是指对于有根树T的两个结点u、v,LCA(T,u,v)表示一个结点x,满足x是u和v的祖先且x的深度尽可能大。特别的,一个点也是它自身的祖先。(想了解更多戳这里

我们以上图为例:

 以4和8的最近公共祖先为例,我们可以发现,4的祖先是4、2、1, 8的祖先是8、5、2、1, 相同的祖先是2和1,而2号节点深度为2,1号节点深度为1,所以2是4和8的最近公共祖先。

再以3和10为例,3的祖先是3、1, 10的祖先是10、6、3、1,按照定义,这两点的LCA是3。

通过以上两个例子我们就能得到几个基础结论:

1、对于任意一颗有根树T,任意两点都存在LCA,因为它们至少有根节点这一个相同的祖先。

2、若A是B的祖先,则LCA(A, B) == A。

我们也可以据此再给最近公共祖先下一个比较严谨定义:对于一颗有根树T,设节点u的祖先组成集合A,节点v的祖先组成集合B,则LCA (u,v) 等于A∩B中深度最大的点。


理论储备好了,就到了写程序的时候。

LCA该如何求呢?我们稍加思考便能知道,既然LCA是两个节点的公共的祖先,那让这两个节点一起往上跳,碰面的点不就是它们的LCA了吗?这思路乍一看没有问题,但却忽略了一个重要的因素:如果这两个节点在同一层,那让它们一起跳确实可以找到LCA,但若它们的深度不同,比如上图中的4和8,稍加模拟就会发现,它们是无法碰面的,准确来说,是不会在路上碰面,而会在根节点1出碰面,按照我们的算法,LCA(4, 8)就是1,然而我们在上文已经推演过,LCA(4, 8)是2。这说明我们的思路有问题。或许有人在这一步就会推翻之前的猜想,从头再来。其实不然,一起向上挑是没问题的,我们只要事先做一个预处理,让两个点先处于同一深度,就能解决这个问题了。代码在下文一起呈现。

既然用到了深度,那我们势必要先遍历一遍树求出每个点的深度,方法较多,这里不再展开讲。

于是我们便能写出求LCA的代码了:

int lca (int x, int y) //求最近公共祖先 
{
    if (dep[x] < dep[y])  swap (x, y); //确保被操作的点的深度更大 
    while (dep[x] > dep[y]) //让深度大的点向上跳,直到两个点深度一样 
    {
        x = fa[x];
    }
    if (x == y)  return x; //如果一个点是另一个点的祖先,返回这个点 
    while (x != y) //一起向上跳 
    {
        x = fa[x];
        y = fa[y];
    }
    return x; //返回LCA 
}

这算法很容易理解,貌似是个好算法,但!是!我们不要忘了TLE这种错误(别问我怎么记住的),我们可以想一下,这种算法是一步一步向上跳,每一层都要走一遍,效率并不高,特别是让两个点到同一深度的步骤,我们明明知道它们分别的位置,却让更深点一步一步跳,浪费了时间。为了解决这个问题,我们要用到另一种算法——倍增。


所谓倍增,就是按2的倍数增大,即1,2,4,8,16……利用我们小学二年级就学过的知识,我们可以轻易推出,2的倍数可以组成任何正整数,所以用倍增不用担心跳不到。需要注意的是,在这里我们要从大往小跳,即跳……16,8,4,2,1,至于为什么这么做,我们煮一个栗子,跳5,如果从小到大跳,那么就是1->2->4,这时我们发现超过了5,就要回溯, 变成1->4,这显然浪费时间,但如果从大到小,就可以直接4->1,节省了时间。利用倍增,时间复杂度降为O(nlogn)。
想要实现倍增,我们的程序需要变动一下,我们将fa数组开成二维,fa[i][j]表示节点i的2^j级祖先,多开一个lg数组,它的作用会在下文介绍。
--------------------------------------------------------------------------
第一步还是遍历树确定每个点的深度。
void dfs (int now, int fatherx) ////now表示当前节点,fatherx表示它的父亲节点
{
    fa[now][0] = fatherx;
    dep[now] = dep[fatherx] + 1;
    for (r_r int i = 1; i <= lg[dep[now]]; i++) 
    {
        fa[now][i] = fa[fa[now][i - 1]][i - 1];
        //这里利用了小学二年级就学过的数学知识:2^i = 2^(i-1) + 2^(i-1)
        //意思是now的2^i祖先等于now的2^(i-1)祖先的2^(i-1)祖先
    }
    for (r_r int i = head[now]; i; i = edges[i].nxt)
    {
        if (edges[i].to != fatherx)  dfs (edges[i].to, now);
    }
}

--------------------------------------------------------------------------

接着是对算法进行一个常数优化,lg数组在这里发挥作用了

    for (r_r int i = 1; i <= n; i++) //预先算出log_2(i)+1的值,用的时候直接调用就可以了
    {
        lg[i] = lg[i - 1] + (1 << lg[i - 1] == i); 
        //这里的(1 << lg[i - 1] == i)类似于bool,若等号成立返回数值1,否则返回数值0 
    } 

这里刚看可能会懵掉,手动推一下能帮助理解。

--------------------------------------------------------------------------

然后就是最重要的求LCA,这里的lg就起到了优化作用。

int lca (int x, int y)
{
    if (dep[x] < dep[y])  swapxs (x, y);
    while (dep[x] > dep[y])
    {
        x = fa[x][lg[dep[x] - dep[y]] - 1];
    }
    if (x == y)  return x;
    for (r_r int k = lg[dep[x]] - 1; k >= 0; k--)
    {
        if (fa[x][k] != fa[y][k])
        {
            x = fa[x][k];
            y = fa[y][k];
        }
    }
    return fa[x][0];
}

有了这些,我们就能轻易求出两个点的LCA了。回到题目,深度可以再遍历时顺便求出,宽度可以单独求出,有了LCA, 距离也很容易求得。

完整代码如下:

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <queue>
#include <vector>
#define r_r register
#define ll long long
using namespace std;
const int maxn = 100010;

inline int read()
{
    int s = 0, f = 1;
    char ch = getchar();
    while (ch < '0' || ch > '9')  {if (ch == '-')  f = -1;  ch = getchar();}
    while (ch >= '0' && ch <= '9')  {s = (s << 1) + (s << 3) + (ch ^ 48);  ch = getchar();}
    return s * f;
}

int maxxs (int x, int y)  {return x > y ? x : y;}  //自定义些基础函数 
int minxs (int x, int y)  {return x < y ? x : y;}
void swapxs (int &x, int &y)  {x ^= y ^= x ^= y;}

int n, tot, lcaxs, disx, max_dep = -1, max_wid = -1;
int head[maxn], dep[maxn], lg[maxn], wid[maxn], fa[maxn][22];

struct node //普通的存图 
{
    int nxt, to;
} edges[maxn << 1];

void addx (int x, int y)
{
    edges[++tot].to = y;
    edges[tot].nxt = head[x];
    head[x] = tot;
}

void make_tree()
{
    n = read();
    for (r_r int i = 1; i <= n - 1; i++)
    {
        int x = read(), y = read();
        addx (x, x);
        addx (y, y);
    }
    for (r_r int i = 1; i <= n; i++) //预处理 
    {
        lg[i] = lg[i - 1] + (1 << lg[i - 1] == i);
    }
}

void dfs (int now, int fatherx)
{
    fa[now][0] = fatherx;
    dep[now] = dep[fatherx] + 1;
    for (r_r int i = 1; i <= lg[dep[now]]; i++)
    {
        fa[now][i] = fa[fa[now][i - 1]][i - 1];
    }
    for (r_r int i = head[now]; i; i = edges[i].nxt)
    {
        if (edges[i].to != fatherx)  dfs (edges[i].to, now);
    }
    max_dep = maxxs (max_dep, dep[now]); //求出深度 
}

int lca (int x, int y)
{
    if (dep[x] < dep[y])  swapxs (x, y);
    while (dep[x] > dep[y])
    {
        x = fa[x][lg[dep[x] - dep[y]] - 1];
    }
    if (x == y)  return x;
    for (r_r int k = lg[dep[x] - 1]; k >= 0; k--)
    {
        if (fa[x][k] != fa[y][k])
        {
            x = fa[x][k];
            y = fa[y][k];
        }
    }
    return fa[x][0];
}

int find_max_wid() //找出宽度 
{
    for (int i = 1; i <= n; i++)
    {
        wid[dep[i]]++;
    }
    for (int i = 1; i <= max_dep; i++)
    {
        max_wid = maxxs (max_wid, wid[i]);
    }
    return max_wid;
}

int find_disx() //根据题意求距离 
{
    int x = read(), y = read();
    lcaxs = lca (x, y);
    disx = (dep[x] - dep[lcaxs]) * 2 + dep[y] - dep[lcaxs]; //题目中公式的应用 
    return disx;
}

int main()
{
    make_tree();
    dfs (1, 0);
    printf ("%d\n%d\n%d\n", max_dep, find_max_wid(), find_disx());
    return 0;
}

代码已做防复制处理,核心代码没有问题,只再一个你不会发现的地方做了改动。


 

Thank you for reading

posted @ 2020-08-18 20:48  Na2S2O3  阅读(209)  评论(0编辑  收藏  举报