无向树中只有一个遍历的最长路径


44

有两种使用深度优先搜索的标准算法来查找无向树中的最长路径:

  • 从随机顶点开始DFS 并找到离它最远的顶点;说是。vv
  • 现在从开始DFS,以找到离它最远的顶点。该路径是图中的最长路径。v

问题是,这可以更有效地完成吗?我们可以用一个DFS或BFS做到吗?

(这可以等效地描述为计算无向树的直径的问题。)


2
您所追求的也称为树的直径。(在树上,“最长的最短路径”和“最长的路径”是同一回事,因为只有一条路径连接任意两个节点。)
拉斐尔

Answers:


22

我们以后期顺序执行深度优先搜索,并按此方式汇总结果,即,我们递归地解决该问题。

对于每个带有子子节点(在搜索树中),有两种情况:u 1u kvu1,,uk

  • 最长的路径位于子树。T u 1T u kTvTu1,,Tuk
  • 中最长的路径包含。 vTvv

在第二种情况下,我们必须将从开始的一条或两条最长路径合并到一个子树中;这些肯定是最深的叶子。如果则路径的长度为,如果则为,其中多子树高度¹。H k + H k 1 + 2 k > 1 H k + 1 k = 1 H = { h T u ii = 1 k }vH(k)+H(k1)+2k>1H(k)+1k=1H={h(Tui)i=1,,k}

在伪代码中,算法如下所示:

procedure longestPathLength(T : Tree) = helper(T)[2]

/* Recursive helper function that returns (h,p)
 * where h is the height of T and p the length
 * of the longest path of T (its diameter) */
procedure helper(T : Tree) : (int, int) = {
  if ( T.children.isEmpty ) {
    return (0,0)
  }
  else {
    // Calculate heights and longest path lengths of children
    recursive = T.children.map { c => helper(c) }
    heights = recursive.map { p => p[1] }
    paths = recursive.map { p => p[2] }

    // Find the two largest subtree heights
    height1 = heights.max
    if (heights.length == 1) {
      height2 = -1
    } else {
      height2 = (heights.remove(height1)).max
    }

    // Determine length of longest path (see above)        
    longest = max(paths.max, height1 + height2 + 2)

    return (height1 + 1, longest)
  }
}

  1. k AA(k)是(顺序统计量)中的最小值。kA

@JeffE关于第二条评论:实际上,这是在最后一行中解决的:height1 + height2这是路径的长度。如果确实是最长的路径,则选择max。上面的文本中也对此进行了说明,所以我不太明白您的问题吗?当然,您必须进行递归才能找出它是否确实是最长的路径,即使不是,它也不会损害递归的正确性。
拉斐尔

@JeffE关于第一个注释,的计算height2显式height1从考虑中删除,因此如何选择两次相同的孩子?引言中也对此进行了解释。
拉斐尔

1
显然,我们说不同的伪代码方言,因为我很难理解您的方言。这将有助于添加一个显式的英语声明,该声明longestPathHeight(T)返回一个对(h,d),其中h的高度为Td直径为T。(对吗?)
JeffE

@JeffE对;我给出了解释,以为代码很清楚,但是显然我对其他伪代码范例的“清除”推断是不够的(我猜这是Scalaesque)。抱歉,我正在澄清代码(希望如此)。
拉斐尔

8

这可以通过更好的方式解决。同样,我们可以通过稍微修改数据结构并使用迭代方法将时间复杂度降低到O(n)。对于使用各种数据结构解决此问题的详细分析和多种方法。

这是我想在我的博客文章中解释的摘要:

递归方法-树直径 解决此问题的另一种方法如下。正如我们上面提到的,直径可以

  1. 完全位于左子树或
  2. 完全位于正确的子树中
  3. 可能跨越根

这意味着直径可以理想地由

  1. 左树的直径或
  2. 右树的直径或
  3. 左子树的高度+右子树的高度+ 1(如果直径跨越根节点,则添加1以添加根节点)

而且我们知道直径是最长的路径,因此,如果直径位于侧面的任何一方,则取最大值1和2;如果跨越根部,则取3。

迭代法–树径

我们有一棵树,我们需要与每个节点有关的元信息,以便每个节点都知道以下内容:

  1. 左孩子的身高,
  2. 合适的孩子的身高和
  3. 其叶节点之间的最远距离。

每个节点获得此信息后,我们需要一个临时变量来跟踪最大路径。当算法完成时,我们在temp变量中具有了直径值。

现在,我们需要以自下而上的方式解决此问题,因为我们不知道根的三个值。但是我们知道叶子的这些价值。

解决步骤

  1. 用leftHeight和rightHeight初始化所有叶子为1。
  2. 用maxDistance初始化所有叶子为0,我们得出一点,如果leftHeight或rightHeight中的任何一个为1,我们使maxDistance = 0
  3. 一次向上移动一个并计算直接父级的值。这很容易,因为现在我们知道了孩子们的这些价值观。
  4. 在给定的节点上

    • 将leftHeight分配为最大值(其左子级的leftHeight或rightHeight)。
    • 将rightHeight分配为最大值(其右子级的leftHeight或rightHeight)。
    • 如果这些值中的任何一个(leftHeight或rightHeight)为1,则将maxDistance设置为零。
    • 如果两个值都大于零,则将maxDistance设置为leftHeight + rightHeight – 1
  5. 将maxDistance保留在一个临时变量中,如果在步骤4中maxDistance大于该变量的当前值,则将其替换为新的maxDistance值。
  6. 在算法的最后,maxDistance中的值为直径。

1
除了不那么笼统(您只处理二进制树)之外,这还和我以前的答案有什么关系?
拉斐尔

9
在我看来,这个答案更易读,更容易理解(您的伪代码非常混乱)。
reggaeguitar

-3

以下是仅使用单个DFS遍历返回直径路径的代码。它需要额外的空间来跟踪迄今为止看到的最佳直径以及从树中的特定节点开始的最长路径。这是一种动态编程方法,它基于以下事实:最长的直径路径要么不包括根,要么是根的邻居的两个最长路径的组合。因此,我们需要两个向量来跟踪此信息。

 int getDiam(int root, vector<vector<int>>& adj_list, int& height, vector<int>& path, vector<int>& diam) {
    visited[root] = true;
    int m1 = -1;
    int m2 = -1;
    int max_diam = -1;
    vector<int> best1 = vector<int>();
    vector<int> best2 = vector<int>();
    vector<int> diam_path = vector<int>();
    for(auto n : adj_list[root]) {
        if(!visited[n]) {
            visited[n] = true;
            int _height = 0;
            vector<int> path1;
            vector<int> path2;
            int _diam = getDiam(n, adj_list, _height, path1, path2);
            if(_diam > max_diam) {
                max_diam = _diam;
                diam_path = path2;
            }
            if(_height > m1) {
                m2 = m1;
                m1 = _height;
                best2 = best1;
                best1 = path1;
            }
            else if(_height > m2) {
                m2 = _height;
                best2 = path1;
            }
        }
    }

    height = m1 + 1;

    path.insert( path.end(), best1.begin(), best1.end() );
    path.push_back(root);

    if(m1 + m2 + 2 > max_diam) {
        diam = path;
        std::reverse(best2.begin(), best2.end());
        diam.insert( diam.end(), best2.begin(), best2.end() );
    }
    else{
        diam = diam_path;
    }


    return max(m1 + m2 + 2, max_diam);
}

2
这不是一个编码站点。我们不鼓励主要由代码块组成的答案。相反,我们需要答案来解释算法背后的思想,并给出简洁的伪代码(不需要任何特定编程语言的知识即可理解)。如何计算从树中的特定节点开始的最长路径?(尤其是最长的路径可能是通过“向上”使DFS树“向上”,即回到根目录开始的)
DW
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.