ラベル 隣接行列 の投稿を表示しています。 すべての投稿を表示
ラベル 隣接行列 の投稿を表示しています。 すべての投稿を表示

2020年1月30日木曜日

俺でも理解できる超丁寧なダイクストラ法アルゴリズム解説

俺でも理解できるようにダイクストラ法のアルゴリズムを解説します。

目次

理解に必要な前提知識

グラフの知識

ダイクストラ法について

ダイクストラ法は単一最短経路を求める場合に使われるアルゴリズムです。
単一最短経路とは、グラフ内で一つ始点を定めてそこから派生する頂点への最短経路を求めること>を言います。

ダイクストラ法の手順

前提として、グラフG = (V(頂点),E(辺))の頂点全体の集合をV、始点をs、
最短経路木に含まれる頂点の集合をSとします。

各計算のステップで、最短経路木の辺と頂点を選びSへ追加していきます。

また、各頂点iについて、S内の頂点のみを経由したsからiへの最短経路のコストをd[i]とします。

ここから、手順に入ります。

1.初期化処理

最短経路木に含まれる頂点の集合をSを以下のように初期化します。

始点sに対して最短経路のコストを示すd[s] = 0を代入し、
始点s以外の頂点の全体集合Vに属する全ての頂点iに対してd[i] = 最大値
を代入します。

2.以下の処理をS = Vになるまで(始点sから各々の頂点の最短経路が求まるまで)続けます。

2.1
V - Sの中から、d[u]が最小である頂点uを選択(最短経路と決定している頂点を除く)します。

2.2
頂点uをSに追加すると同時に、uに隣接しかつV - Sに属する全ての頂点vに対する
値を以下のように更新します。

if d[u] + w(u,v)(実際にかかる移動コスト) < d[v]
   d[v] = d[u] + w(u,v)

2.の各計算ステップ直後に,
u[v]には、sからSの頂点のみを経由したvまでの最短コストが記録されていることになります。
つまり1,2の処理が全て終了すると、u[v]に,sからvまでの最短コストが記録されることになります。

ダイクストラ法の図解

プロセスがわかりやすいように、ダイクストラ法を図解してみます。

下図のようなグラフがあり、始点をAをとします。 Aから各々の頂点への最短経路をダイクストラで求めてみます。

まず、始点Aを0で初期化し、各々への頂点に探索が完了していないことを示すために最大値を代入(変数d[]が対象となる)します。

出典:MIT open course ware

ここから、2.のプロセスに入ります。
最短経路木を示す頂点の集合をSとして、Sを以下のように表します。

S = {} {A B C D E }(Sから各頂点の最短経路の重さを示します。)

step1

まず、始点(s)である頂点AをSに追加します。

S = {A} {0 ∞ ∞ ∞ ∞ }

続いて、頂点Aに辺があり、最短経路未決定の頂点の最短経路を更新します。

最短経路か判定する式は、

if d[u] + w(u,v) < d[v]
   d[v] = d[u] + w(u,v)

なので、Aに隣接する頂点B,Cは頂点uが始点で重みが0なので、 辺への重みのみが加算され、以下のようになります。

S = {A} {0 10 3 ∞ ∞ }

step2

また、2.1のプロセスに戻ります。
2.1:V - Sの中から、d[u]が最小である頂点uを選択します。

つまり、最短距離未決定の最小頂点C(重み3)を選ぶことになりますが、
なぜ、最小コストの頂点を選んだ時点で、その地点が最短経路になるのかを、
図で考えてみます。

なぜ、最小コストの頂点が最短経路になるのか

下図のように始点から辺が3つ伸びているグラフがあるとします。
グラフより、各々の3つの頂点からその3つの頂点への辺があるとしても、
sから各々の頂点への3つの辺の中に必ず最短となる経路があることがわかります。

これは、辺が2つでも4つでも同じことです。

step2に戻ります。
Sに最短経路である頂点Cを追加します。

S = {A C} {0 10 3 ∞ ∞ }

続いて、Cにつながる頂点への最短経路更新プロセスに入り、
図を参考に以下のように更新します。

S = {A C} {0 7 3 11 5 }

step3

プロセス1に戻って、探索を開始する頂点を決定します。
最短点であるE(5)が選ばれるので、EをSに追加します。

S = {A C E} {0 7 3 11 5 }

プロセス2に移り、頂点Eにつながる頂点の最短点の更新を行います。

更新する頂点はないので、結果は前と変わりません。

S = {A C E} {0 7 3 11 5 }

step4

プロセス1に戻って、探索を開始する頂点を決定します。
最短点であるB(7)が選ばれるので、BをSに追加します。

S = {A C E B} {0 7 3 11 5 }

プロセス2に移り、頂点Bにつながる頂点の最短点の更新を行います。 頂点Dへの最短経路が9に変わります。

S = {A C E B} {0 7 3 9 5 }

step5

プロセス1に戻って、探索を開始する頂点を決定します。
ここで、全ての最短経路がもとまったので、処理を終了します。

始点sから頂点uへの最短経路はd[u]に入っていることになります。

ダイクストラ法のサンプルプログラム

隣接行列を使った、ダイクストラの実装方法について解説します。

隣接行列とは

隣接行列には、以下のように一次元に頂点、2次元に辺への重みを入れます。
頂点から頂点への辺が繋がっていない場合は最大値を定義しその最大値を代入します。

例として、頂点が3つ(A,C,B)とし、 AからB(重み3)への辺
BからC(重み4)への辺
CからA(重み5)への辺
をもつグラフを隣接行列にすると以下のようになります。

matrix[][] = 
{∞,3,∞},
{∞,∞,4},
{5,∞,∞};

隣接行列への説明が終わったところで、実装に入ります。
まず、必要変数を定義します。

// 最大頂点数
#define MAX 100
// 未訪問または、辺がないことを示す最大数値
#define INFTY 1<<22
// 未訪問の頂点を示す
#define WHITE 0
// 訪問済の頂点を示す
#define GRAY 1
// 最短経路決定済の頂点を示す
#define BLACK 2
// 隣接行列頂点から頂点への辺の重みが入っている
// 例:gMatrix[0][1] 頂点0から1への重み INFTYの場合辺がないことを示す
int gMatrix[MAX][MAX];
// 最小の重みを記録する
int minV;
// u[v]に始点sからvまでの最短コストを保存する
int u[MAX];
// 始点から書く頂点への訪問状態を記録
int color[MAX];

では、ダイクストラを求める関数の解説をします。
まずは、初期化プロセスです。

void dijkstra(int n){
    // 1.初期化
    for(int i = 0;i < n;++i){
        u[i] = INFTY;
        color[i] = WHITE;
    }
    // 0を始点とする(頂点A,B,C...としCから始めたい場合は、u[2]=0とする)
    u[0] = 0;
    color[0] = GRAY;

初期はプロセスは、そのままですね。

続いて、最短経路を決定するロジックの部分です。

    while(1){
        minV = INFTY;
        // uは探索対象となる頂点を示す
        int u = -1;
        // 2.1 d[u]が最小である頂点を決定する
        for(int i = 0;i < n;++i){
            // 最短経路決定ずみの頂点は含めない
            if(minV > weight[i] && color[i] != BLACK){
                u = i;
                minV = weight[i];
            }
        }
        // u == -1の場合始点から各々への頂点への最短経路が求まっていることになる
        if(u == -1)break;
        // d[u]が最小となる頂点が最短経路になることは、解説済
        color[u] = BLACK;
        // 頂点からつながる各々の頂点への最短経路を更新する
        for(int v = 0;v < n;++v){
            // 辺が存在し、最短経路決定済頂点以外の経路が対象になる。
            if(color[v] != BLACK && gMatrix[u][v] != INFTY){
                // 2.2最短コストを更新する
                // この処理の終了後weight[v]sからS内の頂点のみを経由したvまでの最短コストが記録される
                if(weight[v] > weight[u] + gMatrix[u][v]){
                    weight[v] = weight[u] + gMatrix[u][v];
                    color[v] = GRAY;
                }
            }
        }
    }

全体のコードは以下になります。

#define MAX 100
#define INFTY 1<<22
#define WHITE 0
#define GRAY 1
#define BLACK 2
int gMatrix[MAX][MAX];

void dijkstra(int n){
    int minV;
    int u[MAX];
    int color[MAX];
    for(int i = 0;i < n;++i){
        u[i] = INFTY;
        color[i] = WHITE;
    }
    u[0] = 0;
    color[0] = GRAY;
    
    while(1){
        minV = INFTY;
        int u = -
        for(int i = 0;i < n;++i){
            if(minV > weight[i] && color[i] != BLACK){
                u = i;
                minV = weight[i];
            }
        }
        if(u == -1)break;
        color[u] = BLACK;
        
        for(int v = 0;v < n;++v){
            if(color[v] != BLACK && gMatrix[u][v] != INFTY){
                if(weight[v] > weight[u] + gMatrix[u][v]){
                    weight[v] = weight[u] + gMatrix[u][v];
                    color[v] = GRAY;
                }
            }
        }
    }
    // 始点からつながる各々の頂点への最短経路を出力
    for(int i = 0;i < n;++i){
        cout << i << " " << ( weight[i] == INFTY ? -1 : weight[i] ) << endl;
    }
}

練習例題一覧

ダイクストラ法で解けるonline judgeの例題をあげます。

例題1

ALDS1_12_B
頂点の数が少ないので、隣接行列を使って解くことができます。 問題の解法ををこちらにまとめました。

例題2

ALDS1_12_C
問題は同じですが、頂点数が多いため解法を工夫する必要があります。
優先度付きqueを使った解法をこちらでまとめています。

例題3

GRL_1_A
例題2と内容は同じですが、与えられる入力が異なります。

まとめ

最短経路の頂点に隣接する頂点のうち、重みが最短のものが最短経路となり、
このロジックを繰り返すことで、最短経路が求まるというのがポイントです。

わかりにくかったり、誤りがあった場合は、コメントください。

参考ソース

理解するのに、プロコンのためのアルゴリズムとデータ構造と
MITのopen course wareに単一最短始点経路とダイクストラ法の解説動画ををみました。

2019年8月23日金曜日

幅優先探索を使用してグラフの頂点から頂点への最短距離を求める方法をALDS_1_11_Cを使って学ぶ

キューを使った幅優先探索をAIZU ONLINE JUDGEの例題を利用して、学びたいと思います。
幅優先探索の概念についてはこちらにまとめています。

では、例題を見ていきます。

例題

与えられた有向グラフ G=(V,E) について、
頂点 1 から各頂点への最短距離 d(パス上の辺の数の最小値)を求めるプログラムを作成してください。
各頂点には 1 から n までの番号がふられているものとします。
頂点 1 からたどり着けない頂点については、距離として-1 を出力してください。

入力

最初の行に G の頂点数 n が与えられます。続く n 行で各頂点 u
の隣接リストが以下の形式で与えられます:

u k v1 v2 ... vk

uは頂点の番号、k は u の出次数、
v1v2...vk は u に隣接する頂点の番号を示します。

制約

1≤n≤100

出力

各頂点について id、d を1行に出力してください。
id は頂点の番号、d は頂点 1 からその頂点までの距離を示します。頂点番号順に出力してください。

解法

幅優先探索は、始点sから距離k + 1にある頂点を探索する前に、距離kの頂点をすべて発見しているので、
始点から各頂点までの最短距離を順番に求めることができます。

幅優先探索のアルゴリズム

1.始点sをキューに入れる

キューが空でない限り以下の操作を繰り返す

  • キューから頂点uを取り出して訪問する(訪問完了)
  • 頂点uに隣接し、未訪問の頂点vについて、始点から頂点までの距離を表す変数d[v]をd[u] + 1と更新し、
    頂点vをキューに入れる

グラフを例に幅優先探索が行われる流れを見る

下図のグラフを例として、頂点0から幅優先探索が行われる流れを追います。

1.始点である頂点0をキューに挿入します。
始点0からの距離を0とします。

0

2.キューから0を取り出して、訪問します。
頂点0に隣接して未訪問の1,3,5の頂点をキューに挿入します。 頂点1,3,5までの距離は頂点0までの距離+1となります。

1,3,5

3.キューの先頭から1を取り出して、訪問します。
頂点1に隣接する未訪問の頂点はないので、次に進みます。

3,5

4.キューの先頭から3を取り出して、訪問します。
頂点3に隣接する未訪問の頂点は、2のみなので、2をキューに挿入します。 頂点2までの距離は、頂点3までの距離+1になります。

5,2

5.キューの先頭から5を取り出して、訪問します。
頂点5に隣接する未訪問の頂点4,6をキューに入れます。
頂点4,6までの距離は頂点5までの距離+1になります。

2,4,6

6.キューの先頭から2を取り出して、訪問します。
頂点2に隣接する未訪問の頂点はないので、次に進みます。

4,6

7.キューの先頭から4を取り出して、訪問します。
頂点4に隣接する未訪問の頂点はないので、次に進みます。

6

8.キューの先頭から6を取り出して、訪問します。
頂点6に隣接する未訪問の頂点はないので、次に進みます。

キューが空になったので、幅優先探索終了です。
探索終了時に頂点0から各々の頂点への最短距離が求められます。

幅優先探索に必要な変数

キューを利用した深さ優先探索に必要な変数は以下のようになります。

変数名例 役割
color[N] 未訪問・訪問中・訪問済みの状態を表すための変数
M[n][n] グラフの隣接行列を表すための変数、頂点が隣接している場合は1(true)など
Queue q 使用するキュー
d[n] 始点からインデックスを頂点番号とする距離を保存しておく

隣接行列を利用した幅優先探索の疑似アルゴリズム

bfs()

 各頂点についてcolorを未訪問に設定する
 各頂点において、d[u]を到達できないことを示す値を入れる
 // 始点をsとします
 color[s] = 訪問中
 d[s] = 0
 q.enque(s)
 
 while Qが空でない
  u = Q.dequeue()
  for vが0から|V| - 1まで
   if M[u][v] && color[v] == 未訪問
    color[v] = 訪問中
    d[v] = d[u] + 1
    Q.enqueue(v)
  color[u] = 訪問済み
  
 

計算量考察

隣接行列を用いた幅優先探索は、各頂点について、すべての頂点に隣接しているかどうかを調べるので、
$O(|V|^2)$のアルゴリズムになります。

c++を用いた解答例

では最後に隣接行列で幅優先探索を行う解答例を載せます


#include<iostream>
#include<queue>

using namespace std;

#define N 100
#define INFTY 99999999

int n,
// 隣接行列
M[N][N];
// 距離 colorも同時に管理する
int d[N];

void bfs(int s){
    queue q;
    q.push(s);
    
    // 距離を初期化
    for(int i = 0;i < n;++i){
        d[i] = INFTY;
    }
    d[s] = 0;
    // 対象の頂点番号
    int u;
    
    while(!q.empty()){
        u = q.front();
        q.pop();
        
        for(int v = 0;v < n;++v){
            // 頂点が隣接しているかつ未訪問
            if(M[u][v] == 1 && d[v] == INFTY){
                // 対象の頂点の距離は前の頂点 + 1
                d[v] = d[u] + 1;
                q.push(v);
            }
        }
    }
    
}

int main(){
    int u,k,v;
    
    cin >> n;
    
    // 隣接行列を初期化
    for(int i = 0;i < n;++i){
        for(int j = 0;j < n;++j){
            M[i][j] = 0;
        }
    }
    
    for(int i = 0;i < n;++i){
        cin >> u >> k;
        // 頂点を移動する
        u--;
        // 頂点の数だけ回す
        for(int j = 0;j < k;++j){
            cin >> v;
            --v;    // ずらす
            M[u][v] = 1;
        }
    }
    
    bfs(0);

    for(int i = 0;i < n;++i){
        cout << i + 1 << " " << ((d[i] == INFTY) ? (-1) : d[i]) << endl;
    }

    return 0;
}

2019年8月22日木曜日

スタックによる深さ優先探索をALDS1_11_Bを使って学ぶ

スタックを使用して、深さ優先探索を行う方法をAIDU_ONLINE_JUDGEのALDS1_11_Bを利用して学びたいと思います。

例題

深さ優先探索(Depth First Search: DFS)は、可能な限り隣接する頂点を訪問する、という戦略に基づくグラフの探索アルゴリズムです。
未探索の接続辺が残されている頂点の中で最後に発見した頂点 v の接続辺を再帰的に探索します。

v の辺をすべて探索し終えると、vを発見したときに通ってきた辺を後戻りして探索を続行します。
探索は元の始点から到達可能なすべての頂点を発見するまで続き、 未発見の頂点が残っていれば、その中の番号が一番小さい1つを新たな始点として探索を続けます。

深さ優先探索では、各頂点に以下のタイムスタンプを押します:

  • タイムスタンプ d[v]: vを最初に発見した発見時刻を記録します。
  • タイムスタンプ f[v]: vの隣接リストを調べ終えた完了時刻を記録します。

以下の仕様に基づき、与えられた有向グラフ G=(V,E)
に対する深さ優先探索の動作を示すプログラムを作成してください:

Gは隣接リスト表現の形式で与えられます。
各頂点には 1 から nまでの番号がふられています。 各隣接リストの頂点は番号が小さい順に並べられています。 プログラムは各頂点の発見時刻と完了時刻を報告します。 深さ優先探索の過程において、訪問する頂点の候補が複数ある場合は頂点番号が小さいものから選択します。 最初に訪問する頂点の開始時刻を 1 とします。

入力

最初の行に G の頂点数 n が与えられます。続く n 行で各頂点 u の隣接リストが以下の形式で与えられます:

u k v1 v2 ... vk

u は頂点の番号、k は u の出次数、v1v2...vk  は u に隣接する頂点の番号を示します。

出力

各頂点について id、 d、 fを空白区切りで1行に出力してください。
id は頂点の番号、d はその頂点の発見時刻、f はその頂点の完了時刻です。頂点の番号順で出力してください。

制限

1≤n≤100

入力例

4
1 1 2
2 1 4
3 0
4 1 3

出力例

1 1 8
2 2 7
3 4 5
4 3 6

スタックを利用した深さ優先探索

スタックにまだ探索中の頂点を一時的に保存しておくことにより、
深さ優先探索を行います。

手順は以下のようになります。

1.一番最初に訪問する頂点をスタックに入れる

2.スタックに頂点が積まれている限り、以下の処理を繰り返します。

  • スタックの先頭に積まれている頂点uを訪問する
  • 現在訪問中の頂点uから次の頂点vへ探索を移行するときに、
    頂点vをスタックに積みます。
    現在訪問中の頂点uに未訪問の隣接する頂点なければuをスタックから解放します。

スタックの処理例

以下のグラフを例にスタックによる、深さ優先探索を行って見たいと思います。

1.最初に訪問する頂点を0としてスタックに積みます

2.スタックの先頭の0を訪問します。
0に隣接し、かつ未訪問で、数の小さい方の頂点1をスタックに積みます。

3.スタックの先頭1を訪問します。
1に隣接しており、未訪問の3をスタックに積みます。

4.スタックの先頭3を訪問します。
3に隣接し、未訪問の2をスタックに積みます

5.スタックの先頭2を訪問します。
頂点2に隣接して未訪問な頂点は存在しないので、2をスタックから削除します。

6.スタック先頭の3を再び訪問します。 頂点3に隣接して、未訪問の頂点6をスタックに積みます。

7.スタックの先頭6を訪問します。
頂点6に隣接して、未訪問の頂点5をスタックに積みます。

8.スタックの先頭5を訪問します。
頂点5に隣接して、未訪問の頂点4をスタックに積みます。

8.スタックの先頭4を訪問します。
頂点4に隣接して、未訪問の頂点の頂点はないので、4をスタックから削除します。

9.スタックの先頭5訪問します。
頂点5に隣接して、未訪問の頂点の頂点はないので、5をスタックから削除します。

10.スタックの先頭6訪問します。
頂点6に隣接して、未訪問の頂点の頂点はないので、6をスタックから削除します。

11.スタックの先頭3訪問します。
頂点3に隣接して、未訪問の頂点の頂点はないので、3をスタックから削除します。

12.スタックの先頭1訪問します。
頂点1に隣接して、未訪問の頂点の頂点はないので、1をスタックから削除します。

13.スタックの先頭0訪問します。
頂点0に隣接して、未訪問の頂点の頂点はないので、0をスタックから削除します。

14.スタックが空となり、すべての頂点の訪問が完了しました。

スタックによる深さ優先探索に必要な変数一覧

スタックによる深さ優先探索に必要な変数は以下のようになります。

変数名例 役割
color[N] 未訪問・訪問中・訪問済みの状態を表すための変数
M[n][n] グラフの隣接行列を表すための変数、頂点が隣接している場合は1(true)など
Stack s 訪問中の頂点を退避しておくためのスタック

スタックによる深さ優先探索の疑似アルゴリズム

以上の処理を疑似アルゴリズムにまとめてみます。

dfs(u)
 S.push(u) // 始点をスタックに追加
 color[u] = 訪問中
 d[u] = ++time; // 訪問時間を代入する
 
 while(sが空でない)
  u = S.top()
  v = next(u) // uに隣接している頂点を昇順になるように取得する
  if v != NIL
   if color[v] == 未訪問
    color[v] = 訪問中
    d[v] = ++time; // 訪問時間を記録
    S.push(v)
   // 隣接する頂点がない場合   
  else
   S.pop()
   color[v] = 訪問済み
   f[u] = ++time; // 探索が終わった時間を記録

stackを使い隣接行列を用いた計算量

各々の頂点について、隣接しているすべての頂点を調べるので、
計算量は$O[n^2]$の計算量になります。

c++による、stackを利用した例題の解法

最後に隣接行列とstackを利用した問題のc++での解法を載せます。


#include<iostream>
#include<stack>
using namespace std;

#define N 100
// 未訪問
#define WHITE 0
// 訪問中
#define GRAY 1
// 訪問済み
#define BLACK 2

int n,M[N][N];
int color[N];
// 訪問時間
int d[N];
// 探索終了時間
int f[N];
// 時間
int t = 0;
// 探索済みの頂点を一から探索しないように、nt[u]にすでに探索済みの番号を入れ、
// 次回は探索した移行の頂点から探索されるようにする
int nt[N];

/**
    uに隣接するvを番号順に取得する
*/
int next(int u){
    // 探索済み移行の頂点から探索を始める
    for(int v = nt[u];v < n;++v){
        nt[u] = v + 1;
        if(M[u][v]){
            return v;
        }
    }
    return -1;
}

/**
    スタックを用いた深さ優先探索
*/
void dfs(int u){
    for(int i = 0;i < n;++i){
        nt[i] = 0;
    }
    
    stack S;
    S.push(u);
    color[u] = GRAY;    // 訪問中
    d[u] = ++t;      // 訪問時間
    
    while(!S.empty()){
        int topU = S.top();
        // 隣接する頂点
        int v = next(topU);
        // 隣接する頂点が見つかった場合
        if(v != -1){
            // 未探索のケース
            if(color[v] == WHITE){
                color[v] = GRAY;
                // 訪問時間
                d[v] = ++t;
                S.push(v);
            }
        }else{
            // 未訪問の頂点が見つからなかった
            S.pop();
            // 探索終了
            color[topU] = BLACK;
            // 探索終了時間
            f[topU] = ++t;
        }
    }
}

int main(){
    int u,k,v;
    cin >> n;
    
    // 初期化
    for(int i = 0;i < n;++i){
        color[i] = WHITE;
        for(int j = 0;j < n;++j){
            M[i][j] = 0;
        }
    }
    
    for(int i = 0;i < n;++i){
        cin >> u >> k;
        // 頂点を移動する
        u--;
        // 頂点の数だけ回す
        for(int j = 0;j < k;++j){
            cin >> v;
            --v;    // ずらす
            M[u][v] = 1;
        }
    }
    for(int i = 0;i < n;++i){
        if(color[i] == WHITE){
            dfs(i);
        }
    }
    
    for(int i = 0;i < n;++i){
        cout << i + 1 << " " << d[i] << " " << f[i] << endl;
    }
    
    return 0;
}

2019年8月21日水曜日

再帰関数による深さ優先探索をALDS1_11_Bを例題にして学ぶ

再帰関数による深さ優先探索をALDS1_11_Bを例題にして、学びます。
深さ優先探索の考え方については、こちらにまとめています。

例題

深さ優先探索(Depth First Search: DFS)は、可能な限り隣接する頂点を訪問する、という戦略に基づくグラフの探索アルゴリズムです。
未探索の接続辺が残されている頂点の中で最後に発見した頂点 v の接続辺を再帰的に探索します。

v の辺をすべて探索し終えると、vを発見したときに通ってきた辺を後戻りして探索を続行します。
探索は元の始点から到達可能なすべての頂点を発見するまで続き、 未発見の頂点が残っていれば、その中の番号が一番小さい1つを新たな始点として探索を続けます。

深さ優先探索では、各頂点に以下のタイムスタンプを押します:

  • タイムスタンプ d[v]: vを最初に発見した発見時刻を記録します。
  • タイムスタンプ f[v]: vの隣接リストを調べ終えた完了時刻を記録します。

以下の仕様に基づき、与えられた有向グラフ G=(V,E)
に対する深さ優先探索の動作を示すプログラムを作成してください:

Gは隣接リスト表現の形式で与えられます。
各頂点には 1 から nまでの番号がふられています。 各隣接リストの頂点は番号が小さい順に並べられています。 プログラムは各頂点の発見時刻と完了時刻を報告します。 深さ優先探索の過程において、訪問する頂点の候補が複数ある場合は頂点番号が小さいものから選択します。 最初に訪問する頂点の開始時刻を 1 とします。

入力

最初の行に G の頂点数 n が与えられます。続く n 行で各頂点 u の隣接リストが以下の形式で与えられます:

u k v1 v2 ... vk

u は頂点の番号、k は u の出次数、v1v2...vk  は u に隣接する頂点の番号を示します。

出力

各頂点について id、 d、 fを空白区切りで1行に出力してください。
id は頂点の番号、d はその頂点の発見時刻、f はその頂点の完了時刻です。頂点の番号順で出力してください。

制限

1≤n≤100

入力例

4
1 1 2
2 1 4
3 0
4 1 3

出力例

1 1 8
2 2 7
3 4 5
4 3 6

解法

隣接リストで与えられる入力に対して、隣接行列を作り、それをGraphとすると Graphに対し、深さ優先探索を再帰関数で行うプログラムは以下のようになります。

void visit(int i){
    // 1
    visited[i] = true;
    // 2
    for(int j = 0;j < max;++j){
        if(Graph[i][j] == 1 && !visited[j]){
            visit(j);
        }
    }
}

1.頂点を探索したことを示すフラグvisitedを用意し、
探索した段階で、フラグを立てます。

2.与えられた頂点から隣接行列を調べていき、隣接する頂点を調べます。
すでに探索している頂点の場合処理を飛ばすことで、ループになることを防いでいます。

タイムスタンプへの対応方法

問題では、頂点の探索開始時刻と探索終了時刻を記録する必要があります。

探索開始時点は、探索する前である、関数visitの2の処理の前におけばよく、
探索終了時点は、2の派生する点の探索が終わった時点で換算することになります。

c++での解法例

それでは、以上を踏まえたc++での解法例を書きます。

#include <iostream>

using namespace std;

#define N 100

int Graph[N][N];
// 頂点を発見した時刻
int d[N];
// 頂点の探索を終えた時刻
int f[N];
// かかった時間
int dTime = 0;
// 頂点を発見したかのフラグ
bool visited[N];

void visit(int i,int max){
    
    visited[i] = true;
    d[i] = ++dTime;
    for(int j = 0;j < max;++j){
        if(Graph[i][j] == 1 && !visited[j]){
            visit(j,max);
        }
    }
    // すべての頂点の探索が終了した
    f[i] = ++dTime;
}

int main(){
    int n,u,k,v;

    cin >> n;
    for(int i = 0;i < n;++i){
        cin >> u >> k;
        // 頂点を移動する
        u--;
        // 頂点の数だけ回す
        for(int j = 0;j < k;++j){
            cin >> v;
            --v;    // ずらす
            Graph[u][v] = 1;
        }
    }
    for(int i = 0;i < n;++i){
        if(!visited[i]){
            visit(i, n);
        }
    }
    
    for(int i = 0;i < n;++i){
        cout << i + 1 << " " << d[i] << " " << f[i] << endl;
    }
    
    return 0;
}

計算量を考える

隣接行列を使い2重ループですべてのパターンを調べているので、
$O(N^2)$の計算量がかかることになります。

2019年8月20日火曜日

ALDS1_11_A問題 隣接リスト・隣接行列についての解法

ALDS1_11_A問題(難易度1)の解法についてまとめます。

問題

グラフ G=(V,E)
の表現方法には隣接リスト(adjacency list) による表現と隣接行列(adjacency matrices)による表現があります。

隣接リスト表現では、Vの各頂点に対して1個、
合計 |V| 個のリスト Adj[|V|] でグラフを表します。
頂点 u に対して、隣接リスト Adj[u] は E に属する辺 (u,vi) におけるすべての頂点 vi を含んでいます。
つまり、Adj[u] は G において uと隣接するすべての頂点からなります。

一方、隣接行列表現では、頂点 i
から頂点 j へ辺がある場合 aij が 1、ない場合 0 であるような |V|×|V| の行列 A
でグラフを表します。

隣接リスト表現の形式で与えられた有向グラフ G の隣接行列を出力するプログラムを作成してください。G は n(=|V|) 個の頂点を含み、それぞれ 1 から n までの番号がふられているものとします。

入力

最初の行に G の頂点数 n が与えられます。
続く n 行で各頂点 u の隣接リスト Adj[u] が以下の形式で与えられます:

u k v1 v2 ... vk

uは頂点の番号、k は u の出次数、v1v2...vk は u に隣接する頂点の番号を示します。

出力

出力例に従い、G の隣接行列を出力してください。aij の間に1つの空白を入れてください。

制限

1≤n≤100

入力例

4
1 2 2 4
2 1 4
3 0
4 1 3

出力例

0 1 0 1
0 0 0 1
0 0 0 0
0 0 1 0

問題解説

隣接リストとして与えられた、数値を隣接行列に直して出力する問題です。
隣接リストや隣接行列を知らなくとも、解説にどういうものかが書いてあるので、
知らなくとも解くことができます。

制約にNについて100以下であると定義されているので、動的に領域を確保しなくとも、
N分の配列を用意し、隣接行列として利用し、そのまま出力に利用することができます。

隣接行列についてはこちらにまとめました。

解法のコツ

動的に要素を受け取る方法

入力を受け取る際に、

k v1 v2 ... vk

とkの数に従い動的に入力を受け取る箇所があります。
これをc++で書くと以下のようになります。

    int n,u,k,v;
  
    cin >> n;
    // nの個数分回す
    for(int i = 0;i < n;++i){
        // 一旦Kを受け取る
        cin >> u >> k;
        u--;
        // 頂点の数だけ回す
        for(int j = 0;j < k;++j){
            cin >> v;
        }
    }

あとは、問題に書いてあることをそのまま出力すればokです。

c++での解法

#include

using namespace std;

#define N 100

int M[N][N];

int main(){
    int n,u,k,v;
  
    cin >> n;
    for(int i = 0;i < n;++i){
        cin >> u >> k;
        // 頂点を移動する
        u--;
        // 頂点の数だけ回す
        for(int j = 0;j < k;++j){
            cin >> v;
            --v;    // ずらす
            M[u][v] = 1;
        }
    }
    
    for(int i = 0;i < n;++i){
        for(int j = 0;j < n;++j){
            // 2個目からは空白が必要
            if(j){
                cout << " ";
            }
            cout << M[i][j];
        }
        cout << endl;
    }
    
    return 0;
}

2019年8月18日日曜日

グラフおける隣接行列について

グラフにおける隣接行列について、概念をまとめたいと思います。

隣接行列とは

隣接行列は、2次元の配列でグラフを表現する方法です。
配列のインデックスが各々の頂点番号を表します。

隣接行列の2次元配列をMとするとM[i][j]が、頂点iと頂点jの関係を示します。
頂点と頂点が辺でつながれている場合は1やtrueで表し、
頂点と頂点間で繋がりがない場合は、0やfalseで表現します。

無効グラフにおける隣接行列

隣接行列の2次元配列をMとします。
無効グラフの隣接行列では、頂点i,jの間に辺があるとすると、
、M[i][j]、M[j][i]の両方の値を1にし、辺がない場合は0にします。 隣接行列は、右上と左下が対象になります。

下図のような無効グラフがあるとします。

これを隣接行列に直すと以下のようになります。
頂点と頂点が辺でつながれている場合は配列の要素が1になります。

有効グラフにおける隣接行列

有効グラフにおける隣接行列では、頂点iから頂点jに向かって辺がある場合、
M[i][j]の値を1とし、辺がない場合0とします。
無効グラフと違い頂点同士必ず相互に1とはなりません。

下図のような有効グラフがあるとします。

この有効グラフの隣接行列は以下のようになります。

隣接行列の長所

配列M[i][j]により、辺(u,v)を参照できるので、頂点uと頂点vの関係をO(1)で参照することができます。

隣接行列の短所

  • 一つの隣接行列では頂点uから頂点vへの関係を一つしか記録できない点
  • 頂点の数の2乗のメモリを使用する点

参考
プログラミングコンテスト攻略のためのアルゴリズムとデータ構造

2018年2月21日水曜日

単一始点最短経路をダイクストラ法の隣接行列で求める

単一始点最短経路の問題をダイクストラ法と隣接行列を使って解きたいと思います。
問題はALDS1_12_Bを使います。

ダイクストラ法とは

ダイクストラ法は以下のように表される。

前提として、グラフG = (V,E)の頂点全体の集合をV、始点をs、
最短経路木に含まれる頂点の集合をSとします。

各計算のステップで、最短経路木の辺と頂点を選びSへ追加していきます。

また、各頂点iについて、S内の頂点のみを経由したsからiへの最短経路のコストをd[i]とします。

ダイクストラ法のロジック

1.初期化処理

最短経路木に含まれる頂点の集合をSを以下のように初期化します。

始点sに対して最短経路のコストを示すd[s] = 0を代入し、
始点s以外の頂点の全体集合Vに属する全ての頂点iに対してd[i] = 最大値
を代入します。

2.以下の処理をS = Vになるまで(始点sから各々の頂点の最短経路がもとまるまで)続けます。

2.1
V - Sの中から、d[u]が最小である頂点uを選択(最短経路と決定している頂点を除く)します。

2.2
頂点uをSに追加すると同時に、uに隣接しかつV - Sに属する全ての頂点vに対する
値を以下のように更新します。

if d[u] + w(u,v)(実際にかかる移動コスト) < d[v]
   d[v] = d[u] + w(u,v)

2.の各計算ステップ直後に,
u[v]には、sからSの頂点のみを経由したvまでの最短コストが記録されていることになります。
つまり1,2の処理が全て終了すると、u[v]に,sからvまでの最短コストが記録されます。

ダイクストラ法の図解

プロセスがわかりやすいように、ダイクストラ法を図解してみます。

下図のようなグラフがあり、始点をAをとします。 Aから各々の頂点への最短経路をダイクストラで求めてみます。

まず、始点Aを0で初期化し、各々への頂点に探索が完了していないことを示すために最大値を代入(変数d[]が対象となる)します。

出典:MIT open course ware

ここから、2.のプロセスに入ります。
最短経路木を示す頂点の集合をSとして、Sを以下のように表します。

S = {} {A B C D E }(Sから各頂点の最短経路の重さを示します。)

step1

まず、始点(s)である頂点AをSに追加します。

S = {A} {0 ∞ ∞ ∞ ∞ }

続いて、頂点Aに辺があり、最短経路未決定の頂点の最短経路を更新します。

最短経路か判定する式は、

if d[u] + w(u,v) < d[v]
   d[v] = d[u] + w(u,v)

なので、Aに隣接する頂点B,Cは頂点uが始点で重みが0なので、 辺への重みのみが加算され、以下のようになります。

S = {A} {0 10 3 ∞ ∞ }

step2

また、2.1のプロセスに戻ります。
2.1:V - Sの中から、d[u]が最小である頂点uを選択します。

つまり、最短距離未決定の最小頂点C(重み3)を選ぶことになりますが、
なぜ、最小コストの頂点を選んだ時点で、その地点が最短経路になるのかを、
図で考えてみます。

なぜ、最小コストの頂点が最短経路になるのか

下図のように始点から辺が3つ伸びているグラフがあるとします。
グラフより、各々の3つの頂点からその3つの頂点への辺があるとしても、
sから各々の頂点への3つの辺の中に必ず最短となる経路があることがわかります。

これは、辺が2つでも4つでも同じことです。

step2に戻ります。
Sに最短経路である頂点Cを追加します。

S = {A C} {0 10 3 ∞ ∞ }

続いて、Cにつながる頂点への最短経路更新プロセスに入り、
図を参考に以下のように更新します。

S = {A C} {0 7 3 11 5 }

step3

プロセス1に戻って、探索を開始する頂点を決定します。
最短点であるE(5)が選ばれるので、EをSに追加します。

S = {A C E} {0 7 3 11 5 }

プロセス2に移り、頂点Eにつながる頂点の最短点の更新を行います。

更新する頂点はないので、結果は前と変わりません。

S = {A C E} {0 7 3 11 5 }

step4

プロセス1に戻って、探索を開始する頂点を決定します。
最短点であるB(7)が選ばれるので、BをSに追加します。

S = {A C E B} {0 7 3 11 5 }

プロセス2に移り、頂点Bにつながる頂点の最短点の更新を行います。 頂点Dへの最短経路が9に変わります。

S = {A C E B} {0 7 3 9 5 }

step5

プロセス1に戻って、探索を開始する頂点を決定します。
ここで、全ての最短経路がもとまったので、処理を終了します。

始点sから頂点uへの最短経路はd[u]に入っていることになります。

隣接行列を用いたダイクストラ法

上記をふまえて以下のように実装します。
上記の対象の処理には番号を振っています。

#define MAX 100
#define INFTY 1<<22
// 未訪問の頂点を示す
#define WHITE 0
// 訪問済の頂点を示す
#define GRAY 1
// 未探索の頂点d[u]にはINFTYが入っている
#define BLACK 2
// 隣接行列頂点から頂点への辺の重みが入っている
// 例:gMatrix[0][1] 頂点0から1への重み INFTYの場合辺がないことを示す
int gMatrix[MAX][MAX];

void dijkstra(int n){
    // 最小の重みを記録する
    int minV;
    // u[v]に始点sからvまでの最短コストを保存する
    int u[MAX];
    // 訪問状態を記録
    int color[MAX];
    // 1.初期化
    for(int i = 0;i < n;++i){
        u[i] = INFTY;
        color[i] = WHITE;
    }
    // 0を始点とする
    u[0] = 0;
    color[0] = GRAY;
    
    while(1){
        minV = INFTY;
        // uは探索対象となる頂点を示す
        int u = -1;
        // 2.1 d[u]が最小である頂点を決定する
        for(int i = 0;i < n;++i){
            // 最短経路決定ずみの頂点は含めない
            if(minV > weight[i] && color[i] != BLACK){
                u = i;
                minV = weight[i];
            }
        }
        if(u == -1)break;
        // 選んだ頂点は最短経路決定利用はダイクストラロジック解説にて
        color[u] = BLACK;
        
        for(int v = 0;v < n;++v){
            // 辺が存在し、最短経路決定済頂点以外の経路が対象になる。
            if(color[v] != BLACK && gMatrix[u][v] != INFTY){
                // 2.2最短コストを更新する
                // この処理の終了後weight[v]sからS内の頂点のみを経由したvまでの最短コストが記録される
                if(weight[v] > weight[u] + gMatrix[u][v]){
                    weight[v] = weight[u] + gMatrix[u][v];
                    color[v] = GRAY;
                }
            }
        }
    }
    // 出力
    for(int i = 0;i < n;++i){
        cout << i << " " << ( weight[i] == INFTY ? -1 : weight[i] ) << endl;
    }
}

プリム法と同様wikiに図解されているかなと思ったんですが、
残念ながらのっていませんでいした。
その代わりにアニメーション図がありました、全体の整理でないんでわかりにくい気がしますが。

それでは、ALDS1_12_Bの問題をふまえての全体のプログラムです。

#include <iostream>
using namespace std;
#define MAX 100
#define INFTY 1<<22
#define WHITE 0
#define GRAY 1
#define BLACK 2

int gMatrix[MAX][MAX];

void dijkstra(int n){
    // 最小の重みを記録する
    int minV;
    // weight[v]に始点sからvまでの最短コストを保存する
    int weight[MAX];
    // 訪問状態を記録
    int color[MAX];
    // 1.初期化
    for(int i = 0;i < n;++i){
        weight[i] = INFTY;
        color[i] = WHITE;
    }
    // 始点
    weight[0] = 0;
    color[0] = GRAY;
    
    while(1){
        minV = INFTY;
        // 頂点を示す
        int u = -1;
        // 2.1weight[u]が最小である頂点を決定する
        for(int i = 0;i < n;++i){
            if(minV > weight[i] && color[i] != BLACK){
                u = i;
                minV = weight[i];
            }
        }
        if(u == -1)break;
        color[u] = BLACK;
        
        for(int v = 0;v < n;++v){
            // 辺が存在する
            if(color[v] != BLACK && gMatrix[u][v] != INFTY){
                // 2.2最短コストを更新する
                // この処理の終了後weight[v]sからS内の頂点のみを経由したvまでの最短コストが記録される
                if(weight[v] > weight[u] + gMatrix[u][v]){
                    weight[v] = weight[u] + gMatrix[u][v];
                    color[v] = GRAY;
                }
            }
        }
    }
    // 出力
    for(int i = 0;i < n;++i){
        cout << i << " " << ( weight[i] == INFTY ? -1 : weight[i] ) << endl;
    }
}

int main(){
    int n;
    cin >> n;
    
    for(int i = 0;i < n;++i){
        for(int j = 0;j < n;++j){
            gMatrix[i][j] = INFTY;
        }
    }
    
    int k,c,u,v;
    for(int i = 0;i < n;++i){
        cin >> u >> k;
        for(int j = 0;j < k;++j){
            cin >> v >> c;
            gMatrix[u][v] = c;
        }
    }
    dijkstra(n);
    return 0;
}

最終更新:2020/1/29

2018年2月20日火曜日

最小全域木問題をプリムのアルゴリズムと隣接行列で解く

最小全域木問題をプリムのアルゴリズムと隣接行列を使って解きたいとおもいます。 問題はALDS_1_12_A:Minimum Spanning Treeを参照してください。

プリム法

プリムのアルゴリズムを使って、最小全域木(MST)を求めるには、以下の通りに行います。

グラフG = (V,E)の頂点全体の集合をV,MSTに属する頂点の集合をTとする
1.Gから任意の頂点rを選らんで、それをMSTのルートとしてTに追加する。
2.以下の処理をT=Vになるまで繰り返す

Tに属する頂点とV-Tに属する頂点をつなぐ辺の中で、重みが最小のものである辺(p(u),u)を選び、
それをMSTの辺とし、uをTに追加する

上を実行する過程がプリム法のwikipediaの図に詳しくまとめられていたので、そちらを見るとわかりやすいと思います。

ここで、最小の重みの辺を保持するために、以下の変数を用います。
ここで、頂点の数n = |V|とします。

gColor[n] : color[v]にvへの訪問状態を色で記録します。
WHITE:未訪問,GRAY:訪問(未確定),BLACK(最小値決定済み)

gMatrix[n][n] : gMatrix[u][v]にuからvへの辺の重みを記録した隣接行列

weight[n] : weight[v]にTに属する頂点とV-Tに属する頂点をつなぐ辺の中で、重みが最小の辺の重みを記録する

parent[n] : parent[v]にMSTにおける頂点vの親を記録する

プリムのアルゴリズムのコード

では、コードを見ていきます。
実施していく過程はwikiの図を見るとわかりやすいと思います。

#define MAX 100
#define INFTY 1 << 21
// 未訪問状態
#define WHITE 0
#define GRAY 1
#define BLACK 2

// 隣接行列
int gMatrix[MAX][MAX];

int prim(int n){
    // 頂点
    int u;
    // 最小の重み
    int minV;
    // Tに属する頂点と(V - T)に属する頂点をつなぐ辺の中で、重みが最小の辺の重みを記録する
    int weight[MAX];
    // MSTにおける頂点vの親を記録する
    int parent[MAX];
    // 訪問状態を記録する
    int color[MAX];
    // 初期化
    for(int i = 0;i < n;++i){
        weight[i] = INFTY;
        parent[i] = -1;
        color[i] = WHITE;
    }
    // 初回探索地点をindex0にする
    weight[0] = 0;
    
    while(1){
        minV = INFTY;
        u = -1;
        // 隣接する地点で
        for(int i = 0;i < n;++i){
            if(minV > weight[i] && color[i] != BLACK){
                // 最小の重みを記録する
                u = i;
                minV = weight[i];
            }
        }
        // 全て訪問した
        if(u == -1)break;
        // 訪問済み
        color[u] = BLACK;
        for(int v = 0;v < n;++v){
            // uとvの間に辺が存在する
            if(color[v] != BLACK && gMatrix[u][v] != INFTY){
                if(weight[v] > gMatrix[u][v]){
                    // 隣接する辺に重みを割り当てる
                    weight[v] = gMatrix[u][v];
                    parent[v] = u;
                    color[v] = GRAY;
                }
            }
        }
    }
    int sum = 0;
    for(int i = 0;i < n;++i){
        if(parent[i] != -1) sum += gMatrix[i][parent[i]];
    }
    
    return sum;
}

O記法を考える

隣接行列を使ったプリムのアルゴリズムは
重みが最小である頂点を探すために、グラフの頂点数調べる必要があるので、
O(|V|^2)のアルゴリズムになります。

では、ALDS_1_12_A:Minimum Spanning Tree
を例題とした全体のコードです。

#include 
using namespace std;

#define MAX 100
#define INFTY 1 << 21
// 未訪問状態
#define WHITE 0
#define GRAY 1
#define BLACK 2

// 隣接行列
int gMatrix[MAX][MAX];

int prim(int n){
    // 頂点
    int u;
    // 最小の重み
    int minV;
    // Tに属する頂点と(V - T)に属する頂点をつなぐ辺の中で、重みが最小の辺の重みを記録する
    int weight[MAX];
    // MSTにおける頂点vの親を記録する
    int parent[MAX];
    // 訪問状態を記録する
    int color[MAX];
    // 初期化
    for(int i = 0;i < n;++i){
        weight[i] = INFTY;
        parent[i] = -1;
        color[i] = WHITE;
    }
    // 初回探索地点をindex0にする
    weight[0] = 0;
    
    while(1){
        minV = INFTY;
        u = -1;
        // 隣接する地点で
        for(int i = 0;i < n;++i){
            if(minV > weight[i] && color[i] != BLACK){
                // 最小の重みを記録する
                u = i;
                minV = weight[i];
            }
        }
        // 全て訪問した
        if(u == -1)break;
        // 訪問済み
        color[u] = BLACK;
        for(int v = 0;v < n;++v){
            // uとvの間に辺が存在する
            if(color[v] != BLACK && gMatrix[u][v] != INFTY){
                if(weight[v] > gMatrix[u][v]){
                    // 隣接する辺に重みを割り当てる
                    weight[v] = gMatrix[u][v];
                    parent[v] = u;
                    color[v] = GRAY;
                }
            }
        }
    }
    int sum = 0;
    for(int i = 0;i < n;++i){
        if(parent[i] != -1) sum += gMatrix[i][parent[i]];
    }
    
    return sum;
}

int main(){
    int n;
    cin >> n;
    
    for(int i = 0;i < n;++i){
        for(int j = 0;j < n;++j){
            int edge;
            cin >> edge;
            gMatrix[i][j] = (edge == -1) ? INFTY : edge;
        }
    }
    
    cout << prim(n) << endl;
    return 0;
}

参考