第5章: アルゴリズムにおけるグラフ
グラフは、オブジェクト間の接続と関係を表すための基本的なデータ構造です。コンピューターサイエンスをはじめ、さまざまな分野で広く活用されています。ソーシャルネットワークやウェブページのリンク、交通、スケジューリング、リソース割り当てなどの問題を解決するのに役立ちます。この章では、無向グラフ、深さ優先探索、幅優先探索、最小スパニングツリー、最短経路などの基本的な性質とアルゴリズムについて探っていきます。
無向グラフ
無向グラフは、頂点(ノード)と辺で構成されます。正式には、無向グラフGを頂点集合Vとエッジ集合Eの組(V, E)として定義します。ここで、Eは頂点の組(v, w)からなる集合で、vとwは隣接しているか、近接しているといいます。頂点の次数は、その頂点に接続する辺の数です。
以下は、簡単な無向グラフの例です:
A --- B
/ / \
/ / \
C --- D --- E
この例では、頂点集合V = {A, B, C, D, E}
、辺集合E = {(A, B), (A, C), (B, D), (B, E), (C, D), (D, E)}
です。
グラフをプログラムで表現する方法には、主に以下の2つがあります:
-
隣接行列: 頂点数をnとすると、n x nのブール行列A。行i列jの要素A[i][j]がtrueであれば、頂点iから頂点jへの辺が存在することを表します。
-
隣接リスト: 頂点数をnとすると、長さnの配列Adj。Adj[v]には、頂点vの隣接頂点のリストが格納されます。
どの表現方法を選ぶかは、グラフの密度(辺の数/頂点の数)や必要な操作によって異なります。隣接行列は単純ですが、疎なグラフでは非効率です。一方、隣接リストは疎なグラフに適しており、頂点の隣接頂点へのアクセスが速いです。
上記の例をJavaの隣接リストで表現すると以下のようになります:
List<Integer>[] graph = (List<Integer>[]) new List[5];
graph[0] = Arrays.asList(1, 2); // A -> B, C
graph[1] = Arrays.asList(0, 3, 4); // B -> A, D, E
graph[2] = Arrays.asList(0, 3); // C -> A, D
graph[3] = Arrays.asList(1, 2, 4); // D -> B, C, E
graph[4] = Arrays.asList(1, 3); // E -> B, D
深さ優先探索 (DFS)
深さ優先探索 (DFS) は、基本的なグラフ探索アルゴリズムの1つで、各ブランチを可能な限り探索してから、バックトラックします。連結成分の発見、トポロジカルソート、サイクルの検出など、多くのグラフ問題を解くのに使用できます。
DFSアルゴリズムは以下のように動作します:
- 始点の頂点 s から開始します。
- 現在の頂点を訪問済みとしてマークします。
- 現在の頂点に隣接する未訪問の頂点 w を再帰的に訪問します。
- 現在の頂点に隣接する全ての頂点が訪問済みの場合は、現在の頂点を探索した頂点にバックトラックします。
- 未訪問の頂点がある場合は、その1つを選んで1から繰り返します。
隣接リストを使ったDFSの簡単なJava実装は以下の通りです:
boolean[] visited;
void dfs(List<Integer>[] graph, int v) {
visited[v] = true;
for (int w : graph[v]) {
if (!visited[w]) {
dfs(graph, w);
}
}
}
完全なDFS探索を行うには、全ての頂点 s について dfs(graph, s)
を呼び出し、visited
を全ての頂点について false
に初期化します。
DFSには多くの応用があります。例えば、無向グラフの連結成分を見つけるのに使用できます。各未訪問の頂点からDFSを開始し、DFSツリーに基づいて各頂点を成分に割り当てます。
幅優先探索 (BFS)
幅優先探索 (BFS) も基本的なグラフ探索アルゴリズムの1つで、レイヤー単位で頂点を探索します。現在の深さレベルの頂点を全て訪問してから、次の深さレベルの頂点に移ります。
BFSアルゴリズムは以下のように動作します:
- 始点の頂点 s から開始し、訪問済みとしてマークします。
- FIFO キューに s をエンキューします。
- 次の...以下は、提供されたマークダウンファイルの日本語翻訳です。コードの部分は翻訳せず、コメントのみ翻訳しています。ファイルの先頭に追加のコメントは付けていません。
キューが空になるまで:
- 頂点 v をデキューする。
- 頂点 v に隣接する未訪問の頂点 w について:
- w を訪問済みとしてマークする。
- w をエンキューする。
ここは、隣接リストを使った BFS の Java 実装です:
boolean[] visited;
void bfs(List<Integer>[] graph, int s) {
// キューを作成する
Queue<Integer> queue = new LinkedList<>();
// 開始頂点 s を訪問済みとしてマークする
visited[s] = true;
// 開始頂点 s をエンキューする
queue.offer(s);
// キューが空になるまでループする
while (!queue.isEmpty()) {
// キューから頂点 v をデキューする
int v = queue.poll();
// 頂点 v に隣接する頂点 w について
for (int w : graph[v]) {
// 頂点 w が未訪問の場合
if (!visited[w]) {
// 頂点 w を訪問済みとしてマークする
visited[w] = true;
// 頂点 w をエンキューする
queue.offer(w);
}
}
}
}
BFS は、重みのない グラフ内の最短経路を見つけるのに特に有効です。ソース頂点から他の任意の頂点までの距離は、それらの頂点間のパスの最小エッジ数です。BFS は必ず最短経路を見つけます。
最小全域木
最小全域木 (MST) は、重み付き無向グラフの中で、全ての頂点を接続し、かつサイクルを含まず、総エッジ重みが最小となるエッジの部分集合です。
MST を見つける代表的なアルゴリズムには、Kruskal のアルゴリズムと Prim のアルゴリズムがあります。
Kruskal のアルゴリズムは以下のように動作します:
- 各頂点を個別の木とする森 F を作成する。
- グラフのすべてのエッジを集めた集合 S を作成する。
- S が空でなく、F が全域木でない間:
- S から最小重みのエッジを取り除く。
- 取り除いたエッジが異なる木を接続する場合、それを F に追加し、2つの木を1つにまとめる。
Prim のアルゴリズムは以下のように動作します:
- 任意の頂点から始まる1つの木で初期化する。
- 木を1エッジ拡張する: 木に接続する未到達の頂点の中で、最小重みのエッジを見つけ、木に追加する。
- すべての頂点が木に含まれるまで、ステップ2を繰り返す。
ここは、Prim のアルゴリズムの Java 実装です:
int minKey(int[] key, boolean[] mstSet, int V) {
// 最小の重みを持つ頂点のインデックスを見つける
int min = Integer.MAX_VALUE, min_index = -1;
.
```以下は、提供されたマークダウンファイルの日本語翻訳です。コードの部分は翻訳せず、コメントのみ翻訳しています。ファイルの先頭に追加のコメントは付けていません。
for (int v = 0; v < V; v++) {
// 頂点vがMST集合に含まれておらず、keyの値が最小の場合
if (!mstSet[v] && key[v] < min) {
min = key[v];
min_index = v;
}
}
return min_index;
}
void primMST(int[][] graph, int V) {
int[] parent = new int[V];
int[] key = new int[V];
boolean[] mstSet = new boolean[V];
// すべての頂点のkeyを最大値に、mstSetをfalseに初期化
for (int i = 0; i < V; i++) {
key[i] = Integer.MAX_VALUE;
mstSet[i] = false;
}
// 開始頂点のkeyを0に、parentを-1に設定
key[0] = 0;
parent[0] = -1;
// V-1回繰り返す
for (int count = 0; count < V - 1; count++) {
// mstSetに含まれていない頂点のうち、keyが最小の頂点を選択
int u = minKey(key, mstSet, V);
mstSet[u] = true;
// uに隣接する頂点vについて
for (int v = 0; v < V; v++) {
// vがmstSetに含まれておらず、uからvへのコストがkeyより小さい場合
if (graph[u][v] != 0 && !mstSet[v] && graph[u][v] < key[v]) {
// vの親をuに、keyをグラフのコストに更新
parent[v] = u;
key[v] = graph[u][v];
}
}
}
printMST(parent, graph, V);
}
最小全域木(MST)には、通信ネットワーク、電力ネットワーク、水道ネットワークの設計、旅行セールスマン問題の近似解などさまざまな応用があります。
最短経路
最短経路問題は、グラフ上の2つの頂点間の経路のうち、エッジの重みの和が最小となる経路を見つける問題です。この問題には、単一始点最短経路、全点対最短経路、単一終点最短経路などさまざまな変種があります。
ダイクストラのアルゴリズムは、非負の重みを持つグラフの単一始点最短経路問題を解くためのグリーディーアルゴリズムです。以下の手順で動作します:
- 最短経路木に含まれる頂点を管理するセット
sptSet
を作成する。 - すべての頂点に無限大の距離値を割り当て、始点の距離値を0に設定する。
sptSet
に含まれていない頂点のうち、距離値が最小の頂点vを選択し、sptSet
に追加する。- vに隣接する頂点wについて、vからwへの距離がwの現在の距離値より小さい場合、wの距離値を更新する。以下は、提供されたマークダウンファイルの日本語翻訳です。コードの部分は翻訳せず、コメントのみ翻訳しています。ファイルの先頭に追加のコメントは付けていません。
頂点 v の距離値と辺 v-w の重みが、頂点 w の距離値よりも小さい場合、w の距離値を更新する。
以下は、Dijkstra のアルゴリズムの Java 実装です:
public void dijkstra(int[][] graph, int src) {
// グラフの頂点数
int V = graph.length;
// 各頂点までの最短距離
int[] dist = new int[V];
// 最短経路木に含まれているかどうかのフラグ
boolean[] sptSet = new boolean[V];
// 初期化
for (int i = 0; i < V; i++) {
dist[i] = Integer.MAX_VALUE;
sptSet[i] = false;
}
// 開始点の距離を 0 に設定
dist[src] = 0;
// 全ての頂点を処理するまでループ
for (int count = 0; count < V - 1; count++) {
// 最小距離の頂点を選択
int u = minDistance(dist, sptSet);
sptSet[u] = true;
// 選択した頂点から到達可能な頂点を更新
for (int v = 0; v < V; v++) {
if (!sptSet[v] && graph[u][v] != 0 && dist[u] != Integer.MAX_VALUE
&& dist[u] + graph[u][v] < dist[v]) {
dist[v] = dist[u] + graph[u][v];
}
}
}
// 最短距離の出力
printSolution(dist);
}
Bellman-Ford アルゴリズムは、単一始点から他の全ての頂点への最短経路を求めるアルゴリズムです。Dijkstra のアルゴリズムとは異なり、負の重みを持つグラフにも対応できます。
このアルゴリズムの手順は以下の通りです:
- 全ての頂点の距離を無限大に初期化し、始点の距離を 0 に設定する。
- 頂点数 - 1 回、全ての辺を緩和する。各辺 u-v について、u からの距離 + 辺の重みが v からの距離より小さい場合、v からの距離を更新する。
- 負の重み回路の検出。全ての辺について、さらに 1 回緩和を行う。距離が更新された場合、負の重み回路が存在する。
以下は、Bellman-Ford アルゴリズムの Java 実装です:
public void bellmanFord(int[][] graph, int src) {
// グラフの頂点数
int V = graph.length;
// 各頂点までの最短距離
int[] dist = new int[V];
// 初期化
for (int i = 0; i < V; i++)
dist[i] = Integer.MAX_VALUE;
dist[src] = 0;
// 頂点数 - 1 回、全ての辺を緩和
for (int i = 1; i < V; i++) {
for (int u = 0; u < V; u++) {
for (int v = 0; v < V; v++) {
if (graph[u][v] != 0 && dist[u] != Integer.MAX_VALUE
&& dist[u] + graph[u][v] < dist[v]) {
// ユーザーvの距離をユーザーuからの距離に更新する
dist[v] = dist[u] + graph[u][v];
}
}
}
}
}
}
for (int u = 0; u < V; u++) {
for (int v = 0; v < V; v++) {
// ユーザーuからユーザーvへの距離が0以外で、ユーザーuの距離がIntegerの最大値ではなく、
// ユーザーuからユーザーvへの距離がユーザーvの現在の距離より短い場合
if (graph[u][v] != 0 && dist[u] != Integer.MAX_VALUE
&& dist[u] + graph[u][v] < dist[v]) {
// グラフに負の重みサイクルが含まれていることを出力する
System.out.println("Graph contains negative weight cycle");
return;
}
}
}
// 最短距離の解を出力する
printSolution(dist);
}
最短経路アルゴリズムは、ナビゲーションシステム、ネットワークルーティングプロトコル、交通計画など、多くの用途があります。これらは、グラフ理論の基本的なツールであり、グラフ処理の多くのタスクに不可欠です。
結論
グラフは、さまざまな問題をモデル化できる汎用的で強力なデータ構造です。この章では、グラフの基本的な性質とタイプを探り、深さ優先探索、幅優先探索、最小全域木、最短経路などの基本的なグラフアルゴリズムを学習しました。
深さ優先探索と幅優先探索は、グラフを系統的に探索する方法を提供し、多くの高度なグラフアルゴリズムの基礎となります。Kruskal's アルゴリズムやPrim's アルゴリズムなどの最小全域木アルゴリズムは、最小の総エッジ重みで全ての頂点を接続するツリーを見つけます。Dijkstra's アルゴリズムやBellman-Ford アルゴリズムなどの最短経路アルゴリズムは、頂点間の最短経路を見つけます。
これらの基本概念とアルゴリズムを理解することは、グラフを効果的に扱い、さまざまな分野の複雑な問題に取り組むために不可欠です。アルゴリズムの学習を進めるにつれ、これらの基礎的な手法に基づいた、より高度なグラフアルゴリズムに遭遇するでしょう。
グラフは、コンピュータサイエンスをはじめとする様々な分野の問題を記述し、解決するための強力な言語を提供します。グラフアルゴリズムを習得することで、広範囲にわたる問題をモデル化し、解決するための汎用的なツールセットを手に入れることができます。# NAL Challenges
Challenge 1: Palindrome
A palindrome is a word, phrase, number, or other sequence of characters that reads the same backward as forward, such as "madam" or "racecar." Write a function that takes a string as input and returns a boolean indicating whether the string is a palindrome.
def is_palindrome(s):
# Remove non-alphanumeric characters and convert to lowercase
s = ''.join(c for c in s.lower() if c.isalnum())
# Compare the string with its reverse
return s == s[::-1]
Challenge 2: Fizz Buzz
Write a program that prints the numbers from 1 to 100. But for multiples of three, print "Fizz" instead of the number, and for the multiples of five, print "Buzz". For numbers which are multiples of both three and five, print "FizzBuzz".
for i in range(1, 101):
# If the number is a multiple of both 3 and 5, print "FizzBuzz"
if i % 3 == 0 and i % 5 == 0:
print("FizzBuzz")
# If the number is a multiple of 3, print "Fizz"
elif i % 3 == 0:
print("Fizz")
# If the number is a multiple of 5, print "Buzz"
elif i % 5 == 0:
print("Buzz")
# Otherwise, print the number
else:
print(i)
Challenge 3: Anagram
An anagram is a word or phrase formed by rearranging the letters of another word or phrase. Write a function that takes two strings as input and returns a boolean indicating whether the two strings are anagrams.
def is_anagram(s1, s2):
# Remove non-alphanumeric characters and convert to lowercase
s1 = ''.join(c for c in s1.lower() if c.isalnum())
s2 = ''.join(c for c in s2.lower() if c.isalnum())
# Check if the sorted strings are equal
return sorted(s1) == sorted(s2)