C++
 Computer >> コンピューター >  >> プログラミング >> C++

C++でグラフの全ノードを訪問する最短経路の長さを求める方法

無向かつ連結なグラフがあり、N 個のノードには 0, 1, 2, ..., N-1 の番号が付けられているものとします。配列 graph の長さは N であり、graph[i] にはノード i と直接接続しているノード j(i ≠ j)がちょうど一度だけ含まれます。この課題では、すべてのノードを訪問する最短経路の長さを求めます。開始地点と終了地点は任意のノードを選べるほか、同じノードや辺を何度でも再訪問・再利用できる点が特徴です。

たとえば、入力が [[1],[0,2,4],[1,3,4],[2],[1,2]] の場合、答えは 4 になります。このとき [0, 1, 4, 2, 3] という経路が一つの解となります。

解法の考え方:ビットマスク+幅優先探索(BFS)

この問題は、各時点での「訪問済みノードの集合」をビットマスクで管理しながら幅優先探索(BFS)を行うことで、効率的に解くことができます。具体的な手順は以下の通りです。

  • キューをひとつ用意します。
  • n := グラフのノード数とします。
  • req := 2^n − 1 とします(すべてのノードを訪問済みにした状態を表すビットマスクです)。
  • 訪問状態を記録するためのマップ(visited)を定義します。
  • i = 0 ~ n−1 の各ノードについて、{0 OR 2^i, i}(そのノードのみ訪問済みのマスクとノード番号のペア)をキューに挿入します。
  • n が 1 の場合は 0 を返します。
  • キューが空になるまで、探索レベル(lvl)ごとに次を繰り返します。
    • sz := 現在のキューのサイズとし、sz 回だけ以下を処理します。
      • curr := キューの先頭要素を取り出します。
      • curr[1](現在のノード)に隣接する各ノード u について処理します。
        • u := graph[curr[1]][i] とします。
        • newMask := curr[0] OR 2^u(u を新たに訪問済みにしたマスク)とします。
        • newMask が req と等しければ、lvl(現在の移動回数)を返します。これが答えです。
        • visited[u] に newMask が既に存在する場合は、以降をスキップします。
        • newMask を visited[u] に追加し、{newMask, u} をキューに挿入します。
  • ループを抜けた場合は −1 を返します(連結グラフのため実際には起こりません)。

重要なポイント

この解法の鍵は、単純な「ノードの訪問済み判定」ではなく、(訪問済みマスク, 現在位置)のペアをひとつの状態として扱い、重複を排除することです。同じノードにいても、そこへ至るまでの訪問履歴(マスク)が異なれば別々の状態として管理する必要があります。また、BFS はレベルごとに近い経路から順に探索するため、最初に req に到達した時点の lvl が必ず最短距離になります。

計算量は状態数が高々 2^N × N 通りであることから、時間・空間ともに O(2^N × N) となり、N が小さい制約下で十分高速に動作します。

それでは、理解を深めるために実装を見てみましょう。

C++ 実装例

#include <bits/stdc++.h>
using namespace std;
void print_vector(vector<auto> v){
   cout << "[";
   for(int i = 0; i<v.size(); i++){
      cout << v[i] << ", ";
   }
   cout << "]"<<endl;
}
class Solution {
   public:
   int shortestPathLength(vector<vector<int> >& graph){
      queue<vector<int> > q;
      int n = graph.size();
      int req = (1 << n) - 1;
      map<int, set<int> > visited;
      for (int i = 0; i < n; i++) {
         q.push({ 0 | (1 << i), i });
      }
      if (n == 1)
      return 0;
      for (int lvl = 1; !q.empty(); lvl++) {
         int sz = q.size();
         while (sz--) {
            vector<int> curr = q.front();
            q.pop();
            for (int i = 0; i < graph[curr[1]].size(); i++) {
               int u = graph[curr[1]][i];
               int newMask = (curr[0] | (1 << u));
               if (newMask == req)
                  return lvl;
               if (visited[u].count(newMask))
               continue;
               visited[u].insert(newMask);
               q.push({ newMask, u });
            }
         }
      }
      return -1;
   }
};
main(){
   Solution ob;
   vector<vector<int>> v = {{1},{0,2,4},{1,3,4},{2},{1,2}};
   cout << (ob.shortestPathLength(v));
}

入力

{{1},{0,2,4},{1,3,4},{2},{1,2}}

出力

4
  1. C++でツリーノードを削除する:合計値が0の部分木を除去するアルゴリズム

    問題概要根がノード0であるような木構造を考えます。この木には、次の情報が与えられています。ノードの総数:nodesi番目のノードの値:value[i]i番目のノードの親:parent[i]求めたいのは、「ノードの値の合計が0になる部分木」をすべて削除した後、木に残っているノードの個数です。たとえば、下図のような木を考えてみましょう。ノードは全部で7つありますが、出力は2になります。これは、値が0であるノード3を根とする部分木と、ノード2を根とする部分木(4 + (-2) + (-1) + (-1) = 0)が削除対象となり、最終的に残るのがノード0とノード1だけだからです。解法の考え方この問題

  2. C++で最小ヒープから値x未満のすべてのノードを出力する方法

    この問題では、最小ヒープ(Min Heap)と値xが与えられ、xより小さい値を持つすべてのノードを出力することが求められます。最小ヒープとは、すべての親ノードがその子ノードの値以下となる特殊な二分木です。この性質により、根(ルート)には常にヒープ内の最小値が格納されます。具体例を使って問題を理解しましょう。X = 45出力 − 2 4 7 10 17 22 33 34この問題を解くには、最小ヒープ全体を先行順トラバーサル(前順走査)で探索し、与えられた値xより小さい値を持つノードのみを出力します。アルゴリズムのポイント最小ヒープでは親ノードの値が必ず子ノード以下であるため、あるノードの値がx以