問題
問題概要
マスの床に
または
のタイルが敷き詰められている。
同じタイルは 2 回以上踏むことはできず、各マスごとに踏むことで得られる得点が決まっている。
上下左右に好きな回数だけ移動し、通過したマスに対応する得点の総和を最大化せよ。
やったこと
- 4 方向のうち移動できるマスに移動するという DFS を時間の許す限り繰り返す→3865159点
ここで面白かったのは、探索順をランダムにするとスコアが下がってしまうということだった。おそらく方向を偏らせることで開始マスから遠いところに行けるため、広いスペースを使えるのではないかと考えている。
- 上の DFS を 1500 ms くらい行って発見したルートのうち、→と移動している部分について↑→↓か↓→↑と移動することが可能な部分について、そうするように変更する→4126905点
ここまでが自力の実装で、どちらにしても青パフォ中位くらい。
ここから解説記事やツイートを少しずつ読みながら実装していった。
なおこれらの経路探索では 4 方向の探索順はランダムにしてある。(初期解の生成では探索順は固定)
ここで 2 マス選んだほうがスコアが良い理由について考察すると、改善前の経路における 2 マスの間のマスを使って新しいルートを探索できることで、より密な(無駄のない)ルートを構築できるからであると考えている。
自分の実装では、1 マス選んで元の経路に戻ってくる方法では、最終的にどのマスに戻ってくるかわからないため、最終的に戻ってきたマスと最初に選んだマスの間のマスを新しいパス探索では使えていないという弱点がある。
その後しばらく定数倍高速化などの小手先の改善を行っていたがスコアとして変化はあまりなかった。
- 2 次元配列から 1 次元配列に変更
- 周囲 4 マスのうち行ける方向を前計算(余談だが、1 次元配列にすると実装が大変かと思いきや、「行ける方向を前計算」するとあまり実装が大変ではないことがわかった)
- 別の繋ぎ方をする際に 2 マスのうち両方から探索を行う(パスの終点近くはさまざまな方向を試すが、始点近くは方向が偏りがちという問題の対策)
以下では説明のため、2 マスの間の距離の最大値を
とし、探索する最大の深さを
とする
若干の定数倍高速化や実装の変更は行っていたとしても、最終的なスコアの上昇に大きく寄与したのはハイパラの調整であった。
感想
今回の問題は解(移動列)が前の状態に依存しており、単純な操作列のスワップでは valid な解を得られないため、近傍をどう設計すれば良いのか解説記事を読むまでわからなかった。
実際には今回のような問題の場合は依存性が局所的であるため、選んだ 2 マスの間についてのみ依存性を考え直すことで近傍を設計できるのだが、これは割と汎用的なテクニックだと思った。
一般的には近傍の計算を高速化すると焼きなましの反復回数が増えて得点が上がることが多い。
近傍計算の高速化方法として以下のようなものがある。
- ある解の評価値を一から計算するのではなく、前の解との差分を考えることで高速に計算する
- ある解から別の解を計算するときに変化のみをメモしておき、ダメだったら逆の操作をして戻すことで状態のコピーに必要な計算コストを減らす
これらの工夫を考えるにあたって、解をどのように表現するか(状態の持ち方)は重要だと感じた。今回の問題ではもともと移動方向を状態として持っていたが、移動方向ではなく実際に通過したマスの列を持つと実装が楽になる。(別に計算コストはあまり変わらないが。)
良いハイパラの勘みたいなものはなかなか身につけることができなさそうで難しい。
短期コンテストだと Optuna などを回す時間もなさそうである。
参考文献
本番 1 位の ats5515 さんのツイート
AHC典型解法シリーズ第2弾「焼きなまし法」]
実装コード
#pragma GCC target("avx2")
#pragma GCC optimize("O3")
#pragma GCC optimize("unroll-loops")
#include "my_template.hpp"
#include "misc/timer.hpp"
#include "misc/xor_shift.hpp"
using namespace std;
#ifdef LOCAL
constexpr f64 SCALE = 3;
#else
constexpr f64 SCALE = 1;
#endif
constexpr f64 TL = 1950 * SCALE;
constexpr f64 TL1 = 100 * SCALE;
#ifdef PARAM
f64 T0;
f64 T1;
int D_MAX;
int DFS_MAX_CALL;
#else
constexpr f64 T0 = 180;
constexpr f64 T1 = 2;
constexpr int D_MAX = 40;
constexpr int DFS_MAX_CALL = 800;
#endif
constexpr int N = 50;
constexpr char dir[4] = {'D', 'R', 'U', 'L'};
array<int, N * N> t, p;
int sx, sy;
int start;
int M;
array<vector<int>, N * N> adj;
vector<vector<vector<int>>> fac;
struct Solver {
int cst_score;
int nst_score;
vector<int> cst_ps;
vector<int> nst_ps;
Solver() {
cin >> sx >> sy;
REP(i, N * N) cin >> t[i];
REP(i, N * N) cin >> p[i];
start = sx * N + sy;
REP(i, N * N) chmax(M, t[i]);
M++;
REP(x, N) {
REP(y, N) {
const int a = x * N + y;
REP(k, 4) {
const int nx = x + dx[k], ny = y + dy[k];
if (!(0 <= nx and nx < N and 0 <= ny and ny < N)) continue;
const int b = nx * N + ny;
if (t[b] != t[a]) {
adj[a].push_back(b);
}
}
}
}
}
void init_dfs() {
cst_score = 0;
cst_ps.clear();
vector<int> tile(M, 0);
vector<int> ps;
tile[t[start]] = 1;
ps.push_back(start);
auto dfs = [&](auto f, int score, int cp) -> void {
if (timer.elapsed() >= TL1) return;
if (cst_score < score) {
cst_score = score;
cst_ps = ps;
}
FORE(np, adj[cp]) {
if (tile[t[np]] == 1) continue;
tile[t[np]] = 1;
ps.push_back(np);
f(f, score + p[np], np);
tile[t[np]] = 0;
ps.pop_back();
}
};
dfs(dfs, p[start], start);
}
void output(vector<int>& ps) {
string ans;
REP(i, LEN(ps) - 1) {
int d = ps[i + 1] - ps[i];
if (d == N) ans += dir[0];
if (d == 1) ans += dir[1];
if (d == -N) ans += dir[2];
if (d == -1) ans += dir[3];
}
print(ans);
}
void update() {
int mi = rnd.rand_int(LEN(cst_ps) - 1);
int mj = rnd.rand_int(mi + 1, min(LEN(cst_ps) - 1, mi + D_MAX));
vector<int> tile(M, 0);
int constant_score = 0;
{
REP(i, LEN(cst_ps)) {
if (!(mi < i and i < mj)) {
const int cp = cst_ps[i];
tile[t[cp]] = 1;
constant_score += p[cp];
}
}
}
vector<int> ps;
int dfs_count = 0;
bool found = false;
auto dfs = [&](auto f, int score, int cp, bool rev) -> void {
if (timer.elapsed() > TL) return;
dfs_count++;
if (dfs_count > DFS_MAX_CALL) return;
const int len = LEN(adj[cp]);
const int kr = rnd.rand_int(LEN(fac[len]));
FORE(k, fac[len][kr]) {
const int np = adj[cp][k];
if (tile[t[np]] == 0) {
tile[t[np]] = 1;
ps.push_back(np);
f(f, score + p[np], np, rev);
if (found) break;
tile[t[np]] = 0;
ps.pop_back();
} else {
if ((rev == 0 and np == cst_ps[mj]) or (rev == 1 and np == cst_ps[mi])) {
nst_score = constant_score + score;
found = true;
nst_ps.clear();
REP(i, mi + 1) nst_ps.push_back(cst_ps[i]);
if (rev) {
RREP(i, LEN(ps)) nst_ps.push_back(ps[i]);
} else {
REP(i, LEN(ps)) nst_ps.push_back(ps[i]);
}
REP(i, mj, LEN(cst_ps)) nst_ps.push_back(cst_ps[i]);
break;
}
}
}
};
if (rnd.rand_double() < 0.5) {
dfs(dfs, 0, cst_ps[mi], false);
} else {
dfs(dfs, 0, cst_ps[mj], true);
}
}
void solve() {
init_dfs();
show("finish init_dfs()");
int count = 0;
f64 t, T;
while (true) {
if (count % 100 == 0) {
t = timer.elapsed() / TL;
if (t >= 1.0) break;
T = T0 * (1.0 - t) + T1 * t;
}
count++;
nst_score = cst_score;
nst_ps = cst_ps;
update();
if (rnd.rand_double() < exp((nst_score - cst_score) / T)) {
cst_score = nst_score;
cst_ps = nst_ps;
}
}
output(cst_ps);
}
};
int main(int argc, char* argv[]) {
timer.reset();
#ifdef PARAM
if (argc == 1 + 4) {
T0 = atof(argv[1]);
T1 = atof(argv[2]);
D_MAX = atoi(argv[3]);
DFS_MAX_CALL = atoi(argv[4]);
}
#endif
fac.resize(5);
REP(sz, 1, 5) {
vector<int> perm(sz);
iota(ALL(perm), 0);
do {
fac[sz].push_back(perm);
} while (next_permutation(ALL(perm)));
}
Solver solver;
solver.solve();
return 0;
}