树的统计

来源:互联网 发布:讲主角进入网络电影 编辑:程序博客网 时间:2024/06/05 01:11

    原题:一颗含有n个结点的树,所有的结点依次编号为1,2,3,……,n.对于编号为v的结点,定义t(v)为v的后代中所有编号小于v的结点个数。输入这棵树,请输出t(1),t(2),t(3),……,t(n)。

 

    当然,对于本题,写出一个O(n2)的算法是一件很容易的事,这显然也不是接下来要推出的理想算法。

    先深度优先遍历该树,按照访问的先后顺序排列结点:7 10 14 12 13 1 9 11 6 5 8 3 15 12 4(DFS序列)。然后把每个结点的子结点先后顺序反转,重新遍历,可得到新的排序结点序列:7 4 3 12 15 9 6 8 5 11 1 10 14 13 2(逆DFS序列)。以结点9为代表,显然,把两次遍历过程中在结点9之后被访问的点圈出来,得到下图。

 

 

    不难发现,两个线框的重叠区域,即为结点9的所有后代结点。看到上面的图,估计有组合数学知识的人都能想到容斥原理。上图中除了线框1和2包括的部分,还包括了结点9本身和它的直系祖先结点。

    定义f(v,S)表示在S所描述的集合中,结点编号小于v的个数。则对于9号结点,有:
    f(9,线框1)+f(9,线框2)+f(9,9的直系祖先)-f(9,整棵树)=f(9,9的后代)

    推广到更一般的情况,有

    f(v,DFS序列中v之后的部分)+f(v,逆DFS序列中v之后的部分)+f(v,v的直系祖先)-f(v,整棵树)=f(v,v的后代)

    显然,我们有f(v,整棵树)=v-1,而f(v,v的后代)就是我们的目标函数t(v),从而有:

    t(v)=f(v,DFS序列中v之后的部分)+f(v,逆DFS序列中v之后的部分)+f(v,v的直系祖先)-(v-1)

 

    求区间内小于或大于某个数的个数,我们可以通过线段树来解决,从而,可以在一次DFS遍历中(O(n))加上动态的线段树的操作(O(logn)),就能以O(nlogn)的时间复杂度和O(n)的空间复杂度求出所有结点的直系祖先中有多少个比它本身小;对DFS序列和逆DFS序列,可以用线段树求出每个元素后面有多少个比它本身小,时间复杂度也为O(nlogn),从而整个算法的时间复杂度为O(nlogn)。

 

照着算法随手写了个,c++代码实现如下:

 

输入:

 

输出:

 

总结:

    这个算法最巧妙的地方是把树对应到了两个序列:DFS序列和逆DFS序列。我们知道,树的数据关系要比线性序列复杂。本来在树中,某个结点和它的后代之间存在具有层次性的拓扑关系;通过变换成序列后,这个关系简化成了一个线性序列中的先后关系。而正是这个简单的先后关系,使我们能够用线段树(或者树状数组)来解决问题。

 

来源:

吴文虎、王建德 《世界大学生程序设计竞赛(ACM/ICPC)高级教程 第一册 程序设计中常用的计算思维方式》