【AtCoder】ABC307 C - Ideal Sheet

問題のリンク

遅すぎるかも。

解法

シートA, Bは少なくとも1つ以上の黒いマスを含み、そのマスは必ずシートXのいずれかの黒いマスに対応する。

よって、それぞれのシートから1マスずつ選び、シートXのどのマスに対応するかを全探索し、シートXのそれぞれの黒いマスに対応するシートA, Bいずれかの黒いマスが存在するかを判定すれば、全探索は  O(H_{X}^{2} W_{X}^{2} ) で、判定は  O(H_{X} W_{X} ) で、合計で  O(H_{X}^{3} W_{X}^{3} ) できる。

実装

AtCoder への提出

#include <bits/stdc++.h>
using namespace std;

#define REP(i, n) for (int i = 0; i < (n); i++)

pair<int, int> f(vector<string>& S) {
    REP(i, S.size()) REP(j, S[i].size()) {
        if (S[i][j] == '#') {
            return {i, j};
        }
    }
}

int count(vector<string>& S) {
    int res = 0;
    REP(i, S.size()) REP(j, S[i].size()) res += S[i][j] == '#';
    return res;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    int HA, WA;
    cin >> HA >> WA;
    vector<string> A(HA);
    REP(i, HA) cin >> A[i];

    int HB, WB;
    cin >> HB >> WB;
    vector<string> B(HB);
    REP(i, HB) cin >> B[i];

    int HX, WX;
    cin >> HX >> WX;
    vector<string> X(HX);
    REP(i, HX) cin >> X[i];

    auto [ia, ja] = f(A);
    auto [ib, jb] = f(B);

    auto oka = [&](int x, int y) -> int { return 0 <= x and x < HA and 0 <= y and y < WA and A[x][y] == '#'; };
    auto okb = [&](int x, int y) -> int { return 0 <= x and x < HB and 0 <= y and y < WB and B[x][y] == '#'; };
    int ans = 0;
    int ca = count(A), cb = count(B);
    REP(i1, HX) REP(j1, WX) {
        if (X[i1][j1] != '#') continue;
        REP(i2, HX) REP(j2, WX) {
            if (X[i2][j2] != '#') continue;
            int ok = 1, cnt = 0;
            REP(i, HX) REP(j, WX) {
                if (X[i][j] != '#') continue;
                ok &= oka(ia + i - i1, ja + j - j1) or okb(ib + i - i2, jb + j - j2);
                cnt += oka(ia + i - i1, ja + j - j1) + okb(ib + i - i2, jb + j - j2);
            }
            ans |= ok and cnt == ca + cb;
        }
    }
    cout << (ans ? "Yes" : "No") << '\n';
    return 0;
}

感想

シートCのサイズがどのくらいかを考える必要がないのがこの解法の優れたところであるように思う。(この部分の見積もりを間違えて通せない人を何人か確認したため。)

std::priority_queue に自作のラムダ式の比較関数を渡す

ABC307 F - Virus 2 でハマったのでメモ。

ツイートは こちら。(教えてくださった方に感謝。)

a < b という関係が成り立つときに a から先に要素を取り出したいとします。

  • ダメなやつ
auto comp = [](const T& a, const T& b) -> bool {
    return a < b;
};

std::priority_queue<T, std::vector<T>, decltype(comp)> que(comp);

※デフォルトで std::less が渡されているのに大きい順に pop されることに想いを馳せてみましょう。

  • 動くやつ
auto comp = [](const T& b, const T& a) -> bool {
    return a < b;
};

std::priority_queue<T, std::vector<T>, decltype(comp)> que(comp);

取り出したくないやつを引数の左側に持ってくると良いです。

だいたい T = std::tuple<int, int, int> みたいなやつが出てきたときにラムダ式を書きたくなるので、 他には std::greater<T> を渡して適宜符号を反転させる方法もあります。

std::sortと同じ感覚でやると大変なことになる(なりました)ので、気を付けましょう。

【AOJ】AOJ 2826 - ゲームバランス

模擬国内2017D

問題のリンク

解説

単調性があるため、 X で二分探索をする。

 X を決め打ったときに、最小で何体モンスターを倒す必要があるか求めることができれば、それが  M 以上かを判定することで解くことができる。

倒すための最小のモンスター数について求める方法を解説する。

レベルが高くなると、倒すことができるモンスターの集合は単調に大きくなっていくため、同じモンスターをレベル  L とレベル  L + D (D \geq 0) のときに倒して、後者の方が戦闘後のレベルが高い(か等しい)ことを示せれば、貪欲にレベルを上げられるだけ上げて良いと示すことができる。

(後述するが、今倒すことができるモンスターの中で最もレベルが上がるモンスターを求めることは比較的容易にできる。)

どちらのレベルでも倒せるモンスターについて、その強さを  s_i とおく。

(1)  s_{i} \leq L \leq L + D のとき

 L  \max(L + 1, X + s_i ) に、 L + D  \max(L + D + 1, X + s_i ) になる。

それぞれの最大値がどの組合せになるかで場合分けし、いずれの場合も  L + D のほうが戦闘後のレベルが小さくならないことを示す。

 ( \mathrm{i} )   L + 1 L + D + 1 のとき

自明。

 ( \mathrm{ii} )   L + 1 X + s_i のとき

このようなことはない。(  \max(L + 1, X + s_i ) = L + 1 であるため  L + 1 \geq X + s_i であるが、そのときは  \max(L + D + 1, X + s_i ) = L + D + 1 になるため。)

 ( \mathrm{iii} )   X + s_i  L + D + 1 のとき

 \max(L + D + 1, X + s_i ) = L + D + 1 となっているのでOK。

 ( \mathrm{iv} )   X + s_i  X + s_i のとき

等しい。

(2)  L \leq s_i \leq L + D のとき

 L  \max(L + 1, X + 2L - s_i ) に、 L + D  \max(L + D + 1, X + s_i ) になる。

 ( \mathrm{i} )   L + 1 L + D + 1 のとき

自明。

 ( \mathrm{ii} )   L + 1 X + s_i のとき

 L \leq s_i より、 L + 1 \leq s_i + 1

 X \geq 1 より、 s_i + 1 \leq s_i + X

よりOK。

 ( \mathrm{iii} )   X + 2L - s_i  L + D + 1 のとき

 (X + 2L - s_i ) - (L + D + 1) = X + L - s_i - D - 1

 X + s_i \leq L + D + 1 だから  X - L + s_i - D - 1 \leq 0

ここで  L - s_i \leq 0 だから、 X + L - s_i - D - 1 \leq X - L + s_i - D - 1 \leq 0

よりOK。

 ( \mathrm{iv} )   X + 2L - s_i  X + s_i のとき

 (X + 2L - s_i ) - (X + s_i ) = 2 (L - s_i ) \leq 0 よりOK。

(3)  L \leq L + D \leq s_i のとき

 L  \max(L + 1, X + 2L - s_i ) に、 L + D  \max(L + D + 1, X + 2L + 2D - s_i ) になる。

 ( \mathrm{i} )   L + 1 L + D + 1 のとき

自明。

 ( \mathrm{ii} )   L + 1 X + 2L + 2D - s_i のとき

 L + D + 1 \leq X + 2L + 2D - s_i を最後に使うと、

 (L + 1 ) - (X + 2L + 2D - s_i ) = 1 - X - L  - 2D + s_i \leq 1 - X - L  - D + s_i \leq 0

よりOK。

 ( \mathrm{iii} )   X + 2L - s_i  L + D + 1 のとき

 L + D + 1 \geq X + 2L + 2D - s_i を最後に使うと、

 (X + 2L - s_i ) - (L + D + 1) = X + L - s_i - D - 1 \leq X + L - s_i + D - 1 \leq 0

よりOK。

 ( \mathrm{iv} )   X + 2L - s_i  X + 2L + 2D - s_i のとき

 (X + 2L - s_i ) - (X + 2L + 2D - s_i ) = - 2D \leq 0 よりOK。

以上より、貪欲にレベルを上げられるだけ上げて良いことが分かった。

次に、現在のレベルが  L のときに、どのモンスターを倒せば最もレベルが上がるかについて考える。(なお、最後のモンスターを倒せるときには倒す)

倒すモンスターの強さを  s_i とする。

(1)  L \leq s_i のとき

 \max(L + 1, X + 2L - s_i ) になるため、 s_i はなるべく小さくなるように選べば良い。

(2)  L \geq s_i のとき

 \max(L + 1, X + s_i ) になるため、 s_i はなるべく大きくなるように選べば良い。

よって  L 以上で最小のモンスターと、 L 以下で最大のモンスターが候補になるので、これらを実際に試せば良い。

実装について

二分探索では、倒す回数が  M 回以上となる最小の  X を求めるため、  X 区間と対応する結果としては、 絶対にクリアできない | M回以上かかってクリア | M回未満でクリア となる。

二分探索の中では、(1)ボスを倒せるか判定→(2)倒す候補について調べる→(3)どれも倒せなかったらクリアできない=無限回かかるとする→(4)レベル更新

を繰り返している。

最後に求めた ok が条件を満たすか確認する。クリアできない場合と、 M 回未満でクリアしてしまう場合がある。

#include <bits/stdc++.h>
using namespace std;

#define REP(i, n) for (int i = 0; i < (n); i++)

void solve(int N, int M) {
    vector<long long> s(N);
    REP(i, N) cin >> s[i];
    auto check = [&](long long X) -> long long {
        long long curlev = 1, count = 0;
        while (true) {
            count++;
            if (curlev + X > s.back()) {
                break;
            }
            long long nextlev = curlev;
            int i = lower_bound(s.begin(), s.end(), curlev) - s.begin();
            for (int j = i - 1; j <= i + 1; j++) {
                if (0 <= j and j < N and curlev + X > s[j]) {
                    nextlev = max(nextlev, curlev + max(1LL, X - abs(s[j] - curlev)));
                }
            }
            if (curlev == nextlev) {
                count = 2000000;
                break;
            }
            curlev = nextlev;
        }
        return count;
    };
    long long ng = 2000000, ok = 1;
    while (ng - ok > 1) {
        long long md = (ok + ng) >> 1;
        if (check(md) >= M) {
            ok = md;
        } else {
            ng = md;
        }
    }
    long long res = check(ok);
    if (res != 2000000 and res >= M) {
        cout << ok << '\n';
    } else {
        cout << -1 << '\n';
    }
    return;
}

int main() {
    int N, M;
    while (cin >> N >> M, !(N == 0 and M == 0)) solve(N, M);
    return 0;
}

CPCTF 2023 writeup

個人参加で13位でした。

[Easy] [Reversing] passcode以外のEasyまでの全ての問題と、MediumのPPCを解きました。

[Newbie] [Misc] 2DCode 1

Hintを開けるとDotCodeだとわかり、free barcode scanner dotcodeなどと調べるとオンラインリーダーがヒットする。

[Newbie] [PPC] Count Coins

実装する。

#include <bits/stdc++.h>
using namespace std;

#define REP(i, n) for (int i = 0; i < (n); i++)
template <class T> using V = vector<T>;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    long long N, X;
    cin >> N >> X;
    vector<int> A(N);
    REP(i, N) cin >> A[i];
    REP(i, N) {
        if (A[i] == 1) {
            X = min(X + 1, 10LL);
        } else {
            X = max(X - 3, 0LL);
        }
    }
    cout << X << '\n';
    return 0;
}

[Newbie] [Crypto] Entrance to the Crypto World

最後にフラッグらしきものがあり、EREVHを見るとCPCTFを2文字ずらしたものだとわかる。

[Newbie] [PPC] Transfer

比較する。

#include <bits/stdc++.h>
using namespace std;

#define REP(i, n) for (int i = 0; i < (n); i++)
template <class T> using V = vector<T>;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    long long A, B, C, D;
    cin >> A >> B >> C >> D;
    cout << min(A + B, C + D) << '\n';
    return 0;
}

[Newbie] [OSINT] Tube

china xinxiuなどと検索するとWikipediaの駅の記事がヒットし、Shenzhen Metro Line 2だと分かる。

[Newbie] [Web] easylogin

Hintを開けると' OR 0=0 --をパスワードとして入力すれば良いことがわかる。 しばらくcpctf.spaceにログインを試みていた。

[Newbie] [Forensics] investigation

zipをダウンロードすると画像がたくさんあり、頑張ってその中からフラッグが書かれたものを探す。

何も考えずにHintを開けたがこれくらいは解けても良かったと思う。

[Newbie] [Shell] netcat

WSLのbash上でnc netcat.cpctf.space 30005を入力するとフラッグが手に入る。

[Newbie] [Misc] welcome to CPCFT

Hintを開けるとトップページとDiscordのreadmeチャンネルとVisualizerにあると書かれている。 VisualizerではF12を押してctrl+f}を探すと見つかる。

Hintなしで解いている人がいてすごい。

[Easy] [OSINT] Shan shui

写真から郵便番号を特定する問題。

最初はparty sky pekingなどと検索していたがダメで、Hint2まで開けると郵便番号を見ろと書いてある。 いろいろ調べると中国市外局番と郵便番号検索がヒットするので773を入れる。 検索結果が複数あり悩むが、0773-2629999で検索すると出てくる物件の名前でGoogle Mapで検索すると郵便番号が541012と出てくるので近そうな541000を入れる。

全探索しても良かったと思った。

[Easy] [Misc] 1dayattack

Hint2まで開けるとMarkupというPixelシリーズのスクリーンショット編集機能の脆弱性だと分かり、記事を読むと解析サイトが見つかる。 問題文中にPixel 4aと書いてあるので解ける。

[Easy] [Misc] 2DCode 2

Hint2まで開けるとQRコードが壊れていると分かる。 ファインダパターンというの部分と、タイミングパターンという- - -の部分を温かみのある手作業で直して適当なQRコードスキャナーで読み取るとフラッグが得られる。

[Easy] [Pwn] Big or Small

nc big-or-small.cpctf.space 30008を入力するとカジノが始まる。 zipにソースコードがあるので読んでみるが特に変なことは思いつかない。 ただ乱数は毎回ランダムである上、100コインスタートなのに100000コイン稼ぐ必要があり愚直にやるのは無理だなとなる。

Hint2を開けると掛け金の下限がないことが分かるので-1000000などと入れて50%が外れるまでやると解ける。

[Easy] [OSINT] Dokoda? 1

Hint1を開けると画像検索しろとあるのでやる。 シンボルプロムナード公園というらしい。 Google Mapのストリートビューを使いながらざっくりとした経度と緯度をURLから読み取った。

多分ここ行ったことあるな。

[Easy] [OSINT] Dokoda? 2

Dokoda? 1と同じくGoogle Mapを使う。 東海工産で調べるとそれらしき場所がヒットするのであとは1と同じ。

[Easy] [PPC] Find cpctf

部分文字列になるかを全箇所試す。

最初substringではなくsubsequenceだと思っていた。これ解けるのかな。

#include <bits/stdc++.h>
using namespace std;

#define REP(i, n) for (int i = 0; i < (n); i++)
template <class T> using V = vector<T>;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    int N;
    cin >> N;
    vector<int> A(N);
    REP(i, N) cin >> A[i];
    string S;
    cin >> S;
    vector<int> cnt(26);
    REP(i, N) cnt[A[i]]++;
    string T = "cpctf";
    int ans = 0;
    REP(i, N - 4) {
        vector<int> need(26);
        REP(j, 5) {
            // S[i + j] + A[i] = T[j]
            need[(T[j] - S[i + j] + 26) % 26]++;
        }
        int ok = 1;
        REP(j, 26) {
            if (need[j] > cnt[j]) {
                ok = 0;
            }
        }
        if (ok) {
            ans = 1;
            break;
        }
    }
    cout << (ans ? "Yes" : "No") << '\n';
    return 0;
}

[Easy] [PPC] Floor Sqrt

 N が大きいので平方分割したくなる。  \lfloor \sqrt{n} \rfloor = k と決めると、 k^2 \leq n \leq k^2+k-1のとき条件を満たすことが分かるのであとは kを全探索する。

#include <bits/stdc++.h>
using namespace std;

#define REP(i, n) for (int i = 0; i < (n); i++)
template <class T> using V = vector<T>;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    long long N;
    cin >> N;
    long long ans = 0;
    for (long long k = 1; k <= 1000000; k++) {
        ans += max(0LL, min(N, k * k + k - 1) - k * k + 1);
    }
    cout << ans << '\n';
    return 0;
}

[Easy] [PPC] Geometric Progression Sum

すべての iについて L_{i} = 1, R_{i} = Nだった場合、前の要素を X倍していけば良い。 区間がもっといろいろ考えられるが、競プロ方言であるところのimos法っぽくやれば良い。 実装ではイベントソートのように実装している。

#include <bits/stdc++.h>
using namespace std;

#define REP(i, n) for (int i = 0; i < (n); i++)
template <class T> using V = vector<T>;

#include <atcoder/modint>
using mint = atcoder::modint998244353;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    int N, X, Q;
    cin >> N >> X >> Q;
    vector<vector<tuple<int, int, int>>> GL(N), GR(N);
    REP(i, Q) {
        int b, l, r;
        cin >> b >> l >> r;
        l--, r--;
        GL[l].push_back({b, l, r});
        GR[r].push_back({b, l, r});
    }
    mint cur = 0;
    vector<mint> A(N), xp(N + 1);
    xp[0] = 1;
    REP(i, N) xp[i + 1] = xp[i] * X;
    REP(i, N) {
        // precalculation
        for (auto& [b, l, r] : GL[i]) {
            cur += b;
        }
        A[i] = cur;
        cur *= X;
        // postcalculation
        for (auto& [b, l, r] : GR[i]) {
            cur -= b * xp[r - l + 1];
        }
    }
    REP(i, N) cout << A[i].val() << " \n"[i + 1 == N];
    return 0;
}

[Easy] [PPC] Whisper Sucu

頂点倍加BFSをする。

もうちょっと真面目に書くと、偶数時刻のうち最も早く到達した時間と、奇数時刻のうち最も早く到達した時間から他の頂点に移動する場合のみを考えれば良いので、その分頂点数を増やす。

#include <bits/stdc++.h>
using namespace std;

#define REP(i, n) for (int i = 0; i < (n); i++)
template <class T> using V = vector<T>;

#include "graph/read_graph.hpp"

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    int N, M;
    cin >> N >> M;
    auto G = read_graph<int>(N, M, true);
    constexpr int INF = 1 << 30;
    vector dp(2, vector(N, INF));
    dp[0][0] = 0;
    queue<pair<int, int>> que;
    que.push({0, 0});
    while (!que.empty()) {
        auto [s, v] = que.front();
        que.pop();
        for (auto &e : G[v]) {
            if (s + 1 == e.cost or e.cost == 3) {
                if (dp[s ^ 1][e.to] > dp[s][v] + 1) {
                    dp[s ^ 1][e.to] = dp[s][v] + 1;
                    que.push({s ^ 1, e.to});
                }
            }
        }
    }
    int ans = min(dp[0][N - 1], dp[1][N - 1]);
    if (ans == INF) ans = -1;
    cout << ans << '\n';
    return 0;
}

[Easy] [Pwn] overrun

時間もないのでHint1を開けると逆アセンブルした結果が書いてある。 char name[50];などと書かれているのでとりあえず長い文字列を入れてみるとフラッグを教えてくれる。

[Medium] [PPC] Digital Clock

 x+y=K (x \geq 0, y \geq 0)を満たす整数 (x \bmod H,y \bmod W)のペアを数える問題。

 x \bmod H,y \bmod W はそれぞれ H,W周期なので、 lcm(H,W)周期で同じものが出てくる。

#include <bits/stdc++.h>
using namespace std;

#define REP(i, n) for (int i = 0; i < (n); i++)
template <class T> using V = vector<T>;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    long long H, W, K;
    cin >> H >> W >> K;
    long long ans = min(H / gcd(H, W) * W, K + 1);
    cout << ans << '\n';
    return 0;
}

[Medium] [PPC] God Field

 N,Mが小さく、また防具が壊れているか壊れていないかの2値なのでbitDPをしたくなる。

dp[i][b]=現在i番目の攻撃まで受け終わっており、使った防具の集合がbである として、現在未使用の防具に対し、今回の攻撃で使う防具の組合せを全探索すれば良い。 このとき、 N要素の集合の冪集合の各要素に対してその部分集合を全体 O(3^N)で列挙するテクニックを使うと、 O(NM3^N)で解ける。 (使わなくても良いかもしれない。)

#include <bits/stdc++.h>
using namespace std;

#define REP(i, n) for (int i = 0; i < (n); i++)
template <class T> using V = vector<T>;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    int N, M;
    long long X;
    cin >> N >> M >> X;
    vector<long long> A(N), B(M), C(M);
    REP(i, N) cin >> A[i];
    REP(i, M) cin >> B[i] >> C[i];
    int N2 = 1 << N, mask = N2 - 1;
    vector dp(N2, X);
    REP(i, M) {
        vector np(N2, 0LL);
        REP(b, N2) {
            int use = b ^ mask;
            vector<int> ts;
            for (int t = use; t; t = (t - 1) & use) ts.push_back(t);
            ts.push_back(0);
            for (auto &t : ts) {
                long long dmg = B[i];
                REP(j, N) dmg -= (t >> j & 1) * A[j];
                dmg = max(dmg, 0LL);
                if (C[i] == 1 and dmg > 0) dmg = X;
                np[b | t] = max(np[b | t], dp[b] - dmg);
            }
        }
        swap(dp, np);
    }
    int ans = *max_element(dp.begin(), dp.end());
    cout << ans << '\n';
    return 0;
}

[Medium] [PPC] Max Min GCD

 C_k A_kまたは B_kの約数となる。

 A_kの約数を列挙し、 B_j (j \geq k)の約数をカウントしておいた配列を見てチェックする。  B_kについても同様。これは kを降順にチェックすると高速に判定できる。

#include <bits/stdc++.h>
using namespace std;

#define REP(i, n) for (int i = 0; i < (n); i++)
template <class T> using V = vector<T>;

#include "math/divisor.hpp"

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    int N;
    cin >> N;
    vector<int> A(N), B(N), C(N);
    REP(i, N) cin >> A[i];
    REP(i, N) cin >> B[i];
    vector<int> ca(100005), cb(100005);
    for (int i = N - 1; i >= 0; i--) {
        auto da = divisor(A[i]);
        auto db = divisor(B[i]);
        for (auto& d : da) ca[d] |= 1;
        for (auto& d : db) cb[d] |= 1;
        long long ans = 1;
        for (auto& d : da)
            if (cb[d]) ans = max(ans, d);
        for (auto& d : db)
            if (ca[d]) ans = max(ans, d);
        C[i] = ans;
    }
    REP(i, N) cout << C[i] << " \n"[i + 1 == N];
    return 0;
}

[Medium] [PPC] Move Road

上下左右に移動できることから、例えば左上から順番に見ていくというようなことはできないことが分かる。

4方向に移動した後どうなるか観察する。

右方向に移動する場合、右のマスが空いている必要があるが、移動した0.5秒後に車が追いかけてくるので車との相対位置は変わらない。

下方向に移動する場合、真下のマスと左下のマスが空いている必要があり、元のマスの左下のマスに移動する。

上方向に移動する場合、真上のマスと左上のマスが空いている必要があり、元のマスの左上のマスに移動する。

左方向に移動する場合、左の2マスが空いている必要があり、元のマスから左に2マス行ったマスに移動する。

静止した場合、左のマスが空いている必要があり、元のマスの左のマスに移動する。

よって実は移動方向が少し変なBFSをすれば良い。

#include <bits/stdc++.h>
using namespace std;

#define REP(i, n) for (int i = 0; i < (n); i++)
template <class T> using V = vector<T>;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    int H, W;
    cin >> H >> W;
    vector<string> S(H);
    REP(i, H) cin >> S[i];
    vector dp(H, vector(W, 0));
    queue<pair<int, int>> que;
    REP(j, W) {
        dp[0][j] = 1;
        que.push({0, j});
    }
    while (!que.empty()) {
        auto [x, y] = que.front();
        que.pop();
        // up
        if (x > 0 and S[x - 1][y] == '.' and S[x - 1][(y + W - 1) % W] == '.' and dp[x - 1][(y + W - 1) % W] == 0) {
            dp[x - 1][(y + W - 1) % W] = 1;
            que.push({x - 1, (y + W - 1) % W});
        }
        // down
        if (x < H - 1 and S[x + 1][y] == '.' and S[x + 1][(y + W - 1) % W] == '.' and dp[x + 1][(y + W - 1) % W] == 0) {
            dp[x + 1][(y + W - 1) % W] = 1;
            que.push({x + 1, (y + W - 1) % W});
        }
        // left
        if (S[x][(y + W - 2) % W] == '.' and S[x][(y + W - 1) % W] == '.' and dp[x][(y + W - 2) % W] == 0) {
            dp[x][(y + W - 2) % W] = 1;
            que.push({x, (y + W - 2) % W});
        }
        // stay
        if (S[x][(y + W - 1) % W] == '.' and dp[x][(y + W - 1) % W] == 0) {
            dp[x][(y + W - 1) % W] = 1;
            que.push({x, (y + W - 1) % W});
        }
    }
    int ans = *max_element(dp[H - 1].begin(), dp[H - 1].end());
    cout << (ans ? "Yes" : "No") << '\n';
    return 0;
}

【Codeforces】Codeforces Round #767

Div. 2 の E まで。

コンテストのリンク

A

 A_i の値が小さい順に  B_i  K に足す。

提出コード

B

 gcd(a) = p ( \geq 2) にするとする。 p 素数だけ考えれば良い。 p の倍数は連続する区間においては  p 個おきに出てくるので、最も頻繁に出てくる 2 の倍数にできれば良い。

1 回の操作で奇数は 1 つ減らせるので、数列に含まれる奇数の個数を求める。

素数が 1 のときはコーナーケースなので注意する。

提出コード

C

mex は要素数が増えても小さくならないので、数列全体の mex を計算し、その mex を達成できる  k のうち最小のものを選ぶことを繰り返せば良い。

mex を計算するために個数配列と fenwick tree と二分探索を使ったが、今回の問題では mex は広義単調減少なので、なくても良いはず。

提出コード

D

まず単体で回文があればその時点で YES を出力。そうでないとき、各要素は長さが 2 または 3 である。

ある部分列が回文になっているとする。

先頭要素と末尾要素に注目する。2 つの要素の長さが同じなら先頭要素と末尾要素で回文を作れる。異なるときには 2+3 か 3+2 になるが、これらも回文にできる。

結局要素を 2 つ選んで回文を作れるかだけ考えれば良く、文字の種類数が少ないことを活かして高速に判定する。(長さ  26^3 の配列を用意するなど)

提出コード

E

いくつかのマスを選択し、選択された各マスについて、隣接するマスを多重集合  S に追加するとする。全てのマスが  S に奇数回含まれるようなマスの選び方を 1 つ求め、選ばれたマス  (i,j) に対する  a_{i,j} の総 XOR を求めれば良い。

なので実は  a_{i,j} の値はあまり重要ではない。

 1 行目から  n-1 行目まで順に、「 (i,j) が偶数回  S に追加されていたなら、マス  (i+1,j) を選択する」という操作を行うと、 n 行目もうまくいく。

証明はできていないが、 n = 2, 4, 6, ... , 1000 について、どの場合も  n 行目まで上手くいっているかを手元実行で判定しても 1 分くらいでできる。

提出コード

証明?

公式 Editorial のコメント欄 に証明っぽいものがあった。

公式の解法 2 によって解の存在が保証されており、1 行目のマスの 1 つを選択しなかったことにしても辻褄を合わせていくことができるので、これを繰り返し行うことで 1 行目のマスを一切選択していない解が構築できる。1 行目を全く選択しなかった場合はどのマスを選ぶかは決定的になるので、上記の操作でも良い。

辻褄の合わせ方については、1 行目のマスの 1 つを選択しないことにすると、そのマスを 1 つの角とする 45° の長方形の全てのマスについて、選択するかしないかを反転させることで辻褄を合わせられる。なぜかというと、その長方形上のマスをすべて選択しても、どのマスも集合に追加された回数が変化しないから。(この説明もなぜ回数が変化しないのかという点で不十分な気がしており、微妙。)

8 × 8 のグリッドについて、ある解が存在したと仮定し、現在 (1, 3) が選択されているがこれを選択しなかったことにして辻褄を合わせたい場合、以下の X のマスについて、選択するかしないかを反転させれば良い。

· · X · · · · ·

· X · X · · · ·

X · X · X · · ·

· X · X · X · ·

· · X · X · X ·

· · · X · X · X

· · · · X · X ·

· · · · · X · ·

【Codeforces】Educational Codeforces Round 127

E まで。

E の計算量解析できないの危機感を感じる。

コンテストのリンク

A

ランレングス圧縮して各要素について個数が 2 個以上か確かめる。

提出コード

B

 x_0 をどうするかで最終的な数列は 3 通りできる。

それぞれにできるかどうかは絶対値の差がすべて 1 以下であるかで判定可能。

提出コード

C

 i を取る個数とし、 f(i) i 個以上取れる日数とすると、求めるものは  \displaystyle \sum_{i=1}^{n} f(i) である。(若干見方を変えている。)

取る個数を決めたら安い順に貪欲に取るべきなのでソートする。

 s (\leq x) を小さいほうから  i 個取るときの初期コストとすると、 f(i) = \dfrac{x-s}{i} + 1 である。

提出コード

D

 a に要素を挿入しても答えが小さくなることはないので、下限として  \displaystyle \sum_{i=1}^{n-1}  |  a_i - a_{i+1} | が得られる。

 \min \{ a \} 以上  \max \{ a \} 以下の要素は適当に挿入できる。

 1 以上  \min \{ a \} - 1 以下の要素と、 \max \{ a \} + 1 以上  x 以下の要素については、 1  x について、それぞれ先頭に入れるか、要素間に入れるか、末尾に入れるかを考えれば良い。(それ以外の要素は適当に途中に挿入できる。)

提出コードのように  1  x を完全に独立に考えると、要素間に入れる場合は良いとして、 1  x をどちらも先頭に入れたりどちらも末尾に入れたりする場合にコストを過小評価していそうだが、冷静に考えるとこのような場合は起こらないことが分かるので、OK。(一方を先頭に、もう一方を末尾に入れても損をしない。)

提出コード

E

 g(i)  i を根としたときの部分木に対する作れる文字列の種類数とし、子から順に木 DP で求める。

 g(l_x)  g(r_x) から  g(x) を求める方法だが、もし  g(l_x)  g(r_x) で同じものがカウントされていない場合、 g(x) = 2 \times g(l_x) \times g(r_x) である。

同じものがカウントされている場合についてよく考えると、 l_x を根とする部分木と、 r_x を根とする部分木に対して行える操作は変わらないので、実は同じものがカウントされているというか、数えているものが完全に同じ。

よって  g(x) = g(l_x) \times g(r_x) である。(スワップの意味がなくなるので。)

同じかの判定は、作れるものの中で辞書順最小のものが同じかで判定すれば良くて、問題は辞書順最小のものをどうやって持つか……と考えていたのだが、

よく考えると各頂点を根とする部分木について、作れるものの中で辞書順最小のものをすべて陽に持っても MLE しない。

証明

頂点が  0 から  2^n - 2 だとして、頂点  0 を深さ  0 とする。

深さ  i の頂点数は  2^i で、各頂点に対する辞書順最小の文字列の長さは  2^{n-i} - 1 なので、文字列の総長は  \displaystyle \sum_{i=0}^{n-1} \left ( 2^{n-i} - 1 \right ) 2 ^{i} = n 2^n - 2^n + 1

となる。(証明終わり)

比較は文字列長のオーダーでできるので、 O(n 2^n) で通る。

提出コード (ACL を使用)

【AtCoder】黄色になりました

ABC275 で AtCoder 黄色になりました。

ほとんどがポエムなので、物好きな方以外は最後の「おすすめコンテンツ」だけ読んでもらえれば十分だと思います。

解いた問題数
コンテストサイト 問題数
AtCoder 1566
Codeforces 309
AOJ 309
yukicoder 125
library-checker 8
合計 2317

「〇日後と決めて解き直す」みたいなことはしていませんが、後述する「コンテスト中に解けなかった問題を解いた」ときに同コンテストの他の問題を見直したり、関連する話題を漁っていて再会した問題をまた解いたり、ということは割としているので、実際にはもう少し多いと言えるかもしれません。

自己紹介
  • 地方国立大 B4
  • 情報系
  • 競プロを始めたのは B1 の秋
  • 中学受験・科学オリンピックは未経験

競プロは中学受験経験者や科オリ経験者が強い側面があると思っていますが、そういう経験はありません。

青上位~くらいになると、旧帝大(特に東大京東工大など)の学生や、有名中高一貫校の中高生が多い気がしているので、競プロ界隈では少数派かもしれません。

練習方法(青→黄)
コンテスト中に考えて解けなかった問題を解く

この練習方法で良い点は「問題を解くことで得られる知識が、次に身に付けるべきこととして適切である確率が高い点」で、悪い点は「ある分野の知識を網羅的に身に付けるのに不向きな点」だと思います。そのため、キーワードを使って関連する話題を調べたり、理解できるまで AC しないことは意識しています。なお、その結果解説を読んでも理解できずに途中で放置されている問題が結構あります。

なお、問題量については、バーチャルコンテストなどを活用すれば確保できると思います。(自分はそこまで手が回っていないですが。)

公式解説をオープンしても良いですが、Twitter で解けた人のツイートを読んで、断片的な情報から解法を考案するというのを自分はやっていました。

AtCoder Type Checker によると、自分は「わずかに、多く解くタイプ」らしく、実感としてもそのレート帯の人が解けるべき問題を全て解いたうえでもう 1 問解けたときにレーティングが上がることが多かったですが、この練習方法が効いたのではないかと勝手に思っています。

網羅的・体系的に学ぶ

競プロ典型 90 問や書籍など、自分が競プロを始めてからの期間だけでもさまざまな競プロコンテンツが出ていますが、だいたい途中で飽きてしまうこともあり、あまり進んでおりません。しかし今の自分の喫緊の課題は蟻本の内容(特にフロー)を理解することなのではないかと思います。

気長にやる

〇か月後までに〇〇色にならないとやばい、みたいなのはなかったので、気長にやりました。もちろん継続的な努力ができる人のほうが上達は早いと思いますが、プレッシャーに押しつぶされてやめてしまう人も結構見てきたので、少しずつ気長に続けるのもありなのかな、と感じます。

チーム練習

そもそも 2021 年秋から競プロを再開したのは ICPC の影響がありますし、定期的に長時間の練習を行う習慣がつくことや、自分が知らないことを知る機会が増えることは良いと思っています。

Codeforces の Gym に無限に問題があるのでこれを使うのがおすすめです。

学んだアルゴリズムなど

学んだが実際のコンテストでは使ったことはないアルゴリズムも結構あり、勝負問で学んだ内容が活かせるかははっきり言って運だと思っています。 そのため、学んだアルゴリズムなどについては明示的には書きません。

ただ、青になったばかりの頃に比べると、何ができるかだけ知っているアルゴリズムも含めれば数はかなり増えました。( 2 倍どころではないです。)また、既に知っていた内容に関しても、取り出すスピードが上がったり、より抽象的に理解できるようになったと感じます。

黄色について

青くらいから既にそうでしたが、Twitter などで個人的に尊敬している方々が結構このあたりの色であることが多いので、少しでも追い付けて嬉しいです。ですが知識量などまだまだ遠いなと思うことも多いです。(競プロにたまに役立つ話題の引き出しの数で差があると感じます。)

競プロは役に立つか

自分の経験としては、学業やインターンなどで役立つのは言うまでもなかったです。(効率が良いかは別問題です。)

個人的には「上位勢の取り組み方が AC 数などで可視化される」点が良いと思っています。競プロを始める前の自分は、どんな分野であれ結果を出している人間に対し「どうせ才能があるから……」と思っていたのですが、そのうちのだいたいは異常な量の努力をしていたということがわかりました。

もちろん本当に才能がある人もいますし、そもそも異常な量の努力ができるのも才能だという主張も同意できますが、それでも「自分ももうちょっと頑張ってみるか」という気持ちになれたのはありがたいなと思います。(まあこれは何かにしっかり取り組んで、他の上位勢と関わることができる環境なら基本的に当てはまるとも思います。)

謝辞

チームメイトをはじめとする同大学の競プロerや、フォロワーの皆さん、いつもありがとうございます。

おすすめコンテンツ

後で思いついたものは適宜追加します。

online-judge-tools
  • サンプルのダウンロードと一括テストが素晴らしいです。
  • なぜか自宅の回線だと自動 submit は壊れるので使っていないです。
  • verification-helper を導入するとライブラリの verify や提出コードへの貼り付けが楽になります。
Luzhuled's LibraryNyaan's Librarymaspy さんのライブラリ など
  • 知らないことが大量にあり勉強したいことを探すのに便利です。
  • ライブラリの実装方法についても参考になります。
  • ところで C++ を使うメリットとして、他の人が書いたライブラリをコンテスト中に使いやすいという点があると思います。
kotatsugame さん、SSRS さんの提出コード
  • 多くのコンテストに参加しており、マクロをあまり使っていないことから、実装の参考にするためによく読ませていただいてます。
デバッグマクロ
  • std::vector などを std::cout で出力できるようにすることなどを指します。
  • 自分は提出するファイルが長くなるのが嫌なので以下のように書いています。( debug.hpp にいろいろ書いてあります。)
#ifdef _RUTHEN
#include <debug.hpp>
#else
#define show(...) true
#endif
clang-format
  • フォーマッタを使うかは意見が分かれるところだと思いますが、「他の人のコードをフォーマッタにかけて見やすくするために普段から何かしらのフォーマッタを使っておく」という考え方もありだと思います。
  • 自分はファイルを保存したタイミングでフォーマッタが走るようにしています。
Competitive Programming Contests Calendar
  • これを Google Calendar に入れて、コンテストの日程を管理しています。
  • CLIST でも良いかもしれません。
AtCoder Clans
  • おそらく自分が競プロを始めてからできたので割と新しいサービスなのですが、さまざまなコンテンツへの導線としてかなり優秀だと感じます。