競技プログラミング練習会

2019 Normal

第8回

最小全域木, 最短経路問題

担当 :  aotsuki

自己紹介

自己紹介

  • 京都大学理学部 2回生
  • 本名:宮嶋 優大
  • ​​AtCoder:水色
  • 競プロでは主にC++を使用
  • ゲーム制作とweb開発をほんの少しだけ
  • ボードゲームとか麻雀やったりする
    • 好きな人は今度一緒にやりましょう
  • 楽しくやっていきましょう

KMC-ID : aotsuki

slack(内部チャット)

皆さんの自己紹介もお願いします

  • KMC-ID or 名前
  • 所属(大学など)
  • プログラミング経験や競プロの経験
  • 何か言いたいこと

ささっと、やりましょう!

今日の内容

今日の内容

  • 最小全域木
    • Prim 法
    • Kruskal 法
  • 最短経路問題
    • Bellman-Ford 法
    • Dijkstra 法
    • Warshall-Floyd 法
    • 経路復元

今日の内容

  • 先週やった内容(グラフ、木)が出てきます
  • 先週休んだ人の中で、グラフや木が分からない人はいますか?
  • 先週出席した人の中で、グラフや木が分からない人はいますか?
  • 復習しましょう!

前提知識

  • グラフの話の時は以下のように表すことが多い
    • V  : 頂点数, 頂点集合など
    • E  : 辺の数, 辺集合など
    • w : 辺の重さ(コスト)など
  • 無向辺(s,t,w) は有向辺(s,t,w) と有向辺(t,s,w)の2辺と捉える

最小全域木

  • グラフ:   頂点と辺の集合
  • 木:     連結かつ閉路を持たないグラフ
  • 全域木:   あるグラフのすべての頂点と一部

           の辺で構成された木

  • 最小全域木: 辺の重み(コスト)の総和が最小と  

          なる全域木 

最小全域木

最小全域木

  • このグラフの最小全域木は赤い所
    • この時のコストは7
  • アルゴリズム
    • Prim
    • Kruskal
    • BFS (幅優先探索)
      • 辺のコストが均一の場合のみ

最小全域木

Prim 法

  • 最小全域木を求めるアルゴリズムの一つ
  • 貪欲法の一種
  • 計算量は \( O(E \log E)\)

Prim 法

  1. 頂点, 辺を入れるための集合V, Eを用意する(どちらも空)

  2. グラフから適当に一つの頂点を選び, Vに加える
  3. Vがグラフのすべての頂点を含むまで, 以下を繰り返す
    1. Vに含まれる頂点 u含まれない頂点 vを結ぶ重みが最小の辺 (u,v)をグラフから選び, Eに加える
    2. vをVに加える
  4. 最終的にグラフ(V,E)が最小全域木となる

Prim法

Priority queue が使える!

Prim 法

Prim 法

Prim 法

Prim 法

Prim 法

Prim 法

typedef int Weight;
struct Edge {  //src:辺の始点,dst:辺の終点,weight:辺の重さ
  int src, dst;
  Weight weight;
  Edge(int src, int dst, Weight weight) :
    src(src), dst(dst), weight(weight) { }
};
bool operator < (const Edge &e, const Edge &f) {
  return e.weight != f.weight ? e.weight > f.weight : //辺は重さが重いものを"小さい"と定義する
    e.src != f.src ? e.src < f.src : e.dst < f.dst;
}
typedef vector<Edge> Edges;
typedef vector<Edges> Graph;

//引数 g:隣接リスト,r:最小全域木の始点
//戻値 total:最小全域木の重み,T:最小全域木の辺集合
pair<Weight, Edges> prim(const Graph &g, int r = 0) {
  int n = g.size();
  Edges T;
  Weight total = 0;
  vector<bool> visited(n);
  priority_queue<Edge> Q;
  Q.push( Edge(-1, r, 0) );    //始め、-1とrを結ぶ辺があると考える
  while (!Q.empty()) {
    Edge e = Q.top(); Q.pop();
    if (visited[e.dst]) continue;
    T.push_back(e);
    total += e.weight;
    visited[e.dst] = true;
    for(auto f=g[e.dst].begin();f!=g[e.dst].end();++f) if(!visited[f->dst]) Q.push(*f);
  }
  return pair<Weight, Edges>(total, T);
}
int main(){
  // ...
  Graph g(v);    //頂点数vの空隣接リストgを生成
  // ...
  g[s].push_back(Edge(s,t,w));    //隣接リストgにsからtに向かう重さwの辺を追加
  // ...
  pair<Weight,Edges>ans=prim(g);    //ansにgに対しての最小全域木の重さと辺集合のpairが入る
  // ...
}

使い方

計算量

  • 最悪、すべての辺を1回ずつ追加 / 読み出しが行われる
    • priority-queue の追加 / 読み出し処理には1回あたり \( O(\log E)\) かかる
  • よって全体の計算量は \( O(E\log E)\)

Kruskal 法

  • 最小全域木を求めるアルゴリズムの一つ
  • 貪欲法の一種
  • 計算量は \( O(E \log V)\)

Kruskal 法

  1. グラフの各頂点がそれぞれの木に属するように,森(=木の集合) Fを生成する  (つまり頂点1個だけからなる木がV個だけ存在する)
  2. グラフの全ての辺を含む集合 Eを生成しソートされた状態にする
  3. Eが空集合になるまで,以下を繰り返す
    1. Eから重みが最小の辺 eを取り出す
    2. その辺 eと繋がっている二つの頂点 u, vが異なる木に属しているならば,辺 eを森 Fに加え,二つの木を連結し一つの木にまとめる
  4. 最終的に森 Fが最小全域木となる.

Kruskal

Union Find が使える!

Kruskal

Kruskal

Kruskal

Kruskal

Kruskal

Kruskal

Kruskal

Kruskal

Kruskal

typedef int Weight;
struct Edge {  //src:辺の始点,dst:辺の終点,weight:辺の重さ
  int src, dst;
  Weight weight;
  Edge(int src, int dst, Weight weight) :
    src(src), dst(dst), weight(weight) { }
};
bool operator < (const Edge &e, const Edge &f) {
  return e.weight != f.weight ? e.weight > f.weight : //辺は重さが重いものを"小さい"と定義する
    e.src != f.src ? e.src < f.src : e.dst < f.dst;
}
typedef vector<Edge> Edges;
typedef vector<Edges> Graph;

//引数 g:隣接リスト
//戻値 total:最小全域木の重み,F:最小全域木の辺集合
pair<Weight,Edges> kruskal(const Graph &g) {
  int n = g.size();
  priority_queue<Edge> Q;
  for(int u=0;u<n;u++){
    for(auto e=g[u].begin();e!=g[u].end();++e){
      if (u < e->dst) Q.push(*e);
    }
  }
  Weight total = 0;
  Edges F;
  while ((int)F.size() < n-1 && !Q.empty()) {
    Edge e = Q.top(); Q.pop();
    if (!same(e.src, e.dst)) {    //unionfindの関数
      F.push_back(e);
      total += e.weight;
      unite(e.src, e.dst);        //unionfindの関数
    }
  }
  return pair<Weight, Edges>(total, F);
}
int main(){
  // ...
     //UnionFindの準備(必要なら)
  // ...
  Graph g(v);    //頂点数vの空隣接リストgを生成
  // ...
  g[s].push_back(Edge(s,t,w));    //隣接リストgにsからtに向かう重さwの辺を追加
  // ...
  pair<Weight,Edges>ans=kruskal(g);    //ansにgに対しての最小全域木の重さと辺集合のpairが入る
  // ...
}

使い方

計算量

  • 辺のソート (priority-queueに追加) に\( O(E\log E)\)
  • すべての辺に対しUnion-Find が行われる \( O(E \alpha (E))\)
    • \(\alpha (x)\)はAckermannの逆関数(実質定数)
  • よって全体の計算量は \( O(E\log E)\)

最短経路問題

最短経路問題

  • 重み付きグラフの与えられた2つの頂点間を結ぶ経路の中で、重みが最小の経路を求める問題
  • 場合によってはその経路を聞かれることもある
  • ビラに載ってたやつですね

最短経路問題

  • アルゴリズム
    • Bellman-Ford
      • 単一の始点から他の全ての頂点
    • Dijkstra
      • 単一の始点から他の全ての頂点, 辺の重みが正
    • Warshall-Floyd
      • 全ての頂点の組に対して最短経路
    • 幅優先探索 (BFS)
      • コストが全部同じ + 単一の始点から

Bellman-Ford 法

Bellman-Ford 法

  • ある特定の頂点から全頂点までの最短距離を求める
  • 負辺 (重みが負の辺)が存在しても大丈夫
  • 負閉路 (和が負になるような閉路) の検出も可能
    • この閉路をぐるぐる周ればいくらでも距離を小さくできる
    • 最短経路は求められないが、これを検出することはできる
  • 計算量は \( O(VE)\)

Bellman-Ford 法

  • (u,v) の重みを wuv とする
  • まず、始点からの最短距離を入れる配列 d をつくる
  • 最初の段階で分かっていることは、始点から始点までの距離 0 ということだけ
    • 始点に 0 を入れておく
  • それ以外はとりあえず  を入れておく

Bellman-Ford 法

次の操作を |V|1 回繰り返す

  • グラフ上の各辺 (u,v)に対して、dv>du+wuv であるなら、 dv を du+wuv に 緩和する
    • いまのところ分かっている v までの最短距離 dv よりも、頂点 uから今見ている辺を使って v へ至る距離の方が近いなら、このルートを採用する

Bellman-Ford 法

Bellman-Ford 法

Bellman-Ford 法

Bellman-Ford 法

Bellman-Ford 法

Bellman-Ford 法

Bellman-Ford 法

Bellman-Ford 法

Bellman-Ford 法

Bellman-Ford 法

Bellman-Ford 法

Bellman-Ford 法

サンプルコード

const int INF = 7+(1e+9);
typedef int Weight;
struct Edge {  //src:辺の始点,dst:辺の終点,weight:辺の重さ
  int src, dst;
  Weight weight;
  Edge(int src, int dst, Weight weight) :
    src(src), dst(dst), weight(weight) { }
};
bool operator < (const Edge &e, const Edge &f) {
  return e.weight != f.weight ? e.weight > f.weight : //辺は重さが重いものを"小さい"と定義する
    e.src != f.src ? e.src < f.src : e.dst < f.dst;
}
typedef vector<Edge> Edges;
typedef vector<Edges> Graph;
//引数 g:隣接リスト,s:始点,dist:各頂点までの距離が入る(負閉路を含む場合,-INF),prev:最短路木の親頂点が入る
//戻値 負閉路が存在しない場合:true,そうでないとき:false
bool shortestPath(const Graph &g,int s,vector<Weight> &dist, vector<int> &prev) {
  int n = g.size();
  dist.assign(n, INF+INF); dist[s] = 0;
  prev.assign(n, -2);
  bool negative_cycle = false;
  for(int k=0;k<n;k++){
    for(int i=0;i<n;i++){
      for(auto e=g[i].begin();e!=g[i].end();e++){
        if(dist[e->dst] > dist[e->src] + e->weight) {
          dist[e->dst] = dist[e->src] + e->weight;
          prev[e->dst] = e->src;
          if (k == n-1) {
            dist[e->dst] = -INF;
            negative_cycle = true;
          }
        }
      }
    }
  }
  return !negative_cycle;
}

//引数 prev:最短路木の親頂点集合,t:終点
//戻値 path:sからtへの最短経路
vector<int> buildPath(const vector<int> &prev, int t) {
  vector<int> path;
  for (int u = t; u >= 0; u = prev[u])
    path.push_back(u);
  reverse(path.begin(), path.end());
  return path;
}
int main(){
  // ...
  Graph g(v);    //頂点数vの空隣接リストgを生成
  // ...
  g[s].push_back(Edge(s,t,w));    //隣接リストgにsからtに向かう重さwの辺を追加
  // ...
  vector<Weight> dist;
  vector<int> prev;
  bool negative_cycle=shortestPath(g,0,dist,prev);
  // ...
}

使い方

計算量

  • 各辺に対して緩和操作を行うので \(O(E)\)
  • その操作を V−1 回繰り返す
    • 全体の計算量は \(O(VE)\)

Dijkstra 法

Dijkstra 法

  • Dijkstra 法は、ある特定の頂点から全頂点までの最短距離を求めるアルゴリズム
  • 全ての辺の重みが正であるという条件の元で成立
    • 負の重みを持つグラフに対しては使えない!
      • ※全ての辺が正なので、負閉路は存在しえない。
  • 計算量は \(O(E \log V)\)

Dijkstra 法

  • (u,v) の重みを wuv とする
  • まず、始点からの最短距離を入れる配列 d をつくる
  • 最初の段階で分かっていることは、始点から始点までの距離 0 ということだけ
    • 始点に 0 を入れておく
  • それ以外はとりあえず  を入れておく

Dijkstra 法

  • まだ最短距離が確定していない頂点のうち、現在の最短距離推定値が最小な頂点を選ぶ
    • この頂点までの距離はここで確定
  • 一番最初は始点 (距離 0) が選ばれる
  • 今のところの推定値最小な頂点を選ぶので、「他の頂点を経由してこの頂点へ到達する方法」がより近くなるはずがない
  • もちろんこれは重みが負の辺が存在すると言えない
    • だからこそ正である必要がある
  • 最小な頂点を取り出すのには min-priority-queue を使う

Dijkstra 法

  • その頂点からの各辺 (u,v) について、 dv>du+wuv となるならば、
  1. dv を du+wuv に緩和
  2. 頂点 v を min-priority-queue へ追加

Dijkstra 法

Dijkstra 法

Dijkstra 法

Dijkstra 法

Dijkstra 法

Dijkstra 法

Dijkstra 法

サンプルコード

const int INF = 7+(1e+9);
typedef int Weight;
struct Edge {  //src:辺の始点,dst:辺の終点,weight:辺の重さ
  int src, dst;
  Weight weight;
  Edge(int src, int dst, Weight weight) :
    src(src), dst(dst), weight(weight) { }
};
bool operator < (const Edge &e, const Edge &f) {
  return e.weight != f.weight ? e.weight > f.weight : //辺は重さが重いものを"小さい"と定義する
    e.src != f.src ? e.src < f.src : e.dst < f.dst;
}
typedef vector<Edge> Edges;
typedef vector<Edges> Graph;
//引数 g:隣接リスト,s:始点,dist:各頂点までの距離が入る,prev:最短路木の親頂点が入る
//戻値 なし
void shortestPath(const Graph &g,int s,vector<Weight> &dist,vector<int> &prev) {
  int n = g.size();
  dist.assign(n, INF); dist[s] = 0;
  prev.assign(n, -1);
  priority_queue<Edge> Q;
  Q.push(Edge(-2, s, 0));
  while(!Q.empty()) {
    Edge e = Q.top(); Q.pop();
    if (prev[e.dst] != -1) continue;
    prev[e.dst] = e.src;
    for(auto f=g[e.dst].begin();f!=g[e.dst].end();f++){
      if (dist[f->dst] > e.weight+f->weight) {
        dist[f->dst] = e.weight+f->weight;
        Q.push(Edge(f->src, f->dst, e.weight+f->weight));
      }
    }
  }
}

//引数 prev:最短路木の親頂点集合,t:終点
//戻値 path:sからtへの最短経路
vector<int> buildPath(const vector<int> &prev, int t) {
  vector<int> path;
  for (int u = t; u >= 0; u = prev[u])
    path.push_back(u);
  reverse(path.begin(), path.end());
  return path;
}
int main(){
  // ...
  Graph g(v);    //頂点数vの空隣接リストgを生成
  // ...
  g[s].push_back(Edge(s,t,w));    //隣接リストgにsからtに向かう重さwの辺を追加
  // ...
  vector<Weight> dist;
  vector<int> prev;
  shortestPath(g,0,dist,prev);
  // ...
}

使い方

計算量

  • 既に決定した頂点へ向かう辺は必ず条件を見たさないので queue へ追加されることはない
  • 今見ている頂点は既に最短経路が決定している
  • つまり、今見ている頂点に再び戻ってくることはない
  • どの辺も高々 1 回 (無向辺の場合、両側から追加されうるので高々 2 回) だけ queue へ追加される
  • 全体で O(E logE)=O(E logV) になる

Warshall-Floyd 法

Warshall-Floyd 法

  • Warshall-Floyd 法は、グラフ上の任意の頂点から任意の頂点までの最短距離を求めるアルゴリズム
  • つまり、一回実行すれば、あとはどの 2 頂点を選んでも、その最短距離を O(1) で答えられる!
  • 恐ろしげに見えるが、実は他のどれよりも実装が簡単
  • なので、制約に余裕があるならたとえオーバースペックでも使ってよい
  • 計算量は \(O(V^3)\) 

Warshall-Floyd 法

  • まずグラフを隣接行列の形で表し、これを G とする
    • 辺がないところは  を入れる
    • 自分自身までの距離 Gii=0 とする
  • このグラフは最初隣り合う頂点以外は通れないことを表している。これを少しずつ緩和していく

Warshall-Floyd 法

  • 全ての頂点 k に対し、
    • 全ての頂点の組 (i,j) に対して、
      • 現段階の i, j の距離 Gij と、 kを経由した i, j の距離 Gik+Gkjの大小を比較する
      • 後者の方が短ければ Gij をその値に緩和する
  • for の三重ループでできる

負閉路検出

  • 負閉路があれば Gii<0 となるような i が存在する
  • もし負閉路がなければ最初に入れた Gii=0 が残っているはず

サンプルコード

const int INF = 7+(1e+9);
typedef int Weight;
struct Edge {  //src:辺の始点,dst:辺の終点,weight:辺の重さ
  int src, dst;
  Weight weight;
  Edge(int src, int dst, Weight weight) :
    src(src), dst(dst), weight(weight) { }
};
bool operator < (const Edge &e, const Edge &f) {
  return e.weight != f.weight ? e.weight > f.weight : //辺は重さが重いものを"小さい"と定義する
    e.src != f.src ? e.src < f.src : e.dst < f.dst;
}
typedef vector<Edge> Edges;
typedef vector<Edges> Graph;
//引数 Graph:隣接リスト,dist:各頂点から各頂点までの距離が入る,inter:最短路木の親頂点が入る
//戻値 負閉路が存在しない場合:true,そうでないとき:false
bool shortestPath(const Graph &g,vector<vector<Weight> > &dist,vector<vector<int> > &inter) {
  int n = g.size();
  dist.assign(n,vector<Weight>(n,INF));
  for(int i=0;i<n;i++){
    dist[i][i]=0;
  }
  for(int i=0;i<n;i++){
    for(auto f=g[i].begin();f!=g[i].end();f++){
      dist[i][f->dst]=f->weight;
    }
  }
  inter.assign(n, vector<int>(n,-1));
  for(int k=0;k<n;k++){
    for(int i=0;i<n;i++){
      for(int j=0;j<n;j++){
        if(dist[i][k]<INF && dist[k][j]<INF && dist[i][j]>dist[i][k]+dist[k][j]){
          dist[i][j] = dist[i][k] + dist[k][j];
          inter[i][j] = k;
        }
      }
    }
  }
  for(int i=0;i<n;i++){
    if(dist[i][i]<0){
      return false;
    }
  }
  return true;
}
void buildPath(const vector<vector<int> > &inter,int s,int t,vector<int> &path) {
  int u = inter[s][t];
  if (u < 0) path.push_back(s);
  else buildPath(inter, s, u, path), buildPath(inter, u, t, path);
}

//引数 inter:最短路木の親頂点,s:始点,t:終点
//戻値 path:sからtまでの最短経路
vector<int> buildPath(const vector<vector<int> > &inter,int s,int t) {
  vector<int> path;
  buildPath(inter, s, t, path);
  path.push_back(t);
  return path;
}
int main(){
  // ...
  Graph g(v);    //頂点数vの空隣接リストgを生成
  // ...
  g[s].push_back(Edge(s,t,w));    //隣接リストgにsからtに向かう重さwの辺を追加
  // ...
  vector<vector<Weight> >dist;
  vector<vector<int> > inter;
  bool negative_cycle=shortestPath(g,dist,inter);
  // ...
}

使い方

計算量

  • コードから明らか
  • 頂点数分の for を 3 重にまわすので \(O(V^3)\)

経路復元

経路復元

  • 基本的には最短距離を求めるだけでよいことが多い
  • しかし、場合によっては最短経路を求める必要がある

経路復元

  • それぞれのアルゴリズムで更新をするときに、「一つ前の頂点」を記録しておく
  • 後でそれをゴールから逆に辿っていけばよい
  • 基本的にどのアルゴリズムでも使える
  • Bellman-Ford と Dijkstra なら割と簡単
  • Warshall-Floyd に使うのが少し難しいかも
    • どこ始点かによって変わるので二次元配列になる

来週以降の予定

来週以降の予定

  • ICPCの問題を解いてもらいます
  • 最初のうちは1人で、チームが決まったらそのチームで解く
  • 再来週あたりにチーム決め兼,ICPC懇親会(ボドゲ大会)を開くかも
  • 組みたい人がいたら、誘っておいたほうが良さそう
    • 実力は同じぐらいが好ましい
      • やってて楽しい

ICPCの簡単な説明

  • 7月に京都大学構内で行われる
  • 同じ大学の3人チームを組む
  • 使える計算機,ディスプレイ,キーボードはそれぞれ一つずつ
    • 一人が入力している間、他の二人は考察
  • データの持ち込み禁止
    • 印刷したものは持ち込み可
  • 詳しい説明はまた今度

コンテスト

  • コンテストはAizu Online Judgeで行います
  • まだしてない人は会員登録をしましょう
  • 下記のURLにアクセスしてください

コンテスト

http://judge.u-aizu.ac.jp/onlinejudge/index.jsp?lang=ja

もしくは

「 AOJ 」で検索

  • 下記のURLにアクセスしてください
  • パスワードは "y" です
  • コンテスト開始までしばしお待ちください

コンテスト

終わり!

解説して欲しいものありますか?

ごはん食べよう!!!

参考文献

第8回:最小全域木, 最短経路問題

By procon2019

第8回:最小全域木, 最短経路問題

発表日時 2019年6月7日(金) 18:30-21:00 https://onlinejudge.u-aizu.ac.jp/beta/room.html#kmc2019_n_8

  • 886