トラバーサルが1つだけの無向ツリーの最長パス


44

2つの深さ優先検索を使用して、無向木で最長パスを見つけるためのこの標準アルゴリズムがあります。

  • ランダムな頂点からDFSを開始し、そこから最も遠い頂点を見つけます。それがv だと言う。vv
  • 次に、からDFSを開始して、最も遠い頂点を見つけます。このパスは、グラフ内で最も長いパスです。v

問題は、これをより効率的に行えるかどうかです。単一のDFSまたはBFSでそれを行うことはできますか?

(これは、無向木の直径を計算する問題と同等に説明できます。)


2
あなたが求めているものは、木の直径とも呼ばれます。(ツリーでは、2つのノードを接続するパスが1つしかないため、「最長最短パス」と「最長パス」は同じものです。)
ラファエル

回答:


22

ポストオーダーで深さ優先の検索を実行し、途中で結果を集計します。つまり、問題を再帰的に解決します。

u 1u k(検索ツリー内)を持つすべてのノードには、2つのケースがあります。vu1,,uk

  • の最長パスは、サブツリーT u 1T u kのいずれかにあります。TvTu1,,Tuk
  • 最長のパス含まれているVをTvv

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. A(k)kA

@JeffE 2番目のコメントに関して:確かに、これは最後の行で処理されます:height1 + height2このパスの長さです。それが実際に最長のパスである場合、それはによって選択されmaxます。上記のテキストでも説明されているので、あなたの問題はあまりわかりませんか?確かに、それが実際に最長のパスであるかどうかを確認するために再帰する必要があります。
ラファエル

@JeffE最初のコメントに関して、の計算はheight2明示的にheight1考慮から除外されるので、どうして同じ子を2回選択できますか?また、それは導入テキストで説明されています。
ラファエル

1
どうやら、私たちはあなたの言葉を理解するのに苦労しているので、異なる擬似コードの方言を話します。これは、明示的な英語の宣言を追加するために役立つだろうlongestPathHeight(T)ペアを返す(h,d)hの高さTdの直径ですT。(そう?)
JeffE

@JeffEそうです; 説明があれば、コードからそれは明らかだと思いましたが、明らかに、他の擬似コードパラダイムの「クリア」の外挿は不十分でした(私はスカラエスクだと思います)。混乱して申し訳ありませんが、コードを明確にしています(うまくいけば)。
ラファエル

8

これはより良い方法で解決できます。また、データ構造を少し変更し、反復アプローチを使用して、時間の複雑さをO(n)に減らすことができます。詳細な分析と、さまざまなデータ構造でこの問題を解決する複数の方法について。

これが私のブログ投稿で説明したいことの要約です:

再帰的アプローチ-ツリーの直径 この問題にアプローチする別の方法は次のとおりです。上で述べたように、直径は

  1. 完全に左のサブツリーにあるか、
  2. 完全に正しいサブツリーにあるか、
  3. ルートにまたがる場合があります

つまり、直径は理想的には

  1. 左の木の直径または
  2. 右の木の直径または
  3. 左サブツリーの高さ+右サブツリーの高さ+ 1(直径がルートノードにまたがるときにルートノードを追加する場合は1)

そして、直径が最も長い経路であることを知っているので、側面のいずれかにある場合は最大1と2を、ルートを通る場合は3を取ります。

反復アプローチ–ツリー直径

ツリーがあり、各ノードが次のことを知るために、各ノードのメタ情報が必要です。

  1. 左の子の高さ、
  2. その右の子の高さと
  3. リーフノード間の最も遠い距離。

各ノードがこの情報を取得したら、最大パスを追跡するための一時変数が必要です。アルゴリズムが終了するまでに、temp変数にdiameterの値があります。

ルートの3つの値がわからないため、この問題をボトムアップで解決する必要があります。しかし、葉のこれらの値はわかっています。

解決する手順

  1. leftHeightとrightHeightを1にしてすべてのリーフを初期化します。
  2. maxDistanceを0にしてすべてのリーフを初期化します。leftHeightまたはrightHeightのいずれかが1の場合、maxDistance = 0になる点になります。
  3. 一度に1つずつ上に移動し、直接の親の値を計算します。子供たちにとってこれらの価値を知っているので、それは簡単でしょう。
  4. 特定のノードで、

    • leftHeightを最大値(左の子のleftHeightまたはrightHeight)として割り当てます。
    • rightHeightを(右の子のleftHeightまたはrightHeight)の最大値として割り当てます。
    • これらの値(leftHeightまたはrightHeight)のいずれかが1の場合、maxDistanceをゼロにします。
    • 両方の値がゼロより大きい場合、maxDistanceをleftHeight + rightHeight – 1にします
  5. 一時変数のmaxDistanceを維持し、手順4でmaxDistanceが変数の現在の値よりも大きい場合は、新しいmaxDistance値に置き換えます。
  6. アルゴリズムの最後では、maxDistanceの値は直径です。

1
あまり一般的ではないことに加えて、これは私の古い答えに何を追加しますか(バイナリツリーのみを扱う)。
ラファエル

9
私の意見では、この答えは読みやすく、理解しやすいです(あなたの擬似コードは非常に紛らわしいです)。
レゲエギター

-3

以下は、単一のDFSトラバーサルのみを使用して直径パスを返すコードです。ツリー内の特定のノードで始まる最長パスだけでなく、これまでに見られた最適な直径を追跡するために、余分なスペースが必要です。これは、最長直径のパスにルートが含まれないか、ルートの近隣の2つの最長パスの組み合わせであるという事実に基づいた動的プログラミングアプローチです。したがって、この情報を追跡するには2つのベクトルが必要です。

 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
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.