boost.graphでjr全線乗り尽くしプランを立てる -...

Post on 13-Jun-2015

2.188 Views

Category:

Technology

2 Downloads

Preview:

Click to see full reader

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/

ありがとうございました

top related