在有向树上计数p-cousins

时间:2016-02-17 07:33:33

标签: algorithm tree language-agnostic graph-algorithm family-tree

我们获得了一个可以使用的定向树。我们定义 p-ancestor p-cousin 的概念如下

p-ancestor:如果节点是其父节点,则节点是另一节点的 1-ancestor 。它是节点的 p-ancestor ,如果它是(p-1)-th ancestor 的父节点。

p-cousin:如果节点共享相同的 p-ancestor ,则该节点是另一个节点的 p-cousin

  

例如,请考虑下面的树。

     

Example

     

4有三个1-cousins i,e,3,4和5,因为它们都有共同之处   1祖先,即1

对于特定树,问题如下。您将获得多对(node, p ),并且应该计算(并输出)相应节点的 p-cousin 的数量。

慢速算法是爬行到 p-ancestor 并为每个节点运行BFS。

什么是(渐近)解决问题的最快方法?

3 个答案:

答案 0 :(得分:1)

如果离线解决方案可以接受,则两次深度优先搜索可以完成这项工作。

假设我们可以将所有n次查询(node, p)从0索引到n - 1

我们可以将每个查询(node, p)转换为另一种类型的查询(ancestor , p),如下所示:

回答查询(node, p),节点的级别为a(从根节点到此节点的距离为a),是祖先级别a的级别数在a - p级。因此,对于每个查询,我们都可以找到谁是祖先:

伪代码

dfs(int node, int level, int[]path, int[] ancestorForQuery, List<Query>[]data){
    path[level] = node;
    visit all child node;
    for(Query query : data[node])
       if(query.p <= level)
          ancestorForQuery[query.index] = path[level - p];
}

现在,在第一个DFS之后,我们有一种新类型的查询(ancestor, p)

,而不是原始查询

假设我们有一个数组count,它在索引i存储具有级别i的节点数。假设a级别的节点x,我们需要计算p个后代的数量,因此,此查询的结果是:

query result = count[x + p] after we visit a -  count[x + p] before we visit a

伪代码

dfs2(int node, int level, int[] result, int[]count, List<TransformedQuery>[]data){
   count[level] ++;
   for(TransformedQuery query : data[node]){
         result[query.index] -= count[level + query.p];
   }
   visit all child node;
   for(TransformedQuery query : data[node]){
         result[query.index] += count[level + query.p];
   }
}

每个查询的结果都存储在result数组中。

答案 1 :(得分:0)

如果p是固定的,我建议采用以下算法:

假设count [v]是v的p子数。最初所有count [v]都设置为0.而pparent [v]是v的p-parent。

现在让我们在树上运行dfs并保留被访问节点的堆栈,即当我们访问某些v时,我们将它放入堆栈中。一旦我们离开v,我们就会弹出。

假设我们在dfs中遇到了一些节点v。让我们计算[stack [size - p]] ++,表明我们是v的p子。还有pparent [v] = stack [size-p]

一旦你的dfs完成,你可以像这样计算所需的p-cousins数: 算[pparent [V]]

这对于dfs是O(n + m),对于每个查询是O(1)

答案 2 :(得分:0)

首先,我将描述一种在O(p)时间内使用O(n)预处理时间和空间回答每个查询的相当简单的方法,然后提一种查询时间可以加速到O的方式(log p )时间因素只是O(log n)额外的预处理时间和空间。

O(p) - 时间查询算法

基本思想是,如果我们写出在树的DFS遍历期间访问的节点序列,使得每个节点都在与树中的级别相对应的垂直位置写出,那么节点的p-cousins在此图中形成水平间隔。请注意,这种“书写”看起来非常像典型的树形图,除非没有连接节点的行,并且(如果使用后序遍历;预订顺序也一样好)父节点总是出现在他们孩子的右边。因此,给定一个查询(v,p),我们将要做的主要是:

  1. 找到给定节点的第p个祖先u。天然这需要O(p)时间。
  2. 找到你的第p个左后代l - 即重复访问当前节点最左边的子节点的过程后到达的节点,p次。天真地需要O(p)时间。
  3. 找到你的第p个右后裔r(同样定义)。天真地需要O(p)时间。
  4. 返回值x [r] - x [l] + 1,其中x [i]是一个预先计算的值,用于记录上述序列中与...相同或同一级别的节点数节点i的左边。这需要一段时间。
  5. 预处理步骤是我们为每个1 <= i <= n计算x [i]的地方。这是通过执行构建第二个数组y []的DFS来实现的,该数据y []记录到目前为止在深度d访问的节点的数量y [d]。具体而言,y [d]对于每个d最初为0;在DFS期间,当我们访问深度为d的节点v时,我们只需递增y [d]然后设置x [v] = y [d]。

    O(log p) - 时间查询算法

    如果树相当平衡,上述算法应该已经足够快 - 但在最坏的情况下,当每个节点只有一个子节点时,O(p)= O(n)。请注意,它在上述4个步骤中的前3个步骤中在树中上下导航,强制执行O(p)时间 - 最后一步需要恒定时间。

    要解决这个问题,我们可以添加一些额外的指针,以便更快地在树上上下导航。一种简单而灵活的方法使用“指针加倍”:对于每个节点v,我们将log2(depth(v))指针存储到连续更高的祖先。为了填充这些指针,我们执行log2(maxDepth)DFS迭代,其中在第i次迭代中,我们将每个节点v的第i个祖先指针设置为其第(i-1)个祖先的第(i-1)个祖先:每个DFS每个节点只需要两次指针查找。使用这些指针,向上移动任何距离p总是最多需要log(p)跳跃,因为每次跳跃的距离可以减少至少一半。可以使用完全相同的过程来填充“左后代”和“右后代”的相应指针列表,以分别将步骤2和3加速到O(log p)时间。