boost.graphでjr全線乗り尽くしプランを立てる -...
TRANSCRIPT
プログラミング生放送+CLR/H+Sapporo.cpp 勉強会@札幌(2014.7.12)
Boost.GraphでJR全線乗り尽くしプラン
を立てる
H.Hiro @ Sapporo.cppTwitter: @h_hiro_
http://hhiro.net/about/
自己紹介
●某研究員(もう学生じゃありません)
●アルゴリズム作るのは本業です●趣味でもプログラム書いてます●C++とかRubyとか他にも何でも●最近はアマチュアサッカーの観戦とかも
ソースコードはこちらです
https://github.com/maraigue/cpp-chinese-postman
今回の内容
JR線全線を乗り尽くす
地図:国土数値情報 鉄道データ N02-08(2008年現在;JR以外の鉄道も入ってます)
鉄道ファンにとって一つのステータス
これをなるべく短い乗車距離で
実現したい
今回のルール
●単に乗り尽くしたら終わりではなく、「出発地点まで帰ってくる」までの乗車距離で考える
●出発地点まで戻るときも含めJR以外の交通機関は利用しないとする
予備知識
グラフ理論
●グラフ:状態(頂点 vertex, node)と、二つの状態を結ぶもの(辺 edge)からなるデータ構造(今回は駅が頂点、路線が辺)
●各辺には、通ることで加算される利益や損失の数値(重み)を与えることができる(今回は距離を重みとし損失とみなす)
桑園 札幌 苗穂 白石 平和
厚別八軒
琴似
22 16 22 36
4422
22
(辺の重みに書かれた 距離の単位は「0.1km」)
補足●今回は、JR線が3方向以上に分岐している駅のみ頂点とする。
●今回は、辺は両方向に行き来できるもののみ扱う(無向グラフ undirected graph)。一方通行は考えない
桑園 札幌 苗穂 白石 平和
厚別八軒
琴似
22 16 22 36
4422
22
(辺の重みに書かれた 距離の単位は「0.1km」)
Boost.Graph
Boost.Graph:グラフ構造を扱うライブラリ// 無向グラフを扱うために必要なものをインクルード#include <boost/graph/undirected_graph.hpp>// グラフの型の定義。// 頂点から出る辺の一覧はstd::vector、頂点の一覧はstd::set、// 無向グラフ、頂点には駅名、辺にはint型で重みを付与typedef boost::adjacency_list<boost::vecS, boost::setS,
boost::undirectedS,boost::property<boost::vertex_name_t, std::string>,boost::property<boost::edge_weight_t, int> > Graph;
// 続く
Boost.Graph:グラフ構造を扱うライブラリ// 続きint main(void){ Graph g; // 頂点を追加 vertex_descriptor v1 = boost::add_vertex("桑園", g); vertex_descriptor v2 = boost::add_vertex("白石", g); // 辺を追加 // 第3パラメータは辺の属性(ここでは重み=距離) edge_descriptor e1 = boost::add_edge(v1, v2, 74, g);}
実際のコードを見てみましょう
// 駅名をキー、頂点を値とする連想配列std::map<std::string, RouteNetwork::vertex_descriptor> names2vertices;
while(!(ifs.eof())){ // (中略:ここで行を読み込む) // s[0] と s[1] に地点名が、 distance に距離が入る // 頂点を追加(まだ存在していないなら) for(size_t i = 0; i <= 1; ++i){ it = names2vertices.find(s[i]); if(it == names2vertices.end()){ vd[i] = boost::add_vertex(s[i], *this); names2vertices.insert(std::make_pair(s[i], vd[i])); }else{ vd[i] = it->second; } } // 辺を追加 boost::add_edge(vd[0], vd[1], distance, *this); total_distance += distance;}
今回の問題設定
今回の問題は、グラフ理論の用語で言えば●無向グラフが与えられたとき●すべての辺を少なくとも1回通る始点と終点が同一である経路で
●経路の辺の重みの総和が最小になるものを求めよ。
ちなみにこの問題は「中国人郵便配達問題」って名前が付いてます
なお、Wikipediaの「中国人郵便配達問題」を書いた"Sinryow"は
私です
もう少しグラフ理論について
必要なことを解説します
今回用いるグラフ理論の概念(1)
橋
225
34
236
628
橋:その辺1本が消えることで路線網が分断される(非連結になる)ような辺
函館
大沼
森 長万部
1256五稜郭
中小国
↓青森
↑三厩
↑桑園
↓東室蘭
353 ━━━━ :橋━━━━ :橋でない
橋:その辺1本が消えることで路線網が分断される(非連結になる)ような辺➔橋は明らかに2回通らないとならない(全路線を通って出発地点に 戻る必要があるため)
今回用いるグラフ理論の概念(2)
オイラーグラフ
オイラーグラフ:スタート地点から一筆書き(全ての辺をちょうど1回ずつ通る経路)でスタート地点に戻ることのできるグラフオイラーグラフでない(一筆書きはできるけど) オイラーグラフである
なぜオイラーグラフを考える?➔もしJRの路線網がオイラーグラフなら、単に一筆書きになる(各区間1回ずつ通る)ように辿るのが最短経路なのは明らか。
➔ただ現実には、2回通らないとならない区間が存在する。【補足】いかなる区間も1回か2回通ればよく、3回通る必要はない。(証明は省略)
なぜオイラーグラフを考える?➔2回通ったところに辺が2本存在すると仮定してオイラーグラフになれば全線乗り尽して出発地点に戻れる。
➔2回通る区間を極力少なくすればよい。
オイラーグラフでない オイラーグラフである
ここを2本に増やす
オイラーグラフの特性●グラフのすべての頂点について、繋がっている辺の数(次数)が偶数ならばオイラーグラフである。逆も成り立つ。
●一部の辺を2本に増やし、上記の条件を満たせるようにすればよい。オイラーグラフでない オイラーグラフである
次数3
次数3
次数2
次数2
次数2 次数2
次数2
次数2
次数4
次数4
問題の解き方
1.橋を見つけて個別のグラフに切り離す。橋は「2回通る」と結論付ける。
函館
五稜郭
中小国
大沼
森
長万部
室蘭東室蘭
苫小牧
沼ノ端追分
新得夕張新夕張
東釧路
根室南千歳
新千歳空港
桑園 白石
新十津川
増毛
滝川
深川
旭川 新旭川
稚内
富良野
様似
岩見沢
1.橋を見つけて個別のグラフに切り離す。橋は「2回通る」と結論付ける。
函館
五稜郭
中小国
大沼
森
長万部
室蘭東室蘭
苫小牧
沼ノ端追分
新得夕張新夕張
東釧路
根室南千歳
新千歳空港
桑園 白石
新十津川
増毛
滝川
深川
旭川 新旭川
稚内
富良野
様似
岩見沢
橋を検出するアルゴリズムhttp://nupioca.hatenadiary.jp/entry/2013/11/03/200006●アルゴリズム自体はシンプルです●逆に、何でこのアルゴリズムでうまくいくのかが不思議です(私も理解してません)
2.次数2以下の頂点を削除し辺を統合する。(やらなくてもいいけど効率化のために)
894 新得254 新夕張追分
↓沼ノ端
↑岩見沢↑富良野
↓東釧路
←南千歳
夕張
橋のため削除
2.次数2以下の頂点を削除し辺を統合する。(やらなくてもいいけど効率化のために)
新得254+894=1148追分
↓沼ノ端
↑岩見沢↑富良野
↓東釧路
←南千歳
夕張
橋のため削除
2.次数2以下の頂点を削除し辺を統合する。(やらなくてもいいけど効率化のために)
函館
五稜郭
中小国
大沼
森
長万部
室蘭東室蘭
苫小牧
沼ノ端追分
新得夕張新夕張
東釧路
根室南千歳
新千歳空港
桑園 白石
新十津川
増毛
滝川
深川
旭川 新旭川
稚内
富良野
様似
岩見沢
2.次数2以下の頂点を削除し辺を統合する。(やらなくてもいいけど効率化のために)
沼ノ端追分
新得南千歳
白石
滝川旭川
富良野
岩見沢
Boost.Graphだとこんな具合// ---------- 変数宣言類は大幅に省略してます ----------std::pair<vertex_iterator, vertex_iterator> vertex_range = boost::vertices(*this);
for(vertex_iterator itv = vertex_range.first; itv != vertex_range.second; ++itv){ if(out_degree(*itv, *this) == 2){ // 次数が2の頂点があったら // 隣接する頂点と辺を覚えておき std::pair<out_edge_iterator, out_edge_iterator> edge_range = boost::out_edges(*itv, *this); for(out_edge_iterator ite = edge_range.first; ite != edge_range.second; ++ite, ++i){ sides[i] = vertex_target(*itv, *ite); distance += boost::get(boost::edge_weight, *this, *ite); links[i] = *ite; } // 元の辺を取り除くとともに新しく辺を加える boost::add_edge(sides[0], sides[1], distance, *this); boost::remove_edge(links[0], *this); boost::remove_edge(links[1], *this); }}
3. (どこの辺を2本に増やすか決めたい)
分割された各グラフについて、「すべての頂点の組に対する最短距離」を求める
白 岩 滝 旭 富 新 南 沼 追
白
岩
滝
旭
富
新
南
沼
追沼ノ端
追分
新得南千歳
白石
滝川旭川
富良野
岩見沢
白 岩 滝 旭 富 新 南 沼 追
白
岩
滝
旭
富
新
南
沼
追
この問題には「フロイド=ワーシャル法」という有名なアルゴリズムがあってBoost.Graphにも入っていますboost::floyd_warshall_all_pairs_shortest_paths(graph, result);resultは「result[vertex1][vertex2] としたときに辺の重みが返る」ものなら何でもよい。例えばstd::map< vertex_descriptor,std::map<vertex_descriptor, int> > (辺の重みがintの場合)
沼ノ端追分
新得南千歳
白石
滝川旭川
富良野
岩見沢
4. (どこの辺を2本に増やすか決めたい)
次数が奇数の頂点を2つずつ組み合わせ、さっき求めた距離の和が最小になるものを見つける
沼ノ端追分
新得南千歳
白石
滝川旭川
富良野
岩見沢
●(白石, 旭川)・(南千歳, 新得)・(沼ノ端, 岩見沢)・(滝川, 富良野)
●(白石, 南千歳)・(沼ノ端, 岩見沢)・(滝川, 新得)・(旭川, 富良野)…
4. (どこの辺を2本に増やすか決めたい)
次数が奇数の頂点を2つずつ組み合わせ、さっき求めた距離の和が最小になるものを見つける
沼ノ端追分
新得南千歳
白石
滝川旭川
富良野
岩見沢
最適な組み合わせ:(白石, 岩見沢)・(南千歳, 沼ノ端)・(滝川, 旭川)・(富良野, 新得)
最適な「2つずつの組み合わせ方」を求めるのは面倒
●全通り組み合わせてみるのは、組み合わせ爆発を起こしてしまう
●「整数計画問題」という形に落とせば、ライブラリに食わせて解ける→今回は"GLPK"を使います
整数計画問題●変数に整数しか入らず、かつ指定された制約式を満たすものの中で特定の式を最大化/最小化する問題制約式・最大化/最小化する式には和・定数倍・等式/不等式のみ利用可能
●変数が多いと一般には低速(組み合わせ爆発が起きる)だが、多くの場合に高速
●今回の場合は、頂点数の3乗に比例する程度の時間で解ける(っぽい)
●GLPK:これを解いてくれるライブラリ
どう整数計画問題にするのか●変数:2つの駅の組み合わせ(使ったもの:1、使わなかったもの:0)
●制約式:どの駅も1回しか登場しない●最小化すべきもの:組み合わせにより生じる移動距離の総和
最小化すべき式:777×白石滝川+348×白石岩見沢+…// 「組の距離×使った辺」を最小にする。各組の距離はさっき求めた0≦白石滝川≦1、0≦白石岩見沢≦1、0≦白石沼ノ端≦1、…// 各組み合わせは「使うか使わないか」の二つだけ白石滝川+白石岩見沢+白石沼ノ端+…=1白石滝川+滝川岩見沢+滝川沼ノ端+…=1// 各駅とも、1回の組み合わせにしか登場しない
GLPKに解かせた結果(JR北海道分)
沼ノ端
追分
新得南千歳
白石
滝川旭川
富良野
岩見沢
GLPKに解かせた結果(JR全国分)
GLPKに解かせた結果(JR全国分)
メモリ不足で解けませんでした…。頂点数140くらい、考えるべき辺が10,000組くらい
最終結果:JR北海道全線(橋と、最小距離組み合わせの結果出た辺のみ2回通ればよい)
函館
五稜郭
中小国
大沼
森
長万部
室蘭東室蘭
苫小牧
沼ノ端追分
新得夕張新夕張
東釧路
根室南千歳
新千歳空港
桑園 白石
新十津川
増毛
滝川
深川
旭川 新旭川
稚内
富良野
様似
岩見沢
━━━━:2回通る━━━━:1回通る
JR北海道の総距離2457.7km(営業キロ)左図の総乗車距離3565.0km(同)総距離の145%
最終結果:JR北海道全線(橋と、最小距離組み合わせの結果出た辺のみ2回通ればよい)
函館
五稜郭
中小国
大沼
森
長万部
室蘭東室蘭
苫小牧
沼ノ端追分
新得夕張新夕張
東釧路
根室南千歳
新千歳空港
桑園 白石
新十津川
増毛
滝川
深川
旭川 新旭川
稚内
富良野
様似
岩見沢
一筆書き経路の例白石→桑園→新十津川→桑園→長万部→森→大沼→五稜郭→函館→五稜郭→中小国→五稜郭→大沼→森→長万部→東室蘭→室蘭→東室蘭→苫小牧→様似→苫小牧→沼ノ端→追分→岩見沢→白石→南千歳→沼ノ端→南千歳→新千歳空港→南千歳→追分→新夕張→夕張→新夕張→新得→東釧路→根室→東釧路→新旭川→稚内→新旭川→旭川→深川→増毛→深川→滝川→富良野→新得→富良野→旭川→深川→滝川→岩見沢→白石
補足:●「2回通る必要のある辺」が求められれば、
実際にどの経路で辿ればよいかは勝手に決まる。
●オイラーグラフの特徴として、「辿れなくなる頂点が存在しなくならない限りは、適当に辿っていても一筆書きになる」というものがあるため。
実際にプログラムで解いてみます
参考文献・資料:●アルゴリズムの大枠の解説:「経営科学OR用語大事典」Saul I. Gass、Carl M. Harris森村英典[監訳]、刀根薫[監訳]、伊理正夫[監訳]朝倉書店 ISBN 4254121318
●グラフ理論の基礎:「グラフ理論入門(原書第4版)」Robin J. Wilson、西関隆夫[訳]・西関裕子[訳]近代科学社 ISBN 4764902966参考資料:http://ocw.hokudai.ac.jp/Course/Faculty/Engineering/GraphTheory/2007/
参考文献・資料:●もっと難しい(理論的に考えたときに計算時間がかかる)問題:「JRの切符として買える最長のものを求める」(最長片道切符)http://www.swa.gr.jp/lop/
ありがとうございました