1. paizaラーニングトップ
  2. レベルアップ問題集
  3. 巡回セールスマン問題メニュー(言語選択)
  4. 問題一覧 Ruby編
  5. 焼きなまし法によるTSP Ruby編

巡回セールスマン問題メニューのサムネイル
焼きなまし法によるTSP Ruby編(paizaランク S 相当)

問題にチャレンジして、ユーザー同士で解答を教え合ったり、コードを公開してみよう!

問題

下記の問題をプログラミングしてみよう!

巡回セールスマン問題とは、都市の集合と各都市間の距離が与えられ、全都市をちょうど1回ずつ訪れたのち出発した都市に戻ってくるような経路 (巡回路) のうち最も短いものを求める問題です。

ここでは、焼きなまし法と呼ばれるアルゴリズムを学習しましょう。このアルゴリズムは発見的解法 (ヒューリスティクス) と呼ばれるもので、ある程度良い解が出力されることが期待されるものの、解の精度は全く保証されません。

一般的な焼きなまし法の概要は、以下の通りです。

・ 解を適当に作り、暫定解 X とする
・ 仮想的な温度 T を設定する
・ 温度 T を受け取り 0 以上 1 以下の実数を返す関数 P を用意する。P は T が大きければ 1 に近い値を、小さければ 0 に近い値を返すように設計する
・ 一定の回数か、解があまり改善されなくなるか、温度 T がある程度下がるまで以下を繰り返す
・ 暫定解 X を少しだけ変更したもの (X の近傍) を 1 つ選択して X' とする。X' が X より真に良い場合は確率 1 で、そうでない場合は確率 P(T) で X を X' で更新する。最後に温度 T を少し下げる
・ X を解として出力する


アルゴリズムの最初の方では温度 T が高いため、悪くなってしまう場合であっても解を更新する確率が高くなります。つまり、アルゴリズムの初期段階では解が大胆に変わることが多くなります。アルゴリズムの実行が進み温度 T が低くなっていくと、良くなる場合でないと滅多に更新されなくなっていきます。このようにすることで、解が局所的最適解に留まってしまうことが防がれ、大域的最適解へと近づいていくことが期待できます (期待ができるというだけで、必ず大域的最適解に辿り着く保証があるというわけではない)。

このヒューリスティクスを巡回セールスマン問題に適用しましょう。本問では、以下のようにします。
・ 巡回路を適当に作り、初期解とする
・ 仮想的な温度 T を設定する
・ 温度 T を受け取り 0 以上 1 以下の実数を返す関数 P を用意する。P は T が大きければ 1 に近い値を、小さければ 0 に近い値を返すように設計する
・ 温度 T がある程度下がるまで以下を繰り返す
・ (*) 巡回路を成す辺を 2 本選び、その辺を繋ぎ変えることを考える。繋ぎ変えることによって巡回路長が短くなるなら繋ぎ変える。短くならないなら、確率 P(T) で繋ぎ変える。最後に温度 T を少し下げる
・ 巡回路を出力する


辺の繋ぎ変え方は 2-opt 法と同様です。例えば、辺 (a, d) と (b, c) を繋ぎ変えると 辺 (a, b) と (c, d) になります。



n 個の都市 (都市 0、都市 1、...、都市 n-1) のデータと、n 個の都市を巡る初期巡回路が与えられます。焼きなまし法を実装し、巡回路を求めてください。ただし、ジャッジの都合上で解を一意にするために、以下の指示に従ってください。

・ 温度 T の初期値は 1000
・ 温度 T の更新は T ← T x 0.99 でおこなう
・ (*) の処理は T >= 0.1 の間繰り返す
・ 関数 P は exp((d_before-d_after)/T) とする (ただし、exp は底がネイピア数 e である指数関数を、d_before と d_after はそれぞれ辺を繋ぎ変える前と後の巡回路長を表す)

(※ 関数 P は d_before ≦ d_after のときに定義されるため、(d_before-d_after)/T は常に 0 以下となります。従って、関数 P は 0 以上 1 以下の値を返します)

(※ なお、これらのパラメータはジャッジの都合により探索回数が少なくなるように設定されています。温度 T の下げ方を緩やかにしたり、関数 P を変えるなどすると、最終的に得られる解が良くなったり悪くなったりします。興味のある人はパラメータを調整しながら実験をやってみてください)

辺を 2 本選ぶ際と確率を計算する際には、以下で指定する疑似乱数生成プロシージャ (xorshift32) を用いてください。(unsigned int は符号なし 32 bit 整数型)

unsigned int state = 813;

unsigned int xorshift32(){
unsigned int x = state;
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
state = x;
return state;
}

void pick_two(int &a, int &b){
while(true){
a = xorshift32()%n;
b = xorshift32()%n;
if(a > b){
swap(a, b);
}
if(!(a+1 < b) || (a == 0 && b == n-1)){
continue;
}
return;
}
}

double rnd(){
return (xorshift32()-1)/4294967294;
}


焼きなまし法の疑似コードを以下に示します。なお、SWAP(a, d, b, c)は巡回路 tour の辺 (a, d) と (b, c) を繋ぎ変えて辺 (a, b) と (c, d) にする処理を表します。

void simulated_annealing(int tour[]){
double t = 1.0;
while(t >= 0.1){
int a, b;
pick_two(a, b);

d_before = (現在の巡回路 tour の巡回路長)
d_after = (現在の巡回路 tour の都市 tour[a] と tour[(a+1)%n] を結ぶ辺と、都市 tour[b] と tour[(b+1)%n] を結ぶ辺を繋ぎ変えた巡回路の巡回路長)

if(d_after < d_before || rnd() <= exp((d_before-d_after)/t)){
// 辺を繋ぎ変え巡回路を更新する
SWAP(tour[a], tour[a+1], tour[b], tour[(b + 1)%n]);
}

// 温度を更新する
t *= 0.99;
}
}


なお、各都市は二次元平面上の点として与えられ、都市間の距離にはユークリッド距離を用いるものとします。
巡回路の始点は任意です。巡回路が正しければ正解と判定されます。

入力される値

n
x_0 y_0
x_1 y_1
...
x_{n-1} y_{n-1}
p_0
p_1
...
p_{n-1}

・ 1 行目に、都市の個数 n が与えられます。
・ 2 行目から、n 行にわたり n 個の都市の座標が与えられます。
・ n+2 行目から、n 行にわたり初期巡回路が与えられます。


入力値最終行の末尾に改行が1つ入ります。
文字列は標準入力から渡されます。 標準入力からの値取得方法はこちらをご確認ください
期待する出力

出力は n 行になります。巡回路は都市番号 (0, 1, ... , n-1) の順列で表し、都市番号を先頭から順に各行に 1 つずつ出力してください。

条件

すべてのテストケースにおいて、以下の条件をみたします。
・ 入力はすべて整数
・ 100 ≦ n ≦ 5,000
・ -1,000 ≦ x_i, y_i ≦ 1,000 (0 ≦ i ≦ n-1)
・ i ≠ j ならば (x_i, y_i) ≠ (x_j, y_j)
・ p は {0, 1, ... , n-1} を並べ替えた順列

入力例1

100
-903 604
758 98
489 -60
155 480
-582 650
963 -868
988 -989
-689 305
896 -560
212 135
-901 333
-967 485
-470 -462
186 314
-73 -309
-973 -244
463 691
-286 365
46 526
293 -161
571 837
398 -992
124 -606
-379 335
-540 -844
853 504
-711 465
-458 717
434 -757
-952 304
254 575
360 644
-912 -43
-712 262
45 -798
-161 296
-859 -274
-101 590
-678 171
219 334
-207 447
241 535
256 -27
560 -926
-272 214
-533 480
-598 -683
757 -233
94 381
105 931
-142 398
-782 271
-454 -210
436 -833
369 -298
956 612
-133 937
-421 -600
-506 21
409 481
910 -646
427 -594
84 -793
732 384
40 611
-82 -207
5 580
-861 -261
937 -466
211 619
203 -689
912 736
-646 -169
750 311
334 0
862 -871
289 -254
-530 -355
242 -203
-59 551
637 -209
291 150
-595 204
-229 -446
729 343
158 -893
987 -817
564 -219
248 928
685 -46
90 -300
288 46
129 -362
-245 615
-801 -213
-420 431
-521 558
-876 645
551 -749
-540 -855
88
20
72
38
98
10
2
48
50
62
32
58
29
47
70
42
24
81
78
4
17
94
53
76
45
89
26
22
95
66
84
46
41
27
6
85
11
77
14
37
69
60
55
86
96
3
99
91
18
57
74
44
67
8
34
59
40
82
15
49
19
64
75
9
1
16
33
80
87
12
21
39
43
90
83
92
54
93
23
30
61
35
13
28
25
79
56
68
73
71
65
63
5
52
36
31
7
97
51
0

出力例1

88
18
9
74
20
71
1
89
55
49
41
30
3
64
91
84
73
68
47
28
8
81
39
48
67
36
99
24
12
83
46
44
17
58
72
61
98
31
69
27
96
56
4
29
94
32
38
33
7
10
45
97
26
0
11
51
95
82
23
50
35
13
78
42
80
60
86
87
76
65
52
77
15
57
92
62
85
14
90
22
34
21
6
5
75
43
53
70
54
19
2
63
25
59
16
37
93
66
40
79

問題一覧へ戻る

ページの先頭へ戻る