C言語での無向グラフの表現方法を図解で詳しく解説

C言語を用いて無向グラフを表現する方法は、アルゴリズムとデータ構造を学ぶ上で重要なスキルです。本記事では、無向グラフの基本概念から具体的な実装方法までを詳しく解説します。また、隣接行列や隣接リストといった代表的な表現方法を取り上げ、それぞれのメリット・デメリットについても触れます。最後に、学習内容を確認するための演習問題を提供し、実際のアプリケーションでの応用例も紹介します。

目次

無向グラフの基本概念

無向グラフとは、頂点とそれを結ぶ辺から構成されるグラフの一種で、辺に方向がないものを指します。無向グラフは、頂点間の関係を対等に扱うため、ソーシャルネットワークや道路網など、さまざまな実世界の問題をモデル化する際に利用されます。無向グラフは、頂点集合 (V) と辺集合 (E) で表され、通常 (G = (V, E)) という形式で記述されます。ここで、辺は一対の頂点 (u) と (v) を結びつけるもので、無向グラフでは (u) と (v) の順序が重要ではありません。

グラフ表現の種類

無向グラフを表現する方法には主に以下の2種類があります。

隣接行列

隣接行列は、グラフの各頂点間の接続関係を行列で表現する方法です。この方法では、頂点の数が (n) の場合、 (n \times n) の二次元配列を用意し、頂点 (i) と頂点 (j) が接続されている場合はその位置に1を、接続されていない場合は0を設定します。

隣接リスト

隣接リストは、各頂点ごとに接続されている頂点のリストを保持する方法です。この方法では、各頂点に対してリンクリストや動的配列を用い、その頂点に隣接する頂点を格納します。隣接行列と比較して、メモリの使用効率が良い点が特徴です。

隣接行列の概要

隣接行列(Adjacency Matrix)は、グラフを二次元配列で表現する方法です。この方法では、頂点の数を ( n ) としたとき、 ( n \times n ) の行列を作成し、行列の要素 ( A[i][j] ) は頂点 ( i ) と頂点 ( j ) の間に辺が存在するかどうかを示します。

隣接行列のメリット

  • 簡単な実装: 行列形式なので、実装が比較的簡単です。
  • 定数時間の接続確認: 2つの頂点が接続されているかどうかを確認する操作は ( O(1) ) 時間で行えます。

隣接行列のデメリット

  • メモリ使用量: 辺の数が少ないスパースグラフの場合でも、メモリ使用量が ( O(n^2) ) となるため非効率です。
  • 追加・削除のコスト: 辺の追加や削除は行列の特定の要素を更新するだけですが、大規模なグラフではメモリコストが高くなります。

隣接行列の実装例

ここでは、C言語を用いて隣接行列を使った無向グラフの実装例を示します。この実装では、頂点と辺を動的に追加する方法を解説します。

隣接行列の初期化

まず、隣接行列を初期化するためのコードを示します。頂点の数を指定し、そのサイズの行列を動的に割り当てます。

#include <stdio.h>
#include <stdlib.h>

// グラフ構造体の定義
typedef struct {
    int numVertices;
    int** adjMatrix;
} Graph;

// グラフの初期化
Graph* createGraph(int vertices) {
    Graph* graph = (Graph*)malloc(sizeof(Graph));
    graph->numVertices = vertices;

    graph->adjMatrix = (int**)malloc(vertices * sizeof(int*));
    for (int i = 0; i < vertices; i++) {
        graph->adjMatrix[i] = (int*)malloc(vertices * sizeof(int));
        for (int j = 0; j < vertices; j++) {
            graph->adjMatrix[i][j] = 0;
        }
    }

    return graph;
}

辺の追加

次に、グラフに辺を追加するための関数を実装します。

// 辺の追加
void addEdge(Graph* graph, int src, int dest) {
    graph->adjMatrix[src][dest] = 1;
    graph->adjMatrix[dest][src] = 1; // 無向グラフの場合
}

グラフの表示

最後に、隣接行列を用いてグラフを表示する関数を実装します。

// グラフの表示
void printGraph(Graph* graph) {
    for (int i = 0; i < graph->numVertices; i++) {
        for (int j = 0; j < graph->numVertices; j++) {
            printf("%d ", graph->adjMatrix[i][j]);
        }
        printf("\n");
    }
}

int main() {
    int vertices = 5;
    Graph* graph = createGraph(vertices);

    addEdge(graph, 0, 1);
    addEdge(graph, 0, 4);
    addEdge(graph, 1, 2);
    addEdge(graph, 1, 3);
    addEdge(graph, 1, 4);
    addEdge(graph, 2, 3);
    addEdge(graph, 3, 4);

    printGraph(graph);

    // メモリ解放
    for (int i = 0; i < vertices; i++) {
        free(graph->adjMatrix[i]);
    }
    free(graph->adjMatrix);
    free(graph);

    return 0;
}

このコードは、5つの頂点を持つ無向グラフを作成し、隣接行列を表示します。

隣接リストの概要

隣接リスト(Adjacency List)は、グラフを表現するためのもう一つの方法です。この方法では、各頂点に対してその頂点に接続されている全ての頂点のリストを保持します。リストは、リンクリストや動的配列として実装されることが一般的です。

隣接リストのメリット

  • メモリ効率: グラフがスパースである場合、隣接行列よりもメモリ効率が良いです。必要なメモリは ( O(V + E) ) であり、ここで ( V ) は頂点数、( E ) は辺の数です。
  • 動的性: 辺の追加や削除が比較的簡単に行えます。隣接リストの末尾に新しいノードを追加するだけで済みます。

隣接リストのデメリット

  • 接続確認: 2つの頂点が接続されているかどうかを確認する操作は、最悪の場合 ( O(V) ) 時間かかることがあります。
  • 実装の複雑さ: 隣接行列に比べると実装がやや複雑になります。

隣接リストの実装例

ここでは、C言語を用いて隣接リストを使った無向グラフの実装例を示します。この実装では、頂点と辺を動的に追加する方法を解説します。

隣接リストの初期化

まず、隣接リストを初期化するためのコードを示します。各頂点に対してリンクリストを割り当てます。

#include <stdio.h>
#include <stdlib.h>

// 辺のノード構造体の定義
typedef struct Node {
    int vertex;
    struct Node* next;
} Node;

// グラフ構造体の定義
typedef struct {
    int numVertices;
    Node** adjLists;
} Graph;

// ノードの作成
Node* createNode(int v) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->vertex = v;
    newNode->next = NULL;
    return newNode;
}

// グラフの初期化
Graph* createGraph(int vertices) {
    Graph* graph = (Graph*)malloc(sizeof(Graph));
    graph->numVertices = vertices;

    graph->adjLists = (Node**)malloc(vertices * sizeof(Node*));
    for (int i = 0; i < vertices; i++) {
        graph->adjLists[i] = NULL;
    }

    return graph;
}

辺の追加

次に、グラフに辺を追加するための関数を実装します。

// 辺の追加
void addEdge(Graph* graph, int src, int dest) {
    // srcからdestへの辺を追加
    Node* newNode = createNode(dest);
    newNode->next = graph->adjLists[src];
    graph->adjLists[src] = newNode;

    // destからsrcへの辺を追加(無向グラフの場合)
    newNode = createNode(src);
    newNode->next = graph->adjLists[dest];
    graph->adjLists[dest] = newNode;
}

グラフの表示

最後に、隣接リストを用いてグラフを表示する関数を実装します。

// グラフの表示
void printGraph(Graph* graph) {
    for (int v = 0; v < graph->numVertices; v++) {
        Node* temp = graph->adjLists[v];
        printf("\n Vertex %d\n: ", v);
        while (temp) {
            printf("%d -> ", temp->vertex);
            temp = temp->next;
        }
        printf("\n");
    }
}

int main() {
    int vertices = 5;
    Graph* graph = createGraph(vertices);

    addEdge(graph, 0, 1);
    addEdge(graph, 0, 4);
    addEdge(graph, 1, 2);
    addEdge(graph, 1, 3);
    addEdge(graph, 1, 4);
    addEdge(graph, 2, 3);
    addEdge(graph, 3, 4);

    printGraph(graph);

    // メモリ解放
    for (int i = 0; i < vertices; i++) {
        Node* temp = graph->adjLists[i];
        while (temp) {
            Node* toFree = temp;
            temp = temp->next;
            free(toFree);
        }
    }
    free(graph->adjLists);
    free(graph);

    return 0;
}

このコードは、5つの頂点を持つ無向グラフを作成し、隣接リストを表示します。

グラフ操作の基本

無向グラフに対する基本的な操作には、頂点や辺の追加、削除、そして探索があります。ここでは、これらの操作を隣接リストを用いた実装を例にとって説明します。

頂点の追加

隣接リストを使用する場合、頂点の追加は比較的簡単です。ただし、頂点の数が動的に変わる場合は、グラフ構造を再構築する必要があります。

辺の追加

辺の追加は、既に実装した addEdge 関数を用いて行います。この関数は、指定した2つの頂点の間に辺を追加します。

頂点の削除

頂点の削除は、頂点に接続されているすべての辺を削除し、その頂点に関する隣接リストを解放する必要があります。以下に、そのコード例を示します。

void removeVertex(Graph* graph, int vertex) {
    // その頂点に接続されているすべての辺を削除
    for (int i = 0; i < graph->numVertices; i++) {
        Node* temp = graph->adjLists[i];
        Node* prev = NULL;
        while (temp != NULL && temp->vertex != vertex) {
            prev = temp;
            temp = temp->next;
        }
        if (temp != NULL) {
            if (prev == NULL) {
                graph->adjLists[i] = temp->next;
            } else {
                prev->next = temp->next;
            }
            free(temp);
        }
    }
    // 頂点自体の隣接リストを解放
    Node* temp = graph->adjLists[vertex];
    while (temp != NULL) {
        Node* toFree = temp;
        temp = temp->next;
        free(toFree);
    }
    graph->adjLists[vertex] = NULL;
}

辺の削除

辺の削除は、指定された頂点間のリンクを解除する操作です。以下に、そのコード例を示します。

void removeEdge(Graph* graph, int src, int dest) {
    Node* temp = graph->adjLists[src];
    Node* prev = NULL;
    while (temp != NULL && temp->vertex != dest) {
        prev = temp;
        temp = temp->next;
    }
    if (temp != NULL) {
        if (prev == NULL) {
            graph->adjLists[src] = temp->next;
        } else {
            prev->next = temp->next;
        }
        free(temp);
    }
    // 無向グラフの場合、反対方向の辺も削除
    temp = graph->adjLists[dest];
    prev = NULL;
    while (temp != NULL && temp->vertex != src) {
        prev = temp;
        temp = temp->next;
    }
    if (temp != NULL) {
        if (prev == NULL) {
            graph->adjLists[dest] = temp->next;
        } else {
            prev->next = temp->next;
        }
        free(temp);
    }
}

グラフの探索

グラフの探索には、深さ優先探索(DFS)や幅優先探索(BFS)などのアルゴリズムが使われます。以下に、DFSの簡単な実装例を示します。

void DFS(Graph* graph, int vertex, int* visited) {
    Node* adjList = graph->adjLists[vertex];
    Node* temp = adjList;

    visited[vertex] = 1;
    printf("Visited %d\n", vertex);

    while (temp != NULL) {
        int connectedVertex = temp->vertex;
        if (visited[connectedVertex] == 0) {
            DFS(graph, connectedVertex, visited);
        }
        temp = temp->next;
    }
}

int main() {
    int vertices = 5;
    Graph* graph = createGraph(vertices);

    addEdge(graph, 0, 1);
    addEdge(graph, 0, 4);
    addEdge(graph, 1, 2);
    addEdge(graph, 1, 3);
    addEdge(graph, 1, 4);
    addEdge(graph, 2, 3);
    addEdge(graph, 3, 4);

    int* visited = (int*)malloc(vertices * sizeof(int));
    for (int i = 0; i < vertices; i++) {
        visited[i] = 0;
    }

    DFS(graph, 0, visited);

    free(visited);
    return 0;
}

このコードは、頂点0からDFSを実行し、訪問した頂点を出力します。

無向グラフの応用例

無向グラフは、実世界のさまざまな問題をモデル化するために広く利用されています。ここでは、無向グラフの具体的な応用例をいくつか紹介します。

ソーシャルネットワーク分析

ソーシャルネットワークでは、ユーザーを頂点、ユーザー同士の関係(友達、フォローなど)を辺として表現することができます。このグラフを用いることで、クラスタリング、コミュニティ検出、インフルエンサーの特定などの解析が行えます。

道路ネットワークのモデリング

都市の道路ネットワークは、交差点を頂点、道路を辺として表現できます。これにより、最短経路探索、交通流シミュレーション、交通渋滞の予測などが可能となります。例えば、ダイクストラ法やA*アルゴリズムを用いて最短経路を計算することができます。

コンピュータネットワーク

コンピュータネットワークでは、ルーターやスイッチを頂点、物理的な接続やリンクを辺としてグラフを構成します。このモデルにより、ネットワークの信頼性解析、ルーティング最適化、ネットワーク障害の検出と対処などが可能です。

バイオインフォマティクス

バイオインフォマティクスでは、遺伝子やタンパク質の相互作用を無向グラフで表現します。頂点は遺伝子やタンパク質を表し、辺はそれらの間の相互作用を示します。このモデルにより、遺伝子ネットワークの解析や疾病関連遺伝子の特定などが行えます。

スケジューリング問題

スケジューリング問題では、タスクを頂点、タスク間の依存関係を辺としてグラフを構成します。このモデルを用いることで、タスクの最適な順序やリソースの効率的な割り当てを決定することができます。クリティカルパス法(CPM)やプログラム評価レビュー技法(PERT)などの手法が利用されます。

無向グラフは、このように幅広い分野で応用されており、問題の構造を視覚的に理解しやすくするための強力なツールとなっています。

演習問題

ここでは、C言語で無向グラフを表現するための理解を深めるための演習問題をいくつか紹介します。各問題に対して、実装を試みてください。

演習問題1: グラフの作成と表示

以下のグラフを隣接行列を用いて実装し、表示してください。

  • 頂点: 0, 1, 2, 3, 4
  • 辺: (0-1), (0-2), (1-2), (1-3), (2-4)
// 提示されたグラフを隣接行列で表現するプログラムを作成してください。

演習問題2: グラフの辺の追加と削除

隣接リストを用いて以下の操作を行うプログラムを実装してください。

  1. グラフを初期化し、以下の辺を追加する:
  • 頂点: 0, 1, 2, 3, 4
  • 辺: (0-1), (0-4), (1-2), (1-3), (1-4), (2-3), (3-4)
  1. 辺 (1-4) を削除し、グラフを表示する。
// 提示された操作を隣接リストで表現するプログラムを作成してください。

演習問題3: 深さ優先探索(DFS)の実装

隣接リストを用いてグラフを作成し、深さ優先探索(DFS)アルゴリズムを実装してください。以下のグラフで頂点0から探索を開始すること:

  • 頂点: 0, 1, 2, 3, 4
  • 辺: (0-1), (0-4), (1-2), (1-3), (1-4), (2-3), (3-4)
// 提示されたグラフで深さ優先探索(DFS)を実装してください。

演習問題4: 幅優先探索(BFS)の実装

隣接リストを用いてグラフを作成し、幅優先探索(BFS)アルゴリズムを実装してください。以下のグラフで頂点0から探索を開始すること:

  • 頂点: 0, 1, 2, 3, 4
  • 辺: (0-1), (0-4), (1-2), (1-3), (1-4), (2-3), (3-4)
// 提示されたグラフで幅優先探索(BFS)を実装してください。

これらの演習問題を通じて、C言語での無向グラフの表現方法と操作についての理解を深めてください。

まとめ

本記事では、C言語を用いた無向グラフの表現方法について解説しました。無向グラフの基本概念から始まり、隣接行列と隣接リストという2つの主要な表現方法について説明し、それぞれのメリットとデメリットを比較しました。また、具体的な実装例を示し、基本的なグラフ操作(追加、削除、探索)を取り上げました。さらに、無向グラフの応用例を紹介し、最後に理解を深めるための演習問題を提供しました。これらの知識を活用して、さまざまなアルゴリズムやデータ構造の問題に取り組んでください。

コメント

コメントする

目次