双方向探索(Bidirectional Search)とは?仕組みとC++実装例を徹底解説
双方向探索(bidirectional search)とは、2つの方向から同時に探索を実行するグラフ探索手法です。1つ目の探索は始点(ソース)からゴールへ向かって順方向に進み、2つ目の探索はゴールから始点へ向かって逆方向に進みます。理想的な状況では、両方の探索はデータ構造の途中で出会うことになります。
双方向探索アルゴリズムは有向グラフ上で動作し、始点(初期ノード)からゴールノードまでの最短経路を求めます。2つの探索はそれぞれの出発点から開始され、両方の探索が同じノード上で出会った時点でアルゴリズムは停止します。
双方向アプローチの重要性
双方向探索は、一方向だけの探索よりも高速な手法であり、グラフを走査するために必要な時間を大幅に削減できます。
このアプローチは、開始ノードとゴールノードが一意に定まっており、両方向の分岐係数が等しい場合に特に有効です。
性能指標
完全性 − 両方の探索にBFS(幅優先探索)を使用する場合、双方向探索は完全(必ず解を見つけられる)です。
最適性 − 探索にBFSを使用し、すべての経路のコストが一様である場合、最適な解を保証します。
時間計算量・空間計算量 − いずれも O(b^{d/2}) です(bは分岐係数、dは探索の深さ)。単一方向のBFSのO(b^d)と比較して、指数関数的な改善が得られます。
C++による実装例
以下は、双方向探索をC++で実装したサンプルコードです。15個のノードを持つグラフに対して、始点0からゴール14までの経路を探索します。順方向と逆方向の2つのキューを用いてBFSを交互に実行し、両方の探索済みノードが重なる交差ノードを検出したら、そこを基点に経路を復元します。
#include <bits/stdc++.h>
using namespace std;
class Graph {
int V;
list<int> *adj;
public:
Graph(int V);
int isIntersecting(bool *s_visited, bool *t_visited);
void addEdge(int u, int v);
void printPath(int *s_parent, int *t_parent, int s,
int t, int intersectNode);
void BFS(list<int> *queue, bool *visited, int *parent);
int biDirSearch(int s, int t);
};
Graph::Graph(int V) {
this->V = V;
adj = new list<int>[V];
};
void Graph::addEdge(int u, int v) {
this->adj[u].push_back(v);
this->adj[v].push_back(u);
};
void Graph::BFS(list<int> *queue, bool *visited,
int *parent) {
int current = queue->front();
queue->pop_front();
list<int>::iterator i;
for (i=adj[current].begin();i != adj[current].end();i++) {
if (!visited[*i]) {
parent[*i] = current;
visited[*i] = true;
queue->push_back(*i);
}
}
};
int Graph::isIntersecting(bool *s_visited, bool *t_visited) {
int intersectNode = -1;
for(int i=0;i<V;i++) {
if(s_visited[i] && t_visited[i])
return i;
}
return -1;
};
void Graph::printPath(int *s_parent, int *t_parent,
int s, int t, int intersectNode) {
vector<int> path;
path.push_back(intersectNode);
int i = intersectNode;
while (i != s) {
path.push_back(s_parent[i]);
i = s_parent[i];
}
reverse(path.begin(), path.end());
i = intersectNode;
while(i != t) {
path.push_back(t_parent[i]);
i = t_parent[i];
}
vector<int>::iterator it;
cout<<"Path Traversed by the algorithm\n";
for(it = path.begin();it != path.end();it++)
cout<<*it<<" ";
cout<<"\n";
};
int Graph::biDirSearch(int s, int t) {
bool s_visited[V], t_visited[V];
int s_parent[V], t_parent[V];
list<int> s_queue, t_queue;
int intersectNode = -1;
for(int i=0; i<V; i++) {
s_visited[i] = false;
t_visited[i] = false;
}
s_queue.push_back(s);
s_visited[s] = true;
s_parent[s]=-1;
t_queue.push_back(t);
t_visited[t] = true;
t_parent[t] = -1;
while (!s_queue.empty() && !t_queue.empty()) {
BFS(&s_queue, s_visited, s_parent);
BFS(&t_queue, t_visited, t_parent);
intersectNode = isIntersecting(s_visited, t_visited);
if(intersectNode != -1) {
cout << "Path exist between " << s << " and "
<< t << "\n";
cout << "Intersection at: " << intersectNode << "\n";
printPath(s_parent, t_parent, s, t, intersectNode);
exit(0);
}
}
return -1;
}
int main() {
int n=15;
int s=0;
int t=14;
Graph g(n);
g.addEdge(0, 4);
g.addEdge(1, 4);
g.addEdge(2, 5);
g.addEdge(3, 5);
g.addEdge(4, 6);
g.addEdge(5, 6);
g.addEdge(6, 7);
g.addEdge(7, 8);
g.addEdge(8, 9);
g.addEdge(8, 10);
g.addEdge(9, 11);
g.addEdge(9, 12);
g.addEdge(10, 13);
g.addEdge(10, 14);
if (g.biDirSearch(s, t) == -1)
cout << "Path don't exist between "
<< s << " and " << t << "\n";
return 0;
}実行結果
Path Traversed by the algorithm 0 4 6 7 8 10 14
この実行結果から、アルゴリズムは始点0からゴール14までの経路として、ノード4 → 6 → 7 → 8 → 10を経由する経路を発見できたことがわかります。このように双方向探索を用いることで、探索範囲を半分に分割して処理できるため、大規模なグラフにおいても効率的に最短経路を求めることが可能です。
-
データ構造入門:最適二分探索木(Optimal BST)で検索コストを最小化する方法
最適二分探索木とはソートされた順序で整数のキー集合が与えられ、同時に各キーの出現頻度を格納した配列 freq も渡されます。この課題は、これらのデータをもとに二分探索木(BST)を構築し、すべての検索にかかるコストの合計を最小にすることです。検索コストは「キーの深さ × 出現頻度」の総和で表されます。そのため、頻度の高いキーほど根に近い浅い位置へ配置できれば、全体のコストを大きく抑えられます。このような木を最適二分探索木(Optimal BST)と呼びます。部分問題の解を保存し、ボトムアップ方式で問題を解決するために、補助配列 cost[n][n] を作成します。このコスト行列には、動的計画法
-
C#で二分探索(バイナリサーチ)を実装する方法|仕組みと計算量を解説
二分探索とは二分探索(バイナリサーチ)は、ソート済みの配列を対象とした高速な検索アルゴリズムです。探索したい値を配列の中央にある要素と比較し、一致しなかった場合は、その値が存在し得ない側の半分の領域を丸ごと除外します。この操作を残りの半分に対して繰り返すことで、効率よく目的の値を見つけ出します。例えば、下図のような配列から「62」という値を探す場合を考えてみましょう。中央の要素との比較結果から、62が存在するのは右側の領域だけであることが分かるため、左半分は完全に除外され、以降は右半分のみが探索対象となります。二分探索の計算量二分探索における各ケースの計算量は以下の通りです。最悪時間計算量O(