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))