TERRYのブログ

最近は競技プログラミングにお熱。

Sponsored Link

競技プログラミングの鉄則 Mayor's Challenge 解説

鉄則本こと競技プログラミングの鉄則の力試し問題のラストを飾るヒューリスティック型課題、Mayor's Challengeの解説です。100ケースで9693万点くらい出ました。

上級者向け解説ですので、初中級者の方はまず先に公式解説pdfをご一読されることをオススメします*1 また、まだ解いていない人は是非自力で解いてみてください。ヒューリスティック問題は自力でいろいろ工夫してみる過程がとても楽しいですよ。

有志によるビジュアライザ(seed=0, 987541点)

問題概要

地区をいい感じに合併して、格差が小さくなるように特別区を作ってね。

atcoder.jp

選挙区割り問題に近いでしょうか。名前は聞いたことがあるのですが、実際に自分で解いてみるのは初めてです。この辺あまり詳しくないので、有識者の方の知見もお待ちしております。

方針

焼きなませるので、焼きなまします。地区ごとの連結性を保ったまま少しずつ特別区の割り当てを変更していきます。

焼きなましの近傍としては、「特別区Xの地区xを特別区Yに変更する」「特別区Xの地区xと特別区Yの地区yの割り当てを交換する」の2つを採用しました。2つ目の近傍を入れるかどうかでかなり性能が変わってきた印象です。

解法

前処理

グリッドのままだと扱いづらいので、グラフに変換します。詳しいやり方は公式解説pdfをご参照ください。

評価関数

この問題のスコアはざっくり以下のように決まります。

  1. 人口の最も多い特別区と人口の最も少ない特別区の人口比を取る
  2. 職員の最も多い特別区と職員の最も少ない特別区の職員比を取る
  3. 両者のうち悪い方に定数をかけてスコアとする

このスコアをそのまま焼きなましの評価関数にしても良いのですが、このスコア関数はやや取り扱いづらいです。*2 というのも、人口または職員数が最大・最小となる2つの特別区しかスコアに寄与せず、他の特別区の割り当てを変更してもスコア関数の値が変化しないためです。イメージとしては、スコア関数のあちこちに平坦な領域ができてしまうような感じでしょうか。

スコア関数のイメージ図

このスコア関数が最大となる条件は、各特別区の人口および職員数が全て等しくなることです。そのため、平均人口\bar A=\sum_{k=1}^KA_k/Lおよび平均職員数\bar B=\sum_{k=1}^KB_k/Lを求め、各特別区の人口a_lおよび職員数b_lについて、平均との誤差を小さくしたい気持ちになります。ということで、今回はその誤差の二乗和を使ったf=\sum_{l=1}^L(a_l-\bar A)^2+50^2\times\sum_{l=1}^L(b_l-\bar B)^2 *3を評価関数として採用することにし、これを最小化することにしました。このようにすることで、評価関数から平坦な領域をなくし、滑らかで焼きなましやすい関数にすることができます。

注意点として、当然ながら生のスコア関数の値と今回作成した評価関数fの形は異なっています。そのため、

  • 焼きなましの解の更新時に生スコアも計算し、生スコアが最も良い解を別途保持しておく
  • 生のスコア関数と評価関数fを線形結合し、時間とともに生スコアの重みを増やす

等の対策を行うと良いでしょう。今回は前者のみ採用しました。

初期解作成

焼きなましでは完全なランダム初期解を選んでも良い解が得られることも多いのですが、今回の問題では初期解の良し悪しの影響が結構大きかった印象です。というわけで、ある程度真面目に初期解を作ります。

今回は、

  1. K個の地区からランダムにL個を選び、それぞれ特別区1, 2, \cdots, Lに割り当てる
  2. 未割り当ての地区kとそれに隣接する特別区lの全てのペア(k, l)について、地区kを特別区lに割り当てたときに評価関数fがどれだけ変化するか計算し、最も良かった割り当てを採用する
  3. 2.を未割り当ての地区がなくなるまで繰り返す

という貪欲法を採用しました。計算量は高々O(LK^2)です。この貪欲が上手く行くかどうかは最初の特別区の割り当てに大きく左右されるので、これを50回ほど繰り返し、評価関数fが最も良かったものを初期解として採用しました。

焼きなましを行わずここまでで止めると、4621万点ほどになりました。人口や職員数にざっくり2倍くらいの差が付いている状態ですね。

近傍1

初期解ができたので、焼きなましを行います。焼きなましの近傍は前述の通り、

  1. 特別区Xの地区xを特別区Yに変更する
  2. 特別区Xの地区xと特別区Yの地区yの割り当てを交換する

の2つを採用しました。まずは前者の解説をします。

といっても、ほぼ公式解説の通りなのであまり説明することはありません。工夫点としては、

  • 各特別区の人口および職員数を保持しておくことで、評価関数の差分計算をO(1)で行えるようにした

くらいでしょうか。連結性の確認はDFSなどで行うしかないので、計算量的にはそこがネックになります。

ここまで実装するだけでも、9315万点ほどを得ることができました

近傍2

近傍1だけだと、1つの地区について特別区の割り当てを変えたとき、一時的にバランスの崩れた状態を経由することになってしまいます。これだと焼きなましがしづらいので、割り当てを交換する近傍を追加しました。具体的な手順は、

  1. 近傍1と同様の手順で、特別区X, Yおよび地区xを選ぶ
  2. 特別区Yに隣接する地区のうち、特別区Xに属するものを列挙する
  3. 列挙した地区のうちからランダムに地区yを選ぶ(そのような地区がなければ諦める)

としました。これを実装するとスコアが大きく改善され、9693万点を得ることができました。*4

atcoder.jp

特別区の割り当ての交換を1対1だけでなく1対2などで行うなど、さらなる工夫を行うことでスコアが上がるかもしれません。是非ともより良いスコアを目指してみてください!

感想

比較的シンプルな設定ながら工夫できる点も色々あり、面白い問題でした。ちょうど4時間コンテストとしてピッタリな問題なのではないでしょうか。

何より解説が丁寧で素晴らしいですね。鉄則本に付いている他の練習問題と合わせて、ヒューリスティック初心者が慣れていくのに非常に良い教材ではないかと思います。

まだ鉄則本を買っていない方、是非ともこの機会に買いましょう!*5

*1:本当に質の高い解説ですので、是非読んでみてください!

*2:とはいっても、これでも9679万点くらいは出るようです。そんな……。

*3:係数として50^2をかけているのは、人口の項と職員数の項のスケールを合わせるためです。

*4:コードテストでseed=0を実行したところ、だいたい25万回ほど焼きなましのループが回っていました。もっとも近傍2の方が計算が重いので、参考まで。

*5:媚びを売っています。