競技プログラミング練習会
2020 Normal
第9回
最小全域木, 最短経路問題
担当 : zeke
自己紹介
自己紹介
- 京都大学理学部 2回生
- 本名:岡島 和希
- AtCoder:水色
- 競プロでは主にC++を使用
- 先週のABCで凡ミスをしてしまい、悔しさのあまり「さえかの」を一気見してしまった
- #zeke-memo
KMC-ID : aotsuki

slack(内部チャット)
皆さんの自己紹介もお願いします
- KMC-ID or 名前
- 所属(大学など)
- プログラミング経験や競プロの経験
- 何か言いたいこと
ささっと、やりましょう!
来週以降
どうしようか?
どうしようか?
- 大体のアルゴリズムは解説済み
- おそらくAtCoder緑までに必要な知識は全部伝えた(はず…?)
- 例年は大学生のためのICPCという大会があって、そのチーム決めなどをする
- しかし、今年はICPC国内予選が未定!!
どうしようか?
- なんかリクエスト(解説してほしい内容、コンテストの解説、精進etc)などがあったらほしいのです
- 今までの練習会でよくわからなったところを重点的に解説とか、直近のコンテストでよくわからなかったところを一緒に考える回とかもできそう…
今日の内容の前に
前回のABCについて
独り言
C問題について
int main() {
cin.tie(0);
ios::sync_with_stdio(false);
cout << fixed << setprecision(10);
ld a,b;
cin>>a>>b;
ld k=a*b;
cout<<(ll)k<<endl;
}Text
776013196293085 2.60
で落ちる
C問題について
ld EPS=0.001
int main() {
cin.tie(0);
ios::sync_with_stdio(false);
cout << fixed << setprecision(10);
ld a;
ld b;
cin>>a>>b;
cout<<(ll)(a*b+EPS)<<endl;
}AC
C問題について
int main() {
cin.tie(0);
ios::sync_with_stdio(false);
cout << fixed << setprecision(10);
ld a;
ld b;
cin>>a>>b;
ld c=b*100;
cout<<(ll)(a*c/100.00)<<endl;
}999990000000010 9.90
で落ちる
C問題について
ld EPS=0.001;
int main() {
cin.tie(0);
ios::sync_with_stdio(false);
cout << fixed << setprecision(10);
ld a;
ld b;
cin>>a>>b;
ll c=b*100.00+EPS;
cout<<(ll)(a*c/100)<<endl;
}AC
C問題について
int main() {
cin.tie(0);
ios::sync_with_stdio(false);
cout << fixed << setprecision(10);
ll a;
string b;
cin>>a>>b;
ll c=(b[0]-'0')*100+(b[2]-'0')*10+(b[3]-'0');
cout<<a*c/100<<endl;
}
AC
C問題について
ld EPS=0.001;
int main() {
cin.tie(0);
ios::sync_with_stdio(false);
cout << fixed << setprecision(10);
ld a;
ld b;
cin>>a>>b;
ll c=b*100.00+EPS;
cout<<a*c/100<<endl;
}全ケースで落ちる!!
なぜ誤差が湧くのか?
今日の内容
今日の内容
- 最小全域木
- 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 法
-
頂点, 辺を入れるための集合V, Eを用意する(どちらも空)
- グラフから適当に一つの頂点を選び, Vに加える
- Vがグラフのすべての頂点を含むまで, 以下を繰り返す
- Vに含まれる頂点 uと含まれない頂点 vを結ぶ重みが最小の辺 (u,v)をグラフから選び, Eに加える
- vをVに加える
- 最終的にグラフ(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 法
- グラフの各頂点がそれぞれの木に属するように,森(=木の集合) Fを生成する (つまり頂点1個だけからなる木がV個だけ存在する)
- グラフの全ての辺を含む集合 Eを生成し,ソートされた状態にする
- Eが空集合になるまで,以下を繰り返す
- Eから重みが最小の辺 eを取り出す
- その辺 eと繋がっている二つの頂点 u, vが異なる木に属しているならば,辺 eを森 Fに加え,二つの木を連結し一つの木にまとめる
- 最終的に森 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)\)
最短経路問題
最短経路問題
- 以前、BFSで迷路の経路探索のようなものをやった
- それはグラフの頂点間の重みがすべて1の時
- 迷路のような問題で、最短距離とそこまでの経路を求めることができる
最短経路問題
- 重み付きグラフの与えられた2つの頂点間を結ぶ経路の中で、重みが最小の経路を求める問題(重みが1とは限らない)
- 場合によってはその経路を聞かれることもある
最短経路問題
- アルゴリズム
-
Bellman-Ford 法
- 単一の始点から他の全ての頂点
-
Dijkstra 法
- 単一の始点から他の全ての頂点, 辺の重みが正
-
Warshall-Floyd 法
- 全ての頂点の組に対して最短経路
-
幅優先探索 (BFS)
- コストが全部同じ + 単一の始点から
-
Bellman-Ford 法
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 となるならば、
- dv を du+wuv に緩和
- 頂点 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);
//distの中身が始点からの最短距離になる
// ...
}使い方
計算量
- 既に決定した頂点へ向かう辺は必ず条件を見たさないので 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 をその値に緩和する
- 全ての頂点の組 (i,j) に対して、
- 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 に使うのが少し難しいかも
- どこ始点かによって変わるので二次元配列になる
コンテスト
終わり!
解説して欲しいものありますか?
ごはん食べよう!!!
参考文献
Copy of 第8回:最小全域木, 最短経路問題
By kmc_procon2020
Copy of 第8回:最小全域木, 最短経路問題
発表日時 2019年6月7日(金) 18:30-21:00 https://onlinejudge.u-aizu.ac.jp/beta/room.html#kmc2019_n_8
- 153