• 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
    %d
    %d
    ", max_dep, find_max_wid(), find_disx());
        return 0;
    }

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


    Thank you for reading

  • 相关阅读:
    Python 数据类型:字典
    .Net开发工程师工具箱
    Javascript事件设计模式(七)
    LINQ学习之旅 (四)
    JavaScript实现抽象类与虚方法(六)
    Javascript中的反射机制(五)
    Javascript中类的实现机制(四)
    Javascript中的函数(三)
    Javascript面向对象基础(二)
    Web应用程序项目XXXX已配置为使用IIS。无法访问IIS 元数据库。您没有足够的特权访问计算机上的IIS
  • 原文地址:https://www.cnblogs.com/Na2S2O3/p/13525896.html
Copyright © 2020-2023  润新知