本記事は U-TOKYO AP Advent Calendar 2017 の15日目です。
はじめに
こんにちは。東京大学工学部 計数工学科 の @_yuki です。
師走も半ばとなり、何かと慌ただしくなってきました。忘年会や帰省なんかもあり、鉄道で移動される方も多いのではないでしょうか。本記事では、そんな風に**真っすぐ目的地に向かうだけが鉄道じゃない!**とばかりに、駅名でしりとりをしてみたいと思います。
ちなみに、単に最長しりとりを構成するだけなら、既に研究対象としても繰り返し取り上げられ[^1]、優れた解法が存在します。卒論で扱ってねとらぼの記事[^2]になった方もいるようで、別の方が実装[^3]されたものもあるので、しりとりの解のみ欲しい方はそちらへどうぞ。
この記事では、**あえて整数計画法のソルバーは使わずに、東京メトロの全143駅から最長しりとりを構成していきます。それによって、このような問題はどのように定式化すると楽なのか?**を考える一例となれればと思います。厳密でないところが一部あります[^4]が、気にしない。
ルール
- 東京メトロの駅だけを使い、駅名でしりとりをしながら回る。
- 同じ駅を使うことなく、なるべくたくさんの駅を回るのが目標。
- 移動経路は不問(どこを目指すかだけでしりとり)。
- 濁点と半濁点は無視、拗音は小文字を大文字に取り換える。つまり「和光市」→「神保町」や「北千住」→「有楽町」を認める。
- 副駅名は連結してもしなくてもよい。つまり「明治神宮前」「明治神宮前<原宿>」はいずれか片方を採用。
考え方
駅名は「矢印」









BuildDictionary
Private Sub BuildDictionary(floop As Object, rloop As Object)
Dim word As Object, nrow As Long, tmpw As String, tmpc As String
'初期化
Set floop = CreateObject("Scripting.Dictionary")
Set rloop = CreateObject("Scripting.Dictionary")
nrow = 1
With Thisworkbook.Worksheets(1)
Do
'単語の取得
tmpw = .Cells(nrow, 1).Value
If tmpw = "" Then Exit Do
'始まりのひらがなによる辞書
tmpc = SplitSonant(Left$(tmpw, 1))
If Not floop.Exists(tmpc) Then
Set word = CreateObject("Scripting.Dictionary")
floop.Add tmpc, word
End If
floop(tmpc).Add tmpw, SplitSonant(Right$(tmpw, 1))
'終わりのひらがなによる辞書
tmpc = SplitSonant(Right$(tmpw, 1))
If Not rloop.Exists(tmpc) Then
Set word = CreateObject("Scripting.Dictionary")
rloop.Add tmpc, word
End If
rloop(tmpc).Add tmpw, SplitSonant(Left$(tmpw, 1))
nrow = nrow + 1
Loop
End With
End Sub
DivideStraight
Private Sub DivideStraight(floop As Object, rloop As Object, fstraight As Object, rstraight As Object)
Dim char As Variant, word As Variant, divide As Boolean, ntmps As Long, i As Long, tmps As Object, tmpw As Object, tmpc As String
'始端につながる直線候補
Set fstraight = CreateObject("Scripting.Dictionary")
Do
divide = False
For Each char In floop
If Not rloop.Exists(char) Then
For Each word In floop(char)
Set tmps = CreateObject("System.Collections.ArrayList")
'別の始端候補の後ろにつながるか
If fstraight.Exists(char) Then
ntmps = fstraight(char).Item(0).Count
For i = 0 To fstraight(char).Count - 1
tmps.Add fstraight(char).Item(i).Clone
Next
Else
ntmps = 1
End If
'直線候補に加える
Set tmpw = CreateObject("System.Collections.ArrayList")
For i = 1 To ntmps
tmpw.Add word
Next
tmps.Add tmpw
'ループとの接続点が同一の候補と統合
tmpc = floop(char)(word)
If fstraight.Exists(tmpc) Then
Select Case tmps.Count - fstraight(tmpc).Count
Case Is > 0
fstraight.Remove tmpc
fstraight.Add tmpc, tmps
Case 0
For i = 0 To tmps.Count - 1
fstraight(tmpc).Item(i).Addrange tmps.Item(i)
Next
End Select
Else
fstraight.Add tmpc, tmps
End If
'ループ候補から削除
rloop(tmpc).Remove word
If rloop(tmpc).Count = 0 Then rloop.Remove tmpc
Next
floop.Remove char
divide = True
End If
Next
Loop While divide
'終端につながる直線候補
Set rstraight = CreateObject("Scripting.Dictionary")
Do
divide = False
For Each char In rloop
If Not floop.Exists(char) Then
For Each word In rloop(char)
Set tmps = CreateObject("System.Collections.ArrayList")
'別の終端候補の前につながるか
If rstraight.Exists(char) Then
ntmps = rstraight(char).Item(0).Count
For i = 0 To rstraight(char).Count - 1
tmps.Add rstraight(char).Item(i).Clone
Next
Else
ntmps = 1
End If
'直線候補に加える
Set tmpw = CreateObject("System.Collections.ArrayList")
For i = 1 To ntmps
tmpw.Add word
Next
tmps.Insert 0, tmpw
'ループとの接続点が同一の候補と統合
tmpc = rloop(char)(word)
If rstraight.Exists(tmpc) Then
Select Case tmps.Count - rstraight(tmpc).Count
Case Is > 0
rstraight.Remove tmpc
rstraight.Add tmpc, tmps
Case 0
For i = 0 To tmps.Count - 1
rstraight(tmpc).Item(i).Addrange tmps.Item(i)
Next
End Select
Else
rstraight.Add tmpc, tmps
End If
'ループ候補から削除
floop(tmpc).Remove word
If floop(tmpc).Count = 0 Then floop.Remove tmpc
Next
rloop.Remove char
divide = True
End If
Next
Loop While divide
End Sub
CutSurplus
Cutsurplusについては改修中[^5]です。恐れ入りますが、公開まで今しばらくお待ちください。
SplitSonant
Private Function SplitSonant(str As String) As String '濁点や拗音などの処理
Dim nchar As Integer
'濁点と半濁点の削除
nchar = Asc(StrConv(Left$(StrConv(str, vbKatakana + vbNarrow), 1), vbHiragana + vbWide))
'拗音と促音の変換
Select Case nchar
Case -32097 To -32089, -32063, -32031 To -32027
If nchar Mod 2 = -1 Then
nchar = nchar + 1
End If
End Select
SplitSonant = Chr(nchar)
End Function