AHC036振り返り
はじめに
RECRUIT 日本橋ハーフマラソン 2024夏(AtCoder Heuristic Contest 036)に参加した。かなり苦戦したので反省も込めて振り返りのためのメモ。
問題
問題は初見では何を言っているのかよくわからかったが、以下のような感じ。
- 与えられたノード数
(=600)のグラフにおいて、N (=600)個のノードを指定された順番で回るT - 移動先に指定できるのは配列
に含まれるノードだけB - 配列
へは配列B 上の任意の長さをコピーできる。このコピーの回数が少ない方がいいA
処理の順番
いかのような順番で処理することを考えた。この順番は最後まで変更していない。
-
個の指定ノードを回るルートを算出するT - ルートから配列
を作成するA -
に何をどのタイミングで入れるかを決めるB
ルートの算出
どんなルートを選ぶか
以後最初に最終的な方式、その後に、試行錯誤した案を示す。試行錯誤した案は結局使わなかったのでタイトルに🆖をつけている。
最短経路ベースで選択する
総パス長さを短くするために、指定ノード間の最短経路を求めた。この時、最短経路は複数の候補が導出される、できる限り同じノードのパスを選択することを考える。それぞれのノード
ノードの重み(左)とコスト最大のパス(右)
各ノードの重みと、最短経路の候補からコストが最も大きいパスを選択した時の総パスは上の図のようになる。色が濃い程重みが大きい事をしめしており、中心の方が大きくなっているのが分かる。
また、コストでパスを選択することで、通過するノードを減らす事ができている。
ノードの重みの分布
この時のノードの重みの分布をグラフ化してみると、重み1のノードが多いので、削除することを考える。グラフ理論を使ってできそうな気はしないではないが、単純にそのノードを通る最短経路を、そのノードなして再計算して経路があるかどうかで判断した。
重みの小さいノードを削除した結果
重み20までのノードについてノードが削除できるかどうか判断して、可能であれば削除した結果。結構すっきりした。
🆖 最短経路以外も候補に含める
当初は、多少遠回りになっても、最短経路以外も考慮した方がいいのでは?と思い、2番目に短い経路に対しても「経路の候補」として採用していた。しかしながら、経路の候補数が膨大になって計算時間がかかるのと、経路長が伸びることのデメリットの方が大きかったので、最短経路のみを考慮することとした。
特に計算時間は大きな課題で、2番目に短い経路まで導出していた最初の提出では、制限時間3秒のところ2.7秒もかかってしまっていた。
🆖 高速道路を作る
できるだけ同じ経路を通したいので、ノードを位置情報を元に5×5のマスに分割し、それぞれのマスの中心に距離的に近いノードを高速道路の入口ノードとした。経路を入口ノードまでの最短経路と、高速道路(入口ノード間の経路)の2段階にすることで共通部分の多い経路を選択する。入り口ノードを動かす事で、最適解を見つけることができないかな?という思いで実装。
高速道路を使った例。ピンクが入口ノード
入口を動かすところまで作ったが、総経路長が長くなってしまい、スコアは伸びなかったのでこの方向性はあきらめた。コンテスト期間中はビジュアライズしてなかったのだが、改めて見ると、中心近くもうまくノードを間引けているので、この路線を詰めてもよかったのかも知れない。
A を求める
配列どのようにして作るのか
配列
- 経路上のノードを全部含んでいないといけない
- できる限り経路と同じ組み合わせを持っている
なので、経路パスの先頭から初めて出てくるノードであれば配列
利用頻度が高いエッジを優先する
初出でないノードをどのタイミングで挿入するか?であるが、エッジの利用頻度を算出しておいて、それが高い組み合わせを残すようにしてみた。正直言うと思いつかなかったので、とりあえずこうしてみた感じである。ただ、まだ出現していないノードの数と、配列
配列
後述するが、マッチングした数だけど
最初の方は、最大値である
既に同じパターンがあった場合は挿入しない
それよりも問題なのは、0の範囲がある事。つまり、この並びと同じ列が既に[0, 1, 2]
というパターンがあった時、[0, 1, 4]
を入れられないため。短くすると今度はバリエーションが減ってスコアが低くなってしまった。
配列
完全に無くなってはいないが、白い部分を減らすことができた。
B に要素を入れる
配列
X のアルゴリズム
旅行好きの
- 現在の配列
に含まれるノードで進めるまで進む。ゴールまで進めたら終了B - 進めないノード
からr_i 個を経路からとりだし、配列L_B から最も多くのprefixがマッチする範囲A を探す。([p, q] は必ず含まれる)r_i - 範囲
を配列[p, q] にコピーし、1に戻るB
ここで注意すべきは、配列[4, 5, 6, 7, 8]
の場合、配列[8, 6, 7, 4, 5]
でも良い。なので、あらかじめ「辞書」を作ってから対応した。ただ、本当に全パターンを作ると
反省点
順位としては200位ぐらいから始まって、あまりスコアを上げる事ができず、最終的な順位としては240位ぐらいであった。配列
おまけ
rust-parallelを使う
アルゴリズムの試行の中で、特定の入力だけに最適化するのはよくないので、複数の入力に対して実行し、その結果を集計する必要がある。それぞれの入力に対する処理は独立で、今時のCPUはマルチコアなので並列化できる。
今回は、rust-parallelを使ってみた。Linux/Macだとパッケージとして提供されているので、導入は簡単である。
rust-parallel -r '(.*)/(.*)' -s 'python3 main.py < {1}/{2} > out/{2}' ::: in/*
記述形式はちょっと独特で、:::
以下が入力で in
以下のファイル一覧を使った。-r
で正規表現を使って入力を解析、ディレクトリ名({1}
)とファイル名({2}
)に分けている。あとは、-s
で実行するスクリプトを書けばいい。ここではout
ディレクトリに出力している。
昔からあるGNU Parallelと同じものではあるが、Rustで最近いろんなツールが作られているなぁということで使ってみたという話。
Discussion