C言語で学ぶ!グラフアルゴリズムの実装方法と応用例

グラフアルゴリズムはコンピュータサイエンスの重要な分野であり、ネットワーク解析や経路探索など多くの応用があります。本記事では、C言語を用いて基本的なグラフアルゴリズムの実装方法を解説します。具体的なコード例や応用例を通じて、実践的なスキルを身につけましょう。

目次

グラフアルゴリズムの基本概念

グラフアルゴリズムを理解するためには、まずグラフ自体の基本的な概念を把握することが重要です。グラフは頂点(ノード)とそれを結ぶ辺(エッジ)で構成されるデータ構造で、さまざまな問題をモデリングできます。以下で主要な用語と概念を紹介します。

グラフの種類

グラフは一般的に以下のように分類されます:

  • 無向グラフ:辺に方向がないグラフ。
  • 有向グラフ:辺に方向があるグラフ。
  • 重み付きグラフ:辺に重み(コスト)が付与されたグラフ。
  • 非重み付きグラフ:辺に重みがないグラフ。

グラフの表現方法

グラフは主に2つの方法で表現されます:

  • 隣接行列:頂点間の接続関係を行列で表す方法。
  • 隣接リスト:各頂点に接続された頂点のリストを持つ方法。

基本用語

  • 頂点(ノード):グラフの各ポイント。
  • 辺(エッジ):頂点間の接続。
  • 経路:ある頂点から別の頂点への一連の辺。
  • サイクル:同じ頂点から開始して終了する経路。
  • 連結グラフ:任意の2頂点間に経路が存在するグラフ。

次に、C言語でグラフをどのように表現するかを学びます。

C言語でのグラフの表現方法

グラフをC言語で表現するには、一般的に隣接行列と隣接リストの2つの方法があります。それぞれの方法には利点と欠点があり、特定の問題や用途に応じて適切な方法を選択する必要があります。

隣接行列

隣接行列は、頂点間の接続関係を行列(2次元配列)で表現します。行列の各要素は、対応する頂点間に辺が存在するかどうかを示します。

#include <stdio.h>
#define V 5

void printGraph(int graph[V][V]) {
    for (int i = 0; i < V; i++) {
        for (int j = 0; j < V; j++) {
            printf("%d ", graph[i][j]);
        }
        printf("\n");
    }
}

int main() {
    int graph[V][V] = {
        {0, 1, 0, 0, 1},
        {1, 0, 1, 0, 0},
        {0, 1, 0, 1, 0},
        {0, 0, 1, 0, 1},
        {1, 0, 0, 1, 0}
    };
    printGraph(graph);
    return 0;
}

この例では、5つの頂点を持つ無向グラフを隣接行列で表現し、行列を出力する関数を実装しています。

利点

  • 簡単に実装できる。
  • 辺の存在確認が高速(O(1))。

欠点

  • メモリ使用量が多い(頂点数が多い場合)。
  • 隣接する頂点の列挙が遅い(O(V))。

隣接リスト

隣接リストは、各頂点に接続された頂点のリストを持つ方法です。メモリ効率が良く、隣接する頂点の列挙が高速です。

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

struct Node {
    int vertex;
    struct Node* next;
};

struct Graph {
    int numVertices;
    struct Node** adjLists;
};

struct Node* createNode(int v) {
    struct Node* newNode = malloc(sizeof(struct Node));
    newNode->vertex = v;
    newNode->next = NULL;
    return newNode;
}

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

void addEdge(struct Graph* graph, int src, int dest) {
    struct Node* newNode = createNode(dest);
    newNode->next = graph->adjLists[src];
    graph->adjLists[src] = newNode;
    newNode = createNode(src);
    newNode->next = graph->adjLists[dest];
    graph->adjLists[dest] = newNode;
}

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

int main() {
    struct Graph* graph = createGraph(5);
    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);
    return 0;
}

この例では、5つの頂点を持つ無向グラフを隣接リストで表現し、グラフを出力する関数を実装しています。

利点

  • メモリ効率が良い(辺の数が少ない場合)。
  • 隣接する頂点の列挙が高速(O(E))。

欠点

  • 実装がやや複雑。
  • 辺の存在確認が遅い(O(V))。

次に、幅優先探索(BFS)の実装方法を学びます。

幅優先探索(BFS)の実装方法

幅優先探索(BFS)は、グラフの探索アルゴリズムの一つで、開始頂点から近い順に探索を進めていきます。BFSはキューを使って実装され、グラフの最短経路問題などに適しています。ここでは、C言語を使ってBFSを実装する方法を紹介します。

BFSの基本概念

BFSは、以下の手順で実行されます:

  1. 開始頂点をキューに追加し、訪問済みとしてマークします。
  2. キューが空になるまで、次の操作を繰り返します:
  • キューの先頭の頂点を取り出し、その頂点に隣接するすべての未訪問の頂点をキューに追加し、訪問済みとしてマークします。

C言語でのBFS実装

以下のコード例は、隣接リストを使ったグラフのBFS実装です。

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

#define SIZE 40

struct Node {
    int vertex;
    struct Node* next;
};

struct Graph {
    int numVertices;
    struct Node** adjLists;
    int* visited;
};

struct Queue {
    int items[SIZE];
    int front;
    int rear;
};

struct Node* createNode(int v) {
    struct Node* newNode = malloc(sizeof(struct Node));
    newNode->vertex = v;
    newNode->next = NULL;
    return newNode;
}

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

void addEdge(struct Graph* graph, int src, int dest) {
    struct Node* newNode = createNode(dest);
    newNode->next = graph->adjLists[src];
    graph->adjLists[src] = newNode;
    newNode = createNode(src);
    newNode->next = graph->adjLists[dest];
    graph->adjLists[dest] = newNode;
}

struct Queue* createQueue() {
    struct Queue* q = malloc(sizeof(struct Queue));
    q->front = -1;
    q->rear = -1;
    return q;
}

int isEmpty(struct Queue* q) {
    if (q->rear == -1)
        return 1;
    else
        return 0;
}

void enqueue(struct Queue* q, int value) {
    if (q->rear == SIZE - 1)
        printf("\nQueue is Full!!");
    else {
        if (q->front == -1)
            q->front = 0;
        q->rear++;
        q->items[q->rear] = value;
    }
}

int dequeue(struct Queue* q) {
    int item;
    if (isEmpty(q)) {
        printf("Queue is empty");
        item = -1;
    } else {
        item = q->items[q->front];
        q->front++;
        if (q->front > q->rear) {
            q->front = q->rear = -1;
        }
    }
    return item;
}

void printQueue(struct Queue* q) {
    int i = q->front;
    if (isEmpty(q)) {
        printf("Queue is empty");
    } else {
        printf("\nQueue contains \n");
        for (i = q->front; i < q->rear + 1; i++) {
            printf("%d ", q->items[i]);
        }
    }
}

void bfs(struct Graph* graph, int startVertex) {
    struct Queue* q = createQueue();

    graph->visited[startVertex] = 1;
    enqueue(q, startVertex);

    while (!isEmpty(q)) {
        printQueue(q);
        int currentVertex = dequeue(q);
        printf("Visited %d\n", currentVertex);

        struct Node* temp = graph->adjLists[currentVertex];

        while (temp) {
            int adjVertex = temp->vertex;

            if (graph->visited[adjVertex] == 0) {
                graph->visited[adjVertex] = 1;
                enqueue(q, adjVertex);
            }
            temp = temp->next;
        }
    }
}

int main() {
    struct Graph* graph = createGraph(6);
    addEdge(graph, 0, 1);
    addEdge(graph, 0, 2);
    addEdge(graph, 1, 2);
    addEdge(graph, 1, 4);
    addEdge(graph, 1, 3);
    addEdge(graph, 2, 4);
    addEdge(graph, 3, 4);

    bfs(graph, 0);

    return 0;
}

このコード例では、隣接リストを使ってグラフを表現し、キューを用いてBFSを実装しています。

コードのポイント

  • createNode:新しいノードを作成します。
  • createGraph:指定された頂点数でグラフを作成します。
  • addEdge:無向グラフに辺を追加します。
  • createQueue:キューを初期化します。
  • enqueue:キューに要素を追加します。
  • dequeue:キューから要素を取り出します。
  • bfs:幅優先探索を実行します。

次に、深さ優先探索(DFS)の実装方法を学びます。

深さ優先探索(DFS)の実装方法

深さ優先探索(DFS)は、グラフ探索アルゴリズムの一つで、可能な限り深く探索を進め、行き止まりに達したらバックトラックする手法です。DFSは再帰を使って実装されることが多く、迷路の探索や連結成分の検出に適しています。ここでは、C言語を使ってDFSを実装する方法を紹介します。

DFSの基本概念

DFSは以下の手順で実行されます:

  1. 開始頂点を訪問し、訪問済みとしてマークします。
  2. 現在の頂点に隣接するすべての未訪問の頂点について再帰的にDFSを実行します。

C言語でのDFS実装

以下のコード例は、隣接リストを使ったグラフのDFS実装です。

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

struct Node {
    int vertex;
    struct Node* next;
};

struct Graph {
    int numVertices;
    struct Node** adjLists;
    int* visited;
};

struct Node* createNode(int v) {
    struct Node* newNode = malloc(sizeof(struct Node));
    newNode->vertex = v;
    newNode->next = NULL;
    return newNode;
}

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

void addEdge(struct Graph* graph, int src, int dest) {
    struct Node* newNode = createNode(dest);
    newNode->next = graph->adjLists[src];
    graph->adjLists[src] = newNode;
    newNode = createNode(src);
    newNode->next = graph->adjLists[dest];
    graph->adjLists[dest] = newNode;
}

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

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

    while (temp != NULL) {
        int connectedVertex = temp->vertex;

        if (graph->visited[connectedVertex] == 0) {
            DFS(graph, connectedVertex);
        }
        temp = temp->next;
    }
}

int main() {
    struct Graph* graph = createGraph(6);
    addEdge(graph, 0, 1);
    addEdge(graph, 0, 2);
    addEdge(graph, 1, 2);
    addEdge(graph, 1, 3);
    addEdge(graph, 2, 4);
    addEdge(graph, 3, 4);
    addEdge(graph, 3, 5);

    DFS(graph, 0);

    return 0;
}

このコード例では、隣接リストを使ってグラフを表現し、再帰を用いてDFSを実装しています。

コードのポイント

  • createNode:新しいノードを作成します。
  • createGraph:指定された頂点数でグラフを作成します。
  • addEdge:無向グラフに辺を追加します。
  • DFS:深さ優先探索を実行します。

このDFS実装では、指定された頂点から開始して、すべての隣接頂点を再帰的に訪問します。訪問した頂点はvisited配列でマークされ、再訪問を防ぎます。

次に、ダイクストラ法の実装方法を学びます。

ダイクストラ法の実装方法

ダイクストラ法は、単一始点最短経路問題を解くためのアルゴリズムで、グラフ内の指定された始点から他のすべての頂点への最短経路を求めます。負の重みを持たないグラフに対して適用されます。ここでは、C言語を使ってダイクストラ法を実装する方法を紹介します。

ダイクストラ法の基本概念

ダイクストラ法は以下の手順で実行されます:

  1. 始点から他のすべての頂点への距離を無限大に初期化し、始点の距離を0に設定します。
  2. 未処理の頂点の中から、最小距離の頂点を選択します。
  3. 選択した頂点から、隣接するすべての頂点の距離を更新します。
  4. すべての頂点が処理されるまで、手順2と3を繰り返します。

C言語でのダイクストラ法実装

以下のコード例は、隣接行列を使ったグラフのダイクストラ法実装です。

#include <stdio.h>
#include <limits.h>

#define V 6

int minDistance(int dist[], int sptSet[]) {
    int min = INT_MAX, min_index;

    for (int v = 0; v < V; v++)
        if (sptSet[v] == 0 && dist[v] <= min)
            min = dist[v], min_index = v;

    return min_index;
}

void printSolution(int dist[], int n) {
    printf("Vertex \t Distance from Source\n");
    for (int i = 0; i < V; i++)
        printf("%d \t\t %d\n", i, dist[i]);
}

void dijkstra(int graph[V][V], int src) {
    int dist[V];
    int sptSet[V];

    for (int i = 0; i < V; i++)
        dist[i] = INT_MAX, sptSet[i] = 0;

    dist[src] = 0;

    for (int count = 0; count < V - 1; count++) {
        int u = minDistance(dist, sptSet);

        sptSet[u] = 1;

        for (int v = 0; v < V; v++)
            if (!sptSet[v] && graph[u][v] && dist[u] != INT_MAX
                && dist[u] + graph[u][v] < dist[v])
                dist[v] = dist[u] + graph[u][v];
    }

    printSolution(dist, V);
}

int main() {
    int graph[V][V] = {{0, 1, 4, 0, 0, 0},
                       {1, 0, 4, 2, 7, 0},
                       {4, 4, 0, 3, 5, 0},
                       {0, 2, 3, 0, 4, 6},
                       {0, 7, 5, 4, 0, 7},
                       {0, 0, 0, 6, 7, 0}};

    dijkstra(graph, 0);

    return 0;
}

このコード例では、隣接行列を使ってグラフを表現し、ダイクストラ法を実装しています。

コードのポイント

  • minDistance:未処理の頂点の中から最小距離の頂点を選択します。
  • printSolution:各頂点の最短距離を出力します。
  • dijkstra:ダイクストラ法の本体で、各頂点の最短距離を計算します。

このダイクストラ法の実装では、始点から各頂点への最短距離を計算し、結果を出力します。隣接行列を使っているため、グラフの頂点数が増えるとメモリ使用量が多くなりますが、単純な実装が可能です。

次に、フロイド-ワーシャル法の実装方法を学びます。

フロイド-ワーシャル法の実装方法

フロイド-ワーシャル法は、全点対間最短経路問題を解くためのアルゴリズムで、グラフ内のすべての頂点間の最短経路を求めます。負の重みを持つグラフにも適用可能で、動的計画法を用いています。ここでは、C言語を使ってフロイド-ワーシャル法を実装する方法を紹介します。

フロイド-ワーシャル法の基本概念

フロイド-ワーシャル法は以下の手順で実行されます:

  1. 各頂点対について、直接の辺が存在する場合はその重みで、存在しない場合は無限大で初期化します。
  2. 各頂点を中継点として、すべての頂点対の最短経路を更新します。
  3. すべての頂点について手順2を繰り返します。

C言語でのフロイド-ワーシャル法実装

以下のコード例は、隣接行列を使ったグラフのフロイド-ワーシャル法実装です。

#include <stdio.h>

#define V 4
#define INF 99999

void printSolution(int dist[][V]) {
    printf("Following matrix shows the shortest distances between every pair of vertices \n");
    for (int i = 0; i < V; i++) {
        for (int j = 0; j < V; j++) {
            if (dist[i][j] == INF)
                printf("%7s", "INF");
            else
                printf("%7d", dist[i][j]);
        }
        printf("\n");
    }
}

void floydWarshall(int graph[][V]) {
    int dist[V][V], i, j, k;

    for (i = 0; i < V; i++)
        for (j = 0; j < V; j++)
            dist[i][j] = graph[i][j];

    for (k = 0; k < V; k++) {
        for (i = 0; i < V; i++) {
            for (j = 0; j < V; j++) {
                if (dist[i][k] + dist[k][j] < dist[i][j])
                    dist[i][j] = dist[i][k] + dist[k][j];
            }
        }
    }

    printSolution(dist);
}

int main() {
    int graph[V][V] = {{0, 5, INF, 10},
                       {INF, 0, 3, INF},
                       {INF, INF, 0, 1},
                       {INF, INF, INF, 0}};

    floydWarshall(graph);
    return 0;
}

このコード例では、隣接行列を使ってグラフを表現し、フロイド-ワーシャル法を実装しています。

コードのポイント

  • printSolution:各頂点対の最短距離を出力します。
  • floydWarshall:フロイド-ワーシャル法の本体で、すべての頂点間の最短距離を計算します。

このフロイド-ワーシャル法の実装では、グラフ内のすべての頂点間の最短距離を計算し、結果を出力します。隣接行列を使っているため、頂点数が増えるとメモリ使用量が多くなりますが、全点対間の最短経路を効率的に求めることができます。

次に、グラフアルゴリズムの応用例として、ソーシャルネットワーク解析について学びます。

応用例:ソーシャルネットワーク解析

グラフアルゴリズムは、ソーシャルネットワーク解析において非常に重要な役割を果たします。ここでは、C言語で実装したグラフアルゴリズムを使って、ソーシャルネットワークの解析方法を具体的に紹介します。

ソーシャルネットワークのモデル化

ソーシャルネットワークは、ユーザーを頂点として、ユーザー間の関係を辺としてモデル化されます。各ユーザー(頂点)間の関係(辺)には重みが付けられることがあります。この重みは、関係の強さや頻度を示します。

重要な指標

ソーシャルネットワーク解析では、以下のような指標が重要です:

  • 次数中心性:各頂点の次数(隣接する頂点の数)を基にした中心性。
  • 近接中心性:他のすべての頂点への最短距離の逆数の和として定義される中心性。
  • 媒介中心性:ある頂点が他の頂点対の間の最短経路にどれだけ多く含まれるかを基にした中心性。

例:次数中心性の計算

次に、C言語を使って簡単なソーシャルネットワークの次数中心性を計算する方法を紹介します。

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

struct Node {
    int vertex;
    struct Node* next;
};

struct Graph {
    int numVertices;
    struct Node** adjLists;
};

struct Node* createNode(int v) {
    struct Node* newNode = malloc(sizeof(struct Node));
    newNode->vertex = v;
    newNode->next = NULL;
    return newNode;
}

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

void addEdge(struct Graph* graph, int src, int dest) {
    struct Node* newNode = createNode(dest);
    newNode->next = graph->adjLists[src];
    graph->adjLists[src] = newNode;
    newNode = createNode(src);
    newNode->next = graph->adjLists[dest];
    graph->adjLists[dest] = newNode;
}

int* calculateDegreeCentrality(struct Graph* graph) {
    int* degrees = malloc(graph->numVertices * sizeof(int));
    for (int i = 0; i < graph->numVertices; i++) {
        struct Node* temp = graph->adjLists[i];
        degrees[i] = 0;
        while (temp) {
            degrees[i]++;
            temp = temp->next;
        }
    }
    return degrees;
}

void printDegreeCentrality(int* degrees, int vertices) {
    for (int i = 0; i < vertices; i++)
        printf("Vertex %d has degree centrality %d\n", i, degrees[i]);
}

int main() {
    struct Graph* graph = createGraph(5);
    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* degrees = calculateDegreeCentrality(graph);
    printDegreeCentrality(degrees, 5);

    return 0;
}

このコード例では、ソーシャルネットワークをグラフとしてモデル化し、各頂点の次数中心性を計算しています。

コードのポイント

  • createNode:新しいノードを作成します。
  • createGraph:指定された頂点数でグラフを作成します。
  • addEdge:無向グラフに辺を追加します。
  • calculateDegreeCentrality:各頂点の次数中心性を計算します。
  • printDegreeCentrality:各頂点の次数中心性を出力します。

ソーシャルネットワーク解析は、ユーザーの行動パターンや関係性を理解するのに役立ちます。次に、学んだ内容を実践するための練習問題を提供します。

練習問題

ここでは、これまで学んだグラフアルゴリズムの実装方法を実践するための練習問題を提供します。これらの問題を通じて、理解を深め、実践的なスキルを向上させましょう。

問題1:隣接行列を用いたグラフの実装

以下のグラフを隣接行列で実装し、隣接行列を出力するプログラムを作成してください。

0 -- 1 -- 2
|  / |  / |
3 -- 4 -- 5

問題2:隣接リストを用いたグラフの実装

上記のグラフを隣接リストで実装し、隣接リストを出力するプログラムを作成してください。

問題3:幅優先探索(BFS)の実装

次のグラフを幅優先探索で探索するプログラムを作成し、探索順序を出力してください。

0 -- 1 -- 2
|  / |  / |
3 -- 4 -- 5

問題4:深さ優先探索(DFS)の実装

次のグラフを深さ優先探索で探索するプログラムを作成し、探索順序を出力してください。

0 -- 1 -- 2
|  / |  / |
3 -- 4 -- 5

問題5:ダイクストラ法の実装

以下の重み付きグラフに対して、頂点0から他のすべての頂点への最短経路を求めるプログラムを作成してください。

0 --(4)--> 1 --(8)--> 2
|         / |         / |
(1)   (2)  (6)    (3) (9)
| /       |/        | /
3 --(1)--> 4 --(2)--> 5

問題6:フロイド-ワーシャル法の実装

以下の重み付きグラフに対して、すべての頂点間の最短経路を求めるプログラムを作成してください。

0 --(4)--> 1 --(8)--> 2
|         / |         / |
(1)   (2)  (6)    (3) (9)
| /       |/        | /
3 --(1)--> 4 --(2)--> 5

問題7:ソーシャルネットワークの次数中心性の計算

次のソーシャルネットワークグラフに対して、各ユーザーの次数中心性を計算し、出力するプログラムを作成してください。

0 -- 1 -- 2
|  / |  / |
3 -- 4 -- 5

これらの練習問題に取り組むことで、グラフアルゴリズムの実装に対する理解が深まるでしょう。頑張ってください!

まとめ

本記事では、C言語を用いたグラフアルゴリズムの実装方法について解説しました。基本的なグラフの概念から始め、隣接行列と隣接リストによるグラフの表現方法、幅優先探索(BFS)、深さ優先探索(DFS)、ダイクストラ法、フロイド-ワーシャル法の具体的な実装方法を紹介しました。また、ソーシャルネットワーク解析の応用例や練習問題を通じて、実践的なスキルを向上させるための方法を学びました。これらの知識を活用し、さらに高度なアルゴリズムや応用例に挑戦してみてください。グラフアルゴリズムの理解と実装は、プログラミングスキルの向上に大いに役立つでしょう。

コメント

コメントする

目次