北大日立マラソン第1回
Hokkaido Univ.& Hitachi 1st New-concept Computing Contest 2017 - AtCoder
スタートダッシュ芸人してました
初日
焼きなましはすぐにわかったので軽く高速化・温度調整したやつを出した(CodinGameにも出たかったので巻いてた(結局出なかったが
初日にやったこと
- 初期解はランダム
- 近傍は正の辺の端点をもう一点に隣接させる(隣接先が空ならMove、他の点があるならSwap)(他の人と同じ)
- 近傍は一様ランダムセレクト
- 温度は5度から1.5度に線形に下げる(12ケースで調整)
- スコア変化量の計算は重みを32回足し引きすると思いますが、うち16回は事前に計算して持って遷移時に更新することで16回に減らせる
正の辺の端点をもう一点に隣接させる近傍なんですが、少なくとも一辺の得点がもらえるため、点のかたまりの遠くに飛ぶなどの明らかに無駄な近傍を除けて遷移効率が良くなります。こういう無駄な近傍を考えて除いたり、最適解ないし良い解を想像して、その形から離れるような近傍を除くというのは焼きなましではわりと頻出でとても効果が大きい。
その後
焼きなまし過程のVisualizerをつくった。赤がWeight=15、青がWeight=1、白線が近傍
↑最初から最後までのダイジェスト(飛び飛び)。
- 存外ふらふらするので初期解は中央にかためてランダム(壁にひっつくと遷移の自由度が下がって良くないので
- 良い解は円に近そうに見える(正方形や長方形よりも円のほうが辺数が多いことは確認した(yowaさんのtweetによると正八角形が最大らしい?
- 良い解は赤い(得点をもらえる辺はWeightが大きい辺に偏る)。ので近傍を一様ランダムセレクトではなく、
round(sqrt(Weight))
で重み付けランダムセレクトした
↑真ん中らへんの遷移を連続で
- 実際の遷移(白線)はかたまりの端と端が多い(かたまりの内側を動かすとスコアが大きく下がりやすく、焼きなましの遷移確率はスコアが大きく下がるときは低いのでそれはそう)。なのでこれをうまく使おうとしたけど難しかった。
その他
- GCCよりClangのほうが1.25倍速かった
- 他の人がやっているような高速化はだいたいやった
- 温度を再調整
↓初日submit
#include <algorithm> #include <cmath> #include <iostream> #include <vector> using namespace std; #define U(v) cerr << #v << ": " << (v) << endl constexpr int dx[] = { -1, 0, 1, -1, 1, -1, 0, 1 }; constexpr int dy[] = { -1, -1, -1, 0, 0, 1, 1, 1 }; class timer { vector<timer> timers; int n = 0; public: double limit = 9.9; double t = 0; timer() {} timer(int size) : timers(size) {} bool elapses() const { return time() - t > limit; } void measure() { t = time() - t; ++n; } void measure(char id) { timers[id].measure(); } void print() { if (n % 2) measure(); for (int i = 0; i < 128; ++i) { if (timers[i].n) cerr << (char)i << ' ' << timers[i].t << 's' << endl; } cerr << " " << t << 's' << endl; } static double time() { #ifdef LOCAL return __rdtsc() / 3.3e9; #else unsigned long long a, d; __asm__ volatile ("rdtsc" : "=a" (a), "=d" (d)); return (d << 32 | a) / 2.8e9; #endif } } timer(128); #include <ctime> class { unsigned y = time(0); //unsigned y = 2463534242; public: unsigned next() { return y ^= (y ^= (y ^= y << 13) >> 17) << 5; } int next(int b) { return next() % b; } int next(int a, int b) { return next(b - a) + a; } double nextDouble(double b = 1) { return b * next() / 4294967296.0; } double nextDouble(double a, double b) { return nextDouble(b - a) + a; } int operator() (int b) { return next(b); } } rando; struct P { int x, y; P() {} P(int x, int y) : x(x), y(y) {} }; int V, E, S; vector<pair<int, int>> edges; char weight[501][501]; int vs[62][62]; char scores[501]; P ps[500]; P bestPs[500]; int eval() { int score = 0; for (auto& e : edges) { auto& a = ps[e.first]; auto& b = ps[e.second]; if (max(abs(a.x - b.x), abs(a.y - b.y)) == 1) score += weight[e.first][e.second]; } return score; } void init() { vector<int> a; for (int i = 0; i < S * S; ++i) a.emplace_back(i); random_shuffle(a.begin(), a.end(), rando); for (int i = 0; i < V; ++i) { auto& p = ps[i]; p.x = a[i] % S + 1; p.y = a[i] / S + 1; vs[p.y][p.x] = i; } } void solve() { init(); copy_n(ps, V, bestPs); int score = eval(); int bestScore = score; for (int i = 0; i < V; ++i) { auto& p = ps[i]; for (int d = 0; d < 8; ++d) scores[i] += weight[i][vs[p.y + dy[d]][p.x + dx[d]]]; } double heat = 0; for (int iter = -1; ; ) { if ((++iter & 0xfff) == 0) { //double p = 1 - iter / 100000000.0; double p = 1 - (timer.time() - timer.t) / timer.limit; if (p < 0) { U(iter); break; } constexpr double InitialHeat = 5; constexpr double FinalHeat = 1.5; heat = 1 / (p * (InitialHeat - FinalHeat) + FinalHeat); } int i, j, x, y; { unsigned r = rando.next(); auto& e = edges[(r >> 4) % edges.size()]; if (r & 8) { i = e.first; j = e.second; } else { j = e.first; i = e.second; } x = ps[j].x + dx[r & 7]; y = ps[j].y + dy[r & 7]; j = vs[y][x]; } if (i == j || x == 0 || x == S + 1 || y == 0 || y == S + 1) continue; auto& p = ps[i]; if (j == V) { int diff = - scores[i]; for (int d = 0; d < 8; ++d) diff += weight[i][vs[y + dy[d]][x + dx[d]]]; if (rando.nextDouble() < exp(diff * heat)) { score += diff; swap(vs[p.y][p.x], vs[y][x]); for (int d = 0; d < 8; ++d) { int v = vs[p.y + dy[d]][p.x + dx[d]]; scores[v] -= weight[i][v]; v = vs[y + dy[d]][x + dx[d]]; scores[v] += weight[i][v]; } scores[i] += diff; p = { x, y }; if (bestScore < score) { bestScore = score; copy_n(ps, V, bestPs); } } } else { int diff = - scores[i] - scores[j]; if (max(abs(x - p.x), abs(y - p.y)) == 1) diff += weight[i][j]; vs[p.y][p.x] = vs[y][x] = V; for (int d = 0; d < 8; ++d) diff += weight[j][vs[p.y + dy[d]][p.x + dx[d]]]; vs[p.y][p.x] = j; for (int d = 0; d < 8; ++d) diff += weight[i][vs[y + dy[d]][x + dx[d]]]; vs[y][x] = i; if (rando.nextDouble() < exp(diff * heat)) { score += diff; for (int d = 0; d < 8; ++d) { int v = vs[p.y + dy[d]][p.x + dx[d]]; scores[v] -= weight[i][v] - weight[j][v]; v = vs[y + dy[d]][x + dx[d]]; scores[v] += weight[i][v] - weight[j][v]; } scores[i] = scores[j] = 0; for (int d = 0; d < 8; ++d) { scores[i] += weight[i][vs[y + dy[d]][x + dx[d]]]; scores[j] += weight[j][vs[p.y + dy[d]][p.x + dx[d]]]; } swap(p, ps[j]); if (bestScore < score) { bestScore = score; copy_n(ps, V, bestPs); } } else { swap(vs[p.y][p.x], vs[y][x]); } } } copy_n(bestPs, V, ps); timer.print(); } int main() { timer.measure(); #ifdef LOCAL int type, seed; cin >> type >> seed; #endif cin >> V >> E; for (int i = 0; i < E; ++i) { int a, b, w; cin >> a >> b >> w; --a; --b; if (w != 0) { edges.emplace_back(a, b); weight[a][b] = weight[b][a] = w; } } cin >> S; S = (int)sqrt(S); { int E; cin >> E; for (int i = 0; i < E; ++i) { int a, b; cin >> a >> b; } } for (int i = 0; i < S + 2; ++i) fill_n(vs[i], S + 2, V); solve(); //cerr << "var d" << type * 6 + seed << " = [" << endl; //cerr << "[" << V << ',' << edges.size() << ',' << S << "]," << endl; //cerr << "["; //for (auto& e : edges) // cerr << e.first << ',' << e.second << ',' << (int)weight[e.first][e.second] << ','; //cerr << "]," << endl; //cerr << "["; //for (int i = 0; i < V; ++i) // cerr << i << ',' << ps[i].x << ',' << ps[i].y << ','; //cerr << "]," << endl; //cerr << "]" << endl; #ifdef LOCAL int score = 0; int esum = 0; int wsum = 0; for (auto& e : edges) { auto& a = ps[e.first]; auto& b = ps[e.second]; if (max(abs(a.x - b.x), abs(a.y - b.y)) == 1) { score += weight[e.first][e.second]; ++esum; } wsum += weight[e.first][e.second]; } cerr << type * 6 + seed << "\t" << score << "\t" << wsum << "\t" << esum << "\t" << edges.size() << "\t" << V << "\t" << S << endl; #else for (int i = 0; i < V; ++i) { auto& p = ps[i]; cout << i + 1 << ' ' << (p.y - 1) * S + p.x << endl; } #endif return 0; }
↓最終submit
#include <algorithm> #include <cmath> #include <iostream> #include <vector> using namespace std; #define U(v) cerr << #v << ": " << (v) << endl constexpr int dx[] = { -1, 0, 1, -1, 1, -1, 0, 1 }; constexpr int dy[] = { -1, -1, -1, 0, 0, 1, 1, 1 }; class timer { vector<timer> timers; int n = 0; public: double limit = 9.95; double t = 0; timer() {} timer(int size) : timers(size) {} bool elapses() const { return time() - t > limit; } void measure() { t = time() - t; ++n; } void measure(char id) { timers[id].measure(); } void print() { if (n % 2) measure(); for (int i = 0; i < 128; ++i) { if (timers[i].n) cerr << (char)i << ' ' << timers[i].t << 's' << endl; } cerr << " " << t << 's' << endl; } static double time() { #ifdef LOCAL return __rdtsc() / 3.3e9; #else unsigned long long a, d; __asm__ volatile ("rdtsc" : "=a" (a), "=d" (d)); return (d << 32 | a) / 2.8e9; #endif } } timer(128); #include <ctime> class { unsigned y = time(0); //unsigned y = 2463534242; public: unsigned next() { return y ^= (y ^= (y ^= y << 13) >> 17) << 5; } int next(int b) { return next() % b; } int next(int a, int b) { return next(b - a) + a; } double nextDouble(double b = 1) { return b * next() / 4294967296.0; } double nextDouble(double a, double b) { return nextDouble(b - a) + a; } int operator() (int b) { return next(b); } } rando; struct P { int x, y; char score; P() {} P(int x, int y) : x(x), y(y) {} }; int V, E, S; int neighborsSize; pair<pair<short, short>, pair<char, char>> neighbors[20000 * 64]; vector<pair<short, short>> edges; char weight[501][501]; short vs[62][62]; P ps[501]; P bestPs[501]; int eval() { int score = 0; for (auto& e : edges) { auto& a = ps[e.first]; auto& b = ps[e.second]; if (max(abs(a.x - b.x), abs(a.y - b.y)) == 1) score += weight[e.first][e.second]; } return score; } void init() { vector<int> a(S * S); for (int i = 0; i < S * S; ++i) a[i] = i; sort(a.begin(), a.end(), [](int i, int j) { return max(abs(i % S - (S - 1) / 2.0), abs(i / S - (S - 1) / 2.0)) < max(abs(j % S - (S - 1) / 2.0), abs(j / S - (S - 1) / 2.0)); }); random_shuffle(a.begin(), a.begin() + V, rando); for (int i = 0; i < S + 2; ++i) fill_n(vs[i], S + 2, V); for (int i = 0; i < V; ++i) { auto& p = ps[i]; p = { a[i] % S + 1, a[i] / S + 1 }; vs[p.y][p.x] = i; } for (int i = 0; i < V; ++i) { auto& p = ps[i]; p.score = 0; for (int d = 0; d < 8; ++d) p.score += weight[i][vs[p.y + dy[d]][p.x + dx[d]]]; } } void solve() { init(); copy_n(ps, V, bestPs); int score = eval(); int bestScore = score; int ni = -1; const double InitialHeat = edges.size() / (double)V < 3 ? 2.5 : V <= 200 ? 3.5 : 5; const double FinalHeat = edges.size() / (double)V < 3 ? 0.5 : 2; double heat = 0; for (int iter = -1; ; ) { if ((++iter & 0xffff) == 0) { //double p = 1 - iter / 200000000.0; double p = 1 - (timer.time() - timer.t) / timer.limit; if (p < 0) { U(iter); break; } heat = 1 / (p * (InitialHeat - FinalHeat) + FinalHeat); } short i, j; unsigned x, y; { if (++ni == neighborsSize) ni = 0; auto& n = neighbors[ni]; i = n.first.first; j = n.first.second; x = ps[j].x + n.second.first; y = ps[j].y + n.second.second; j = vs[y][x]; } if (i == j || max(x - 1, y - 1) >= (unsigned)S) continue; auto& p = ps[i]; if (j == V) { int diff = - ps[i].score; for (int d = 0; d < 8; ++d) diff += weight[i][vs[y + dy[d]][x + dx[d]]]; if (rando.nextDouble() < exp(diff * heat)) { score += diff; swap(vs[p.y][p.x], vs[y][x]); for (int d = 0; d < 8; ++d) { short v = vs[p.y + dy[d]][p.x + dx[d]]; ps[v].score -= weight[i][v]; v = vs[y + dy[d]][x + dx[d]]; ps[v].score += weight[i][v]; } p.score += diff; p.x = x; p.y = y; if (bestScore < score) { bestScore = score; copy_n(ps, V, bestPs); } } } else { vs[p.y][p.x] = j; vs[y][x] = i; int diff = - ps[i].score - ps[j].score; for (int d = 0; d < 8; ++d) diff += weight[j][vs[p.y + dy[d]][p.x + dx[d]]]; for (int d = 0; d < 8; ++d) diff += weight[i][vs[y + dy[d]][x + dx[d]]]; if (rando.nextDouble() < exp(diff * heat)) { score += diff; for (int d = 0; d < 8; ++d) { short v = vs[p.y + dy[d]][p.x + dx[d]]; ps[v].score -= weight[i][v] - weight[j][v]; v = vs[y + dy[d]][x + dx[d]]; ps[v].score += weight[i][v] - weight[j][v]; } ps[i].score = ps[j].score = 0; for (int d = 0; d < 8; ++d) { ps[i].score += weight[i][vs[y + dy[d]][x + dx[d]]]; ps[j].score += weight[j][vs[p.y + dy[d]][p.x + dx[d]]]; } swap(p.x, ps[j].x); swap(p.y, ps[j].y); if (bestScore < score) { bestScore = score; copy_n(ps, V, bestPs); } } else { vs[p.y][p.x] = i; vs[y][x] = j; } } } copy_n(bestPs, V, ps); //timer.print(); } int main() { timer.measure(); #ifdef LOCAL int type, seed; cin >> type >> seed; #endif cin >> V >> E; for (int i = 0; i < E; ++i) { int a, b, w; cin >> a >> b >> w; --a; --b; if (w != 0) { edges.emplace_back(a, b); for (int i = 0; i < (int)round(sqrt(w)); ++i) { for (int d = 0; d < 8; ++d) { neighbors[neighborsSize++] = { { a, b }, { (char)dx[d], (char)dy[d] } }; neighbors[neighborsSize++] = { { b, a }, { (char)dx[d], (char)dy[d] } }; } } weight[a][b] = weight[b][a] = w; } } random_shuffle(neighbors, neighbors + neighborsSize, rando); cin >> S; S = (int)sqrt(S); { int E; cin >> E; for (int i = 0; i < E; ++i) { int a, b; cin >> a >> b; } } solve(); #ifdef LOCAL int score = 0; int esum = 0; int wsum = 0; for (auto& e : edges) { auto& a = ps[e.first]; auto& b = ps[e.second]; if (max(abs(a.x - b.x), abs(a.y - b.y)) == 1) { score += weight[e.first][e.second]; ++esum; } wsum += weight[e.first][e.second]; } cerr << type * 500 + seed << "\t" << score << "\t" << wsum << "\t" << esum << "\t" << edges.size() << "\t" << V << "\t" << S << endl; #else for (int i = 0; i < V; ++i) { auto& p = ps[i]; cout << i + 1 << ' ' << (p.y - 1) * S + p.x << endl; } #endif return 0; }
WindowsでPython2/3の共存が壊れたらPYTHONHOMEを削除する
Windowsではpy
とpy -2
でPython2/3の共存ができますが、再インストールやマイナーバージョンのアップグレードなどで以下のように壊れることがあります。
>py Fatal Python error: Py_Initialize: unable to load the file system codec File "C:\python27\lib\encodings\__init__.py", line 123 raise CodecRegistryError,\ ^ SyntaxError: invalid syntax Current thread 0x00000e1c (most recent call first):
>py -2 File "C:\Python36\lib\site.py", line 177 file=sys.stderr) ^ SyntaxError: invalid syntax
これらは環境変数のPYTHONHOME
を削除すると直りました。このPYTHONHOME
はPython2/3の両方から参照されるらしく、削除するとそれぞれのデフォルト値が使われうまく動くようになるようです。
もしPythonをデフォルトのパス以外にインストールしている場合、さらに何らかのケアが必要かもしれません。
MM94 ConnectedComponent
問題文 https://community.topcoder.com/longcontest/?module=ViewProblemStatement&rd=16958&pm=14678
基本方針
順列の左端N個が確定しているとすると順列の後ろにどんな値を足しても左上N×Nの領域の値は変わらない。なのでこの確定領域の最良コンポーネントのスコアを途中状態のスコアとできる。
そして現在の順列の後ろに値を足して一番途中スコアが大きくなるような順列を次の状態とするgreedy/beamsearchをすると最良コンポーネントが徐々に大きくなり良い解が得られる。
高速化
途中スコアの愚直な計算はO(S2)であるためgreedyですら終わらないが、新しく足す値によって確定するマス(下図赤マス)と接するマス(下図緑マス)は現確定領域の"外周に限られる"ことを利用して差分計算/更新ができ高速になる。
具体的には緑マスそれぞれをノードとしたUnionFind(各ノードはそのマスが属すコンポーネントのsizeとsumを持つ)を現状態として持つ。次に緑UFを元に緑赤UFを作る。次に連結箇所をuf.unite()。最後に赤だけのUFを作ればそれが次状態になる。このとき連結を調べているのが外周付近だけであるためO(S2)がO(S)に落ちてgreedy/beamsearchが実行時間に収まる。また、UFを舐めればsumとsizeを持っているため最良コンポーネントとそのスコアがわかる。
このUnionFindを使って連結情報を右に右に伝えていくというのはこの問題でも使われてるっぽい。 http://acm-icpc.aitea.net/index.php?plugin=attach&refer=2017%2FPractice%2F%E6%A8%A1%E6%93%AC%E5%9B%BD%E5%86%85%E4%BA%88%E9%81%B8%2F%E8%AC%9B%E8%A9%95&openfile=H.pdf
ビーム幅
同じseedで繰り返し実行したときのスコアの分散が大きめだったので、ビーム幅の増加によるスコア上昇よりも、細いビーム幅で繰り返したときの当たる確立の上昇のほうが強いと踏んで、ビーム幅1のビームサーチを時間いっぱい繰り返した。だけど1stと2ndは大きなビーム幅で1回ビームサーチらしいので外したっぽい。分散が大きいというのが違うのか、分散が大きいときは~のほうが強いというのが違うのか。
評価関数
最良コンポーネントの近くに他のコンポーネントがあって繋がると大きなスコアのプラスになるとき積極的に繋がりにいきたい。
これを評価関数に加える。他のコンポーネントはsumが大きいほど、sizeが大きいほど、近いほど(繋がりやすいことと連結に近づくため)良いとしたい。なので最良ではないコンポーネントそれぞれに対して、仮に最良コンポーネントと繋がったときのスコアの上昇量にpow(0.9, 最短距離)をかけたものを途中スコアに足した。
文章だとわかりづらいので擬似コード
double maxComponentScore = maxComponent.sum * sqrt(maxComponent.size); double score = maxComponentScore; for (auto& c : nonmaxComponents) { double connectedScore = (maxComponent.sum + c.sum) * sqrt(maxComponent.size + c.size); double diff = connectedScore - maxComponentScore; if (diff > 0) score += diff * pow(0.9, manhattanDistanceNearest(c, maxComponent)); } score; // これがビームサーチの評価値として使われる
1st, 2nd, 3rdまでの評価関数のプラスアルファがバラバラなのおもろい。
山登り
Sがごく小さいときはビームサーチの結果を山登り。ランダム解から登るより、ビームサーチからのほうが良かった。近傍は2つをswapと、1つを別のところにinsert(つまりrotate)の2種類。左上からビームサーチする都合、左上が高密度のコンポーネントができるので左上へのinsertで改善しやすかった。なのでその近傍を先んじて試すようにした。2つの近傍自体に優劣はないと思うんだけど、初期解が影響して優劣が生まれるというのがおもしろかった。
評価関数はO(S2)だがy_kawanoさんがchokudai contest(本問題と似ていて連結数を大きくする問題)で使っていたテクで数倍早くした。自力で思いつける類ではないので感謝
得点計算の高速化はDFSの基点となる位置をキャッシュしたのが効いた #chokudai_003
— y_kawano (@y_kawano) 2017年2月21日
やりたかったけどできなかったこと
非0マスの割合によって連結の容易さがテストケースごとにかなり変わる。最終コンポーネントがスッカスカだったりミッチミチだったり。この特徴に注目して何かヒューリスティクスを刺したかった。
Example scores:
0) 232.826115373684 1) 23147.483599734984 2) 7426823.502824272 3) 121935.2517568238 4) 148933.58515123444 5) 2259336.504998536 6) 733271.118536657 7) 3257617.5326836635 8) 4670802.284891322 9) 1676018.7939363925
Image
seed=2, score=21907 pic.twitter.com/iIdLh9hhtg
— hakomo (@hakomof) 2017年8月24日
seed=3, score=7488480 pic.twitter.com/sYuESDhLFf
— hakomo (@hakomof) 2017年8月24日
seed=7, score=726365 pic.twitter.com/oJEr6GoE1l
— hakomo (@hakomof) 2017年8月24日
TCO17MMR2 AbstractWars
問題文 https://community.topcoder.com/longcontest/?module=ViewProblemStatement&rd=16928&pm=14599
ヒューリスティクス的なアイデアが色々考えられて今までで一番好きな問題。
MMおつかれさまでした
— hakomo (@hakomof) 2017年5月25日
ビームサーチで速攻、序盤に奪う基地の探索
中終盤は1000をあふれるなら攻撃、攻撃箇所は最小費用流で決定
ビームサーチと最小費用流の評価関数は敵が同士討ちをしやすいほど良い
勝89.9% 負6.3% 分3.8%
— hakomo (@hakomof) 2017年5月25日
速攻は21.6% 速攻がtomerunさんの半分以下だ・・・
本当はもっとたくさん書いたんだけど長くなったのでがんばったところだけ。
以下抜粋。
敵同士をつぶしあわせる、ビームサーチの評価関数、最小費用流のコスト
自分が攻撃される確率が低いほどよい
初期配置を↓とする。
テキトーに近い敵baseを奪うと↓のようになり、これは敵自入り混じった乱戦になりpowerが不利な自分が負ける。
しかし↓のように敵baseを奪うと敵は距離の近いbaseを攻撃する確率が高いため自分をあまり攻撃せず敵同士でつぶしあってくれる。その間にsizeを着々と貯めれば100の倍数ボーナスで差がつくので勝てる。
このような自分が攻撃される確率が低い(相対的に敵同士でつぶしあいやすい)配置になるように敵を攻撃する。
敵の初撃に耐えるほどよい
序盤、敵baseを奪いすぎるとsizeが小さいため簡単に再奪取されることが多い。これを軽減するために奪えなかった敵base近くの自baseのsizeを大きくしたい。
localityを1.1とすると敵baseが各baseを攻撃する確率は計算できてattackTも決めれば初撃ターン・初撃サイズも求まり、自baseが奪われる確率も計算できる。そこでattackTの値域から均等に3つ583,750,917を選びその平均を初撃に耐える確率としそれが高いほどよいとした。
これで奪いすぎるのを軽減したり、敵baseの近くに壁をつくる効果が期待できる。
ビームサーチの評価関数
ビームサーチの評価関数 = 奪ったbaseの数 - 自分が攻撃される確率の和 - 敵の初撃で奪われる確率の和
狙ったわけではないが、自分が攻撃される確率が長期的な評価値で、敵の初撃で奪われる確率が短期的な評価値で、バランスがよさそう。
攻撃されやすい敵baseを攻撃するのは後回し
↓のような状況のとき上真ん中を奪うと左右からぼこされて再奪取されやすい。
しかし↓のように上右から奪うと真ん中の攻撃は左右に分散しやすく、左の攻撃は真ん中にいきやすい。そのため奪った後も守りやすい。
このように攻撃されやすい敵baseは奪った後も攻撃にさらされやすいので後回しにする。また攻撃されて弱った後なら楽に奪えるという効果もある。
最小費用流のコスト
最小費用流のコスト = 到着時刻 + 50 * -log(その敵baseが自分を攻撃する確率) + 40 * -log(max(1e-9, 1 - その敵baseが他の敵に攻撃される確率の和))
空を飛んでいるtroopはbaseと接触してはじめて効果がでる(敵baseを奪うにしろ、自baseに合流して100の倍数ボーナスを増やすにしろ、敵baseを削って100の倍数ボーナスを減らすにしろ)ので滞空時間、到着時刻は短いほうがよい。ので到着時刻にペナルティを加算した。
Example scores
0) 1700.3533398049744 1) 1806.7049780062375 2) 1978.8025098890039 3) 1962.538169965643 4) 1983.4413356595355 5) 1663.3656903521724 6) 1829.6786713895922 7) 1486.8174209242056 8) 1690.0970680903945 9) 1810.6962420903901
GIF
— hakomo (@hakomof) 2017年5月25日
— hakomo (@hakomof) 2017年5月25日
劣勢のときはまとまった数が残ってるうちに最遠2点間を往復する(これで2000ターン逃げ切ることもある) pic.twitter.com/7IrIc1I4Fe
— hakomo (@hakomof) 2017年5月25日
Chokudai Contest 3 考えたこと
問題文 A: ○×ブロック - Chokudai Contest 003 | AtCoder
5864点18位でした。
. + -
ブロックひとつを異なる種類の. + -
ブロックに変えてスコアが上がるようなら保持、下がるようなら元に戻す山登りをした。
遷移でスコアが変わらなくても、連結ブロック数の2乗和が大きくなるときは遷移した(スコアに関係ない連結ブロックが成長したら良さそう)
上下にo
とo
もしくはx
とx
で挟まれた空間にはブロックを新設しない方がよさそうだった。連結を分断しやすそうなので。ただ間に合わなかったので確かめてない。
下の方からブロックを変えていくというのを2周した。
ブロックを変更していない列の落下処理はキャッシュして省略することで高速化(差分計算)、元に戻るときの情報を残しておいて元に戻すのを高速化などをして、試行回数は22万回くらいだった。
ブロックは落下するので下の方にかたよる。なので落下しない-
ブロックを上の方に置くのはよくない。なのでそもそも遷移を試さない。
むずかしかったー
MM92 考えたこと
問題文 https://community.topcoder.com/longcontest/?module=ViewProblemStatement&rd=16857&pm=14485
MMの方針
— hakomo (@hakomof) 2017年1月19日
誤差ゼロの焼きなましをひたすら高速化。
近傍はライトを1つ動かす。
最後に極近傍山登りで微調整(こちらも誤差ゼロ)
試行回数は15万回~350万回
感想
CutTheRootsに高速化焼きなましって意味で似てるなー、CutTheRootsってたしか最上位厳密スコア求めてるんだよなーこれも厳密スコアかなーで突っ走った結果、近似スコアだったでござる。 初日で方針ミスって、しかも下手に良いスコアとってしまい方針ミスに気付けなかったので次からは気をつけたい。
自分の方針だと色んな箇所に色んな高速化をしていて高速化ゲー(もちろん高速化だけでは厳しいけど)としてはかなり楽しい(そして練習になる)問題だと思いました。(ただこの記事ではほとんど省略されてます)
焼きなまし
近傍はライトを1つ動かす。 焼きなまし序盤はライトを大きく動かして、終盤は小さく動かす。動かす距離は時間の経過とともに指数的に小さくした(試行回数の少なさを補うにはこれが良いと思った)。
壁の近くはすぐにライトがカットされるので良い位置ではないだろうと遷移しないようにしてみたがあまり効果なし。
遷移先に偏りのない605Kでるプログラムでinner_xとinner_y(小数点以下の座標)の分布を見ると、ものっそい偏ってた。
ヒートマップの色はテキトーだけど光源の45%がinner_x == 0 || inner_y == 0だった。おそらくライトの半径が整数で壁も整数刻みなので、inner_x == 0 || inner_y == 0だと壁と壁の間にすっぽりぴったし収まりやすいのだと思われる。なのでinner_x == 0 || inner_y == 0に遷移しやすくしてみたのだけどあまり効果なし(がーん)。
山登り
CutTheRootsしかりTerrainCrossingしかり座標が広い問題は最後に極端に狭い近傍で山登りというのをみなさまやってらっしゃる。のでライトを1つ周囲8dotに動かす山登りを最後にした。
高速化
100dot x 100dot(1セル)をunsigned long long[100][2]の格ビットで表現する。
放射光のビットパターンと円形の光のビットパターンのandビット演算をすれば1つのライトのビットパターンが求まる。ライトとライトのorビット演算をすればライトが重なったビットパターンが求まる。あとはビットの数を数えれば(__builtin_popcountll)厳密なスコアが求まる。
円形の光のビットパターンは事前に計算して持ってくる。
放射光のビットパターンは、 まず、放射光の境界線の角度が、光源から外に向かって貰うDPで求まる。 次に、境界線の角度と光源の座標から境界線の直線の式が求まる。 そして、直線の式に求めたいビット列のy座標を代入すれば何ビット目から何ビット目まで立ってるかがわかるのでビット列を求める。
他の(でも重要な)高速化
あるセルをあるライトが完全に照らしているとき、他のライトの処理の省略、代替、遅延、枝刈による高速化(これが重要だけど説明できず)。
壁による遮蔽なしライトの重なりなしなセルのスコアは2次元累積和でO(1)で求まる。
差分計算もしてます(これも重要だけど説明できず)。
Codeforces Marathon Round1 Online Exam
問題文 http://codeforces.com/contest/684/problem/A2
タイトルのマラソンで考えたことが書いてあります。過程を書いたので長いです。
暫定スコアは77261点、暫定順位は3位でした!がんばった!
69000点
2つのアイデアが必要でした。
1.ビット区間を反転してスコアが下がったら戻す
テキトーなビット列をクエリに投げてスコアを確認します。
1.そのビット列の一部を反転したものをクエリに投げます。
2.その結果、スコアが上がればビット列は反転したまま、スコアが下がればビット列を元に戻します。
1.2.をくりかえせばスコアは上がり続けます。
ではどんな一部を反転すればよいでしょうか。
1回のクエリで反転するビットの数は際限なく多いほうがよいです。
1つのビットを反転させたときのスコアの改善は高々1
2つのビット~は高々2
3つのビット~は高々3
...と反転するビットの数が多いほどスコアの改善が大きいことが期待できます。
また、スコア番目以降のビットを反転してもスコアは変わりません。つまり情報がとれない。そして初期スコアはK*2の4000前後で、クエリごとに上がるので4000ちょっとのビットから情報がとれます。
また、最初のクエリは初期ビット列のスコアの確認に使いますし、最後のクエリの結果を元にさらにクエリを投げることもできません。なので使えるクエリの数は98で、めいっぱい使います。
また、同じビットを何度も調べるのは意味がない(一度調べて改善したビットを反転しても半分以上の確率でスコアが下がるため)のでクエリごとに反転するビットは重複しないほうがよさそうです。
以上より4000ちょっとのビット列を等しく98分割してクエリごとに順番に反転するという方針が立ちます。 4000 / 98 = 40.8... なので長さ41のビット区間×98あたりのスコアが一番よくなります。
2.クエリの結果-1番目のビットは確実に誤答
左端から順番に採点して誤答の数がKになったら即座に終了ですから当然直前のビットは必ず誤答です。
この誤答はクエリを投げた直後に正し、以降このビットは不変としたほうがよいです。というのも正さずに放っておくと以降のクエリでこのビットが間違ってるやで~とまた言われてしまうので(間違っていることはすでにわかっているのでもったいない)。
1つのビットを正したときのスコアの上昇期待値は+2 です。1つのビットを正しているので確実に+1され、次のビットも正しければ+2、その次のビットも正しければ+3と続きます。次以降のビットが正しい確率は半分なので 1 * 0.5 + 2 * 0.25 + 3 * 0.125 + 4 * 0.0625 ... = 2 になります。+2点×99クエリ×100ケースで19800点くらい上がります。
上述のビット区間の反転と組み合わせるときにずれるので補正します。
1.あるビット列のクエリを投げて古いスコアをゲット
2.確実な誤答を正す
3.ビット区間を反転してクエリを投げて新しいスコアをゲット。新しいスコアと古いスコアを比較
このとき古いスコアは確実な誤答を正す前のスコアなのでその分ずれます。じゃあ期待値分+2すればよいかというと+1.333くらいがよいようです。たいていは+1か+2のどちらかだからだと思います。
また、上述のビット区間の反転とこれが重なってしまうのももったいないです。上述のビット区間の反転の全体の範囲が4000ちょっとが最適なのは(もっと広くてもよさげなのに)これが効いてるやも。んで、できるだけ重ならないようにするには速やかにビット列のスコアを4200くらいまで上げればよい、これはビット列をクエリごとに改善してれば十分か。あるクエリであるビット区間を反転したほうがスコアが上がることがわかったときに、あえて反転せずにためてためて最後のクエリでどばっと反転させてどばっとスコアを上げるテク(後述)を使ったときにやりすぎて(1~99クエリ目でほとんどスコアを上げなかったときに)死んだ。
この2つをやると69300点くらい出ました。
74000点
69000点の時点で75000点の人がいたのでその差を考えると1ケース60ビットの差があり、これは1クエリや2クエリの差ではないことがわかります。毎クエリやっていることが違う、毎クエリもっとたくさんの情報が得られると考えました。
そこでクエリを観察するとスコアが大きく変わるときと小さく変わるときがあります。この大きく変わるときとは正誤が偏っている(めっちゃ当たってるかめっちゃ外れてる)ということです。そして小さく変わるときは正誤が半分ずつに近いということです。
このことよりスコアの変化量から正誤の偏りが推定できます。正誤の偏りはずばりそのビット区間の改善の余地を意味します。偏りが小さいほど改善の余地が大きいです。たとえば長さ100のビット区間のうち90が当たっているときはあと10しか改善できませんし、90が外れているときは反転すれば90が当たっているときと同じですし、長さ100のビット区間のうち50が当たっているときは50も改善の余地があります。
そこで改善の余地が大きい偏りが小さいビット区間に優先的にクエリを投げるとよさそうということがわかります。あるビット区間にクエリを投げるべきかどうかはある区間を含むより広い区間の偏りがわかっていないといけない、つまりそのより広い区間にクエリが投げられていないといけないです。
そこでこのようなクエリが考えられます。
誤答率というのは正誤の偏りの言い換えです、同じことを言っています。んでこのクエリの投げ方は以下のクエリの投げ方と同じクエリ数で同じ情報量が得られます。つまりその情報群から最適な戦略をとったきにまったく同じスコアになります(誤差を無視すれば)。
この2つのクエリの投げ方からは同じ情報量が得られ、相互に入れ替えが可能です。ここで69000点解法を思い出してみます。4000ビットを98分割して等間隔にクエリを投げていましたね。
これを
こうして
こうして
こうじゃ!
なんか Binary Indexed Tree っぽいな? BIT 違いですあんまり関係ないです。これはまだ等価な置き換えをしただけなのでスコアは変わりません。ここで改善の余地が大きい偏りが小さいビット区間に優先的にクエリを投げるように変えます。
投げているクエリの数(赤線の数)が変わっていないこと、ビット区間を仕切る黒縦線がばらついているのに注目してください。これで同じクエリの数で改善の余地が少ないビットへのクエリを節約して、改善の余地の大きいビットにたくさんクエリを投げれています。
これを実装すれば74000点が・・・出ません。誤差が大きくなりすぎます。このマラソンは誤差との壮絶な戦いでした。この誤差マラソン中にやってよかったことは誤差を無視したらどうなるのかを常に意識するとです。それは正解ビット列をカンニングしながら推定値を使うところをすべて真値を使うプログラムを書くことです。これをすると実装がバグってるのか誤差が大きすぎるかの切り分けができる、誤差0のときのスコアつまり誤差に対する上界がわかる、上界を元に1位に届くか判断できる、誤差でどのぐらいスコアが下がっているのかがわかるです。
でまあ以上のようなことをやって上界が78000点(1位オーバー)ですが誤差ありでは71000点くらいしかでず誤差が大きいことがわかりました。そこで誤差を小さくできれば1位に届くと思い、誤差が大きい箇所を調べました。誤差が大きい箇所は3つでした。
1.広いビット区間の反転は誤差が大きい
さっき述べましたがたくさんビットを反転するとスコアの変化量が大きいのでそれにともない誤差も大きくなります。広いビット区間のクエリは控えたいですね。
2.区間ACの誤答率と区間ABの誤答率から区間BCの誤答率を算出推定するときに誤差が累積する。
クエリ値からの推定は1次ソースなので誤差は常に一定値以下ですが区間と区間から区間の誤答率を求めるたびに誤差が累積します。ビット区間が狭くなるほど深くなるほど誤差が大きくなります。あまり深くしなければよさそうですね。
3.区間BCの誤答率がわかった時点で区間BCを反転した方がスコアが上がるかどうかがわかりますがこれを即時反転してしまうと誤差る
少し前に後述って言ってたやつです。わかった時点で反転してスコアを改善してしまうとスコアの上昇量を推定しないといけなくなり誤差がでます。反転したほうがスコアが上がるという情報を覚えておいて最後のクエリの直前にまとめて反転すると誤差を0にできます。
1.2.より長さ88の区間46個を初期ビット区間集合として深さを3に制限する。3.をやる。パラメーター調整をする。これで74000点がでました。上界が78000点なので軽減したとはいえ結構誤差がのこってます。
77000点
誤差は限界まで小さくしたので他に何か問題がないか考えてます。反転するビット区間が広すぎると誤差が大きすぎてよくないことがわかりました。逆に狭すぎるとスコアの変化量が小さすぎるのでよくないこともわかっています。
このことから誤差が大きすぎず(長さが広すぎず)、スコアの変化量が小さすぎない(長さが小さすぎない)最適なビット区間の長さがあるのではと考えました。74000点解法は広い区間から徐々に狭い区間に狭めていくという解法でこの考察が正しいとするとよくないですね。なんとかしたい。
そこで広い区間を狭い2つの区間に分割した後にもう一度統合すれば最適な区間の長さを保持できるのではと考えました。ここで統合という概念がでてきたのでここからは区間(連続している)という表現は正しくなくなったので以降ビット部分列という表現に変わります。
問題はどのビット部分列と部分列を統合するかです。これは誤答率ができるだけ近しい部分列と部分列を統合するのがよいです。理由は情報量の減少が小さいから。たとえば誤答率が10%の部分列と90%の部分列を統合してしまうと誤答率が50%になり情報がうすまってしまいます。
以上のことから偏りが小さく、かつ誤答率が近しい部分列を2つ選んで統合、分割して片方のビットを反転してクエリを投げるというのを繰り返すと改善の余地が大きいところを集中的に最適な長さのビット部分列で殴り続けられるので76000点がでました。
あとパラメーター調整とかヒューリティスとか誤差を小さくしたりあやしげな工夫をいろいろして77000点がでました。
リファクタしまくってたら100行くらいになった。
#include <algorithm> #include <iostream> #include <vector> using namespace std; const int N = 5000, K = 2000, X = 100; bool a[N + 1]; bool b[N + 1]; void invert(const vector<int>& v) { for (int i : v) { if (!b[i]) a[i] = !a[i]; } } int query(double prev, const vector<int>& v) { invert(v); for (int i = 0; i < N; ++i) cout << (int)a[i]; cout << endl; cout.flush(); int p; cin >> p; a[p - 1] = !a[p - 1]; b[p - 1] = true; if (p < prev) invert(v); return p; } struct S { vector<int> v; double w; double score(const S& s, int t) const { return s.w - w + abs(s.w + w - v.size()) * (t < 3 ? 0.74 : t < 9 ? 0.5 : 0.3); } bool operator < (const S& s) const { return w < s.w; } }; int main() { const int NUM = 42; const int LEN = 98; const int LEN2 = LEN / 2; const int Q = 3954; const double P = 1.34; const double DIFF = 4.8; vector<int> v; for (int i = 0; i < N; ++i) v.emplace_back(i); int p = query(K * 2, v); double prev = (p < Q ? query(0, vector<int>()) : max(p, K * 4 - p)) + P; S q[NUM]; for (int i = 0; i < NUM; ++i) { for (int j = i * LEN; j < (i + 1) * LEN; ++j) q[i].v.emplace_back(j); int p = query(prev, q[i].v); q[i].w = LEN2 - abs(p - prev) / DIFF; prev = min<double>(max(prev + P, p + P), N); } for (int t = X - (p < Q) - NUM - 3; t; --t) { sort(q, q + NUM); S* s = q; for (int i = 2; i < NUM; ++i) { if (s[0].score(s[1], t) > q[i - 1].score(q[i], t)) s = q + i - 1; } S ns[2]; for (int i = 0; i < LEN * 2; ++i) ns[i < LEN].v.emplace_back(s[i % 2].v[i / 2]); int p = query(prev, ns[0].v); double diff = p - prev; for (int i = min(p, (int)prev); i < max(p - 1.0, prev); ++i) diff += b[i] && (p > prev ? -1 : 1); ns[0].w = LEN2 - abs(diff) / DIFF; ns[1].w = s[0].w + s[1].w - LEN2 - diff / DIFF; s[0] = ns[0]; s[1] = ns[1]; prev = min<double>(max(prev + P, p + P), N); } auto& s = *min_element(q, q + NUM, [LEN2](const S& a, const S& b) { return abs(a.w - LEN2) < abs(b.w - LEN2); }); p = query(prev, vector<int>(s.v.begin(), s.v.begin() + LEN2)); if (s.w - (p - prev) / DIFF > LEN2) invert(vector<int>(s.v.begin() + LEN2, s.v.end())); s.w = 0; for (auto& s : q) { if (s.w > LEN2) invert(s.v); } query(0, vector<int>()); return 0; }