接着剤の精進日記

競プロでの精進や研究に関係したことを書いていきます。

AtCoder Beginner Contest 185(ABC185)

atcoder.jp

A - ABC Preparation

$ min(A_1, A_2, A_3, A_4) $を出力
提出コード

void solve(){
    int mi = 101;
    REP(i,4){
        int a;
        cin >> a;
        chmin(mi, a);
    }
    cout << mi << endl;
}

B - Smartphone Addiction

実際にシミュレートをし、途中で0以下になればNo
提出コード

void solve(){
    ll N, M, T;
    cin >> N >> M >> T;
    vector<ll> A(M), B(M);
    REP(i,M) cin >> A[i] >> B[i];
    ll cur = N;
    ll pre = 0;
    REP(i,M){
        cur -= (A[i] - pre);
        if(cur <= 0){
            cout << "No" << endl;
            return;
        }
        cur = min(N, cur + B[i] - A[i]);
        pre = B[i];
        // dump(cur);
    }
    cur -= T - pre;
    if(cur > 0) cout << "Yes" << endl;
    else cout << "No" << endl;
}

C - Duodecim Ferra

$ dp[i][j] := i $個目まで見たときの長さの総和が$ j $のときの場合の数としてDPをする
想定解は$ _{L - 1} C _{11} $に等しい、気づかなかった
提出コード

void solve(){
    int L;
    cin >> L;
    vector dp(13, vector<ll>(222));
    dp[0][0] = 1;
    REP(i,12){
        REP(j,222){
            // i本目を何cmで切るか
            for(int k=1;k<=189;k++){
                if(j + k > L) continue;
                if(dp[i][j] == 0) continue;
                dp[i+1][j+k] += dp[i][j];
            }
        }
    }
    ll ans = 0;
    cout << dp[12][L] << endl;
}

D - Stamp

連続した白色の区間の長さが最小の部分がネックになるのでその長さを$ mi $とする
白色の区間の長さを$ k $とするとその区間は$ \lceil \frac{k}{mi} \rceil $回操作を行う必要があるので、その総和が答え
マス$ A $に$ 0, N+1 $を追加すると実装が少し楽
提出コード

void solve(){
    ll N, M;
    cin >> N >> M;
    vector<ll> A(M);
    REP(i,M) cin >> A[i];
    if(M == 0){
        cout << 1 << endl;
        return;
    }
    if(N == M){
        cout << 0 << endl;
        return;
    }
    ll mi = LINF;
    A.emplace_back(0);
    A.emplace_back(N+1);
    ll sz = A.size();
    sort(A.begin(), A.end());
    REP(i,sz-1){
        if(A[i+1] - A[i] - 1 == 0) continue;
        chmin(mi, A[i+1] - A[i] - 1);
        // dump(mi);
    }
    ll ans = 0;
    REP(i,sz-1){
        ans += (A[i+1] - A[i] - 1 + mi - 1) / mi;
    }
    cout << ans << endl;
}

E - Sequence Matching

$ dp[i][j] := A $を $ i $文字目まで、$ B $を $ j $文字目まで見たときの$ x + y $の最小としてDPをする
これは編集距離と同様のことをやっている(本番中全然気づかなかった)
提出コード

void solve(){
    int N, M;
    cin >> N >> M;
    vector<int> A(N), B(M);
    REP(i,N) cin >> A[i];
    REP(j,M) cin >> B[j];
    vector dp(N+1, vector(M+1, 0));
    REP(i,N+1) dp[i][0] = i;
    REP(j,M+1) dp[0][j] = j;
    for(int i=1;i<=N;i++) for(int j=1;j<=M;j++){
        dp[i][j] = min({dp[i-1][j] + 1, dp[i][j-1] + 1, dp[i-1][j-1] + (A[i-1] != B[j-1])});
    }
    cout << dp[N][M] << endl;
}

F - Range Xor Query

XORの区間和と一点更新はBITかセグ木に載せられるのでどちらかを使えばいい
本番中はセグ木を使った
提出コード

void solve(){
    int N, Q;
    cin >> N >> Q;

    SegTree<long long> seg(N, [](long long a, long long b) {
            return a ^ b;
        },
        0);
    vector<long long> A(N);
    for (int i = 0; i < N; ++i) {
        cin >> A[i];
        seg.set(i, A[i]);
    }
    seg.build();
    REP(i,Q){
        ll T, X, Y;
        cin >> T >> X >> Y;
        X--;
        if(T == 1){
            ll x = seg.get(X, X+1);
            seg.update(X, x ^ Y);
        }
        else{
            cout << seg.get(X, Y) << endl;
        }
        // seg.print();
    }
}

鹿島建設プログラミングコンテスト2020(AtCoder Regular Contest 110)

atcoder.jp

A - Redundant Redundancy

$ 2, 3, ..., N $のLCMは$2, 3, ..., N $の倍数になるのでこれに1を加えたものが条件を満たす
提出コード

void solve(){
    ll n;
    cin >> n;
    ll ans = 1;
    for(int i=1;i<=n;i++) ans = lcm(ans, i);
    cout << ans + 1 << endl;
}

B - Many 110

Tの先頭が110のどこから始まるかで場合分けをする
$ |T| \leq 2 $のときは答えを場合分けしておく
$ |T| \geq 3 $のときは先頭3文字だけを場合分けする
このとき、一番初めに条件を満たす連続する部分文字列の末尾のindexを求めると答えは$ 10^{10} - \frac{index-1}{3} $ 提出コード

void solve(){
    ll N;
    string T;
    cin >> N >> T;
    if(T == "1") cout << (ll)1e10 * 2 << endl;
    else if(T == "0") cout << (ll)1e10 << endl;
    else if(T == "10") cout << (ll)1e10 << endl;
    else if(T == "01") cout << (ll)1e10 - 1 << endl;
    else if(T == "11") cout << (ll)1e10 << endl;
    else{
        bool ok = true;
        int index = (int)T.size();
        if(T.substr(0,3) == "110"){
            string s = "110";
            for(int i=3;i<T.size();i++) if(T[i] != s[i%3]) ok = false;
        }
        else if(T.substr(0,3) == "101"){
            string s = "101";
            index++;
            for(int i=3;i<T.size();i++) if(T[i] != s[i%3]) ok = false;
        }
        else if(T.substr(0,3) == "011"){
            string s = "011";
            index += 2;
            for(int i=3;i<T.size();i++) if(T[i] != s[i%3]) ok = false;
        }
        else ok = false;

        if(!ok){
            cout << 0 << endl;
            return;
        }
        cout << (ll)1e10 - (index - 1) / 3 << endl;
    }
}

C - Exoswap

$ 1, 2, ..., N-1 $の順に$ P_i = i $となるようにソートをしていく
途中ですでに行った操作が必要になる場合は-1
また、操作を行った回数が$ N - 1 $にならない場合も-1
それ以外の場合は上記の操作を行うことで答えが求められる
提出コード

void solve(){
    int n;
    cin >> n;
    vector<int> a(n), b(n);
    REP(i,n){
        cin >> a[i];
        a[i]--;
    }

    vector<int> idx(n);
    REP(i,n) idx[a[i]] = i;
    vector<bool> used(n);
    vector<int> ans;
    for(int i=0;i<n;i++){
        if(idx[i] > i){
            for(int j=idx[i];j>i;j--){
                if(used[j]){
                    cout << -1 << endl;
                    return;
                }
                ans.emplace_back(j);
                used[j] = true;
                swap(idx[a[j]], idx[a[j-1]]);
                swap(a[j], a[j-1]);
            }
        }
    }
    if(ans.size() != n-1) cout << -1 << endl;
    else for(auto x : ans) cout << x << endl;
}

AtCoder Regular Contest 109(ARC109)

atcoder.jp

A - Hands

算数でも出来そうだけど頭を壊しそうなのでダイクストラとかで最短経路を求める
提出コード

void solve(){
    int a, b, x, y;
    cin >> a >> b >> x >> y;
    --a, --b;
    Dijkstra<int> dijk(2*111, INF);
    for(int i=0;i<100;i++){
        if(i < 99) dijk.make_edge(i, i+1, y);
        if(i < 99) dijk.make_edge(i+1, i, y);
        if(i < 99) dijk.make_edge(i+1, i+100, x);
        if(i < 99) dijk.make_edge(i+100, i+1, x);
        if(i < 99) dijk.make_edge(i+100, i+100+1, y);
        if(i < 99) dijk.make_edge(i+101, i+100, y);
        dijk.make_edge(i, i+100, x);
        dijk.make_edge(i+100, i, x);
    }
    dijk.solve(a);
    cout << dijk.cost[100+b] << endl;
}

B - log

$ n+1 $の丸太で出来るだけ多くの丸太を作ってから残りの丸太を買うのが最適になる
$ n+1 $の丸太で作れるものは$ \frac{x(x+1)}{2} \leq n+1 $を満たす最大の$ x $となる
この$ x $を求めるには二分探索とかで求めればいい(C++なら$ \sqrt n $でも通る)
よって答えは$ n - x + 1 $
提出コード

void solve(){
    ll n;
    cin >> n;
    ll i = 1;
    for(;i*(i+1)<=2*n+2;i++){}
    cout << n - i + 2 << endl;
}

C - Large RPS Tournament

文字列$ s $の対戦結果は周期を持つため、全ての対戦結果をシミュレートする必要がない
そのため $ t = s + s $としてシミュレートを行い、その結果を$ s = t $として$ k $回シミュレートした時の先頭の文字が答えになる
提出コード

vector<vector<char>> win = {{'R', 'R', 'P'},
                            {'R', 'S', 'S'},
                            {'P', 'S', 'P'}};
void solve(){
    int n, k;
    string s;
    cin >> n >> k >> s;
    string t = s;
    map<char, int> mp;
    mp['R'] = 0, mp['S'] = 1, mp['P'] = 2;
    for(int j=k;j>0;j--){
        t = "";
        for(int i=0;i<4*n;i+=2){
            t += win[mp[s[i%n]]][mp[s[(i+1)%n]]];
        }
        s = t.substr(n);
    }
    cout << s[0] << endl;
}

Codeforces Round #686 (Div. 3)

codeforces.com

A. Special Permutation

偶数なら先頭から2個ずつペアにしてswapする
奇数の場合も同様にして最後の1要素だけ適当にswapさせる
$ n, 1, 2, 3, ..., n-1 $で良かったね
提出コード

void solve(){
    int n;
    cin >> n;
    vector<int> a(n);
    iota(a.begin(), a.end(), 1);
    for(int i=0;i<n-1;i+=2) swap(a[i], a[i+1]);
    if(n > 2) swap(a[n-1], a[n-3]);
    REP(i,n) cout << a[i] << " ";
    cout << endl;
}

B. Unique Bid Auction

問題文の通りに一人だけが選んだ数の中で最小のものを求めて、そのindexを出力する
提出コード

void solve(){
    int n;
    cin >> n;
    vector<int> a(n);
    vector<int> cnt(n+1);
    REP(i,n){
        cin >> a[i];
        cnt[a[i]]++;
    }
    int ans = -1;
    int mi = -1;
    for(int i=1;i<=n;i++){
        if(cnt[i] == 1){
            mi = i;
            break;
        }
    }
    REP(i,n) if(a[i] == mi) ans = i + 1;
    cout << ans << endl;
}

C. Sequence Transformation

いい実装方法思いつかなかった
RLEして各値ごとにindexを見る
直前に見たindexを$ pre=0 $ として$ pre \lt idx_{cur} $ならその間の区間は消す必要がある
また、最後に見たindexが数列の最後の要素でなければその区間も消す必要がある
提出コード

void solve(){
    int n;
    cin >> n;
    vector<int> a(n);
    REP(i,n) cin >> a[i];
    auto res = runLengthEncoding(a);
    vector<int> cnt(n+1);
    vector<vector<int>> idx(n);
    REP(i,res.size()) idx[res[i].first-1].emplace_back(i);
    int m = res.size();
    int ans = INF;
    REP(i,n){
        int tmp = 0;
        int pre = 0;
        if(idx[i].size() == 0) continue;
        REP(j,idx[i].size()){
            if(idx[i][j] > pre) tmp++;
            pre = idx[i][j];
        }
        if(pre < m-1) tmp++;
        // dumps(i, tmp);
        chmin(ans, tmp);
    }
    cout << ans << endl;
}

D. Number into Sequence

素因数分解をする
条件を満たす数列の長さの最大は、$ N $の各素因数の数のmax - 1になる
そのため最も多い素因数の数 - 1個その素因数を出力し、残った素因数の積に1つその素因数をかけてあげれば条件を満たす
提出コード

void solve(){
    ll n;
    cin >> n;
    auto res = prime_factorize(n);
    ll ma = 0;
    ll prime = 0;
    for(auto [p, cnt] : res) if(chmax(ma, cnt)) prime = p;
    ll rest = n;
    while(rest % prime == 0) rest /= prime;
    cout << ma << endl;
    REP(i,ma-1) cout << prime << " ";
    cout << rest * prime << endl;
}

E. Number of Simple Paths

$ n $頂点 $ n $辺の連結な無向グラフなので、木に辺を1本足したグラフになる
これは必ずサイクルを1個含み、そのサイクルから毛が生えたようなグラフになる(なもりグラフ?)
毛の生えた部分に注目するとサイクルのどの頂点から生えてるかでグループ分け出来る
そのグループ間の中のパスは必ず一意になる
逆に異なるグループ間のパスだとサイクルを右・左回りのどちらを通るかで2通りになる
そのため、全体のパスの個数から各グループごとに重複分を引いてあげればいい
サイクルは1個しかないのでdfsとかで十分
提出コード

void solve(){
    ll n;
    cin >> n;
    vector<vector<int>> g(n);
    REP(i,n){
        int u, v;
        cin >> u >> v;
        g[--u].emplace_back(--v);
        g[v].emplace_back(u);
    }
    vector<int> v;
    int pos = -1;
    vector<bool> seen(n), finished(n);
    auto dfs = [&](auto && self, int cur, int par = -1){
        seen[cur] = true;
        v.emplace_back(cur);
        for(auto nv : g[cur]){
            if(nv == par) continue;
            if(finished[nv]) continue;
            if(seen[nv] and !finished[nv]){
                pos = nv;
                return;
            }
            self(self, nv, cur);
            if(pos != -1) return;
        }
        v.pop_back();
        finished[cur] = true;
    };
    dfs(dfs, 0);
    vector<ll> cnt(n);
    queue<tuple<int, int, int>> q;
    vector<bool> used(n);
    set<int> cycle;
    while(!v.empty()){
        int cur = v.back();
        cycle.insert(cur);
        v.pop_back();
        if(cur == pos) break;
    }
    for(auto x : cycle){
        q.emplace(x, -1, x);
        used[x] = true;
    }
    while(!q.empty()){
        auto [cur, par, group] = q.front(); q.pop();
        cnt[group]++;
        for(auto nv : g[cur]){
            if(nv == par) continue;
            if(used[nv]) continue;
            q.emplace(nv, cur, group);
        }
    }
    ll ans = n * (n - 1);
    for(auto c : cnt) if(c > 0) ans -= c * (c - 1) / 2;
    cout << ans << endl;
}

接着剤の「CodinGame Fall Challenge 2020」 参加記

はじめに

CodinGameのコンテスト(CodinGame Fall Challenge 2020)に参加しました
結果は世界59/7,036位(Legend)、日本15/320位でした
今回のゲーム終始なにもわからない状態だったのでとてもつらかった
前回に引き続きLegendになれたので満足、いつかは上位争いしたいね
f:id:tkm-kyudo:20201123205836p:plain

ルール

例のごとくツカモさんがルール要約を書いてくださっているのでそちらを参照してください
呪文で素材を生成してポーションを作成して相手よりスコアを稼いだほうが勝ちです
tsukammo.hatenablog.com

やったこと

最終的にやったことをまとめます
1. ビームサーチで最大15手探索を行い最も良い評価の行動を行う
2. 初手6ターンは評価関数で最も価値の高い呪文をlearnする
3. 7ターン目以降はビームサーチを行い、探索の1ターン目のみlearnも含めて探索を行う
4. 敵が各BREWを行えるのに必要な最短ターンを探索し、それよりも遅くBREWする行動は探索の評価から外す
5. 自分が5個すでに作っていて、敵の最大スコアを超えるなら確定killを取る、逆に相手が6個目を取れる状況でBREW出来なかったらtier-1以上の素材を最大化するようにする
以下、リーグごとに順を追って考察改善したこと、もといポエムです

Wood2

貪欲に一番高いものを取れば昇格

Wood1

難しくありませんでした?
インベントリに複数種類の素材があると強いと思ったので、素材の種類数が減らないようにCASTをするようなルールベースでやってみる
また、行動しないよりもしたほうがいいので素材の種類数が減る場合でもCASTするように使える呪文をランダムに使う
BREW出来るときは優先で取ったほうが良さそうなのでBREW出来るならCASTよりBREWを優先するようにした
結構ギリギリ?昇格で今回難しいな〜ってなってました

Bronze

learnが解禁される
learnは取らないよりも取ったほうが自明に強そうなのでとりあえずlearnを実装
この時点での上位陣のリプレイを見ていると初手10ターンくらいlearnをしてたので同じように10ターンくらいlearnをする
BREWとCASTはWood1のときのものをそのまま使うと初日は30位とかそれくらいで就寝

CASTの探索を効率的に行いたいので考察をする
インベントリの状態は$ _{14} C _{4} = 1001 $通りとかなり少ないので全探索しても良さそう
$ dp[a][b][c][d] := $ インベントリの状態がtier-0がa個、tier-1がb個、tier-2がc個、tier-3がd個あるときの価値が最大のものとしてbfsを行う
インベントリの評価には各素材の価値$ [1.5, 2.5, 3, 4] $とBREWのpriceに早いターンに取ったほうが価値が高くなるように重みを付けたもので評価する
tier-0とtier-1がちょっとだけ価値高いのは、呪文で消費するときに使いやすいくてインベントリにあると強そうだなと思ったため
探索パートの処理が結構重かったので†定数倍高速化†をしたら結構軽くなって敵のシミュレートを出来るくらいの余裕が生まれたので良かったかな(この時点ではやってない)
Silverが解禁されてそのまま昇格

Silver

TLで初期呪文縛ったほうが強いんじゃね?みたいな話題が流れてたのでとりあえず試してみる
$ [2, 0, 0, 0] $の呪文は結構強いのでこれ以外のもの使わないようにしてみる
これが意外と強かった
とりあえず実装!で初期呪文縛っただけなので詰んだ状況になると†直立不動†するお婆ちゃんの情報がTLに多く流れたとかなんとか
結局、初期呪文を含めた探索をすることになるのですが、初期呪文を縛るメリットに探索の効率があったかなーと思います(40msとかだったのが5~10msくらいに減少)
探索に余裕ができたので、敵の各BREWまでの最小ターンシミュレートを実装
大体これくらいのことをやってGoldが解禁されそのまま昇格

Gold

Silverのときに敵のBREWに必要な最小ターン数を実装したので評価に組み込む
具体的には、次のターンに敵の取れる価値が最大のBREWを自分が今のターンに取れるなら取るようにする(相手の邪魔をする)
相手が今のターンで取れるもので次のターン以降に自分がBREWをするような探索は枝を刈る
敵がすでに5回BREWをしていて、敵が今のターンでBREWを取れるもので次ターン以降に取ろうとする探索は意味がなさそう(取れるときにはだいたい取りそうなので)、これも枝を刈る
また、探索をbfsでやっていたものをpriority_queueでダイクストラっぽく価値の最大のものを探索するようにした
chokudai searchとかも試してみたんだけど評価関数が悪いのかあんまりいい動きをしなかったのでボツ

このくらいのときにLegendが開放されて初動で20人くらいで今回やべ〜となっていた、やべえ〜(語彙消失)

やっぱりビーム系の探索で探索に多様性を持たせたほうが絶対強いのでビームサーチを実装してみることに
今まで使ってたダイクストラっぽいものをベースになんちゃってビームサーチ(多分挙動はビームサーチと変わらないはず)を実装
これを投げるとGOLD3位とかで誰かボスたおしてくれ〜となっていた

結局見守っていてもボスとレート差1.5とかついてしまったので伝家の宝刀†リサブガチャ†を敢行
何回かやってると上がれそうな雰囲気になってテンションがおかしなことになってスクショを連打しながら奇声を発していた(隣室の人ごめんなさい…)
以下、スクショの様子
f:id:tkm-kyudo:20201123173621p:plainf:id:tkm-kyudo:20201123173624p:plainf:id:tkm-kyudo:20201123173628p:plainf:id:tkm-kyudo:20201123173634p:plainf:id:tkm-kyudo:20201123173642p:plainf:id:tkm-kyudo:20201123173648p:plainf:id:tkm-kyudo:20201123173653p:plain
感動のフィナーレ

今回ほんとにきつかったので上がった瞬間叫んじゃいました

Legend

正直Goldでもう案も出し尽くしてサブミットガチャしてたのでこれといって改善できたものはないんですよね(前回もそんな感じだった)
一応パラメータをガチャガチャしてビーム幅を少しだけLegend昇格時のものより大きくしたものが一番良さそうだったのでそれを最終提出

こんな感じでシステス前の暫定順位が55位
最終順位は59位でフィニッシュ

前回同様10日間フルでやり続けたので疲れました
今回は本当に最初からわからなくて苦戦しそうだなあと思ってたし、案の定何もわからない状態が続いてしんどかったなあ
最終的にはLegend到達出来たしよかった、やっぱり楽しいんだよね

最終提出のコードは706行で前回よりちょっとだけ実装多かった
今回は色々書いたり消したりしてたので書いた量自体は前回の倍くらいは書いてそう

やりたかったこと

・learnをもう少し詰めたかった、持っている呪文との相性とか(探索である程度なんとかなりそう感はある)
・高速化を頑張って探索の幅や深さを広げたかった

知見

・Zobrist hashとかで重複盤面を省けるらしい
・ビーム幅と探索の深さを広げるだけでも結構強くなるっぽい?
・相手のターンがnターンで終わるとき(ゲームが終局するとき)自分の手の深さをnにするといいかも?(余計な深さがいらない)

感想

いつの間にか10日間くらい吹き飛ばしたけどやっぱり楽しかったね
†人生†をやらないといけないのでやらないとね…
こどげ前にハル研プロコンとかもあったんだけどこどげに全ツッパするために発表資料作成とかに費やしたので参加できなかったのはちょっと残念だけど致し方なし
次の開催は多分社会人になってからとかかな?今ほど時間で殴れないだろうけど社会人になってからも参加したいね