gracetory’s blog

東池袋にある合同会社グレストリのエンジニアブログです

世界遺産を巡るルートを考えてみた(巡回セールスマン問題)

f:id:grnishi:20201002155453p:plain

はじめに

もう10月です。10月は旧暦で「神無月」と言いますが、由来は八百万の神が出雲大社に出かけているので神様がいなくなることからこのように言われています。ちなみに出雲地方では10月は「神在月」と言います。

本題

COVID-19の影響で外出が難しい日々が続いています。そういえば私は旅行が好きだという事を忘れていました(忘れる事では無い)

いつか世界遺産を全部巡りたいなーと思いながら今日に至るわけですが、試しに日本の世界遺産をすべて巡るにはどういうルートが良いか知りたくなりました。

先日、巡回セールスマン問題の新しいアルゴリズムが提案されていました。

www.quantamagazine.org

有効性のほどはさておき、新しいアルゴリズムが出てくるのは何となくわくわくするものです。

というわけで巡回セールスマン問題を解くように世界遺産を巡る旅に出たいと思います。

巡回セールスマン問題とは

私が説明するよりも遥かに詳しいwikipediaさんを参照してください。

ja.wikipedia.org

世界遺産の一覧と場所

世界遺産と言っても「姫路城」のような場所が特定できるものから、「明治日本の産業革命遺産」のように色んな場所に分散しているものがあります。

という事で下記のGoogle Places APIに世界遺産名称を入れて緯度経度を決めました。

developers.google.com

世界遺産名称 緯度経度 GoogleMapへのリンク
法隆寺地域の仏教建造物 34.6140766,135.7356826 GoogleMap
姫路城 34.839449,134.6939047 GoogleMap
屋久島 30.3445936,130.5127142 GoogleMap
白神山地 40.4568626,140.1645095 GoogleMap
古都京都の文化財
白川郷・五箇山の合掌造り集落 36.2577967,136.9061975 GoogleMap
原爆ドーム 34.395483,132.453592 GoogleMap
厳島神社 34.2959885,132.3198262 GoogleMap
古都奈良の文化財 34.6889918,135.8398589 GoogleMap
日光の社寺 36.75535,139.5973058 GoogleMap
琉球王国のグスク及び関連遺産群
紀伊山地の霊場と参詣道
知床 44.1996561,145.2396745 GoogleMap
石見銀山遺跡とその文化的景観 35.105142,132.438759 GoogleMap
小笠原諸島 26.9641919,142.1063337 GoogleMap
平泉 38.9866085,141.1137868 GoogleMap
富士山 35.3606255,138.7273634 GoogleMap
富岡製糸場と絹産業遺産群 36.255121,138.8874275 GoogleMap
明治日本の産業革命遺産 32.7640423,129.8629041 GoogleMap
国立西洋美術館本館 35.7158676,139.7761806 GoogleMap
「神宿る島」宗像・沖ノ島と関連遺産群
長崎と天草地方の潜伏キリシタン関連遺産 32.7433902,129.8706662 GoogleMap
百舌鳥・古市古墳群 34.5472489,135.4964928 GoogleMap

※古都京都の文化財、琉球王国のグスク及び関連遺産群、紀伊山地の霊場と参詣道、「神宿る島」宗像・沖ノ島と関連遺産群の4つはAPIは結果が返ってきませんでした。

※※小笠原諸島は海の上でした。

使い物になりませんでしたが一応ソースコードを。

<?php
    $heritage_list = [
        "法隆寺地域の仏教建造物",
        "姫路城",
        "屋久島",
        "白神山地",
        "古都京都の文化財",
        "白川郷・五箇山の合掌造り集落",
        "原爆ドーム",
        "厳島神社",
        "古都奈良の文化財",
        "日光の社寺",
        "琉球王国のグスク及び関連遺産群",
        "紀伊山地の霊場と参詣道",
        "知床",
        "石見銀山遺跡とその文化的景観",
        "小笠原諸島",
        "平泉",
        "富士山",
        "富岡製糸場と絹産業遺産群",
        "明治日本の産業革命遺産",
        "国立西洋美術館本館",
        "「神宿る島」宗像・沖ノ島と関連遺産群",
        "長崎と天草地方の潜伏キリシタン関連遺産",
        "百舌鳥・古市古墳群",
    ];

    $apikey = "Your API key";

    foreach ($heritage_list as $heritage_name) {
        $url = "https://maps.googleapis.com/maps/api/place/findplacefromtext/json?input=" . $heritage_name . "&inputtype=textquery&fields=geometry&key=" . $apikey;

        $handler = curl_init();
        curl_setopt($handler, CURLOPT_URL, $url);
        curl_setopt($handler, CURLOPT_HTTPGET, true);
        curl_setopt($handler, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($handler, CURLOPT_HEADER, true);
        $response = curl_exec($handler);
        $header_size = curl_getinfo($handler, CURLINFO_HEADER_SIZE);
        $body = substr($response, $header_size);
        $location = json_decode($body,true)["candidates"][0]["geometry"]["location"];

        echo $heritage_name . " " . $location["lat"] . " " . $location["lng"] . "\n";
    }

というわけで以下のように場所を調整しました

世界遺産名称 緯度経度 GoogleMapへのリンク
法隆寺 34.6140766,135.7356826 GoogleMap
姫路城 34.839449,134.6939047 GoogleMap
屋久島世界遺産センター 30.3087865,130.6316354 GoogleMap
白神山地ビジターセンター 40.5787865,140.2986528 GoogleMap
元離宮二条城 35.0142299,135.748218 GoogleMap
白川郷観光協会 36.2619973,136.9068822 GoogleMap
原爆ドーム 34.395483,132.453592 GoogleMap
厳島神社 34.2959885,132.3198262 GoogleMap
東大寺 34.6889851,135.8398158 GoogleMap
日光東照宮 36.7578036,139.5993576 GoogleMap
首里城 26.2183181,127.7153514 GoogleMap
熊野本宮大社 33.8405706,135.7734753 GoogleMap
知床自然センター 44.0915368,145.0229217 GoogleMap
石見銀山 35.105142,132.438759 GoogleMap
小笠原世界遺産センター 27.0939,142.1910567 GoogleMap
中尊寺金色堂 39.0013596,141.0998908 GoogleMap
富士山総合指導センター 35.3365541,138.7338886 GoogleMap
富岡製糸場 36.255121,138.8874275 GoogleMap
旧集成館反射炉跡 31.6179619,130.5769884 GoogleMap
国立西洋美術館本館 35.7158676,139.7761806 GoogleMap
中津宮 33.8973186,130.4318567 GoogleMap
大浦天主堂 32.7341535,129.8701372 GoogleMap
百舌鳥古墳群 34.5472489,135.4964928 GoogleMap

※なお、開始地点は会社の住所とします。

コスト

時間なのか距離なのか、はたまた金額なのか何をコストとして解を求めるかですが、とりあえず単純な距離でいいかな。

どうやって距離を計算しようかなと色々考えたのですが、めんどくさいので直線距離とする事にします。計算にはヒュベニの距離計算式を用いました。

www.kashmir3d.com

<?php
    function distance($lat1, $lng1, $lat2, $lng2) {
        // ラジアンに変換
        $rad_lat1 = deg2rad($lat1);
        $rad_lng1 = deg2rad($lng1);
        $rad_lat2 = deg2rad($lat2);
        $rad_lng2 = deg2rad($lng2);

        // 平均緯度
        $p = ($rad_lat1 + $rad_lat2) / 2.0;
        // 緯度差
        $dp = $rad_lat1 - $rad_lat2;
        // 経度差
        $dr = $rad_lng1 - $rad_lng2;
        // 子午線曲率半径
        $m = 6334834 / sqrt((1 - 0.006674 * sin($p) * sin($p))^3);
        // 卯酉線曲率半径
        $n = 6377397 / sqrt(1 - 0.006674 * sin($p) * sin($p));

        // 距離計算
        $d = sqrt(($m*$dp)*($m*$dp)+($n * cos($p) * $dr) * ($n * cos($p) * $dr));
        return $d;
    }

データ

データは2次元配列で、

$data[0][1] = 1000;
$data[0][2] = 1100;
$data[0][3] = 1200;
~~~中略~~~
$data[1][2] = 1300;
$data[1][3] = 1400;
$data[1][4] = 1500;
~~~後略~~~

といった感じのデータで予め計算しておきjsonで保存しておきます。

A→BもB→Aも同じ距離としています。

では始めます。

最近傍法(Nearest Neighbor法)

現在の地点から一番近い場所を選んで行く方法です。いわゆる貪欲法と呼ばれるもので単純です。

<?php
    $heritage_data = json_decode(file_get_contents("heritage_data.json"), true);
    $distance_data = json_decode(file_get_contents("distance_data.json"), true);

    // 結果格納用配列
    $result = [];

    // 最初の場所
    $current_idx = 0;
    // 最初の地点は会社の住所
    $current = array_pick($heritage_data, $current_idx);
    // 最初の地点を保存しておく
    $start = $current;
    // 最初の場所を結果に入れる
    array_push($result, $current);
    while (true) {
        // 全部終わったら終了
        if (!$heritage_data) break;

        // 一番近い場所を探す
        $current_idx = search_nearest_spot($distance_data, $current_idx, $heritage_data);
        $current = array_pick($heritage_data, $current_idx);
        array_push($result, $current);
    }
    // 最初の地点を最後に入れる
    array_push($result, $start);

    // 一番近い場所を探す
    function search_nearest_spot($distance_data, $current_idx, $heritage_data) {
        $min_distance = INF;
        $min_idx = -1;
        foreach ($heritage_data as $idx => $data) {
            if ($current_idx > $idx) {
                $distance = $distance_data[$idx][$current_idx];
            } else {
                $distance = $distance_data[$current_idx][$idx];
            }
            if ($min_distance > $distance) {
                $min_distance = $distance;
                $min_idx = $idx;
            }
        }

        return $min_idx;
    }

    // 配列の任意のキーを取り出して削除する
    function array_pick(&$array, $idx) {
        $d = $array[$idx];
        unset($array[$idx]);
        return $d;
    }
会社
国立西洋美術館本館
日光東照宮
富岡製糸場
富士山総合指導センター
白川郷観光協会
元離宮二条城
東大寺
法隆寺
百舌鳥古墳群
熊野本宮大社
姫路城
石見銀山
原爆ドーム
厳島神社
中津宮
大浦天主堂
旧集成館反射炉跡
屋久島世界遺産センター
首里城
小笠原世界遺産センター
中尊寺金色堂
白神山地ビジターセンター
知床自然センター
会社

総コスト: 5319296.9714737

ふむ。首里城から小笠原諸島、小笠原諸島から中尊寺、最後の知床から戻ってくるあたりが厳しいルートになりましたね。

Random Insertion法

未訪問の場所を一つ選んで現在の巡回ルートの中で一番距離の短い場所に挿入して巡回ルートを更新していく方法です。

未訪問の場所の選び方をRandomにするのがRandom Insertion法です。

 function ri($distance_data, $heritage_data) {
        $result = [];
        $count = count($result);

        // 最初の場所
        $current_idx = 0;
        // 最初の地点は会社の住所
        $current = array_pick($heritage_data, $current_idx);
        // 最初の地点を保存しておく
        $start = $current;
        // 最初の場所を結果に入れる
        array_push($result, $current);

        while (true) {
            // 未訪問の都市が無くなったら終了
            if (count($heritage_data) == 0) break;

            // ランダムに一つ選択
            $key = array_rand($heritage_data);

            $insert_position = search_position($heritage_data[$key]["idx"], $result, $distance_data);
            array_splice($result, $insert_position, 0, [$heritage_data[$key]]);

            unset($heritage_data[$key]);
        }

        // 最初の地点を最後に入れる
        array_push($result, $start);

        return $result;
    }

    function search_position($spot, $result, $distance_data) {
        // 現在の完成しているルートの数
        $count = count($result);
        // 完成しているルートの最後からの距離を最小としておく
        $min_distance = get_distance($distance_data, $result[$count-1]["idx"], $spot);
        $position = $count;
        for ($i = 1; $i < $count; $i++) {
            $new_dist = get_distance($distance_data, $result[$i-1]["idx"], $spot) + get_distance($distance_data, $result[$i]["idx"], $spot) - get_distance($distance_data, $result[$i-1]["idx"], $result[$i]["idx"]);
            if ($new_dist < $min_distance) {
                $min_distance = $new_dist;
                $position = $i;
            }
        }
        return $position;
    }

    function get_distance($distance_data, $i, $j) {
        if ($i > $j) {
            return $distance_data[$j][$i];
        } else {
            return $distance_data[$i][$j];
        }
    }
会社
国立西洋美術館本館
日光東照宮
中尊寺金色堂
知床自然センター
白神山地ビジターセンター
富岡製糸場
富士山総合指導センター
白川郷観光協会
元離宮二条城
東大寺
法隆寺
熊野本宮大社
百舌鳥古墳群
姫路城
石見銀山
原爆ドーム
厳島神社
中津宮
大浦天主堂
旧集成館反射炉跡
屋久島世界遺産センター
首里城
小笠原世界遺産センター
会社
総コスト: 5200741.949292

あんまり変わっていませんね。

2-opt法

ある巡回路に対して、適当な2つの辺を入れ替えた際のコストを計算し、そのコストが小さくなる場合は入れ替える。それを入れ替えが無くなるまで繰り返します。

辺を3つにする3-opt法、4つにする4-opt法などがあります。

2-opt法は最初に巡回路を決めておく必要があるので、前述の最近傍法で求めたものを初期の巡回路としています。

 // 2-opt法
    function opt2($result, $distance_data, $heritage_data) {
        $count = count($result);
        while (true) {
            $t = 0;
            for ($i = 0; $i < ($count-2); $i++) {
                $ii = $i + 1;
                for ($j = $i + 2; $j < ($count-1); $j++) {
                    if ($j == ($count - 1)) {
                        $jj = 0;
                    } else {
                        $jj = $j + 1;
                    }
                    if ($i != 0 || $j != 0) {
                        $dist1 = get_distance($distance_data, $result[$i]["idx"], $result[$ii]["idx"]);
                        $dist2 = get_distance($distance_data, $result[$j]["idx"], $result[$jj]["idx"]);
                        $dist3 = get_distance($distance_data, $result[$i]["idx"], $result[$j]["idx"]);
                        $dist4 = get_distance($distance_data, $result[$ii]["idx"], $result[$jj]["idx"]);
                        if (($dist1+$dist2) > ($dist3+$dist4)) {
                            $reversed_route = array_reverse(array_slice($result, $ii, $j - $i));
                            array_splice($result,$ii, $j - $i, $reversed_route);
                            $t = 1;
                        }
                    }
                }
            }
            if ($t == 0) break;
        }
        return $result;
    }
会社
国立西洋美術館本館
小笠原世界遺産センター
首里城
屋久島世界遺産センター
旧集成館反射炉跡
大浦天主堂
中津宮
厳島神社
原爆ドーム
石見銀山
姫路城
百舌鳥古墳群
熊野本宮大社
法隆寺
東大寺
元離宮二条城
白川郷観光協会
富士山総合指導センター
富岡製糸場
日光東照宮
白神山地ビジターセンター
知床自然センター
中尊寺金色堂
会社

総コスト: 5197746.2476218

交差も無くなりましたしなんとなく改善した気がします。小笠原諸島から首里城は遠いなー。そもそも小笠原諸島が遠いんですが。

他にも

貪欲法、動的計画法、遺伝的アルゴリズム、Simulated Annealingなどなどバリエーションは豊富でアルゴリズムの勉強にもよく用いられています。

最後に

世界遺産を巡るルートを巡回セールスマン問題としていくつかの方法で解いてみました。

数がそれほどでもないですし結構場所がばらけている事からこれといって結果に差はありませんでした。

夏休みのスタンプラリーなんかをこうやって解いてみると面白いかもしれません。