2020年6月13日土曜日

最大長方形問題のスタックを使用した解法をまとめる

AIZU ONLINE JUDGEのDPL_3_B: Largest Rectangleを題材に、最大長方形を求める問題の解法をまとめます。

問題では、綺麗なタイルが0、汚れたタイルが1で与えられ、
綺麗なタイルでのみ作れる最大長方形の面積を求めます。

白を綺麗なタイル、黒を汚れたタイルとして、下図のようにタイルの情報が渡されたとします。

そして、各々の地点における最大の高さを別の配列(Tとする)に保存しておきます。

スタックを用いた最大長方形の求め方

ここでは、スタックを利用して、最大長方形を求めてみます。
四角形の面積を求めるためのクラス・構造体(rectとする)を作成し、これをスタックに入れます。

スタックにプッシュするrectは最大長方形を調べていく過程でまだ拡張される可能性のあるものになります。

最大長方形を調べるプロセスで、スタックに入っている最大長方形がこれ以上拡張されない場合、スタックにある対象のrectを取り出し、面積を計算し、最大面積を更新します。
これを繰り返すことで、最大面積を求めていきます。

具体例で最大面積を求めるプロセスを確認する

では、具体例でプロセスを確認します。
まず、rectを定義します。

rectの定義

struct Rectangle{
    // indexの地点の高さ(Tに保存されている)
    int height;
    // stackに追加した始点のindex x
    int pos;
};

heightとposを掛けることで、保存したrectの面積を求めることができるようにします。

rectをstackに保存するプロセス

長方形の最大面積を求めるために、タイルの高さを保存した2次元配列(T)を左上から右下へ走査していきます。

その配列の同じ行を左から右に操作しその行の高さ計算が終了したら、
stackを解放します。

その際、以下の条件の時にスタックを入れる対象となるrectを作成し、pushします。

スタックが空の場合

高さと位置を表すx座標をもつrectを作成して、スタックにpushします。

対象の高さがstackのトップよりも高い場合

対象となるrectを作成し、スタックにpushします。

なぜ、スタックにpushするのかというと、この位置の四角形の面積が最大面積を更新する可能性があるからです。

以下の図の5行目のケースを考えてみます。


最初の地点の高さ1、pos:0はスタックが空なので、そのまま積み上げます。

次の地点は高さが2になってます。
stackトップの高さは先ほど積み上げた1なので、この地点のrect(height:2,pos:1)もスタックに追加します。

高さが2なので、右に走査していく過程で高さが2以上のタイルがあれば、先ほど追加した高さ1のrectの面積よりも大きくなる可能性がありるので、スタックに保存する必要があります。

工程を考えるとわかるように、スタックにトップに積まれているrectの高さは最大のものになります。

対象の高さがスタックのトップよりも低い場合

対象の高さがスタックのトップよりも低い場合は、
下図のケースのようにスタックに積んであるrectが長方形でなくなってしまうため、
対象の高さよりも高いrectを取り出して面積を計算する必要があります。


対象の高さがスタックのトップの高さと同じ場合

スタックのトップにあるrectの高さはスタックの中で最大なので、
スタックに入っている全てのrectの横幅が拡張されることになります。

なので、次のindexに進みます。

タイルの列の最後の位置まで到達した場合

スタックに積んである全てのrectの面積を計算し、最大の面積が更新されるかチェックします。

このプロセスを行の数繰り返せば最大面積が求められます。

このように、スタックを利用することで、タイル毎ごとに面積を計算する手間を省くことができます。

c++による最大長方形を求めるコード

今までの解説をもとに、コードを書きます。

#include <bits/stdc++.h>
using namespace std;
#define MAX 1400

struct Rectangle{
    // indexの地点の高さ
    int height;
    // 始点のX
    int pos;
};

// buf = Tの一行の配列
// size = Tの一行のサイズ
int getLargestRectangle(int size,int buf[]){
    // stackとして長方形を保存
    stack<Rectangle> s;
    int maxV = 0;
    // 配列の最後を0にすることで、範囲外後に最大面積を計算するようにする
    buf[size] = 0;
    // stoperまで回す
    for(int i = 0;i <= size;++i){
        Rectangle rect;
        // 高さ代入
        rect.height = buf[i];
        // 横の位置は計算を始める時点の位置
        rect.pos = i;
        // stack空ならば追加
        if(s.empty()){
            s.push(rect);
        }
        else{
            // stackのトップにあるrectの高さより高い場合
            if(s.top().height < rect.height){
                s.push(rect);
            }
            // stackのトップにあるrectの高さよりも低い場合(保存してあった最大の高さが無効になるので、無効になる高さの面積を計算する必要がある)
            else if(s.top().height > rect.height){
                // 新しく積むrectの幅を出すために保存しておく。
                int targetX = i;
                // スタックに入っているrectが対象のrectの高さ以上の場合最大面積を計算する
                while(!s.empty() && s.top().height >= rect.height){
                    Rectangle pre = s.top();
                    s.pop();
                    // 面積を計算する
                    int area = pre.height * (i - pre.pos);
                    maxV = max(maxV,area);
                    // 始点Xを入れ替える(stackに積むrectの高さは取り出したrectの高さよりも低いので、取り出した地点が、長方形を計算する際のX座標になる)
                    targetX = pre.pos;
                }
                rect.pos = targetX;
                s.push(rect);
            }
        }
    }
    return maxV;
}

int H,W;
// タイル情報を2次元で保存0:綺麗なタイル,1:汚れたタイル
int tiles[MAX][MAX];
// 高さを格納する2次元配列
int T[MAX][MAX];

int getLargestRectangle(){
    // 各々の地点における高さを格納するプロセス
    for(int j = 0;j < W;++j){
        for(int i = 0;i < H;++i){
            // 汚れたタイル
            if(tiles[i][j]){
                T[i][j] = 0;
            }
            else{
                // 対象のindex地点の高さを格納する
                // 上の地点 + 1の高さになる
                T[i][j] = (i > 0) ? T[i - 1][j] + 1 : 1;    
            }
        }
    }
    // 最大面積保存用
    int maxV = 0;
    // 最大高さ分回す
    for(int i = 0;i < H;++i){
        maxV = max(maxV,getLargestRectangle(W,T[i]));
    }
    return maxV;
}

int main(){
    scanf("%d %d",&H,&W);
    for(int i = 0;i < H;++i){
        for(int j = 0;j < W;++j){
            scanf("%d",&tiles[i][j]);
        }
    }
    printf("%d\n",getLargestRectangle());
}

最大長方形を求める問題の解法をまとめる

1.タイルの列における高さを保存する配列を作る。

2.高さを保存する配列を行ごとに回して、一行における最大の面積を求める。
その際にスタックを利用して、計算量を減らすようにする。

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

2020年4月23日木曜日

トポロジカルソートの概要とそのプログラム

トポロジカルソートとは

閉路(頂点から辺がa→b→c→aのというようにサイクルがあるグラフのこと)のない有効グラフのことをDAGといいますが、
DAGの全ての有効辺が左から右へ向かうように、すべての頂点を水平状に並べる(各辺(u,v)について、uがvよりも先に位置するように並べる)ことを

トポロジカルソートといいます。

幅優先探索によるトポロジカルソート解説

幅優先探索で、入次数(頂点に入ってくる辺数のことで英語でindeg)が0の頂点を順番に訪問して、連結リストの末尾に追加していきます。

訪問した頂点uに探索完了フラグを立てて、その頂点からでている辺の先にある頂点vの入次数を一つ減らします。
これはその辺を削除することで対応します。

辺を削除したことで、vの入字数が0になったとき、vに訪問することができるようになるので、
vをキューに追加します。

幅優先探索によるトポロジカルソート擬似アルゴリズム

topologicalSort()
  // 全てのノードの探索フラグVを初期化
  // 全てノードについて、入次数indeg[u]を設定する
  for uが0から|V| - 1まで
    if indeg[u] == 0 && 検索フラグ == FALSE
      bfs(u)

bfs(s)
  Queue.push(s)
  V[s] = true
  while Queue not empty
    u = Queue.degueue()
    out.push_back(u)  // 次数(頂点に接合する辺の数)が0の頂点を出力用連結リストに追加

    for uに隣接するノードv
      indeg[v]--;
      if indeg[v] == 0 && V[v] == false
        V[v] = true;
        Q.enqueue(v)

AIZU ONLINE JUDGEのトポロジカルソート問題の解答

トポロジカルソートの実装例として、AIZU ONLINE JUDGEを例題にc++のコードをあげます。

#include<iostream>
#include<vector>
#include<algorithm>
#include<queue>
#include<list>
using namespace std;

static const int MAX = 100000;

vector<int>G[MAX];
list<int>Out;
bool V[MAX] = {false};
int N;
int indeg[MAX] = {0};

void bfs(int s){
    queue<int> q;
    q.push(s);
    V[s] = true;
    
    while(!q.empty()){
        int u = q.front();
        q.pop();
        Out.push_back(u);
        
        for(int i = 0;i < G[u].size();++i){
            int v = G[u][i];
            indeg[v]--;
            
            if(indeg[v] == 0 && !V[v]){
                V[v] = true;
                q.push(v);
            }
        }
    }
}

void tsort(){
    for(int u = 0;u < N;++u){
        for(int i = 0;i < G[u].size();++i){
            int v = G[u][i];
            indeg[v]++;
        }
    }
    
    for(int u = 0;u < N;++u){
        if(indeg[u] == 0 && !V[u]){
            bfs(u);
        }
    }
    
    for(list<int>::iterator it = Out.begin();it != Out.end();++it){
        cout << *it << endl;
    }
}

int main(){
    int s,t,m;
    cin >> N >> m;
    
    for(int i = 0;i < m;++i){
        cin >> s >> t;
        G[s].push_back(t);
    }
    tsort();
}

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

2020年4月8日水曜日

わかりやすいベルマンフォードアルゴリズムの解説と実装例

単一始点最短経路問題で、辺にマイナスの重みがあるパスがあることがあります。
その際の最短経路を算出する方法としてベルマンフォード法があります。

このベルマンフォード法を図解します。

ベルマンフォードアルゴリズム解説

初期化

頂点をv, 辺をe,頂点uから辺をeを伝って移動する際の重みをw(u,v)とします。
また、開始点から各々の頂点への最短経路を記録した変数をdとします。

まず、dに∞を代入します。
始点(source)には0を代入します。

for each v ∈ V:
  d[v] = ∞
d[s] = 0

頂点から頂点への最短距離を更新する

頂点の数 - 1だけforループを回し、その中で、辺の数分forループを回し、
辺をつなぐ頂点から頂点への最短経路が短くなる場合に最短経路dを更新する処理を行います。
(わかりにくいので、後に図解します)

for i ← 1 to |v| - 1:
  do for each edge (u,v) ∈ E
    do if d[v] > d[u] + w(u,v)
      d[v] = d[u] + w(u,v)

負のサイクルがあるかを検出する

最短経路の確認が終わった後、
最後に負のサイクルがあるかを確認します。

負のサイクルとは、たとえばA → B → C → A
と頂点と辺がつながっているとして、それぞれの重みを{1,2,3,-7}とすると、
A → B → C → Aへの経路は1 + 2 + 3 - 7 = -1
になり、

さらに頂点Aから同じ辺を巡回すると、
A → B → C → A = -1 + 1 + 2 + 3 - 7 = -2

となり、巡回すればするほど、最短経路が短くなるルートのことです。

負のサイクルがある場合は、全ての頂点への最短経路を求められないので、
負のサイクルがあることの報告をします。

最短経路更新プロセスが終わったあと、 再度、最短経路更新できるかで、負のサイクルの検出を行います。

do for each edge (u,v) ∈ E
  do if d[v] > d[u] + w(u,v)
      negative weight cycle

これで、ベルマンフォードのプロセスは終わりです。

図でベルマンフォードの工程を確認する

ベルマンフォードの工程を図解します。

以下のようなグラフがあるとします。
#がついた番号は辺の番号を表します。
今回は#1から最短経路を更新する辺のループプロセスをスタートしますが、
どの辺からループをスタートしても結果は変わりません。
また、頂点の下の数値を最短経路とします。
始点を0にし、それ以外の頂点は∞で初期化します。

ベルマンフォード1回目の試行

辺#1からスタートします。
辺#1はBからEにつながっており、重みは2です。
d[E]は∞なので、∞ > ∞ + 2はfalseとなるので、更新処理は行われません。

辺#2.#3も同様になります。

辺#4はd[A]が0
d[B](∞) > d[A](0) - 1がtrueとなるので、
d[B] = -1
となります。

辺#5も同様で
d[C] = 4

辺#6は更新されず、

#7は
d[C](4) > d[B](-1) + 3がtrueとなり
d[C] = 2になります。

#8は更新されれません。

これで、辺を全てチェックしたので、
一回目の施工が終わります。
このプロセスを頂点 - 1回繰り返すことになります。

1回目の施工を終えると、最短経路は以下のように更新されます。

ベルマンフォード2回目の試行

#1はd[B] = -1なので、d[E] = 1に更新

#2はd[B] = -1なので、d[D] = 1

#3,#4,#5,#6,#7は変わらず

#8は
d[D](0) > d[E](1) - 3がtrueとなるので、
d[D] = -2になります。

2回目の試行の結果は下図ようになります。

ベルマンフォード3回目の試行

3回目以降は、更新処理をしても何も更新されません。
全ての辺の最短経路を更新するプロセスで、
更新がなければ、次の更新時も同じく更新できないので、
ループを抜けて、最短経路を報告します。

負のサイクルのチェック工程

辺の最短経路更新処理が終わったら、負のサイクルチェックするため同じく全辺の最短経路が更新できるか調べる工程に入ります。
今回は、上記の試行の通り更新は行われないので、負のサイクルは存在せず、
始点から各々の頂点への最短経路を求めることができました。

負のサイクルがあるときの処理を確認する

先の試行からわかるように、負のサイクルがあると辺の更新処理を行うたびに、
最短経路が更新されます。

なので、最短経路の更新プロセスが終わった後で、最短経路が更新されれば、
負のサイクルがある
ことがわかります。

このグラフでは、頂点B → E → D → Bがサイクルになっているので、
このEからDへの重みを-3から-4に変更して負のサイクルに変えて、
考察してみます。

ベルマンフォード負のサイクル1回目の試行

結果は先例の1回目と変わりません

ベルマンフォード負のサイクル2回目の試行

d[D] = -3になります。

ベルマンフォード負のサイクル3回目の試行

d[B] = -2,d[C] = 1に変わります。
これ以降も常に最短経路が短くなりつづけることがわかります。

ベルマンフォードの例題とc++による解答プログラム

AIZU ONLINE JUDGEの例題をもとに、c++の解答を載せます。

#include <vector>
#include <iostream>
using namespace std;
#define MAX 100000
#define INF __INT_MAX__

struct edge{
    // 始点,終点,重み
    int s,t,w;
};

vector edges;
int d[MAX];

void BellmanFord(int startVertex,int vNum,int eNum){
 fill(d,d + MAX,INF);
 d[startVertex] = 0;
 
 // 頂点分回すことで、最後のサイクルは負のサイクルを確認するプロセスになる
 for(int i=0;i < vNum;++i){
  bool update = false;
  for(int j = 0;j < eNum;++j){
   edge e = edges[j];
   if(d[e.s] != INF && d[e.t] > d[e.s] + e.w){
    d[e.t] = d[e.s] + e.w;
    update = true;
    // コード省略のため最短経路を求める工程で負のサイクルプロセス確認工程を行う
    if(i == vNum - 1){
     cout << "NEGATIVE CYCLE" << endl;
     return;
    }
   }
  }
  if(!update)break;
 }

 for(int i = 0;i < vNum;++i){
  if(d[i] == INF){
      cout << "INF"<< endl;
  }
  else cout<< d[i] << endl;
 }
}

int main(){
    int v,e,r;
 cin >> v >> e >> r;
 for(int i = 0;i < e;++i){
  int s,t,w;
  cin >> s >> t >> w;
  edges.push_back({s,t,w});
 }
 BellmanFord(r,v,e);
}

2020年4月1日水曜日

pythonによる最大公約数と最小公倍数の求め方

pythonを使った最大公約数と最小公倍数の求め方をまとめます。

目次

最大公約数とは

その名の通り、比較する各々の数値の共通する最大の約数のことです。

比較する数値が12,36として、それぞれの約数を上げると、

12 = {1,2,3,4,6,12}

36 = {1,2,3,4,6,9,12,36}

共通する最大の約数は12なので、最大公約数は12になります。

再帰関数を使って最大公約数をもとめる

再帰関数とユーグリットの互徐法を使って最大公約数を求めてみます。

ユーグリットの互徐法とは、
x >= yのときgcd(x,y)とgcd(y,xをyで割ったあまり)は等しいというものです。
詳しい証明などは別記事でまとめています。

この定理と再帰関数を使うと以下のように書くことができます。

def gcd(x,y):
    return gcd(y,x % y) if y > 0 else x

最小公倍数とは

最小公倍数とは、各々の数値の共通する最小の倍数を意味します。

12,36を例に上げると36ということになります。

最小公倍数を利用して、最大公約数を求める

最小公倍数は最大公約数を利用すると簡単に求めることができます。

比較する数値をa,bとすると、aとbを乗算したものをaとbの最大公約数で割ることで求めることができます。

これをpythonのコードにすると以下のようになります。

def lcm(a, b):
    return ((a*b) // gcd(a,b))

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に単一最短始点経路とダイクストラ法の解説動画ををみました。

2020年1月23日木曜日

単一始点最短経路(Single Shortest Path)の解法と勉強方法と例題まとめ

このページは、私が単一始点最短経路(Single Shortest Path)を理解する際参考にしたものをまとめたものです。

単一始点最短経路(Single Shortest Path)の概念について

MITのOpen soft ware courseに単一始点最短経路の概念説明のvideoがあるので、 それを参考にするといいかと思います。
遷移先に資料のpdfなども上がっています。

補講動画(細かいネタが載っているので、余裕があれば)

単一始点最短経路(Single Shortest Path)の解法アルゴリズム

解法として主にダイクストラ法とベルマン–フォード法があります。 この2つが基本になるっぽい。

ダイクストラ法とは

単一始点最短経路(Single Shortest Path)を解くための代表的なアルゴリズムです。
ただし、重みが-になる場合は適応できません。

MITのopen course videoにてダイクストラ法についての解説があるので、
参考にするといいと思います。

こちらは、ダイクストラを高速化方法について、解説しています。

ベルマンフォード法とは

グラフの-の重みにも対応した解法アルゴリズムで、
その分ダイクストラよりも複雑になります。

mitのopen courseにベルマンフォード法について解説した動画があるので、
参考にすると良いです。

単一始点最短経路(Single Shortest Path)の例題まとめ

単一始点最短経路を扱った例題をまとめます。

例題1

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

例題2

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

例題3

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

例題4

GRL_1_B
重みに負の要素があり、ダイクストラ法を使うことができないので、
ベルマンフォードを使う必要があります。