A - Ward the Village 解説
by
mech_39
延長戦1,000,000,000点到達解法
全体方針
本解法は以下の3章からなる.
- 移動ベクトルの選択(ローカルで計算するので予算ゼロ)
- 差分更新ビームサーチで初期解を構築(予算 1.2 s)
- 厳密差分評価の焼きなまし + 区間 DP LNSで磨く(タイムリミットまで)
ビームサーチはPrussianBlue さんの解法を参考にしました. (https://atcoder.jp/contests/ahc070/submissions/78855323)
なお,延長戦では生成AIを使用しています.ここで解説する解法を生成AI禁止の4時間で実装することは難しいかもしれません.
1. 移動ベクトルの選択
1.1 位置は「ベクトル使用回数」だけで決まる
移動ベクトルを \(v_0,v_1,v_2\),ターン \(t\) までの \(v_1,v_2\) の使用回数を \(c_1,c_2\) とすると,
\[ \mathrm{pos}(t)=(t+1)v_0+c_1(v_1-v_0)+c_2(v_2-v_0)\pmod N . \]
つまり位置は手順の順序に依存せず,使用回数のみで決まる.
1.2 極小三角形族
採用したのは
\[ v_0=(70,75),\quad v_1=(71,76),\quad v_2=(72,75). \]
差分は \(v_1-v_0=(1,1)\),\(v_2-v_0=(2,0)\).回転座標 \((i+j,\ i-j)\) で見ると 3 手は
\[ (145,-5),\quad(147,-5),\quad(147,-3) \]
となり,どの 2 手も高々 2 しか違わない.
毎手の「ドリフト速度」を回転座標の 2 軸それぞれで ±2 刻みに操舵できる.つまり \(k\) 手先の位置は基準軌道の周りを幅 \(O(k)\) で連続的に埋め,深い先読みがそのまま精密な位置調整として機能する.
一方で \((t+1)v_0\) の基準ドリフト自体は毎ターン大きく飛ぶため,盤面全体への到達性も保たれる.この「大きく回りながら微調整だけ選ぶ」構造が,ビーム・LNSと相性が良かった.
族の特定は,\(\mathrm{pos}(t)\) の格子としての性質で枝刈りしつつ 95,000 通りの三つ組を実測評価して行った.
2. 差分更新ビームサーチ
ベースは PrussianBlue さんの解法.
- 全 10000 ターンを 2 手ずつ(5000 レベル)進める幅 128〜320 のビームサーチ
- 各ノードが \(102\times102\) の「最寄りお札距離場」を保持し,お札設置を BFS 緩和で増分更新する厳密評価
- ノード評価 = 累積実コスト − 重み × 累積 gain(gain は距離が下がった各セルの生存確率モデル)
- 終端マスでの重複除去と親あたり子数上限で多様性を確保
本解はこれを差分更新ビームサーチにした.元解はノード生成のたびに距離場を memcpy 複製するため,1 走行あたり 557,048 回 × 10,404 B = 5.8 GB の複製が発生し,メモリ帯域律速だった.観察は
ビーム木の生存辺は 1 レベルあたり 350〜1,100 本しかない
こと.そこで距離場は全体で 1 本だけ持ち,各ノードには BFS 緩和の \((\text{cell},d_{\mathrm{old}},d_{\mathrm{new}})\) undo ログのみを保存する.ノード間の移動は木上の LCA まで undo → 逆適用で行い,レベルごとに走査方向を反転して折り返しコストを消す.
これを実装すると1.6〜1.9 倍の高速化となり,そのままビーム幅に還元された.
3. 焼きなまし
3.1 全体像
- 状態は 10000 手の移動列そのもの
- 近傍は swap(90 %)/ 窓シャッフル(10 %)/ 250 イテレーションに 1 回の区間 DP LNS
- 棄却時は undo ログの逆再生で完全復元.最良解の同期は変更ターンだけを dirty リストで書き戻す
これを支えるのが以降の厳密差分評価器で,手元実測では 約 \(1.4\times10^7\) 反復を実現した.
3.2 厳密差分評価の帳簿
「移動列の一部を書き換えたときの \(\Delta D\)」を厳密かつ局所的に求めるため,次の構造を常に整合させて持つ.
firstStamp[c]: セル \(c\) に最初にお札が立つターン.セルごとの先頭 4 訪問ターンを 16 bit × 4 でパックしたSIMD ソート済みリストで管理し(挿入・削除はベクトル比較 + popcount シフト),5 件以上はスピル配列に溢して最小値だけ昇格させるdistTurn[t]/achieveCnt[t]: \(d_t\) と,その距離をちょうど達成しているお札の枚数cellRec/turnRec: セル ⇄(ターン, 距離)の相互パック.空間走査が 32 bit ロード 1 回で判定できるnzWord: \(d_t\ge1\) のターンの 64 bit ビットセットblockMax+ 接尾辞 max / 接尾辞 popcount : 64 ターンブロック単位の \(\max d_t\) と非ゼロ数(走査コスト見積り用)maxCover[c]: セル \(c\) を中心とする半径 \(k\) のリングが,どこかのターンの達成距離になり得る \(k\) の上限(65,536 イテレーションごとに再構築)
更新の対称性が鍵で,
- お札の追加は距離が下がるターン(と同距離のカウント増)だけを触る
- お札の削除は
achieveCntが 0 になったターンだけrecomputeTurnでリングを \(k+1\) から外側へ再探索する(settledFlagで同一ターンの二重減算を防止.\(k=1,2\) は完全展開)
3.3 direct / ring の 2 走査戦略
差分適用で「影響を受けるターン」を列挙する方法を 2 つ持ち,毎回作業量見積りで安い方を選ぶ.
- direct : 時間範囲 \([t_0,t_1)\) の \(d_t\ge1\) ターンを
nzWordで列挙し,変更セルとの距離を直接計算する.作業量 ≈ 範囲内の非ゼロターン数(接尾辞 popcount で \(O(1)\) 見積り) - ring : 変更セルの半径 \(k_{\max}\) 以内を空間走査する.作業量 ≈ \(2(k_{\max}+1)^2\times\) 係数.\(k_{\max}\le24\) は事前計算した SIMD グループ表で 4 セル同時判定,\(k_{\max}\le4\) は完全展開
\(k_{\max}\) は区間 \(\max d_t\)(blockMax)と maxCover の両方で絞る.追加側はさらに時間範囲を 128/512/2048 で切った「前半 direct + 後半 ring」の混合案も比較する.局所変更 1 回の処理が影響範囲に比例するオーダーに収まり,反復数がそのまま桁で伸びた.
3.4 軽量な近傍
- swap : \(t_0\) と \(t_0+g\)(\(g=1/2/3\) を確率 \(0.5/0.3/0.2\))の 2 手を交換.区間 \([t_0,t_0+g]\) の中間位置だけがずれる最小近傍
- 窓シャッフル : 長さ 3〜6 の窓を Fisher–Yates で並べ替え(8 バイト比較で無変化案は棄却)
- 狙い撃ち : \(d_t\ge1\) のターンを一様サンプルし,怪異の半径 \(\min(d_t,4)\) 内の目標マスを選ぶ.\(v_a\to v_b\) を怪異前に \(k\) 個・\(v_b\to v_a\) を怪異後に \(k\) 個(\(k\le3\))交換する最短の窓を全 6 方向から探し,既存訪問を目標マスへ平行移動してお札を 1 枚寄せる
3.5 DP による LNS
250 イテレーションに 1 回,窓 \([l,r)\)(長さ 12〜26,一様)を丸ごと作り直す.
removeWindowStampsで窓内のお札を帳簿から一時除去する- 節 1.1 の格子構造より,窓内の使用回数を保存する並べ替え全体を「使用済み \(v_1\) 数 \(q_1\) × 使用済み \(v_2\) 数 \(q_2\)」を状態とする DP で厳密に走査できる.終端位置の一致は自動で保たれ,遷移は各ステップ 3 種のみ
- DP のコストは \(\min(\lVert\mathrm{pos}_k-e_{l+k}\rVert_1,\ \mathrm{refDist}_{l+k})\cdot\sqrt{l+k+1}\)(\(\mathrm{refDist}\) = 窓除去後の参照距離)から次節の将来価値を引いたもの
- DP 解を挿入し,厳密差分で出た本物の \(\Delta\) で採否を判定する(\(\Delta\le0\) は必ず受理,それ以外は温度スケール付き閾値受理)
DP は窓内お札同士の相互寄与を無視した近似だが,採否は常に厳密値で行うため安全.窓は \(l\ge1\) が前提で,\(l=0\) だと区間 \([0,r)\) を覆うお札が消え「各ターンは常に何かのお札に覆われている」という帳簿の不変条件が壊れる.
3.6 将来価値場 — 窓の外を目的関数に入れる
この LNS の本体は窓内ではなく窓の外にある.
窓のお札を抜いた瞬間の distTurn は,そのまま「窓を無視したときの参照距離 \(\mathrm{refDist}_t\)」になっている(再シミュレーション不要).そこから将来価値場
\[ V[c]=\sum_{t\ge r}\max\!\left(0,\ \min(\mathrm{refDist}_t,6)-\lVert c-e_t\rVert_1\right)\sqrt{t+1} \]
を作り,DP の目的関数を
\[ \text{窓内実コスト}-0.8\sum_k V[\mathrm{pos}_k] \]
とする.窓内の各お札が,窓より先のターンをどれだけ救うかを 1 マス単位で評価していることになる.
窓内だけを最適化すると全く採用されず壊滅的に悪化した. つまり LNS の性能はほぼこの将来項で決まっていた.
3.7 将来価値場のチェックポイント化
\(V\) を毎回 \(t\ge r\) の全ターンから散布し直すと LNS 1 回が重すぎる.そこで
- 512 ターン刻みの接尾辞スナップショット \(\mathrm{ck}[j]=\sum_{t\ge512j}(\cdots)\) を 20 本保持し,16,384 イテレーションごとに再構築する
- LNS ごとには (a) \(r\) からスナップショット境界までの残差ターンの散布と,(b) 窓除去でログに載ったターンについてスナップショット時距離との差分だけの再散布を行う
散布で触れたセルのみを記録してゼロクリアも差分化し,\(V\) の構築が窓近傍のコストに収まる.