ARC 169

A. Please Sign

https://atcoder.jp/contests/arc169/tasks/arc169_a

解説 AC

こちらの動画を見て理解した

https://youtu.be/A79qGX7OFMM?si=PPJI_IB_vRbaFFaZ

void solve() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    int N;
    cin >> N;
    vll A(N);
    rep(i, N) cin >> A[i];
    vint depth(N);
    vll B(N);
    B[0] = A[0];
    rep2(i, 1, N) {
        int p;
        cin >> p;
        p--;
        depth[i] = depth[p] + 1;
        B[depth[i]] += A[i];
    }

    string ans = "0";
    for (int i = N - 1; i >= 0; i--) {
        if (B[i] == 0)
            continue;
        if (B[i] < 0)
            ans = "-";
        if (B[i] > 0)
            ans = "+";
        break;
    }
    cout << ans << endl;
}

B. Subsegments with Small Sums

https://atcoder.jp/contests/arc169/tasks/arc169_b

ABC 402

https://atcoder.jp/contests/abc402

C - Dislike Foods

食材 $i$ を使用している料理の番号、料理 $j$ が使用している苦手な食材の数をそれぞれ保存しておく。 食材 $B_i$ を克服したとき、料理 $j$ が食材 $B_i$ を使用していたら、料理 $j$ の苦手な食材の数を1減らす。苦手な食材の数が 0 になったら食べられる料理の個数を1増やす。 これを繰り返す

void solve() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    int N, M;
    cin >> N >> M;
    vvll A(M);
    rep(i, M) {
        int K;
        cin >> K;
        rep(j, K) {
            ll a;
            cin >> a;
            a--;
            A[i].push_back(a);
        }
    }

    vll B(N);
    rep(i, N) {
        cin >> B[i];
        B[i]--;
    }

    // shokuzai[i]: 食材 i を使っている料理の番号
    vvll shokuzai(N);
    rep(i, M) {
        for (ll a : A[i]) {
            shokuzai[a].push_back(i);
        }
    }

    // nums[i]: 料理 i に含まれる苦手な食材の数
    vll nums(M, 0);
    rep(i, M) nums[i] = A[i].size();

    ll ans = 0;
    for (ll b : B) {
        for (ll i : shokuzai[b]) {
            nums[i]--;
            if (nums[i] == 0)
                ans++;
        }
        cout << ans << endl;
    }
}

D - Line Crossing

AC したが、実験して多分これで通るんじゃないかなぁくらいののりで提出して AC した。

第17回 アルゴリズム実技検定 (PAST 017)

https://atcoder.jp/contests/past17-open

H - 履修登録

解説 AC.

dp[i][j] を $i$ 限目までの科目をこなして $j$ 単位取得するのに必要な最小の労力とする。

void solve() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    ll n, m;
    cin >> n >> m;
    vll a(n), b(n), c(n);
    rep(i, n) cin >> a[i] >> b[i] >> c[i];

    vector<vector<pair<ll, ll>>> lec(5001);
    rep(i, n) {
        lec[b[i]].push_back({a[i], c[i]});
    }

    vvll dp(5001, vll(m + 1, INF));
    dp[0][0] = 0;
    rep2(i, 1, 5001) {
        rep(j, m + 1) {
            // i 限目の科目を取らない場合
            chmin(dp[i][j], dp[i - 1][j]);

            // i 限目の科目を取る場合
            rep(k, lec[i].size()) {
                chmin(dp[i][j], dp[i - 1][max(0ll, j - lec[i][k].second)] + lec[i][k].first);
            }
        }
    }

    ll ans = dp[5000][m];
    if (ans == INF)
        ans = -1;
    cout << ans << endl;
}

K - 正しいチェックディジット

https://atcoder.jp/contests/past17-open/tasks/past17_k

第18回 アルゴリズム実技検定 (PAST 018)

https://atcoder.jp/contests/past18-open

G - 二回の交換

$A = B$ のとき、$A_1, A_2$ を交換する操作を2回行えば良い。

$A \neq B$ のときを考える。 交換操作を行うする左端の index として $i < j$ なる $i,j$ を選ぶ。 このとき $(A_i, A_{i+1})$ の交換と $(A_j, A_{j+1})$ の交換操作は一般に非可換である。 一方で今回の問題では交換回数が2回なので次の2通りの置換のみ考えれば十分である。

\begin{eqnarray*} \begin{pmatrix} 1 & 2 & 3 \\ 3 & 1 & 2 \end{pmatrix} \begin{pmatrix} 1 & 2 & 3 \\ 2 & 3 & 1 \end{pmatrix} \end{eqnarray*}

題意を満たすような操作方法は以下の3通りである

  • (1) $i$ が小さい方から処理していき $A_i \neq B_i$ なる部分があれば $(A_i, A_{i+1})$ を交換する操作を繰り返し操作回数が2回かつ、最終的に $A = B$ となる
  • (2) $i$ が大きい方から処理しておき $A_i \neq B_i$ なる部分があれば $(A_{i-1}, A_{i})$ を交換する操作を繰り返し操作回数が2回かつ、最終的に $A = B$ となる
  • (3) 一度の交換で $A = B$ となり、かつ、操作前または操作後の数列において同じ数字が連続している部分が存在している

(2) に関しては数列を反転させて (1) と同じ操作を行えば良い。

ABC 401

https://atcoder.jp/contests/abc401

D - Logical Filling

https://atcoder.jp/contests/abc401/tasks/abc401_d

解説 AC

vector<pair<char, int>> runLengthEncode(const string& input) {
    vector<pair<char, int>> encoded;
    int size = input.size();
    for (int i = 0; i < size; ++i) {
        int count = 1;
        while (i + 1 < size && input[i] == input[i + 1]) {
            ++i;
            ++count;
        }
        encoded.emplace_back(input[i], count);
    }
    return encoded;
}

void solve() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    ll N, K;
    string S;
    cin >> N >> K >> S;

    rep(i, N) {
        if (i + 1 < N && S[i] == '?' && S[i + 1] == 'o') {
            S[i] = '.';
        }
        if (i + 1 < N && S[i] == 'o' && S[i + 1] == '?') {
            S[i + 1] = '.';
        }
    }

    int no = 0;
    for (char c : S)
        if (c == 'o')
            no++;
    if (no == K) {
        rep(i, N) if (S[i] == '?') S[i] = '.';
        cout << S << endl;
        return;
    }

    auto ps = runLengthEncode(S);
    int m = 0;
    for (auto [c, num] : ps) {
        if (c == 'o')
            m += num;
        if (c == '?')
            m += (num + 1) / 2;
    }

    if (m > K) {
        cout << S << endl;
        return;
    }

    for (auto [c, num] : ps) {
        if (c != '?') {
            rep(i, num) cout << c;
            continue;
        }

        if (num % 2 == 0) {
            rep(i, num) cout << '?';
        } else {
            rep(i, num) {
                cout << (i % 2 == 0 ? 'o' : '.');
            }
        }
    }
    cout << endl;
}

E - Reachable Set

解説 AC

ABC 297

https://atcoder.jp/contests/abc297

A. Double Click

https://atcoder.jp/contests/abc297/tasks/abc297_a

B. chess960

https://atcoder.jp/contests/abc297/tasks/abc297_b

C. PC on the Table

https://atcoder.jp/contests/abc297/tasks/abc297_c

D. Count Subtractions

https://atcoder.jp/contests/abc297/tasks/abc297_d

E. Kth Takoyaki Set

https://atcoder.jp/contests/abc297/tasks/abc297_e

ダイクストラみたいな感じで解く。距離 0 のところから始めてコスト $A_i$ で別のノードに移動。距離最初のノードからまたコスト $A_i$ で移動・・・というのを繰り返す。訪れたノードの数が $K+1$ になったら終了(最初の 0 のノードを数えているので $+1$ している)。

void solve() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    int N, K;
    cin >> N >> K;
    vll A(N);
    rep(i, N) cin >> A[i];

    set<ll> check, stock;
    stock.insert(0);

    while ((int)check.size() != K + 1) {
        auto it = stock.begin();
        ll x = *it;
        stock.erase(it);

        check.insert(x);
        for (ll a : A)
            stock.insert(x + a);
    }
    cout << *check.rbegin() << endl;
}

F. Minimum Bounding Box 2

https://atcoder.jp/contests/abc297/tasks/abc297_f

ABC 324

https://atcoder.jp/contests/abc324

A. Same

https://atcoder.jp/contests/abc324/tasks/abc324_a

B. 3-smooth Numbers

https://atcoder.jp/contests/abc324/tasks/abc324_b

C. Error Correction

https://atcoder.jp/contests/abc324/tasks/abc324_c

D. Square Permutation

https://atcoder.jp/contests/abc324/tasks/abc324_d

E. Joint Two Strings

https://atcoder.jp/contests/abc324/tasks/abc324_e

$S_i$ の部分列と $T$ の prefix との共通部分の長さが $k$ となる $i$ の個数を $h_k$ とする。 同様に $S_i$ の部分列と $T$ の suffix との共通部分の長さが $k$ となる $i$ の個数を $t_k$ とする。

$T$ の prefix との共通部分の長さが $k$ となる文字には、$T$ の suffix との共通部分の長さが $N-k$ 以上となる文字を連結させれば $T$ を部分列として含むことができる。

よって求める個数は

\begin{align*} \sum_{k=0}^{N} \left\{ h_k \cdot \left( \sum_{j = N-k}^N t_{j} \right) \right\} \end{align*}

ABC 305

https://atcoder.jp/contests/abc305

A. Water Station

https://atcoder.jp/contests/abc305/tasks/abc305_a

B. ABCDEFG

https://atcoder.jp/contests/abc305/tasks/abc305_b

https://atcoder.jp/contests/abc305/tasks/abc305_c

D. Sleep Log

https://atcoder.jp/contests/abc305/tasks/abc305_d

https://atcoder.jp/contests/abc305/tasks/abc305_e

頂点 $i$ に体力 $h_i$ の警備員がいるとき、隣接する頂点に警備員が移動して体力 $h_i - 1$ の警備員がいるとみなしても良い。 体力が大きい警備員がいる頂点から順に隣接する頂点に移動させて、各頂点に体力が 0 以上の警備員がいる頂点が列挙すべき頂点

void solve() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    int N, M, K;
    cin >> N >> M >> K;
    vvint graph(N);
    rep(i, M) {
        int u, v;
        cin >> u >> v;
        u--, v--;
        graph[u].push_back(v);
        graph[v].push_back(u);
    }

    vll powers(N, -1);
    rep(i, K) {
        int p, h;
        cin >> p >> h;
        p--;
        powers[p] = h;
    }

    // power, node id
    using P = pair<ll, int>;
    priority_queue<P> pq;
    rep(i, N) {
        if (powers[i] > 0)
            pq.push({powers[i], i});
    }

    while (pq.size()) {
        auto [power, id] = pq.top();
        pq.pop();

        if (powers[id] > power)
            continue;

        for (int nx : graph[id]) {
            if (powers[nx] >= power - 1)
                continue;
            powers[nx] = power - 1;
            pq.push({powers[nx], nx});
        }
    }

    vint ans;
    rep(i, N) {
        if (powers[i] >= 0)
            ans.push_back(i + 1);
    }
    cout << ans.size() << endl;
    print(ans);
}

F. Dungeon Explore

https://atcoder.jp/contests/abc305/tasks/abc305_f

第15回 アルゴリズム実技検定 (PAST 015)

https://atcoder.jp/contests/past15-open

E - 合計得点

bit 全探索で良いが $N$ 個から $K$ 個選ぶ方法でも解ける

void solve() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, k;
    cin >> n >> k;
    vll a(n);
    rep(i, n) cin >> a[i];

    vint v;

    auto dfs = [&](auto dfs, int used) -> void {
        if (__builtin_popcount(used) == k) {
            v.push_back(used);
            return;
        }

        int id = -1;
        rep(i, n) {
            if ((used >> i) & 1)
                id = i;
        }
        rep2(i, id + 1, n) {
            dfs(dfs, used | (1 << i));
        }
    };

    dfs(dfs, 0);

    ll ans = 0;
    for (int used : v) {
        rep(i, n) {
            if ((used >> i) & 1)
                ans += a[i];
        }
    }
    cout << ans << endl;
}

F - 番号付け

void solve() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    int N;
    cin >> N;
    vint A(N);
    rep(i, N) cin >> A[i];

    map<int, vint> mp;
    rep(i, N) {
        mp[A[i]].push_back(i);
    }

    int cnt = 1;
    vint ans(N);
    for (auto it = mp.begin(); it != mp.end(); it++) {
        auto [_, ids] = *it;
        for (int id : ids) {
            ans[id] = cnt++;
        }
    }
    print(ans);
}

G - N-SAT

void solve() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    int N, M;
    cin >> N >> M;
    vvint as(M), bs(M);
    rep(i, M) {
        int k;
        cin >> k;
        rep(j, k) {
            int a, b;
            cin >> a >> b;
            a--;
            as[i].push_back(a);
            bs[i].push_back(b);
        }
    }

    rep(t, 1 << N) {
        vint xs;
        rep(i, N) {
            xs.push_back((t >> i) & 1);
        }

        int cnt = 0;
        rep(i, M) {
            int ok = 0;
            int ki = as[i].size();
            rep(j, ki) {
                if (xs[as[i][j]] == bs[i][j])
                    ok = 1;
            }
            if (ok)
                cnt++;
        }

        if (cnt == M) {
            yesno(true);
            return;
        }
    }
    yesno(false);
}

H - 和で表現

解説 AC。愚直解で AC 取れてしまったが解説読んだら二分探索だった。

ABC 400

https://atcoder.jp/contests/abc400

C. 2^a b^2

https://atcoder.jp/contests/abc400/tasks/abc400_c

AC したがなぜ AC したのかよくわからず AC した。sqrt が誤差を生むので出てきた数の周辺で検算をするようにして AC したが知識がないとsqrt を使ったときに落とせるテストケースもかけないのでかなり難しい問題に感じた。

$2^a$ を動かして取りうる $b^2$ の個数を求める方法だと $X = 16 = 2^2 \times 2^2 = 2^4 \times 1^2$ のように同じ数に対して重複してカウントしてしまうことがある。

2の冪はすべて $2^a$ の方に押し込んで、$b$ は奇数という制約をかけると重複しない。

\begin{align*} 2^k \times b^2 \leq N &\Rightarrow b^2 \leq N / 2^k \\ &\Rightarrow b \leq \sqrt{N/2^k} \\ &\Rightarrow b \leq \lfloor \sqrt{N/2^k} \rfloor ~~~(\because b \text{ is an integer}) \end{align*}

であり $[1, \lfloor \sqrt{N/2^k} \rfloor]$ に含まれる奇数の個数は $\displaystyle \left\lfloor \frac{\lfloor \sqrt{N/2^k} \rfloor + 1}{2} \right\rfloor$ である。

第19回 アルゴリズム実技検定 (PAST 019)

https://atcoder.jp/contests/past19-open

C - 良さそうな数 / Goodish or Not

DP で解いたが、解説読んで普通に桁を変えるのを全探索すればいいだけだった

DP で解く場合は dp[i][j] を左から $i$ 桁目を $j$ にしたときのそれまで変更回数の最小値とする。

// DP 解放
void solve() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    string S;
    cin >> S;

    int n = S.size();
    if (n == 1) {
        yesno(true);
        return;
    }

    vint v(n);
    rep(i, n) {
        v[i] = S[i] - '0';
    }

    vvint dp(n + 1, vint(10, INF));
    rep2(i, 1, 10) {
        if (v[0] != i)
            dp[0][i] = 1;
        else
            dp[0][i] = 0;
    }

    rep2(i, 1, n) {
        rep(j, 10) {
            int change = v[i] != j;
            if (j - 1 >= 0) {
                chmin(dp[i][j], dp[i - 1][j - 1] + change);
            }
            if (j + 1 <= 9) {
                chmin(dp[i][j], dp[i - 1][j + 1] + change);
            }
            chmin(dp[i][j], dp[i - 1][j] + change);
        }
    }

    int ok = 0;
    rep(i, 10) {
        if (dp[n - 1][i] <= 1)
            ok = 1;
    }
    yesno(ok);
}
// 全探索解法
void solve() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    string S;
    cin >> S;

    int n = S.size();

    vint v(n);
    rep(i, n) {
        v[i] = S[i] - '0';
    }

    auto check = [](vint& v) -> bool {
        bool ok = true;
        int n = v.size();
        if (n == 1)
            return true;

        if (v[0] == 0)
            return false;

        rep(i, n - 1) {
            if (abs(v[i] - v[i + 1]) > 1)
                ok = false;
        }

        return ok;
    };

    rep(i, n) {
        vint t = v;
        rep(j, 10) {
            t[i] = j;
            if (check(t)) {
                yesno(true);
                return;
            }
        }
    }

    yesno(false);
}

K - 隣接禁止

木 DP の問題

064

https://atcoder.jp/contests/abc064

A. i i’s

https://atcoder.jp/contests/agc064/tasks/agc064_a

解説読んでもどうやったらその思考になるのかわからず。 結局諦めた

B. Red and Blue Spanning Tree

https://atcoder.jp/contests/agc064/tasks/agc064_b

C. Erase and Divide Game

https://atcoder.jp/contests/agc064/tasks/agc064_c

D. Red and Blue Chips

https://atcoder.jp/contests/agc064/tasks/agc064_d

E. Cross Sum Construction

https://atcoder.jp/contests/agc064/tasks/agc064_e

F. No Permutations

https://atcoder.jp/contests/agc064/tasks/agc064_f

ABC 399

https://atcoder.jp/contests/abc399

A. Hamming Distance

https://atcoder.jp/contests/abc399/tasks/abc399_a

B. Ranking with Ties

https://atcoder.jp/contests/abc399/tasks/abc399_b

C. Make it Forest

https://atcoder.jp/contests/abc399/tasks/abc399_c

D. Switch Seats

https://atcoder.jp/contests/abc399/tasks/abc399_d

$A_i = A_j = a, (i < j)$ とする。

(1) $i+1=j$ のとき

$a$ が隣接しているので求める整数対の対象にならない

(2) それ以外のとき

求める整数対の候補となるのは

  • $A_{i-1} = A_{j-1}$ (baba 型)
  • $A_{i-1} = A_{j+1}$ (baab 型)
  • $A_{i+1} = A_{j-1}$ (abba 型)
  • $A_{i+1} = A_{j+1}$ (abab 型)

の4つのケースである。

(2.1) $A_{i+1} = A_{j-1}$ のとき

  • $i+2 = j$ のとき、同じ数字を参照しているので対象外. ($a b a$ で $b$ を参照しているケース)
  • $i+3 = j$ のとき、同じ数字が隣接しているので対象外. ($a b b a$ で $b$ を参照しているケース)
  • それ以外のとき、$(\min(a,b), \max(a,b))$ を答えに追加する

(2.2) それ以外の3ケースのとき

第2回 アルゴリズム実技検定 (PAST 002)

https://atcoder.jp/contests/past202004-open

K - 括弧

https://atcoder.jp/contests/past202004-open/tasks/past202004_k

void solve() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    int N;
    string S;
    cin >> N >> S;

    S = "_" + S;

    vll C(N + 1), D(N + 1);
    rep(i, N) cin >> C[i + 1];
    rep(i, N) cin >> D[i + 1];

    // dp[i][j]: i 文字目まで見たときの、対応が取れていない左カッコの数が j のときの最小コスト
    vvll dp(N + 1, vll(N + 1, INF));
    dp[0][0] = 0;
    rep2(i, 1, N + 1) {
        rep(j, N) {
            if (S[i] == '(') {
                chmin(dp[i][j + 1], dp[i - 1][j]);

                // 反転
                if (j - 1 >= 0)
                    chmin(dp[i][j - 1], dp[i - 1][j] + C[i]);
            } else {
                if (j - 1 >= 0)
                    chmin(dp[i][j - 1], dp[i - 1][j]);

                // 反転
                chmin(dp[i][j + 1], dp[i - 1][j] + C[i]);
            }

            // i 番目の要素を削除
            chmin(dp[i][j], dp[i - 1][j] + D[i]);
        }
    }
    cout << dp[N][0] << endl;
}

ARC 149

A. Repdigit Number

https://atcoder.jp/contests/arc149/tasks/arc149_a

B. Two LIS Sum

https://atcoder.jp/contests/arc149/tasks/arc149_b

実験していて $A_i$ で sort して $B_i$ の LIS を求めるだけで良さそうだなと思って実装したら AC した。

void solve() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;
    vector<pair<int, int>> ps(n);
    rep(i, n) {
        int x;
        cin >> x;
        ps[i].first = x;
    }
    rep(i, n) {
        int x;
        cin >> x;
        ps[i].second = x;
    }

    sort(all(ps));

    ll ans = n;
    vint v(n + 1, INF);
    rep(i, n) {
        auto it = upper_bound(all(v), ps[i].second);
        *it = ps[i].second;
    }

    ans += lower_bound(all(v), INF) - v.begin();
    cout << ans << endl;
}

C. Avoid Prime Sum

https://atcoder.jp/contests/arc149/tasks/arc149_c

ARC 194

A. Operations on a Stack

https://atcoder.jp/contests/arc194/tasks/arc194_a

void solve() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;
    vll a(n + 1);
    rep(i, n) cin >> a[i + 1];

    // dp[i][j]: i 回目の操作で操作 j を行ったときの S の総和の最大値
    // j = 0: 何もしない
    // j = 1: a[i] を入れる
    vvll dp(n + 1, vll(2, -INF));
    dp[0][1] = 0;
    rep2(i, 1, n + 1) {
        chmax(dp[i][1], dp[i - 1][1] + a[i]);
        chmax(dp[i][1], dp[i - 1][0] + a[i]);
        if (i - 2 >= 0) {
            // i-1 回目で要素を入れて、i 回目で要素を取り出すと何もしていないのと同じ
            chmax(dp[i][0], dp[i - 2][1]);
            chmax(dp[i][0], dp[i - 2][0]);
        }
    }
    cout << max(dp[n][0], dp[n][1]) << '\n';
}

B. Minimum Cost Sort

https://atcoder.jp/contests/arc194/tasks/arc194_b

ABC 244

https://atcoder.jp/contests/abc244

A. Last Letter

https://atcoder.jp/contests/abc244/tasks/abc244_a

B. Go Straight and Turn Right

https://atcoder.jp/contests/abc244/tasks/abc244_b

C. Yamanote Line Game

https://atcoder.jp/contests/abc244/tasks/abc244_c

D. Swap Hats

https://atcoder.jp/contests/abc244/tasks/abc244_d

E. King Bombee

https://atcoder.jp/contests/abc244/tasks/abc244_e

dp[k][i][j]: $k$ 回移動後、頂点 $i$ にたどり着くまでに頂点 $X$ を偶数($j=0$)/奇数($j=1$)回訪れる経路の数

void solve() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    ll N, M, K, S, T, X;
    cin >> N >> M >> K >> S >> T >> X;
    S--, T--, X--;
    vvint graph(N);
    rep(i, M) {
        int u, v;
        cin >> u >> v;
        u--, v--;
        graph[u].push_back(v);
        graph[v].push_back(u);
    }

    // dp[k][i][j]: k 回移動後、頂点 i にたどり着くまでに頂点 X を偶数(j=0)/奇数(j=1)回訪れる経路の数
    vector<vector<vector<mint>>> dp(K + 1, vector<vector<mint>>(N, vector<mint>(2)));
    dp[0][S][0] = 1;

    rep2(i, 1, K + 1) {
        rep(j, N) {
            for (int nx : graph[j]) {
                if (nx == X) {
                    dp[i][nx][0] += dp[i - 1][j][1];
                    dp[i][nx][1] += dp[i - 1][j][0];
                } else {
                    dp[i][nx][0] += dp[i - 1][j][0];
                    dp[i][nx][1] += dp[i - 1][j][1];
                }
            }
        }
    }

    cout << dp[K][T][0].val() << endl;
}

F. Shortest Good Path

https://atcoder.jp/contests/abc244/tasks/abc244_f

ABC 286

https://atcoder.jp/contests/abc286

A. Range Swap

https://atcoder.jp/contests/abc286/tasks/abc286_a

B. Cat

https://atcoder.jp/contests/abc286/tasks/abc286_b

C. Rotate and Palindrome

https://atcoder.jp/contests/abc286/tasks/abc286_c

D. Money in Hand

https://atcoder.jp/contests/abc286/tasks/abc286_d

E. Souvenir

https://atcoder.jp/contests/abc286/tasks/abc286_e

ワーシャルフロイドで解く。距離が短い場合は価値も更新. 距離が同じ場合は価値を高い方に更新する。 はじめわーシャルフロイドの添字の実行順があったことを忘れて WA したが、なぜか3回回したら AC した。 まぐれと思ったが3回実行すると正しい答えが返ってくることは証明されているらしい。

https://qiita.com/tmaehara/items/f56be31bbb7a468a04ed

コード中の注意点として距離が大きいときの条件と同じときの条件をまとめて書いてはいけない

OK

if (dist[i][j] > dist[i][k] + dist[k][j]) {
    dist[i][j] = dist[i][k] + dist[k][j];
    value[i][j] = value[i][k] + value[k][j] - prices[k];
}
if (dist[i][j] == dist[i][k] + dist[k][j]) {
    chmax(value[i][j], value[i][k] + value[k][j] - prices[k]);
}

NG

if (dist[i][j] >= dist[i][k] + dist[k][j]) {
    dist[i][j] = dist[i][k] + dist[k][j];
    chmax(value[i][j], value[i][k] + value[k][j] - prices[k]);
}

NG 版で書くと距離が大きいときの価値の値を使ってしまうことがあるため WA する。

ABC 320

https://atcoder.jp/contests/abc320

A. Leyland Number

https://atcoder.jp/contests/abc320/tasks/abc320_a

B. Longest Palindrome

https://atcoder.jp/contests/abc320/tasks/abc320_b

C. Slot Strategy 2 (Easy)

https://atcoder.jp/contests/abc320/tasks/abc320_c

D. Relative Position

https://atcoder.jp/contests/abc320/tasks/abc320_d

E. Somen Nagashi

https://atcoder.jp/contests/abc320/tasks/abc320_e

並んでいる人は id が小さい順で常に並んでいる、列から離れた人は帰ってくる時間が小さい順に並んでいると考えればそれぞれを priority queue で管理すれば問題が解けることがわかる

void solve() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, m;
    cin >> n >> m;

    vll ans(n);
    priority_queue<int, vint, greater<int>> people;

    // time, id
    using P = pair<ll, int>;
    priority_queue<P, vector<P>, greater<P>> tpq;

    rep(i, n) people.push(i);
    rep(i, m) {
        ll t, w, s;
        cin >> t >> w >> s;

        // そうめんが流れてくるまでに列に戻ってくる人を列に戻す
        while (tpq.size() && tpq.top().first <= t) {
            auto [t, id] = tpq.top();
            tpq.pop();
            people.push(id);
        }

        // 列に誰もいなかったら何もしない
        if (people.size() == 0)
            continue;

        auto p = people.top();
        people.pop();
        ans[p] += w;
        tpq.push({t + s, p});
    }

    for (ll x : ans)
        cout << x << '\n';
}

F. Fuel Round Trip

https://atcoder.jp/contests/abc320/tasks/abc320_f

ABC 398

https://atcoder.jp/contests/abc398

A. Doors in the Center

https://atcoder.jp/contests/abc398/tasks/abc398_a

B. Full House 3

https://atcoder.jp/contests/abc398/tasks/abc398_b

C. Uniqueness

https://atcoder.jp/contests/abc398/tasks/abc398_c

D. Bonfire

https://atcoder.jp/contests/abc398/tasks/abc398_d

煙の位置は変えずに焚き火と高橋くんの位置を移動させる。

void solve() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    ll n, row, col;
    cin >> n >> row >> col;
    string s;
    cin >> s;

    using P = pair<ll, ll>;
    set<P> memo;
    memo.insert({0, 0});

    int si = 0, sj = 0;

    for (char c : s) {
        if (c == 'N') {
            si++;
            row++;
        }
        if (c == 'W') {
            sj++;
            col++;
        }
        if (c == 'S') {
            si--;
            row--;
        }
        if (c == 'E') {
            sj--;
            col--;
        }
        memo.insert({si, sj});
        if (memo.count({row, col})) {
            cout << 1;
        } else {
            cout << 0;
        }
    }
    cout << endl;
}

E. Tree Game

https://atcoder.jp/contests/abc398/tasks/abc398_e

ARC 177

A. Exchange

https://atcoder.jp/contests/arc177/tasks/arc177_a

B. Puzzle of Lamps

https://atcoder.jp/contests/arc177/tasks/arc177_b

C. Routing

https://atcoder.jp/contests/arc177/tasks/arc177_c

条件1について考える。赤色のマスを紫色に塗ってもスタートとゴールの連結性は変わらないから紫色に変えるべきマスは青色のマスである。 同様に条件2を達成するためには赤色のマスを紫色に変える必要がある。 よって条件1, 2を満たすために必要な紫色に変えるマスは独立して考えることができる。

よって問題の部分問題は次のように言い換えることができる。

各マスは赤か白で塗られており $c_{i,j} = 'R'$ のとき赤、$c_{i,j} = 'W'$ のとき白で塗られている。赤マス間は自由に行き来することできる。 スタートとゴールは赤マスである。 スタートとゴールが連結なるためには最低何マス追加で赤マスに変える必要があるか。

これを青マスについても同様に解いて、その和を求めればよい。

白マスへの移動はコスト $+1$、それ以外の移動は コスト $0$ であると考えると、スタートからゴールまでの最短経路を求める問題として解くことができる。

void solve() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;
    vector<vector<char>> B(n, vector<char>(n, '_')), R(n, vector<char>(n, '_'));
    rep(i, n) {
        rep(j, n) {
            char c;
            cin >> c;
            if (c == 'R')
                R[i][j] = c;
            else
                B[i][j] = c;
        }
    }

    vint di = {1, 0, -1, 0};
    vint dj = {0, -1, 0, 1};

    auto cal = [&](vector<vector<char>>& grid, int si, int sj, int ti, int tj) -> ll {
        vvll dist(n, vll(n, INF));
        dist[si][sj] = 0;

        using P = tuple<int, int, int>;
        priority_queue<P, vector<P>, greater<P>> que;
        que.push({0, si, sj});

        while (que.size()) {
            auto [cost, i, j] = que.top();
            que.pop();

            if (dist[i][j] < cost)
                continue;

            rep(d, 4) {
                int ni = i + di[d], nj = j + dj[d];
                if (clamp(ni, 0, n - 1) != ni || clamp(nj, 0, n - 1) != nj)
                    continue;
                if (grid[ni][nj] == '_') {
                    if (dist[ni][nj] <= dist[i][j] + 1)
                        continue;
                    dist[ni][nj] = dist[i][j] + 1;
                } else {
                    if (dist[ni][nj] <= dist[i][j])
                        continue;
                    dist[ni][nj] = dist[i][j];
                }
                que.push({dist[ni][nj], ni, nj});
            }
        }

        return dist[ti][tj];
    };

    ll ans = cal(R, 0, 0, n - 1, n - 1) + cal(B, 0, n - 1, n - 1, 0);
    cout << ans << endl;
}

D. Earthquakes

https://atcoder.jp/contests/arc177/tasks/arc177_d

ARC 182

A. Chmax Rush

https://atcoder.jp/contests/arc182/tasks/arc182_a

解説 AC

$i$ 番目の操作は、左操作か、右操作か、どちらでも良いかの3通りがある。 $i$ 番目の操作が左か右操作のいずれかに確定している場合はそれを実行しなければならないので、$2^\text{どちらの操作をしても良い回数}$ が答えとなる

void solve() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, q;
    cin >> n >> q;
    vint P(q), V(q);
    rep(i, q) {
        cin >> P[i] >> V[i];
        P[i]--;
    }

    // kind[i]: i 回目にどの操作を必要があるか
    // -1 左操作
    // 1  右操作
    // 0  どちらでも良い
    vint kind(q);

    auto change = [&](int i, int op) -> bool {
        if (kind[i] == 0) {
            kind[i] = op;
            return true;
        }
        if (kind[i] != op) {
            // 操作が矛盾
            return false;
        }
        return true;
    };

    rep(i, q) {
        rep2(j, i + 1, q) {
            if (V[i] <= V[j])
                continue;
            if (P[i] == P[j]) {
                cout << 0 << endl;
                return;
            }

            bool ok = true;
            if (P[i] < P[j]) {
                // i 番目の操作で左操作
                // j 番目の操作で右操作
                // をしなければならない
                ok = ok && change(i, -1);
                ok = ok && change(j, 1);
            }
            if (P[i] > P[j]) {
                // i 番目の操作で右操作
                // j 番目の操作で左操作
                // をしなければならない
                ok = ok && change(i, 1);
                ok = ok && change(j, -1);
            }
            if (!ok) {
                cout << 0 << endl;
                return;
            }
        }
    }

    mint two = 2;
    ll cnt = 0;
    rep(i, q) cnt += kind[i] == 0;
    cout << two.pow(cnt).val() << endl;
}

B. |{floor(A_i/2^k)}|

https://atcoder.jp/contests/arc182/tasks/arc182_b

ABC 322

https://atcoder.jp/contests/abc322

A. First ABC 2

https://atcoder.jp/contests/abc322/tasks/abc322_a

B. Prefix and Suffix

https://atcoder.jp/contests/abc322/tasks/abc322_b

C. Festival

https://atcoder.jp/contests/abc322/tasks/abc322_c

D. Polyomino

https://atcoder.jp/contests/abc322/tasks/abc322_d

E. Product Development

https://atcoder.jp/contests/abc322/tasks/abc322_e

解説 AC.

void solve() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, k, p;
    cin >> n >> k >> p;
    vll c(n + 1);
    vvll A(n + 1, vll(k));
    rep(i, n) {
        cin >> c[i + 1];
        rep(j, k) {
            cin >> A[i + 1][j];
        }
    }

    vector<map<vll, ll>> dp(n + 1);
    dp[0][vll(k, 0)] = 0;
    rep2(i, 1, n + 1) {
        dp[i] = dp[i - 1];
        for (auto [v, val] : dp[i - 1]) {
            vll t = v;
            rep(j, k) {
                t[j] += A[i][j];
                if (t[j] > p)
                    t[j] = p;
            }
            if (dp[i].count(t)) {
                chmin(dp[i][t], val + c[i]);
            } else {
                dp[i][t] = val + c[i];
            }
        }
    }

    vll v(k, p);
    if (dp[n].count(v)) {
        cout << dp[n][v] << endl;
    } else {
        cout << -1 << endl;
    }
}

F. Vacation Query

https://atcoder.jp/contests/abc322/tasks/abc322_f

ABC 303

https://atcoder.jp/contests/abc303

A. Similar String

https://atcoder.jp/contests/abc303/tasks/abc303_a

B. Discord

https://atcoder.jp/contests/abc303/tasks/abc303_b

C. Dash

https://atcoder.jp/contests/abc303/tasks/abc303_c

D. Shift vs. CapsLock

https://atcoder.jp/contests/abc303/tasks/abc303_d

E. A Gift From the Stars

https://atcoder.jp/contests/abc303/tasks/abc303_e

葉から始めて距離が3離れたノードは別の star graph の葉に相当する。また葉の隣のノードは star graph の中心。 これよりある葉から出発して $3i+1$ 離れたノードは star graph の中心だったものである。 これより中心の頂点番号が取れるのでそのノードにおける次数を昇順に出力すればよい。

void solve() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;
    vvint graph(n);
    rep(i, n - 1) {
        int u, v;
        cin >> u >> v;
        u--, v--;
        graph[u].push_back(v);
        graph[v].push_back(u);
    }

    vint sz;

    auto dfs = [&](auto dfs, int now, int p, int label) -> void {
        if (label == 1) {
            sz.push_back(graph[now].size());
        }

        for (int nx : graph[now]) {
            if (nx == p)
                continue;
            dfs(dfs, nx, now, (label + 1) % 3);
        }
    };

    rep(i, n) if (graph[i].size() == 1) {
        dfs(dfs, i, -1, 0);
        break;
    }

    sort(all(sz));
    print(sz);
}

F. Damage over Time

https://atcoder.jp/contests/abc303/tasks/abc303_f

ARC 140

A. Right String

https://atcoder.jp/contests/arc140/tasks/arc140_a

解説 AC。

答えが $N$ の約数のいずれかになることまではわかったが最終的な答えの求め方を間違えた。

$d|N$ となる $d$ について考える。$S$ を下記のように $d$ 個ずつに分割する。

12$\cdots$d
$S_1$$S_2$$\cdots$$S_d$
$S_{d+1}$$S_{d+2}$$\cdots$$S_{2d}$
$\vdots$$\vdots$$\ddots$$\vdots$
$S_{N-d+1}$$S_{N-d+2}$$\cdots$$S_N$

列 $i$ に対して最頻出文字に全ての文字を置き換えるのに必要な操作回数を $c_i$ とすると $\sum_i c_i \leq K$ のとき題意の操作を実行してできる文字種は $d$ になる。よって $f(S) \leq d$. 同様に全ての $N$ の約数について調べ $f(S) \leq d$ を達成できる最小の $d$ が答え。

void solve() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, k;
    string s;
    cin >> n >> k >> s;

    ll ans = n;
    rep2(d, 1, n) {
        if (n % d)
            continue;

        int sum = 0;
        rep(i, d) {
            vint cnt(26, 0);
            for (int j = i; j < n; j += d) {
                cnt[s[j] - 'a']++;
            }
            sum += *max_element(all(cnt));
        }
        if (n - sum <= k)
            chmin(ans, d);
    }
    cout << ans << endl;
}

競プロ典型 90 問

https://atcoder.jp/contests/abctypical90

001. Yokan Party(★4)

https://atcoder.jp/contests/typical90/tasks/typical90_a

解説 AC

最小値を最大化する問題は二分探索を使うのが定石らしい。

最小値を $m$ としたときの分割数が $K+1$ 以上ならば最小値を $m$ 以上にすることができる。そうでないならば最小値を $m$ にすることはできない。

void solve() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    ll N, L, K;
    cin >> N >> L >> K;
    vll A(N);
    rep(i, N) cin >> A[i];

    A.insert(A.begin(), 0);
    A.push_back(L);
    vll diff;
    rep(i, N + 1) {
        diff.push_back(A[i + 1] - A[i]);
    }

    int m = diff.size();
    ll ac = 0, wa = L + 1;
    while (abs(ac - wa) > 1) {
        ll wj = (ac + wa) / 2;
        int i = 0;
        ll cnt = 0;
        ll sum = 0;
        rep(i, m) {
            sum += diff[i];
            if (sum >= wj) {
                sum = 0;
                cnt++;
            }
        }

        if (cnt >= K + 1) {
            ac = wj;
        } else {
            wa = wj;
        }
    }
    cout << ac << endl;
}

002 - Encyclopedia of Parentheses(★3)

https://atcoder.jp/contests/typical90/tasks/typical90_b

ARC 173

A. Neq Number

https://atcoder.jp/contests/arc173/tasks/arc173_a

$x$ 以下の Neq Number の個数を求める関数を書いて二分探索すれば良いという方針は立ったが実装に手間取った。 結局うまく実装できず解説を読んだり解説放送を見たが詳細な実装方法については触れておらず結局自分で考えた。

$f(x)$ を $x$ 以下の Neq Number の個数とする。$f(x) = K$ となる最小の $x$ が求める答えとなる。 $f(x)$ は広義単調増加関数であるから二分探索で答えを求めることができる。

$d$ 桁の Neq Number の個数を考える。左の桁を 0 桁目とすると 0 桁目は 1 から 9 までの 9 通りである。 $i > 0$ 桁目の数は 0~9 の10通りのうち $i-1$ 桁目とは異なる数であるであるから 9 通りである。 よって $d$ 桁の Neq Number の個数は $9^{d}$ 通りである。

$f(x)$ の実装について考える。

$x$ が1桁のとき

$f(x) = x$ である。

$x$ が $d$ 桁のとき

まず $d$ 桁未満の Neq Number の個数は $\sum_{k=1}^{d-1} 9^{k}$ である。

ABC 397

https://atcoder.jp/contests/abc397

A. Thermometer

https://atcoder.jp/contests/abc397/tasks/abc397_a

B. Ticket Gate Log

https://atcoder.jp/contests/abc397/tasks/abc397_b

C. Variety Split Easy

https://atcoder.jp/contests/abc397/tasks/abc397_c

D. Cubes

https://atcoder.jp/contests/abc397/tasks/abc397_d

解説 AC

$d = x-y$ とすると

\begin{align*} x^3 - y^3 &= (d+y)^3 - y^3 \\ &= d^3 + 3d^2y + 3dy^2 + y^3 - y^3 \\ &= d^3 + 3d^2y + 3dy^2 \\ &= N \end{align*}

となる。ここで $3d^2y + 3dy^2 > 0$ だから $d^3 < N$ である。

さらに

\begin{align*} &x^3 - y^3 = N \\ &(x-y)(x^2 + xy + y^2) = N \\ &d((d+y)^2 + (d+y)y + y^2) = N \\ &(d^2 + 2dy + y^2) + (dy + y^2) + y^2 = N/d \\ &3y^2 + 3dy + d^2 - N/d = 0 \end{align*}

ABC 216

https://atcoder.jp/contests/abc216

E - Amusement Park

https://atcoder.jp/contests/abc216/tasks/abc216_e

貪欲解法

明らかに、楽しさが大きいアトラクションから乗るのが最適である。 愚直に priority queue などに数値を入れてシミュレーションをすると時間がかかりすぎて TLE になる。

void solve() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    ll n, k;
    cin >> n >> k;
    vll a(n);
    rep(i, n) cin >> a[i];

    map<ll, ll> mp;
    for (ll x : a)
        mp[x]++;

    ll ans = 0;
    while (mp.rbegin() != mp.rend() && k) {
        auto it = mp.rbegin();
        auto nit = next(it);

        ll v1 = it->first;
        ll n1 = it->second;
        ll v2 = 0;
        if (nit != mp.rend())
            v2 = nit->first;

        if ((v1 - v2) * n1 <= k) {
            k -= (v1 - v2) * n1;
            ans += cal(v2, v1) * n1;
            mp.erase(v1);
            if (v2)
                mp[v2] += n1;
        } else {
            ll q = k / n1;
            ll r = k % n1;
            ans += cal(v1 - q, v1) * n1;
            v1 -= q;
            ans += v1 * r;
            k = 0;
        }
    }
    cout << ans << endl;
}

二分探索解法

楽しさが 0 以下のアトラクションに乗る意味はないから楽しさが 0 より大きい場合を考える。 問題は楽しさが $1, 2, \cdots, A_1, 1, 2, \cdots, A_2, \cdots, 1,2, \cdots, A_N$ である $\sum_i A_i$ 個のアトラクションから $K$ 個選んで乗ったときの楽しさの総和の最大値を求める問題と読み替えることができる。

ABC 202

D - aab aba baa

https://atcoder.jp/contests/abc202/tasks/abc202_d

はじめの文字が a となる場合の数は $A-1$ 個の a と $B$ 個の b の並び替え方の場合の数だから $\frac{(A+B+1)!}{(A-1)!B!}$ である。 よって $K > \frac{(A+B+1)!}{(A-1)!B!}$ のときは b から始まる。

この性質を使って先頭から文字を決めていく。

void solve() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    ll a, b, k;
    cin >> a >> b >> k;

    int m = a + b;
    vvll dp(m + 1, vll(m + 1, 0));
    dp[0][0] = 1;
    rep(i, m + 1) {
        rep(j, m + 1) {
            if (i > 0)
                dp[i][j] += dp[i - 1][j];
            if (j > 0)
                dp[i][j] += dp[i][j - 1];
        }
    }

    string ans = "";
    auto dfs = [&](auto dfs, ll na, ll nb, ll rem) {
        if (na == 0 && nb == 0) {
            return;
        }

        if (na == 0) {
            ans.push_back('b');
            dfs(dfs, na, nb - 1, rem);
            return;
        }
        if (nb == 0) {
            ans.push_back('a');
            dfs(dfs, na - 1, nb, rem);
            return;
        }

        ll v = dp[na - 1][nb];
        if (rem <= v) {
            ans.push_back('a');
            dfs(dfs, na - 1, nb, rem);
            return;
        }

        ans.push_back('b');
        dfs(dfs, na, nb - 1, rem - v);
    };

    dfs(dfs, a, b, k);

    cout << ans << endl;
}

ABC 194

D - Journey

https://atcoder.jp/contests/abc194/tasks/abc194_d

クーポンコレクター問題

void solve() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;
    double ans = 0;
    rep2(i, 1, n) {
        ans += 1.0 / i;
    }
    printf("%.9lf\n", ans * n);
}

ARC 197

A - Operations on a Stack

https://atcoder.jp/contests/arc194/tasks/arc194_a

$d(i,1)$ を $i$ 番目の要素を使うときの総和の最大値、$d(i,0)$ を $i$ 番目の要素を使わないときの総和の最大値とする。

$i$ 番目の要素を使うとき、$i-1$ 番目の要素を使う場合、使わない場合の値いずれかの場合からの遷移がある。

$i$ 番目の要素を使わないとき、$i-1$ 番目の要素を積んで、その後その要素を捨てる操作になるから、$i-2$ 番目の要素を使う場合、使わない場合のうちの大きい方の値を選べばよい。

\begin{align} d(i,1) &= \max(dp(i-1, 1), dp(i-1, 0)) + A_i \nonumber \\ d(i,0) &= \max(dp(i-2, 1), dp(i-2, 0)) \nonumber \end{align}

void solve() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;
    vll a(n + 1);
    rep(i, n) cin >> a[i + 1];

    vvll dp(n + 1, vll(2, -INF));
    dp[0][1] = 0;
    rep2(i, 1, n + 1) {
        chmax(dp[i][1], dp[i - 1][1] + a[i]);
        chmax(dp[i][1], dp[i - 1][0] + a[i]);
        if (i - 2 >= 0) {
            chmax(dp[i][0], dp[i - 2][1]);
            chmax(dp[i][0], dp[i - 2][0]);
        }
    }
    cout << max(dp[n][0], dp[n][1]) << '\n';
}

ABC 325

E - Our clients, please wait a moment

https://atcoder.jp/contests/abc325/tasks/abc325_e

類題: https://atcoder.jp/contests/abc395/tasks/abc395_e

社用車だけを使ったときの場合のグラフ $G$ と電車だけを使った場合のグラフ $G^\prime$ を作り、$G$ の各ノードから同じ都市に対応する $G^\prime$ のノードへコスト 0 の辺を張る。

都市 $i$ に対応する $G$ におけるノード番号を $i$, $G^\prime$ におけるノードの番号を $N + i$ とする。 求める答えはノード 1 から出発してノード $N$ またはノード $2N$ に到達するまでの最小コストである。 ($1 \rightarrow N+1$ への移動はコスト 0 でできるから出発は 1 から出発するケースだけ考えればよい)

struct Edge {
    ll to, cost;
};

void solve() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, a, b, c;
    cin >> n >> a >> b >> c;

    vvll D(n, vll(n));
    rep(i, n) rep(j, n) cin >> D[i][j];

    vector<vector<Edge>> graph(n * 2);
    rep(i, n) rep(j, n) {
        if (i == j)
            continue;
        graph[i].push_back({j, D[i][j] * a});
        graph[j].push_back({i, D[i][j] * a});
        graph[n + i].push_back({n + j, D[i][j] * b + c});
        graph[n + j].push_back({n + i, D[i][j] * b + c});
    }
    rep(i, n) graph[i].push_back({n + i, 0});

    vll dist(n * 2, INF);
    dist[0] = 0;

    // cost, pos
    using P = pair<ll, ll>;
    priority_queue<P, vector<P>, greater<P>> pq;

    pq.push({0, 0});
    while (pq.size()) {
        auto [cost, now] = pq.top();
        pq.pop();

        if (dist[now] < cost)
            continue;

        for (auto [to, cost] : graph[now]) {
            if (dist[to] <= dist[now] + cost)
                continue;

            dist[to] = dist[now] + cost;
            pq.push({dist[to], to});
        }
    }

    cout << min(dist[n - 1], dist[n * 2 - 1]) << '\n';
}

ソラで書くセグメント木から ACL へ移行する

はじめに

木マスター養成講座で普通のセグメント木ををソラでかけるようになったあとに、「遅延評価セグメント木をソラで書きたいあなたに」 (以降ソラ版と呼ぶ)を読んで遅延評価セグメント木をソラでかけるようになった。

とはいえ、ソラで書くの面倒なので ACL に移行するために「AtCoder LibraryのLazy Segtreeの使い方」を読んでみたものの、lazy 配列の使い方がソラ版と違っていて対応関係がよくわからなかったのでまとめる。

主な違いまとめ

  • ソラ版は lazy node に入れた値を自分の data node に適用するが、ACL 版だと子の data node に適用している
  • ソラ版は lazy node に配列が被覆している範囲分も含めて値が入っているが、ACL 版だと範囲分は含まれていない
      • 区間加算で範囲に $x$ を加算する場合を考える
      • ソラ版は範囲の長さが $n$ なら lazy node に $n \times x$ が入っているが、ACL 版だと $x$ が入っている
      • ACL 版の場合は長さの情報は data node にもたせて lazy node のデータを data node に適用するときに使う
  • ソラ版の eval 関数は ACL 版の mapping 関数と composition 関数を同時にやっているようなもの

ACL 版を使うときの S, op, e, F, mapping, composition はそれぞれ何を表しているのか

ABC 396

C - Buy Balls

https://atcoder.jp/contests/abc396/tasks/abc396_c

嘘解法で解いてしまった。配列の範囲外参照していたが AC してしまった。

黒・白ともに降順にソートしておく。 値が正の黒ボールをまず全て取る。その時の数を $N_b$ とする。 次に $N_b$ 個を超えないように正の値の白ボールを取る。その時の数を $N_w$ とする。

$N_w < N_b$ のときは、追加で白ボールをとっても価値は増えないのでこれ以上取る意味はない。

$N_w = N_b$ のときを考える。 このとき、白ボールを取得すると黒ボールの個数を超えてしまうため、黒ボールも同時に取得する必要がある。 よって、追加で白ボールを取ったほうが良い状況は、$B_{N_b+1} + W_{N_w+1} > 0$ が成り立つときである。 これをボールがなくなるか、条件を満たさなくなるまで繰り返す。

void solve() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, m;
    cin >> n >> m;

    vll b(n), w(m);
    rep(i, n) cin >> b[i];
    rep(i, m) cin >> w[i];

    sort(rall(b));
    sort(rall(w));

    ll ans = 0;
    int nb = 0;
    rep(i, n) {
        if (b[i] >= 0) {
            ans += b[i];
            nb++;
        }
    }

    int nw = 0;
    while (nw < m && nb > nw && w[nw] > 0) {  // コンテスト中のコードだと nw < m を入れ忘れた
        ans += w[nw];
        nw++;
    }
    while (nw < m && nb < n && w[nw] + b[nb] >= 0) {
        ans += w[nw] + b[nb];
        nw++;
        nb++;
    }
    cout << ans << endl;
}

解説の想定解法

ABC 340

E - Mancala 2

https://atcoder.jp/contests/abc340/tasks/abc340_e

区間加算、一点取得の問題。ac-library を使うと lazy segment tree が必要になってセットアップが大変だが自分でセグメントツリーを実装すると意外と簡単。

かつっぱ氏の木マスター養成講座がわかりやすい ref: https://youtu.be/g30mEb4jE2g?si=g-oLairJqPWzWHn6

struct SegmentTree {
    vll seg;
    int len;

    SegmentTree(int n) {
        rep(i, 30) {
            if (n <= (1 << i)) {
                len = 1 << i;
                break;
            }
        }
        seg.resize(len * 2, 0);
    }

    ll get(int p) {
        p += len;
        ll ans = seg[p];
        while (p / 2) {
            p /= 2;
            ans += seg[p];
        }

        return ans;
    }

    void add(int l, int r, ll x) {
        l += len, r += len;
        while (l < r) {
            if (l & 1) {
                seg[l] += x;
                l++;
            }
            l /= 2;
            if (r & 1) {
                seg[r - 1] += x;
                r--;
            }
            r /= 2;
        }
    }
};

void solve() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    ll n, m;
    cin >> n >> m;

    SegmentTree seg(n);
    rep(i, n) {
        ll a;
        cin >> a;
        seg.add(i, i + 1, a);
    }

    rep(i, m) {
        int b;
        cin >> b;
        ll x = seg.get(b);
        seg.add(b, b + 1, -x);

        b++;
        ll q = x / n;
        ll r = x % n;
        seg.add(0, n, q);
        if (b + r >= n) {
            seg.add(b, n, 1);
            seg.add(0, b + r - n, 1);
        } else {
            seg.add(b, b + r, 1);
        }
    }

    rep(i, n) {
        if (i != 0)
            cout << ' ';
        cout << seg.get(i);
    }
    cout << endl;
}

ABC 328

E - Modulo MST

$N = 8$ のときの完全グラフのときの辺の数は $\displaystyle \frac{N(N-1)}{2} = \frac{8 \times 7}{2} = 28$ でここから $N-1=7$ ほんの辺を選ぶ選び方は $_{28}\mathrm{C}_7 = 1,184,040$ 通りなので全探索が間に合う。 各頂点について自身を含めて他のどの頂点に辺を張るかということを考える場合でも $N^{N-1} = 8^7 = 2,097,152$ こっちの方法でも間に合う。

$N-1$ 本の辺を張る実装

void solve() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    ll n, m, k;
    cin >> n >> m >> k;

    vector<tuple<ll, ll, ll>> es;
    rep(i, m) {
        ll u, v, w;
        cin >> u >> v >> w;
        u--, v--;
        es.emplace_back(u, v, w);
    }

    ll ans = INF;
    auto dfs = [&](auto dfs, int depth, vint used) {
        if (used.size() == n - 1) {
            dsu uf(n);
            ll sum = 0;
            for (int i : used) {
                auto [u, v, w] = es[i];
                if (uf.same(u, v))
                    return;
                uf.merge(u, v);
                sum += w;
                sum %= k;
            }
            chmin(ans, sum);
            return;
        }
        if (depth == m) {
            return;
        }

        dfs(dfs, depth + 1, used);
        used.push_back(depth);
        dfs(dfs, depth + 1, used);
    };

    dfs(dfs, 0, {});
    cout << ans << endl;
}

辺が存在するかわからないがとりあえず考えうるパターンを確認する実装

ABC 386

D - Diagonal Separation

https://atcoder.jp/contests/abc386/tasks/abc386_d

解説 AC

黒マスの座標を $(x,y)$ とすると $(i,j) \in [1,x] \times [1,y]$ のマスに白マスが存在しないことが条件である。 与えられたマスを辞書順に sort して上記の条件と合致するか確認する。 頭から順に確認していき

  • 白マスならばそれまで出た白マスの中で最小の $y$ 座標を記録する
  • 黒マスならばそれまで出た白マスの中で最小の $y$ 座標より大きいか確認する

とすればよい。

void solve() {
    int n, m;
    cin >> n >> m;

    vector<tuple<int, int, char>> qs;
    rep(i, m) {
        int x, y;
        char c;
        cin >> x >> y >> c;
        qs.emplace_back(x, y, c);
    }
    sort(all(qs));

    int cnum = INF;
    for (auto [x, y, c] : qs) {
        if (c == 'B') {
            if (cnum <= y) {
                yesno(false);
                return;
            }
        } else {
            chmin(cnum, y);
        }
    }
    yesno(true);
}

E - Maximize XOR

https://atcoder.jp/contests/abc386/tasks/abc386_e

ABC 395

D - Pigeon Swap

https://atcoder.jp/contests/abc395/tasks/abc395_d

想定と違う方法で解けたがすごく複雑なことをしてしまった。

提出コードでは鳩 $i$ が配置されたときの index とその時の巣の番号と、箱を swap したときの履歴を管理して解いた。 種別 1 のクエリで鳩を移動させたあとに箱の swap があった場合は、swap の履歴をたどって最終的な巣の番号を求めた。

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

    // 配置されたときの index, その時の巣の番号
    vector<pair<int, int>> hato(N);
    // 入れ替えがあったときの index, 移動先の巣の番号
    vector<vector<pair<int, int>>> hako(N);
    rep(i, N) {
        hato[i] = {-1, i};
        hako[i].push_back({-1, i});
    }

    map<pair<int, int>, pair<int, int>> memo;

    rep(i, Q) {
        int t;
        cin >> t;
        if (t == 1) {
            int a, b;
            cin >> a >> b;
            a--, b--;

            hato[a] = {i, b};
        } else if (t == 2) {
            int a, b;
            cin >> a >> b;
            a--, b--;

            hako[a].push_back({i, b});
            hako[b].push_back({i, a});
        } else {
            int a;
            cin >> a;
            a--;

            auto [id, num] = hato[a];
            vector<pair<int, int>> olds;
            while (true) {
                auto it = upper_bound(all(hako[num]), make_pair(id, INF));
                if (it == hako[num].end())
                    break;
                olds.push_back(*it);
                if (memo.count(*it)) {
                    auto x = memo[*it];
                    id = x.first;
                    num = x.second;
                } else {
                    id = it->first;
                    num = it->second;
                }
            }
            if (olds.size())
                olds.pop_back();
            // 経路圧縮
            for (auto [a, b] : olds) {
                memo[{a, b}] = {id, num};
            }
            hato[a] = {id, num};
            cout << num + 1 << endl;
        }
    }
}

解説放送を見たあとの復習

ABC 365

E - Xor Sigma Problem

https://atcoder.jp/contests/abc365/tasks/abc365_e

$\sum_{i,j} f(i,j)$ 系の問題は $i \leq j$ に関する $f(i,j)$ の値を表に書いてみて規則性を見つけると解けることが多い。

\begin{align} f(i,j) &= A_i \oplus A_{i+1} \oplus \cdots \oplus A_j \nonumber \\ S_i &= \sum_{j=i}^N f(i,j) \nonumber \end{align} と定義する。

表: $f(i,j)$ の値

i\j123
1011000
21101
310

\begin{align} \sum_{i=1}^{N-1} \sum_{j=i+1}^N f(i,j) &= \sum_{i=1}^{N} \left\{ \left( \sum_{j=i}^{N} f(i,j) \right) - f(i,i) \right\} \nonumber \\ &= \sum_{i=1}^{N} \left\{ \left( \sum_{j=i}^{N} f(i,j) \right) - A_i \right\} \nonumber \\ &= \sum_{i=1}^{N} S_i - A_i \nonumber \end{align}

ABC 253

E - Distance Sequence

https://atcoder.jp/contests/abc253/tasks/abc253_e

$f(i,j)$ を $A_i = j$ となる場合の数とすると

\begin{align} f(1,j) &= 1 ~~~(\forall j \in [1,M]) \nonumber \\ f(i+1,j) &= \sum_{k=1}^{j-K} f(i,k) + \sum_{k=j+K}^{M} f(i,k) \nonumber \end{align}

となる。naive に実装すると以下のようになり、計算量が $O(NM^2)$ となるため問題の制約では TLE になる。

vector<vector<mint>> dp(N + 1, vector<mint>(M + 1, 0));
rep(i, M) dp[1][i + 1] = 1;
rep2(i, 2, N + 1) {
    rep2(k, 1, M + 1) {
        rep2(j, 1, M + 1) {
            if (abs(k - j) >= K)
                dp[i][k] += dp[i - 1][j];
        }
    }
}

mint ans = 0;
rep2(i, 1, M + 1) ans += dp[N][i];
cout << ans.val() << endl;

ここで dp[i-1] の累積和計算しておくことで一番内側の loop 内の処理を $O(1)$ で行うことができる。 $K = 0$ のときに dp[i-1][j] からの寄与をダブルカウントしないように注意する

ABC 371

E - I Hate Sigma Problems

https://atcoder.jp/contests/abc371/tasks/abc371_e

sample 2 の $f(i,j)$ の値を表にまとめると以下のようになる.

A_i \ A_j542232441
5123344445
412233334
21122334
2122334
312334
21223
4112
412
11

この表より各行の和の値は以下のように遷移することが推察される。

ABC 394

E - Palindromic Shortest Path

https://atcoder.jp/contests/abc394/tasks/abc394_e

解説 AC

短い回分から両端に同じ文字を追加していくことで回分の長さを増やしていく。

void solve() {
    int n;
    cin >> n;
    vector<string> delta(n);
    rep(i, n) cin >> delta[i];

    vvll dist(n, vll(n, -1));
    // 回分の最短距離が決まっている path を que に入れる
    queue<pair<int, int>> que;

    rep(i, n) {
        dist[i][i] = 0;
        que.push({i, i});
    }

    rep(i, n) rep(j, n) {
        if (i != j && delta[i][j] != '-') {
            dist[i][j] = 1;
            que.push({i, j});
        }
    }

    while (que.size()) {
        auto [i, j] = que.front();
        que.pop();

        rep(k, n) {
            rep(l, n) {
                if (delta[k][i] != '-' && delta[k][i] == delta[j][l] &&
                    dist[k][l] == -1) {
                    // i -> j への path の回分の長さはすでに決まっている
                    // delta[k][i] == delta[j][l] ならば、k -> i -> j -> l という path が回分になる
                    // que には距離が短い順に入っているので一番最初に見つかった k -> l の path を最短として良い
                    // k -> i -> j -> l なる path を考えるときに (i,j) の全ての組み合わせを調べる必要はないということ
                    dist[k][l] = dist[i][j] + 2;
                    que.push({k, l});
                }
            }
        }
    }

    for (auto v : dist)
        print(v);
}

ABC 338

D - Island Tour

https://atcoder.jp/contests/abc338/tasks/abc338_d

解説 AC

void solve() {
    ll n, m;
    cin >> n >> m;
    vll x(m);
    rep(i, m) {
        cin >> x[i];
        x[i]--;
    }

    vll cumsum(n * 2 + 1, 0);

    rep(i, m - 1) {
        ll mi = min(x[i], x[i + 1]);
        ll mx = max(x[i], x[i + 1]);

        ll dr = mx - mi;
        ll dl = n - dr;
        cumsum[mi] += dl;
        cumsum[mx] -= dl;
        cumsum[0] += dr;
        cumsum[mi] -= dr;
        cumsum[mx] += dr;
        cumsum[n] -= dr;
    }

    rep(i, n * 2) cumsum[i + 1] += cumsum[i];

    ll ans = INF;
    rep(i, n) chmin(ans, cumsum[i]);
    cout << ans << endl;
}

ABC 276

E - Round Trip

https://atcoder.jp/contests/abc276/tasks/abc276_e

start から BFS をする。

  • start から右移動から始めたところには 1
  • start から左移動から始めたところには 2
  • start から上移動から始めたところには 3
  • start から下移動から始めたところには 4

といった具合に番号を振っていく。(番号が異なれば良いので上下左右と番号の対応が上記と違っても問題ない) 異なる番号が隣り合うところがあれば異なる経路で同じ場所にたどり着けるということなので Yes、そのような場所がなければ No を出力する。

sample 1 の場合の番号の付け方の例は以下のよう

 3   3   3  1
-1   3  -1  1
 2   0   1  1
 2  -1  -1  1
void solve() {
    int H, W;
    cin >> H >> W;
    vector<string> grid(H);
    rep(i, H) cin >> grid[i];

    int si, sj;
    rep(i, H) rep(j, W) if (grid[i][j] == 'S') si = i, sj = j;
    vvint mark(H, vint(W, -1));

    queue<pair<int, int>> q;
    q.push({si, sj});
    mark[si][sj] = 0;

    vint di = {0, 1, 0, -1};
    vint dj = {1, 0, -1, 0};

    while (q.size()) {
        auto [i, j] = q.front();
        q.pop();

        int c = mark[i][j];
        int start = c == 0;

        rep(d, 4) {
            if (start)
                c = d + 1;
            int ni = i + di[d], nj = j + dj[d];
            if (clamp(ni, 0, H - 1) != ni || clamp(nj, 0, W - 1) != nj)
                continue;
            if (grid[ni][nj] == '#')
                continue;
            int m = mark[ni][nj];
            if (m > 0 && m != c) {
                yesno(true);
                return;
            }
            if (m == -1) {
                mark[ni][nj] = c;
                q.push({ni, nj});
            }
        }
    }
    yesno(false);
}

ABC 393

D - Swap to Gather

なんとなく中央の 1 に寄せると良さそうという雰囲気で実装して AC してしまった

void solve() {
    int N;
    string S;
    cin >> N >> S;

    vint ids;
    rep(i, N) {
        if (S[i] == '1')
            ids.push_back(i);
    }

    ll ans = 0;
    int mid = ids[ids.size() / 2];
    int zid = mid + 1;
    for (int i = mid + 1; i < N; i++) {
        while (S[zid] == '1' && zid < N)
            zid++;
        if (i <= zid)
            continue;
        if (S[i] == '1') {
            ans += i - zid;
            swap(S[i], S[zid]);
        }
    }

    zid = mid - 1;
    for (int i = mid - 1; i >= 0; i--) {
        while (S[zid] == '1' && zid >= 0)
            zid--;
        if (zid <= i)
            continue;
        if (S[i] == '1') {
            ans += zid - i;
            swap(S[i], S[zid]);
        }
    }
    cout << ans << endl;
    // cout << S << endl;
}

E - GCD of Subset

愚直に全ての $A_i$ についての約数を調べて出現回数をメモって、各 $i$ に対して $A_i$ の約数のうち出現回数が $K$ を超えている中で最大のものを出力とすると TLE ぎりぎりだがなんとか AC できた。 しかし、どうやら想定解ではなかったらしい。

ABC 351

D - Grid and Magnet

https://atcoder.jp/contests/abc351/tasks/abc351_d

3時間近くかかったが自力 AC

磁石が置かれていないマスを空マスと呼ぶことにする。 磁石と隣接していない空マス同士を union-find で連結する。 連結部分のサイズと連結部分の外縁にある磁石と隣接する空マスの数の合計のうち最大値を出力する。

int h, w;
vector<string> grid;
vvint memo;
vvint visited;
vint di = {0, 1, 0, -1};
vint dj = {1, 0, -1, 0};

pair<int, int> ind2sub(int x) {
    return {x / w, x % w};
}

int sub2ind(int i, int j) {
    return i * w + j;
}

int canMove(int i, int j) {
    int ok = 1;
    rep(d, 4) {
        int ni = i + di[d], nj = j + dj[d];
        if (clamp(ni, 0, h - 1) != ni || clamp(nj, 0, w - 1) != nj)
            continue;
        if (grid[ni][nj] == '#')
            ok = 0;
    }

    return ok;
}

void solve() {
    cin >> h >> w;
    grid = vector<string>(h);
    rep(i, h) cin >> grid[i];

    dsu uf(h * w);

    rep(i, h) rep(j, w) {
        if (grid[i][j] == '.' && canMove(i, j)) {
            for (auto [ii, jj] : vector<pair<int, int>>({{1, 0}, {0, 1}})) {
                int ni = i + ii, nj = j + jj;
                if (clamp(ni, 0, h - 1) != ni || clamp(nj, 0, w - 1) != nj)
                    continue;
                if (canMove(ni, nj)) {
                    uf.merge(sub2ind(i, j), sub2ind(ni, nj));
                }
            }
        }
    }

    int ans = 1;
    for (auto v : uf.groups()) {
        set<pair<int, int>> stop;
        {
            auto [i, j] = ind2sub(v[0]);
            if (grid[i][j] == '#')
                continue;
            else if (!canMove(i, j)) {
                continue;
            }
        }
        for (int id : v) {
            auto [i, j] = ind2sub(id);
            rep(d, 4) {
                int ni = i + di[d], nj = j + dj[d];
                if (clamp(ni, 0, h - 1) != ni || clamp(nj, 0, w - 1) != nj)
                    continue;
                if (grid[ni][nj] == '.' && !canMove(ni, nj))
                    stop.insert({ni, nj});
            }
        }
        chmax(ans, (int)v.size() + (int)stop.size());
    }

    cout << ans << endl;
}

ABC 363

D - Palindromic Number

https://atcoder.jp/contests/abc363/tasks/abc363_d

解説 AC

ll intpow(ll x, ll n) {
    long long ret = 1;
    while (n > 0) {
        if (n & 1)
            ret *= x;
        x *= x;
        n >>= 1;
    }
    return ret;
}

void solve() {
    ll N;
    cin >> N;

    if (N == 1) {
        cout << 0 << endl;
        return;
    }

    N--;
    for (ll d = 1;; d++) {
        if (N <= intpow(10, (d - 1) / 2) * 9) {
            ll b = intpow(10, (d - 1) / 2) + N - 1;
            string s = to_string(b);
            s.resize(s.size() * 2 - d % 2, ' ');
            rep(i, d / 2) {
                s[s.size() - i - 1] = s[i];
            }
            cout << s << endl;

            return;
        } else {
            N -= intpow(10, (d - 1) / 2) * 9;
        }
    }
}

ABC 349

D - Divide Interval

https://atcoder.jp/contests/abc349/tasks/abc349_d

証明せずに貪欲で AC した。

左端の数から始まる良い数列のうち最長のものを採用していく。 左端の数が $l = 2^i j$ とするとき、良い数列は次の通りである

長さ数列
$2^i$$S(2^i j, 2^i (j+1))$
$2^{i-1}$$S(2^{i-1} 2j, 2^{i-1} 2(j+1))$
$\vdots$
$1$$S(2^1 2^{i-1}j, 2^1 2^{i-1}(j+1))$
$0$$S(2^0 2^{i}j, 2^0 2^{i}(j+1))$

このうち $R$ を超えないもので最長のものを採用していく。

void solve() {
    ll l, r;
    cin >> l >> r;

    vector<pair<ll, ll>> ans;
    while (r - l >= 1) {
        ll cnt = 0;
        while ((l & (1LL << cnt)) == 0 && cnt <= 60)
            cnt++;

        while (l + (1LL << cnt) > r)
            cnt--;

        ans.push_back({l, l + (1LL << cnt)});
        l += 1LL << cnt;
    }

    cout << ans.size() << endl;
    for (auto [x, y] : ans) {
        cout << x << ' ' << y << endl;
    }
}

ABC 368

D - Minimum Steiner Tree

https://atcoder.jp/contests/abc368/tasks/abc368_d

$V_1$ から DFS して、子または自身が $V_i$ に含まれていればそのノードを残す。

vvint graph;
vint visited;
set<int> memo;
int ans = 0;

bool dfs(int now) {
    bool res = false;
    if (memo.count(now))
        res = true;

    for (int nx : graph[now]) {
        if (visited[nx])
            continue;
        visited[nx] = 1;

        bool x = dfs(nx);
        res = res || x;

        // short-circuit で以下のよう書くと dfs が実行されない
        // res = res || dfs(nx)
    }

    if (res)
        ans++;
    return res;
}

void solve() {
    int n, k;
    cin >> n >> k;
    graph = vvint(n);
    rep(i, n - 1) {
        int u, v;
        cin >> u >> v;
        u--, v--;
        graph[u].push_back(v);
        graph[v].push_back(u);
    }

    vint nodes(k);
    rep(i, k) {
        cin >> nodes[i];
        nodes[i]--;
    }

    for (int x : nodes)
        memo.insert(x);

    visited = vint(n, 0);
    visited[nodes[0]] = 1;

    dfs(nodes[0]);
    cout << ans << endl;
}

ABC 295

D - Three Days Ago

解説 AC

void solve() {
    string S;
    cin >> S;

    int n = S.size();
    vvll v(n + 1, vll(10, 0));
    rep(i, n) {
        v[i + 1][S[i] - '0']++;
    }

    rep(k, 10) {
        rep(i, n) {
            v[i + 1][k] += v[i][k];
            v[i + 1][k] %= 2;
        }
    }

    vll cnt(1 << 10, 0);
    rep2(i, 0, n + 1) {
        int x = 0;
        rep(k, 10) {
            x *= 2;
            if (v[i][k])
                x += 1;
        }
        cnt[x]++;
    }

    ll ans = 0;
    rep(i, 1 << 10) {
        ans += cnt[i] * (cnt[i] - 1) / 2;
    }
    cout << ans << endl;
}

ABC 336

D - Pyramid

https://atcoder.jp/contests/abc336/tasks/abc336_d

解説 AC.

void solve() {
    ll N;
    cin >> N;
    vll A(N);
    rep(i, N) cin >> A[i];

    A.push_back(0);
    A.insert(A.begin(), 0);

    N += 2;
    vll dl(N, 0), dr(N, 0);
    rep2(i, 1, N) {
        dl[i] = min(dl[i - 1] + 1, A[i]);
    }

    rep(i, N - 1) {
        int k = N - 1 - i;
        dr[k - 1] = min(dr[k] + 1, A[k - 1]);
    }

    ll ans = 0;
    rep(i, N) {
        chmax(ans, min(dr[i], dl[i]));
    }
    cout << ans << endl;
}

ABC 278

E - Grid Filling

https://atcoder.jp/contests/abc278/tasks/abc278_e

$\mathrm{ rows }(i, m)$ を 行番号が $[0,i)$ の範囲(0-index)での数字 $m$ の出現回数、$\mathrm{ cols }(j,m)$ を列番号が $[0,i)$ の範囲での数字 $m$ の出現回数とする。

$[i,i+h)$, $[j,j+w)$ の領域が塗りつぶされたときに、残りのマスに数字 $m$ の出現する場合以下の式の少なくとも1つが true となる。

\begin{align} &\mathrm{ cols }(j-1,m) > 0 \\ &\mathrm{ cols }(W,m) - \mathrm{ cols }(j+w-1,m)) > 0 \\ &\mathrm{ rows }(i-1,m) > 0 \\ &\mathrm{ rows }(H, m) - \mathrm{ rows }(i+h-1,m) > 0 \end{align}

  • eq (1): 塗りつぶしたマスより左側の領域に対応
  • eq (2): 塗りつぶしたマスより右側の領域に対応
  • eq (3): 塗りつぶしたマスより上側の領域に対応
  • eq (4): 塗りつぶしたマスより下側の領域に対応

出現する数字は $[1,300]$ の範疇なので愚直に全通り出現するか確認し、出現した数の種類数を出力する。

ARC 185

A - mod M Game 2

解説 AC だが、かなり解像度が低い。

$\displaystyle \sum_{k=1}^{N} k = \frac{1}{2}N(N+1)$ より両者のカードの総和は $N(N+1)$。 最後の Bob の出す手が $b$ のとき Alice が最後に出した手までの総和は $N(N+1) - b$。 $N(N+1) - b$ が $0$ にできたら Bob の勝ち、それ以外のときは Alice の勝ち。

\begin{align} N(N+1) - b &\equiv 0 \mod M \\ N(N+1) &\equiv b \mod M \\ \end{align}

$b \in [1,N]$ より、$1 \leq (N(N+1) \mod M) \leq N$ のときは Bob の勝ち。それ以外のときは Alice の勝ち

void solve() {
    ll T;
    cin >> T;
    rep(_, T) {
        ll N, M;
        cin >> N >> M;

        ll x = N * (N + 1) % M;
        string ans = "Alice";
        if (clamp(x, 1LL, N) == x) {
            ans = "Bob";
        }
        cout << ans << endl;
    }
}

ABC 296

D - M<=ab

https://atcoder.jp/contests/abc296/tasks/abc296_d

解説 AC。

条件より $X = ab \wedge X \geq M$ である。また $a,b$ は対称だから $a \leq b$ としても答えは変わらない。

\begin{align} M \leq X &= ab \Leftrightarrow \nonumber \\ \frac{M}{a} &\leq b \nonumber \\ \biggl\lceil \frac{M}{a} \biggr\rceil &\leq b \nonumber \\ \end{align}

$\lceil M/a \rceil$ は $b$ の下界だから $a \leq b$ と合わせて

\begin{align} &a \leq \biggl\lceil \frac{M}{a} \biggr\rceil \nonumber < \frac{M}{a} + 1 \nonumber \\ \Leftrightarrow ~ &a(a-1) < M \nonumber \end{align}

ABC 280

D - Factorial and Multiple

https://atcoder.jp/contests/abc280/tasks/abc280_d

解説 AC

ルジャンドルの定理と二分探索で解く

ref: 予備ノリ 一度聞いたら忘れない『ルジャンドルの定理』の授業

ll f(ll n, ll p) {
    ll ans = 0;
    ll b = p;
    while (n / b) {
        ans += n / b;
        b *= p;
    }

    return ans;
}

void solve() {
    ll K;
    cin >> K;

    vector<pair<ll, ll>> div;
    {
        ll x = K;
        for (ll i = 2; i * i <= x; i++) {
            if (x % i != 0)
                continue;

            ll cnt = 0;
            while (x % i == 0) {
                x /= i;
                cnt++;
            }
            div.emplace_back(i, cnt);
        }
        if (x != 1)
            div.emplace_back(x, 1);
    }

    ll ac = K, wa = 1;
    while (abs(ac - wa) > 1) {
        ll wj = (ac + wa) / 2;
        ll ok = 1;
        for (auto [p, cnt] : div) {
            if (f(wj, p) < cnt)
                ok = 0;
        }
        if (ok)
            ac = wj;
        else
            wa = wj;
    }

    cout << ac << endl;
}

ABC 372

D - Buildings

解説 AC.

void solve() {
    ll n;
    cin >> n;
    vll h(n);
    rep(i, n) cin >> h[i];

    vll ans(n);
    vll st;
    for (int i = n - 1; i >= 0; i--) {
        ans[i] = st.size();
        while (st.size() != 0 && st.back() < h[i]) {
            st.pop_back();
        }
        st.push_back(h[i]);
    }
    print(ans);
}

E - K-th Largest Connected Components

https://atcoder.jp/contests/abc372/tasks/abc372_e

解説 AC

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

    dsu uf(N);
    vvint nodes(N);
    rep(i, N) {
        nodes[i].push_back(i);
    }

    rep(i, Q) {
        int q, a, b;
        cin >> q >> a >> b;
        if (q == 1) {
            int u = a - 1, v = b - 1;
            if (uf.same(u, v))
                continue;
            int ou = uf.leader(u);
            int ov = uf.leader(v);
            int newl = uf.merge(u, v);
            vint tmp = nodes[ou];
            for (auto x : nodes[ov])
                tmp.push_back(x);
            sort(rall(tmp));
            vint newv;
            for (int i = 0; i < 10 && i < tmp.size(); i++) {
                newv.push_back(tmp[i]);
            }
            nodes[newl] = newv;
        } else {
            int v = a - 1, k = b;
            int l = uf.leader(v);
            if (nodes[l].size() >= k) {
                cout << nodes[l][k - 1] + 1 << endl;
            } else {
                cout << -1 << endl;
            }
        }
    }
}

ARC 114

B - Special Subsets

条件より $i$ から $f(i)$ に辺を張ったグラフを作り、このうちサイクルを成しているサブグラフの頂点集合が $T$ になる。 各サブグラフは独立しているからサイクルを成しているサブグラフの個数を $n$ とすると $2^n - 1$ が答え。

void solve() {
    ll N;
    cin >> N;
    vll f(N);
    rep(i, N) {
        cin >> f[i];
        f[i]--;
    }

    scc_graph graph(N);
    rep(i, N) {
        graph.add_edge(i, f[i]);
    }

    vvint scc = graph.scc();
    ll cnt = 0;
    for (auto& x : scc) {
        if (x.size() > 1) {
            cnt++;
        } else {
            // 大きさ1のときは自己ループしていればカウント
            if (x[0] == f[x[0]])
                cnt++;
        }
    }

    mint ans = ((mint)2).pow(cnt) - 1;
    cout << ans.val() << endl;
}

ABC 218

E - Destruction

https://atcoder.jp/contests/abc218/tasks/abc218_e

$C_i$ が小さい順にクラスカル法で最小全域木を作る。 使わない辺のうち、コストが負のものは捨てて、コストが正のものを答えとして計上していく。

計上しない負のコストの辺はグラフから取り除かずそのままにしている辺に相当する。

void solve() {
    ll N, M;
    cin >> N >> M;

    vector<Edge> es(M);
    rep(i, M) {
        ll a, b, c;
        cin >> a >> b >> c;
        a--, b--;
        es[i] = {a, b, c};
    }

    priority_queue<Edge, vector<Edge>, greater<Edge>> pq;
    for (auto e : es)
        pq.push(e);
    dsu uf(N);
    vll dust;
    while (pq.size()) {
        auto [u, v, c] = pq.top();
        pq.pop();

        if (uf.same(u, v)) {
            if (c > 0)
                dust.push_back({c});
        } else {
            uf.merge(u, v);
        }
    }

    ll ans = 0;
    for (ll x : dust) {
        if (x > 0)
            ans += x;
    }
    cout << ans << endl;
}

ABC 391

D - Gravity

https://atcoder.jp/contests/abc391/tasks/abc391_d

嘘解法で通してしまった。$x$ が同じ時ブロックではあとからのほうが $y$ が大きいとしてしまったがそんな制約はなかった。 $y$ に sort 入れなかったときにバグる入力例

4 2
1 2
1 1
2 1
2 2
2
1 2
1 3

解法

列 $x$ のあるブロックのうち下から $j$ 番目のブロックの $y$ 座標を $y_{x,j}$ とする。

$k$ 回目の行が消える操作は $t_k = \max \\{ y_{x,k} | x \in [1,W], x \in \mathbb{Z} \\}$ の時刻に実行される。 このような $y_{x,k}$ が存在しない場合は $t_k = \infty$ とする。

ブロック $i$ が 列 $X_i$ の下から k 番目のブロックであるとき、消えるのは時刻 $t_k$ のときである。 これらの情報からクエリの時刻においてブロック $i$ が存在しているかどうかを判定する。

ABC 304

D - A Piece of Cake

ピースの数は多いので愚直に全探索はできない。 イチゴが入るピースの数は少ないので各イチゴがどのピースに入るかという基準で考える。 イチゴが入るピースの右下の座標をそのピースの代表としてそのピースに入るイチゴの数を数えばよい。イチゴがどのピースに属するかはそのイチゴのすぐ右と下にある分割線を二分探索で探せば良い。

void solve() {
    ll W, H, N;
    cin >> W >> H >> N;

    vll p(N), q(N);
    rep(i, N) {
        cin >> p[i] >> q[i];
    }

    ll na;
    cin >> na;
    vll a(na + 1);
    rep(i, na) {
        cin >> a[i];
    }
    a.back() = W;

    ll nb;
    cin >> nb;
    vll b(nb + 1);
    rep(i, nb) {
        cin >> b[i];
    }
    b.back() = H;

    // イチゴが入るピースの右下の座標
    map<pair<ll, ll>, ll> cnt;
    rep(i, N) {
        ll rh, rw;
        rh = *lower_bound(all(a), p[i]);
        rw = *lower_bound(all(b), q[i]);

        cnt[{rh, rw}]++;
    }

    // ピースの数が最大数より少なかったらイチゴが 0 個のピースを sentinel
    // として一つ追加
    if ((ll)cnt.size() < (na + 1) * (nb + 1))
        cnt[{-1, -1}] = 0;

    ll maxv = 0, minv = INF;
    for (auto it = cnt.begin(); it != cnt.end(); it++) {
        chmin(minv, it->second);
        chmax(maxv, it->second);
    }
    cout << minv << ' ' << maxv << endl;
}

ABC 347

D - Popcount and XOR

考察

自力で解けたが1日考えた。

$c = \mathrm{popcount}(C)$ とする。$a

$X \oplus Y = C$ のことは一旦忘れ $\mathrm{popcount}(X) = a, \mathrm{popcount}(Y) = b$ だけを考える。 $X$, $Y$ で極力同じ場所の bit を立てた場合 $\mathrm{popcount}(X \oplus Y) = |a-b|$。 $X$, $Y$ で同じ場所に bit を建てなかった場合 $\mathrm{popcount}(X \oplus Y) = a+b$ であるから $X \oplus Y = C$ になる必要条件は

$$ |a-b| \leq c \leq a+b $$

である。また同じ場所に立っていた bit を別の場所で立てた場合 popcount の値は2ずつ変わるので $a+b$ と $c$ の parity は同じにならなければならない。

たとえば、下記のように $X$ の 2 bit 目の bit を下げて 4 bit 目を上げると xor を取ると popcount の値が2増える。逆に $X,Y$ のどちらかだけに立っていた bit の位置を1つの場所に移動させると popcount の値が 2 減る。 \begin{align} X = 00111 \Rightarrow X^\prime &= 10011 \nonumber \\ Y &= 01111 \nonumber \end{align}

ABC 318

D - General Weighted Max Matching

$N$ が奇数のとき、頂点を一つ追加してその頂点との辺の重みは $0$ としても結果は変わらない。よって以後 $N$ は偶数として扱う。

2頂点ずつ $N/2$ 組に分ける組み合わせの数は

$$ \frac{\binom{n}{2} \times \binom{n-2}{2} \times \cdots \times \binom{2}{2}}{2^{N/2} (N/2)!} $$

である。$N=16$ のとき $\displaystyle \frac{16!}{2^8 8!} = 2,027,025$ なので全探索しても十分間に合う。

実装をどうするかを考える。まずは $N = 4$ という具体例について考えてみる。 このときの分け方は次の3通りが考えられる。

  • $(1, 2), (3, 4)$
  • $(1, 3), (2, 4)$
  • $(1, 4), (2, 3)$

これより

  • まだ選んでいないものから一番 index が若いものを選ぶ
  • 相方はそれ以降の index からまだ選んでいないものを選ぶ
    • 該当するものを全て試す
  • 次の組についても同様にして選んで行く
  • N/2 組みできた時点で終了する

という再帰関数を書けば全探索ができる。

ll N, m;
vvll D(16, vll(16, 0));
ll ans = 0;

void dfs(int state, int depth, ll sum) {
    if (depth == (N + 1) / 2) {
        chmax(ans, sum);
    }

    int l = 0;
    rep(i, m) if (!(state & (1 << i))) {
        l = i;
        state ^= (1 << i);
        break;
    }

    rep2(i, l + 1, m) {
        if (!(state & (1 << i))) {
            state ^= (1 << i);
            dfs(state, depth + 1, sum + D[l][i]);
            state ^= (1 << i);
        }
    }
}

void solve() {
    cin >> N;

    m = N + N % 2;
    rep(i, N) {
        rep2(j, i + 1, N) {
            cin >> D[i][j];
        }
    }

    dfs(0, 0, 0);

    cout << ans << endl;
}

E - Sandwiches

https://atcoder.jp/contests/abc318/tasks/abc318_e

ABC 290

D - Marking

方針がなかなか決まらなくて諦めかけたがなんとか自力 AC。

$x_k$ を $k$ 番目に印をつけられるマスの番号とする。 すでに印がつけられていたとしても i の操作で移動したとすると

\begin{align} x_1 &= 0 \nonumber \\ x_2 &= x_1 + D \mod N \nonumber \\ x_3 &= x_2 + D \mod N \nonumber \\ \vdots \nonumber \\ x_k &= x_{k-1} + D \mod N \nonumber \\ \end{align}

これより、$x_k = x_1 + (k-1)D \mod N$ ということがわかる。

操作 ii が初めて起こるのは $\alpha D \equiv 0 \mod N$ となる最小の正の整数 $\alpha$ のときである。すなわち、$\alpha D$ が $N$ の倍数となるような最小の正の整数 $\alpha$ を見つけることができれば周期がわかる。よって操作 ii が何回行われたのかもわかる。

ABC 352

E - Clique Connect

https://atcoder.jp/contests/abc352/tasks/abc352_e

最小全域木を構成するアルゴリズムであるクラスカル法を思いつけると解ける。 クラスカル法は辺のコストが小さい順に閉路ができないように辺を採用していくアルゴリズムである。

$C_i$ が小さい部分集合から辺を構成し順にクラスカル法で辺を張っていく。このとき、部分集合内の頂点同士が連結になりさえすれば良いので $\\{(A_1, A_j)| j \in [2, K_i], j \in \mathbb{Z} \\}$ という辺だけを考えるだけで良い。

最終的に連結グラフになっていたらそのコストを出力し、なってなかったら $-1$ を出力する。

void solve() {
    ll N, M;
    cin >> N >> M;

    vvll A(M);
    vll C(M);
    rep(i, M) {
        ll k, c;
        cin >> k >> c;
        C[i] = c;
        rep(j, k) {
            ll a;
            cin >> a;
            a--;
            A[i].push_back(a);
        }
    }

    vector<pair<ll, int>> cs;
    rep(i, M) {
        cs.push_back({C[i], i});
    }

    sort(all(cs));

    dsu uf(N);
    ll ans = 0;
    for (auto [c, i] : cs) {
        int k = A[i].size();
        rep2(j, 1, k) {
            int a = A[i][0], b = A[i][j];
            if (uf.same(a, b))
                continue;
            uf.merge(a, b);
            ans += c;
        }
    }

    if (uf.groups().size() != 1)
        ans = -1;
    cout << ans << endl;
}

ARC 154

B - New Place

https://atcoder.jp/contests/arc154/tasks/arc154_b

$S$ の $i$ 文字目、$T$ の $j$ 文字目をそれぞれ $s_i$, $t_j$ と書くことにする。 削除した文字は一旦プールに入れておくということにする。

後ろから文字を比較していって

  • 一致した場合
    • 次の文字を比較する
  • 一致しなかった場合
    • プールにある場合
      • それを使って次の文字を比較する
    • プールにない場合
      • その文字を調達してこないといけないので調達したい文字を含めて $S$ の先頭の文字を削除する
        • 調達できなかった場合はその時点で終了
      • 削除した回数をカウントしつつ、削除した文字はプールに入れておく。ただし調達対象の文字はプールに入れない
    • 次の文字を比較する
void solve() {
    int N;
    cin >> N;
    string S, T;
    cin >> S >> T;

    ll ans = 0;
    map<char, int> store;
    int tl = N - 1, sh = 0, sl = N - 1;
    while (tl >= 0 && sl >= 0 && sh <= sl) {
        if (S[sl] == T[tl]) {
            tl--, sl--;
        } else if (store[T[tl]]) {
            store[T[tl]]--;
            tl--;
        } else {
            while (sh <= sl) {
                ans++;
                if (S[sh] == T[tl]) {
                    sh++, tl--;
                    break;
                } else {
                    store[S[sh]]++;
                    sh++;
                }
            }
            if (sh > sl) {
                cout << -1 << endl;
                return;
            }
        }
    }

    cout << ans << endl;
}

ABC 281

D - Max Multiple

https://atcoder.jp/contests/abc281/tasks/abc281_d

$dp(i,j,r)$ を $i$ 番目までの数字から $j$ 個使ってできた総和を D で割った余りが $r$ のときの総和の最大値とする

void solve() {
    ll N, K, D;
    cin >> N >> K >> D;
    vll A(N + 1);
    rep(i, N) cin >> A[i + 1];

    // dp[i][j][r]: i 番目までの数字から j 個を選んだときの和を D
    // で割ったあまりが r のときの和の最大値
    vector<vvll> dp(N + 1, vvll(K + 1, vll(D, -INF)));
    dp[0][0][0] = 0;
    rep2(i, 1, N + 1) {
        rep(j, K + 1) {
            // i 番目の数字を使わない
            dp[i][j] = dp[i - 1][j];

            // i 番目の数字を使う
            if (j > 0) {
                rep(r, D) {
                    if (dp[i - 1][j - 1][r] < 0)
                        continue;
                    ll x = dp[i - 1][j - 1][r] + A[i];
                    chmax(dp[i][j][x % D], x);
                }
            }
        }
    }

    ll ans = dp[N][K][0];
    if (ans < 0)
        ans = -1;
    cout << ans << endl;
}

ABC 331

E - Set Meal

https://atcoder.jp/contests/abc331/tasks/abc331_e

解説 AC

2025/2/23 retry

副菜を降順にソートしておく。主菜を固定したときに選べる副菜のうち一番高いものを採用する。全ての主菜に対して同じことをしてそのうちの最大を出力。 選ぶ必要のない副菜の検索を飛ばすことで $N+L$ 回程度の比較しかされないので十分早い。

void solve() {
    ll n, m, l;
    cin >> n >> m >> l;
    vll a(n), b(m), c(l), d(l);
    rep(i, n) cin >> a[i];
    rep(i, m) cin >> b[i];
    rep(i, l) {
        cin >> c[i] >> d[i];
        c[i]--, d[i]--;
    }

    vector<pair<ll, int>> B;
    rep(i, m) B.push_back({b[i], i});

    sort(rall(B));

    set<pair<int, int>> ban;
    rep(i, l) ban.insert({c[i], d[i]});

    ll ans = 0;
    rep(i, n) {
        rep(j, m) {
            if (!ban.count({i, B[j].second})) {
                chmax(ans, a[i] + B[j].first);
                break;
            }
        }
    }
    cout << ans << endl;
}

ABC 382

D - Keep Distance

https://atcoder.jp/contests/abc382/tasks/abc382_d

$1 \leq x \leq y \leq z \leq 6$ を満たす $(x,y,z)$ の組み合わせを求めるのと同じ要領で解ける。

\begin{align} x^\prime &= x \nonumber \\ y^\prime &= y+1 \nonumber \\ z^\prime &= z+2 \nonumber \end{align}

とすると $1 \leq x^\prime < y^\prime < z^\prime \leq 8$ となる。 $(x^\prime, y^\prime, z^\prime)$ が決まると $(x,y,z)$ も一通りに決まるから $(x^\prime, y^\prime, z^\prime)$ の組み合わせを調べれば良い。 やることとしては $1 \sim 8$ までの数字から重複なく3つ選び小さい方から順に $(x^\prime, y^\prime, z^\prime)$ とすれば良い。

実際の問題について考える

\begin{align} &A_1 + 10 \leq A_2 \Rightarrow A_1 \leq A_2 - 10 < A_2 - 9 \nonumber \\ &A_2 + 10 \leq A_3 \Rightarrow A_2 - 9 \leq A_3 - 19 \Rightarrow A_2 - 9 < A_3 - 18 \nonumber \\ &\vdots \nonumber \end{align}

ABC 373

D - Hidden Weights

$u_i \rightarrow v_i$ への辺の重みは $w_i$, $v_i \rightarrow u_i$ への辺を $-w_i$ で張り適当なところを $0$ でスタートして新たに訪問したノードに重みに応じて値をおいていく。 負の重みの辺を追加するのがミソ

struct Edge {
    ll to, weight;
};

void dfs(vector<vector<Edge>>& graph, vll& ans, int now) {
    for (auto [to, w] : graph[now]) {
        if (ans[to] != INF)
            continue;
        ans[to] = ans[now] + w;
        dfs(graph, ans, to);
    }
}

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

    vll u(M), v(M), w(M);
    rep(i, M) {
        cin >> u[i] >> v[i] >> w[i];
        u[i]--, v[i]--;
    }

    vector<vector<Edge>> graph(N);

    rep(i, M) {
        graph[u[i]].push_back({v[i], w[i]});
        graph[v[i]].push_back({u[i], -w[i]});
    }

    vll ans(N, INF);
    rep(i, N) {
        if (ans[i] == INF) {
            ans[i] = 0;
            dfs(graph, ans, i);
        }
    }
    print(ans);
}

ARC 191

A - Replace Digits

https://atcoder.jp/contests/arc191/tasks/arc191_a

コンテスト中考えていたこと

$T$ を降順に sort して $S$ が大きくなる限りは $T$ の文字に置き換えていく。改変後の $S$ を $S^\prime$ とする。 余った文字はすでに置き換えた文字の元々あった場所より、余った文字の元々の index が小さければその文字を捨てる(捨てる文字を先に差し込んでからあとから置き換え対象の文字で上書きできるので捨てても問題ない)。それもできなかったら余った文字の中で元々の index が一番後ろとなるものを $S^\prime$ の最後の文字に置き換えればいいと思った。

提出して WA になった。

https://atcoder.jp/contests/arc191/submissions/62131491

このケースだと $S = 912, T = 111$ というケースにおいて $S = 911$ となってしまうが $S$ の 1 のところに $T$ の 1 で置き換えることで最終的に $S = 912$ のままにできる。

よって index の考慮に加えてすでに $S$ の中にある文字も捨てて良い。これで AC した

void solve() {
    int N, M;
    cin >> N >> M;
    string S, T;
    cin >> S >> T;

    vector<pair<char, int>> t;
    rep(i, M) t.push_back({T[i], i});
    sort(rall(t));

    set<int> used;
    int i = 0, j = 0;
    while (i < N && j < M) {
        if (S[i] < t[j].first) {
            S[i] = t[j].first;
            used.insert(t[j].second);
            i++;
            j++;
        } else {
            i++;
        }
    }
    if (j < M) {
        set<char> freq;
        for (char c : S)
            freq.insert(c);
        rep2(k, j, M) {
            if (freq.count(t[k].first)) {
                used.insert(t[k].second);
                continue;
            }
            auto it = used.lower_bound(t[k].second);
            if (it != used.end()) {
                continue;
            }

            S[N - 1] = t[k].first;
            used.insert(t[k].second);
        }
    }
    cout << S << endl;
}

https://atcoder.jp/contests/arc191/submissions/62132625

Educational DP Contest

L - Deque

https://atcoder.jp/contests/dp/tasks/dp_l

結局のところ両者ともに自分の得点が最大になるように行動すればよい

まずは実験

  • 要素が1つのとき

$X = a_1, Y = 0$

  • 要素が2つのとき

$X = \max\\{ {a_1, a_2} \\}, Y = \min \\{ a_1, a_2 \\}$

  • 要素が3つのとき

$X = \max \\{a_1 + (a_2, a_3 \text{の範囲で後手のときの得点}), (a_1, a_2 \text{の範囲で後手のときの得点}) + a_3 \\}$

解法

$\mathrm{dp}(i,j)$ を $[i,j]$ の範囲での先手の得点の最大値とすると

$$ X = \mathrm{dp}(i,j) \\\\ Y = \left( \sum_{k=i}^{j} a_k \right) - \mathrm{dp}(i,j) $$

となる。

$\mathrm{dp}(i,j)$ の値は先頭の要素をとった場合と末尾の要素をとった場合の得点を比較して最大となる方を採用すれば良い。 先頭の要素をとったときの得点は先頭の要素の値と、$(i+1,j)$ の範囲で手番が相手だったときの自分が得られる得点の和が先手の得点となるから $a_i + \sum_{k=i+1}^{j} a_k - \mathrm{dp}(i+1,j)$ となる。 同様にして末尾の要素をとったときの得点は $\sum_{k=i}^{j-1} a_k - \mathrm{dp}(i,j-1) + a_j$。

ABC 390

B - Geometric Sequence

https://atcoder.jp/contests/abc390/tasks/abc390_b

等比数列の問題

解き方が思いつかなくてテンパった。

$\lfloor \frac{A_{i+1}}{A_i} \rfloor$ を $i = 1\sim N-1$ ですべて一致するか見ようとしたが項比が非整数のときに対応できなくてサンプルでまず落ちる。項比を double で扱ったとしても 999999998 999999999 1000000000 のような数列のときに誤差のせいで $\frac{999999998}{999999999} == \frac{999999999}{1000000000}$ が true を返してしまう。 有理数だったらちゃんと比較できるのだけれどもということを思いついてようやく整数演算で確かめる方法を思いついた。

$\displaystyle \frac{A_{i+1}}{A_{i}} = \frac{A_{i+2}}{A_{i+1}} \rightarrow A_{i+1}^{2} = A_{i} A_{i+2}$ が成り立っているかを確かめればいい

void solve() {
    ll N;
    cin >> N;

    vector<double> v(N);
    rep(i, N) {
        cin >> v[i];
    }

    if (N <= 2) {
        yesno(true);
        return;
    }

    rep(i, N - 2) {
        ll a = v[i], b = v[i + 1], c = v[i + 2];
        if (b * b != a * c) {
            yesno(false);
            return;
        }
    }
    yesno(true);
}

C - Paint to make a rectangle

黒マスがある行番号の最小値、最大値をそれぞれ $i_{\mathrm{ min }}, i_{\mathrm{ max }}$ とし, 黒マスのある列番号の最小値、最大値をそれぞれ $j_{\mathrm{ min }}, j_{\mathrm{ max }}$ とする。

ABC 366

https://atcoder.jp/contests/abc366

D - Cuboid Sum Query

3次元累積和の問題

https://atcoder.jp/contests/abc366/tasks/abc366_d

$f_i(a,b) = \sum_{i=a}^{b}$ とする。

\begin{align} \sum_{i=a}^{x} &\sum_{i=b}^{y} \sum_{i=c}^{z} \nonumber = f_i(a,x)f_j(b,y)f_k(c,z) \nonumber \\ &= \left\{ f_i(1,x) - f_i(1,a-1) \right\} \nonumber \\ &~~~ \times \left\{ f_j(1,y) - f_j(1,b-1) \right\} \nonumber \\ &~~~ \times \left\{ f_k(1,z) - f_k(1,c-1) \right\} \nonumber \\ &= f_i(1,x)f_j(1,y)f_k(1,z) - f_i(1,x)f_j(1,y)f_k(1,c-1) \nonumber \\ &~~~ - f_i(1,x)f_j(1,b-1)f_k(1,z) + f_i(1,x)f_j(1,b-1)f_k(1,c-1) \nonumber \\ &~~~ - f_i(1,a-1)f_j(1,y)f_k(1,z) + f_i(1,a-1)f_j(1,y)f_k(1,c-1) \nonumber \\ &~~~ + f_i(1,a-1)f_j(1,b-1)f_k(1,z) - f_i(1,a-1)f_j(1,b-1)f_k(1,c-1) \nonumber \\ \end{align}

ABC 389

C

https://atcoder.jp/contests/abc389/tasks/abc389_c

deque にヘビの頭の座標と長さを入れる。 タイプ2 で全体の要素の座標を更新すると要素数分更新する必要があるので非効率。出ていったヘビの長さを記録しておき、あとからその値を引く

  • タイプ1 のときは最後尾のヘビの座標が $x$, 長さが $L$ のときは $(x+L, l)$ を deque に追加する
  • タイプ2 のとき、先頭のヘビの座標と長さが $(x,l)$ のとき $sub = x+l$ と記録しつつ、先頭のヘビを deque から抜く。deque にいるヘビの数が 0 のときは $sub = 0$ にする
  • タイプ3 のとき $deque[k].first - sub$ を出力する
void solve() {
    ll Q;
    cin >> Q;

    ll sub = 0;

    // head pos, length
    deque<pair<ll, ll>> que;
    rep(i, Q) {
        ll x;
        cin >> x;
        if (x == 1) {
            ll l;
            cin >> l;
            if (que.size()) {
                auto [prevpos, prevlen] = que.back();
                que.push_back({prevpos + prevlen, l});
            } else {
                que.push_back({0, l});
            }
        } else if (x == 2) {
            auto [pos, len] = que.front();
            que.pop_front();
            sub = pos + len;
            if (que.empty())
                sub = 0;
        } else {  // x==3
            ll k;
            cin >> k;
            k--;
            cout << que[k].first - sub << endl;
        }
    }
}

D

https://atcoder.jp/contests/abc389/tasks/abc389_d

ABC 388

D - Coming of Age Celebration

https://atcoder.jp/contests/abc388/tasks/abc388_d

0-index で考える。$i$ 番目の宇宙人が成人したときにもらう石の数を $C_i$ とし、 $i$ 番目の宇宙人がこれから成人する宇宙人に渡す石の数を $D_i$ とすると $D_i = \min(A_i + C_i, N - i - 1)$ である。 $i$ 番目の宇宙人が最終的に持っている石の数は $A_i + C_i - D_i$ になる。

$C_i$ の管理方法を工夫することでこの問題を解く。 $i$ 番目の宇宙人が何番目の宇宙人にまで石を配るかを考えると $i+D_i$ 番目の宇宙人にまで石を配ることになる。この数を multiset に保存していく。 $j > i$ 番目の人がもらう石の数は $ j \leq x, x \in multiset$ を満たす $x$ の個数と一致する。$j$ 番目の人のときに $x < j$ となる要素は意味がないから予め消しておくと multiset の大きさが $j$ 番目の宇宙人がもらう石の数に等しくなる。これにより $C_j$ が求まる。$C_j$ が求まれば $D_j$ も決まるので、$D_j > 0$ ならば $j + D_j$ を multiset に入れる。これにより答えが求まる。

ABC 254

D

https://atcoder.jp/contests/abc254/tasks/abc254_d

命題1

$i \times j \text{ が平方数} \leftrightarrow \frac{i\times j}{f(i) \times f(j)} \text{ が平方数}$

($\rightarrow$)

$i \times j$ が平方数であるから $i \times j$ は以下のようにかける。

$$ i \times j = \prod_{k} p^{2r_k}_{k} $$

ここで、$p_k$ は素数であり、$r_k$ は非負整数。 $f(i)$, $f(j)$ も平方数であるから

$$ f(i) = \prod_{k} p^{2n_{k}}_{k} $$$$ f(j) = \prod_{k} p^{2m_{k}}_{k} $$

とかける。ここで $n_k$, $m_k$ は非負整数。

よって

$$ \frac{i\times j}{f(i) \times f(j)} = \prod_{k} p^{2(r_k - n_k - m_k)}_k $$

であるから、因数が偶数となるため平方数。

$(\leftarrow)$

$\frac{i\times j}{f(i) \times f(j)}$ が平方数であるから、$\frac{i\times j}{f(i) \times f(j)} = x^2$ となるある正の整数 $x$ が存在する。 同様にして $f(i) = y^2, f(j) = z^2$ とかける。ここで $y, z$ は正の整数.

ABC 223

C - Doukasen

解説の解説

ぶつかる地点を $x$ とする。左端から $x$ まで行く時間を $t_l$, 右端から $x$ まで行く時間を $t_r$ とすると当たり前だが $t_l = t_r$ が成り立つ。 一方、 左端からだけ燃やしたときにすべてが燃えるのにかかる時間は $T = \sum^{N}_{i=1}\frac{A_i}{B_i}$ である。 $t = t_l$ とすると $2t = T$ であるから

$t = \frac{1}{2} \sum_{i=1}^{N}\frac{A_i}{B_i}$

である。

ABC 247

A - Move Right

問題

$0 s_0 s_1 s_2$ を出力すればよい

void solve() {
    string s;
    cin >> s;
    cout << 0 << s.substr(0, 3) << endl;
}

提出コード

B - Unique Nicknames

人 $i$ について, 次の条件 $P$, $Q$ のうちいずれかが成り立っていればその人にあだ名をつけることは可能である.

  • P: $s_i$ をニックネームとして選んだとき, 全ての $i \neq j$ に対して $s_i \neq s_j \wedge s_i \neq t_i$
  • Q: $t_i$ をニックネームとして選んだとき, 全ての $i \neq j$ に対して $s_i \neq s_j \wedge s_i \neq t_i$

これが全ての人に対して調べる.

void solve() {
    int n;
    cin >> n;
    vector<string> S(n), T(n);
    rep(i, n) cin >> S[i] >> T[i];

    rep(i, n) {
        int ok = 1;
        rep(j, n) {
            if (i == j)
                continue;
            if (S[i] == S[j] || S[i] == T[j])
                ok = 0;
        }
        if (ok)
            continue;

        ok = 1;
        rep(j, n) {
            if (i == j)
                continue;
            if (T[i] == S[j] || T[i] == T[j])
                ok = 0;
        }
        if (!ok) {
            cout << "No" << endl;
            return;
        }
    }

    cout << "Yes" << endl;
}

提出コード

ABC 246

A - Four Points

問題

辺が $x$ 軸に平行または $y$ 軸に平行であるから, 長方形を構成する4点に関して $x$ 座標が同じ点が2つずつ, $y$ 座標が同じ点が2つずつある.

この問題では3点が与えられるので残る1点の座標は

  • $x$ 座標: 3点の $x$ 座標の XOR
  • $y$ 座標: 3点の $y$ 座標の XOR

となる.

提出コード

B - Get Closer

問題

$(A, B)$ 方向への単位ベクトルを求めれば良いので $\displaystyle \left(\frac{A}{\sqrt{A^2 + B^2}}, \frac{B}{\sqrt{A^2 + B^2}} \right)$ が答え.

提出コード

C - Coupon

問題

まず各商品に対して $\frac{A_i}{X}$ 枚のクーポンを使う. クーポンが足りなくなったらそのまま.

クーポンが余った場合, 各商品に払わないといけない金額は $X$ 未満になっているので, 残高が高い順にクーポンを適用させていけばよい.

提出コード

D - 2-variable Function

問題

$f(a,b) = a^3 + a^2b + ab^2 + b^3$ とする. $f(a,b) \geq N$ を満たす最小の $f(a, b)$ を求める.

ABC 243

##
[問題]()
[提出コード]()

A - Shampoo

問題

単純にシミュレーションをするか, $V <= A+B+C$ のときは3人が1順するごとに $A+B+C$ 減るので $V$ を V % (A + B + C) スタートにしても問題ない. この状況で誰が使うときに不足するかを調べればよい.

void solve() {
    int v, a, b, c;
    cin >> v >> a >> b >> c;

    v %= a + b + c;
    if (v - a < 0) {
        cout << "F" << endl;
        return;
    }
    v -= a;
    if (v - b < 0) {
        cout << "M" << endl;
        return;
    }
    cout << "T" << endl;
}

提出コード

ABC 242

A - T-shirt

問題

  • $X \leq A$ なら 1
  • $X > B$ なら 0
  • それ以外なら $\displaystyle \frac{C}{B-A}$

を出力

提出コード

B - Minimize Ordering

問題

文字を昇順に並べ替えたものが答え

提出コード

C - 1111gal password

問題

$dp[i][x]$: $i$ 桁の整数で, $i$ 桁目が $x$ である問題の条件を満たす整数の場合の数とする. また初期状態として $\forall x, dp[1][x] = 1$ とする.

$i$ 桁の整数で $i$ 桁目が $x$ となるような整数の場合の数は $i-1$ 桁目の数と $x$ の差の絶対値が1以下となるような $i-1$ 桁の整数の数の合計であるから

dp[i][x] += dp[i-1][x]
if (x-1 >= 1) dp[i][x] += dp[i-1][x-1]
if (x+1 <= 9) dp[i][x] += dp[i-1][x+1]

となる.

ABC 241

A - Digit Machine

問題

a[a[a[0]]] を出力すれば良い.

void solve() {
    vint a(10);
    rep(i, 10) cin >> a[i];

    cout << a[a[a[0]]] << endl;
}

提出コード

B - Pasta

問題

map にそれぞれの麺の長さの数を保存して, $i$ 日目に麺が残っていたら 1 decrement, 残っていなかったら No を出力.

最終的に $M$ 日間の食事計画に問題なければ Yes を出力.

提出コード

C - Connect 6

問題

$(i, j)$ を始点として上下左右斜め8方向に対して6マスの中に白の数が2つ以下となっているかを全探索する.

方向が違う以外は調べる処理の中身は同じなので関数化しておくと楽.

bool search(int i, int j, int di, int dj) {
    // (i, j): start point
    // (fi, fj): final point
    int fi = i + di * 5;
    int fj = j + dj * 5;
    if (fi < 0 || fi >= n || fj < 0 || fj >= n)
        return false;

    int rem = 6;
    int r = i, c = j;
    int numwhite = 0;
    rep(i, rem) {
        numwhite += grid[r][c] == '.';
        r += di;
        c += dj;
    }

    if (numwhite <= 2) {
        return true;
    }
    return false;
}

提出コード

ARC 136

A - A ↔ BB

問題

文字列のうち A or B が連続する区間はいったん全ての ABB にして その区間の文字を B のみからなるようにした後に先頭から近い方の BBA に変換していくのを繰り返すのが最適.

CABAC -> CBBBBBC -> CAABC

模範解答を見て知ったが string(n, 'A') とすると A が $n$ 文字の並んだ文字列を生成できるらしい. またどうやら C++ の string で str1 += str2 とするのは str2 の長さ分を加える計算量しか掛からないっぽい. str1 += str2str1push_backstr2 の要素を1個ずつ入れるのと計算量としては変わらないっぽい.

string string_cancat(int n) {
    string s = "";
    rep(i, n) s += ('a' + (i % 5));
    return s;
}

void test(int n) {
    clock_t t = clock();
    string s = string_cancat(n);
    double time = (double)(clock() - t) / CLOCKS_PER_SEC;
    printf("n = %10d:\ttime = %.9lf sec\n", n, time);
}

void solve() {
    int n = 10;
    while (n < 100000000) {
        test(n);
        n *= 10;
    }
}
n =         10: time = 0.000002000 sec
n =        100: time = 0.000009000 sec
n =       1000: time = 0.000013000 sec
n =      10000: time = 0.000090000 sec
n =     100000: time = 0.000807000 sec
n =    1000000: time = 0.012396000 sec
n =   10000000: time = 0.049614000 sec

提出コード

ABC 240

A - Edge Checker

問題

$|a-b| = 1$ または $a = 1, b = 10$ のときは Yes, それ以外のときは No

提出コード

B - Count Distinct Integers

問題

$N$ 個の要素全部 set に突っ込んで最後に size を出力

提出コード

C - Jumping Takahashi

問題

$dp[i][x]$ を $i$ 回目のジャンプで座標 $x$ に行けるかとし 初期値 $dp[0][0] = true$ とする.

$dp[i-1][x] = true$ ならば $dp[i][x+a] = dp[i][x+b] = true$ に更新する.

提出コード

D - Strange Balls

問題

筒の中に入っているボールの数を同じ数字が書かれたボールに関しては圧縮してもつ.

筒の中に [2, 3, 3, 2, 4, 4, 4, 5] と入っていたら [{2, 1}, {3, 2}, {2, 1}, {4, 3}, {5, 1}] という風に {数字, 個数} として保持する.

ABC 239

A - Horizon

問題

問題分通りの値を出力するだけ.

提出コード

B - Integer Division

問題

$X > 0$ のときは素直に整数除算 $X / 10$ を出力する.

$X < 0$ のとき, $X \equiv 0 \mod 10$ なら整数除算 $X / 10$ を出力し, $X % 10 \neq 0$ なら $X / 10 - 1$ を出力する.

void solve() {
    ll x;
    cin >> x;
    if (x < 0 && x % 10 != 0) {
        cout << x / 10 - 1 << endl;
    } else {
        cout << x / 10 << endl;
    }
}

余談

ARC 135

A - Floor, Ceil - Decomposition

数字 $x$ をそのままにするか, $\lfloor \frac{x}{2} \rfloor$, $\lceil \frac{x}{2} \rceil$ に書き換えるかは $x < \lfloor \frac{x}{2} \rfloor \times \lceil \frac{x}{2} \rceil$ となれば書き換えたほうが全体の積が大きくなり, その他の場合は $x$ のままにするのがよい.

おおよそ $\displaystyle \left\lfloor \frac{x}{2} \right\rfloor \times \left\lceil \frac{x}{2} \right\rceil \approx \frac{x^2}{4}$ となることを考え, $y = x$ と $y = \frac{x^2}{4}$ のグラフを描くと $0 \leq x \leq 4$ の区間では $y = x$ が, $x > 4$ の区間では $y = x^2 / 4$ のほうが大きいことがわかる.

arc135_a

$g(x) = \max(x, \left\lfloor \frac{x}{2} \right\rfloor \times \left\lceil \frac{x}{2} \right\rceil)$ とすると

ABC 237

C - kasaka

先頭にある連続する a の数を $x$, 末尾に連続する a の数を $y$ とすると $x > y$ のとき, どうやっても回文にはできない. それ以外のとき, 先頭と末尾の a を消して残った文字が回文になっていれば Yes, そうでなければ No.

初め以下のようなコードを書き AC を取れてしまったが, これは嘘解法.

void solve() {
    string s;
    cin >> s;
    int l = 0, r = (int)s.size() - 1;
    while (l < r) {
        // 頭に a を追加する
        if (s[l] != 'a' && s[r] == 'a') {
            r--;
        } else if (s[l] == s[r]) {
            l++;
            r--;
        } else {
            cout << "No" << endl;
            return;
        }
    }
    cout << "Yes" << endl;
    return;
}

axyaxa という文字列のとき上のプログラムだと Yes を出力するが, 本来は No を出力すべき

ABC 238

A - Exponential or Quadratic

問題

愚直に $2^n$ を計算すると $n$ が最大で $10^9$ のためオーバーフローする.

$n$ が十分大きいところでは $2^n > n^2$ となるため, そのようなところを探す.

  • $n = 1$: $2^n = 2 > 1 = n^2$
  • $n = 2$: $2^n = 4 = 4 = n^2$
  • $n = 3$: $2^n = 8 < 9 = n^2$
  • $n = 4$: $2^n = 16 = 16 = n^2$
  • $n = 5$: $2^n = 32 > 25 = n^2$
  • $n = 6$: $2^n = 64 > 36 = n^2$
  • $\vdots$

$n = 2, 3, 4$ のときは題意は false であるがそれ以外では true となる.

ARC 134

A - Bridge and Sheets

すでに敷いたシートの右端を $r$ とすると, $r < x$ となる $x$ までをカバーするのに必要なシーツの枚数は $\displaystyle \left\lceil \frac{x-r}{W} \right\rceil$ である. これをひたすら繰り返す

提出コード

B - Reserve or Reverse

$l = 1, r = N$ として以下を繰り返すと辞書順最小になる

  • $[l,r]$ の範囲にある文字のうち $s_l$ 未満の最小の文字のうちで一番右側のあるものの index を $r^\prime$ とすると $s_l$ と $s_{r^\prime}$ を交換し $r = r^\prime - 1$ に更新する.
  • そのようなものがない場合は $l$ をインクリメントする
  • $l \geq r$ となったら終了する.
void solve() {
    int n;
    cin >> n;
    string s;
    cin >> s;
    vvint a(26);
    rep(i, n) {
        a[s[i] - 'a'].push_back(i);
    }

    int l = 0, r = n - 1;
    while (l < r) {
        // [l,r] の範囲にある文字のうち s[l] 未満の最小の文字のうちで一番右側にあるものの index を探す
        int ok = 0;
        rep(c, 26) {
            if (s[l] - 'a' <= c)
                break;

            // 右端以上になっている要素は消す
            while (!a[c].empty() && a[c].back() > r)
                a[c].pop_back();

            // 一つも要素がない or 左端より小さい値しかなかったら交換できないので飛ばす
            if (a[c].empty() || a[c].back() < l)
                continue;
            r = a[c].back();
            ok = 1;
            break;
        }
        if (ok) {
            swap(s[l], s[r]);
            r--;
        }
        l++;
    }
    cout << s << endl;
}

$r^\prime$ の探し方を二分探索で探すという方法もある.

ABC 236

B - Who is missing?

問題

出現回数が3回の数字を出力すればよい.

愚直に解くと以下のようになる. これで AC は一応取れる.

void solve() {
    int n;
    cin >> n;
    vint card(n + 1);
    rep(i, 4 * n - 1) {
        int a;
        cin >> a;
        card[a]++;
    }

    rep2(i, 1, n + 1) {
        if (card[i] != 4)
            cout << i << endl;
    }
}

別解

数字 $x$ を XOR で偶数回作用させると 0 になり, 奇数回作用させると $x$ になることを思い出すと, 出力すべき数字だけ奇数回だけ出現するので, $XOR(A_1, A_2, \cdots, A_{4N-1})$ が答えとなる.

void solve() {
    int n;
    cin >> n;
    int x = 0;
    rep(i, 4 * n - 1) {
        int y;
        cin >> y;
        x ^= y;
    }
    cout << x << endl;
}

tourist の提出コード を見てこの解放を知った.

ARC 133

A - Erase by Value

問題

$A_i > A_{i+1}$ となる最初の $A_i$ を消したときが辞書順最小となる.

$A_i < A_{i+1}$ となる $A_i$ を消した場合, 辞書順であとにくる数列に変化してしまうので数列が広義単調増加しているうちはその文字は消すべきではない.

B - Dividing Subsequence

問題

LCS の亜種. 「$b_i$ は $a_i$ の倍数」という条件を $a_i = b_i$ に変えると LCS と同じになる. ただし, LCS を使うと計算量が $O(N^2)$ になってしまうので, そのままでは適用することができない.

$Q_j$ が $P_i$ の倍数となるような index のペア $(i,j)$ に関して, 第1要素で昇順, 第2要素で降順に並べたものに対して, 第2要素の LIS の長さを求めると問題の答えになる.

入力例を使って実際に考えてみる.

3 1 4 2
4 2 1 3

このとき, ペアを辺と捉えると下図のようになる. arc133_b_1

ここで, 辺を交差しないようにできるだけ多く選択したい. 第1要素を横軸, 第2要素を縦軸に辺をプロットすると下図のようになる.

arc133_b_2

これより, 2辺 $(a_i, b_i)$, $(a_j, b_j)$ が交差しないための条件は $a_i < a_j \wedge b_i < b_j$ が成り立つことである. この条件を満たすように辺を選んだときの, 選べる辺の最大値は先に述べたように第2要素の LIS の長さとなる.

ABC 235

D - Multiply and Rotate

問題

  • 操作1: $x$ を $a$ 倍する
  • 操作2: $x$ を文字列とみなして, 末尾の数字を先頭に移動させる とする.

DFS or BFS を使って, 1 からスタートして N になるまでの操作回数の最小値を求める. $N$ からスタートして $1$ にする方でも可.

DFS の計算量の正確な見積もり方はわからないが, $\log_2(10^6) = 19.93...$ であるので, 7桁の数を $a$ で割れる回数は高々20回程度で 操作2をしたところでそんなにパターン数は多くなさそうに思えるが, 本当にそうなのか微妙なところ. 際どいテストケース持ってこられたときに TLE にならないか自信がない.

E - MST + 1

問題

クエリを先読みする問題. 元々のグラフの辺と, クエリによる辺を合わせて最小全域木を作ることを考える. 作っている際にクエリ由来の辺を使ったほうが得なときは, そのクエリを Yes, そうでない場合は No を出力する. プリム法だとグラフの構築が必要になるので, クラスカル法で解くほうが楽そう.

ARC 122

A - Many Formulae

問題

$dp[i][x]$: $i$ 番目までの数字を使う, かつ, $A_i$ を $x~(x = + \text{ or } -)$ とするような式の合計とする.

具体的に書いてみるとわかりやすいが, 遷移は以下のようになる. \begin{align} dp[i][+] &= (dp[i-1][+] + dp[i-1][-]) + (i-1 \text{ までを使ったときの式の場合の数}) \times A_i \ dp[i][-] &= dp[i-1][+] - (A_{i-1} \text{を足したときの式の場合の数}) \times A_i \end{align}

arc122_a

掛け合わせる数はフィボナッチ数列になっている. $DP$ テーブル一つだけで処理するのは面倒(pair を使って合計と個数を記録していればできる)なので掛ける数は別にテーブルに保持しておくと楽

void solve() {
    int n;
    cin >> n;
    vector<ll> a(n + 1);
    rep(i, n) cin >> a[i + 1];

    vector<vector<mint>> dp(n + 1, vector<mint>(2, 0)), mul(n + 1, vector<mint>(2, 0));
    dp[1][0] = a[1];
    mul[1][0] = 1;
    rep2(i, 2, n + 1) {
        mul[i][0] = mul[i - 1][0] + mul[i - 1][1];
        mul[i][1] = mul[i - 1][0];
        dp[i][0] = dp[i - 1][0] + dp[i - 1][1] + mul[i][0] * a[i];
        dp[i][1] = dp[i - 1][0] - mul[i][1] * a[i];
    }

    cout << (dp[n][0] + dp[n][1]).val() << endl;
}

提出コード

ARC 128

A - Gold and Silver

問題

$i$ 日目に金を銀に, $j$ 日目に銀を金に交換すると金の量は $A_i / A_j$ 倍になる. ここで

$$ \frac{A_i}{A_j} = \frac{A_i}{A_{i+1}} \times \frac{A_{i+1}}{A_{i+2}} \times \cdots \times \frac{A_{j-1}}{A_j} $$

であるから $i$ 日目に金を銀に, $j$ 日目に銀を金に交換することは

  • $A_i / A_{i+1}$
    • $i$ 日目に金を銀に交換
    • $i+1$ 日目に銀を金に交換
  • $A_{i+1} / A_{i+2}$
    • $i+1$ 日目に金を銀に交換
    • $i+2$ 日目に銀を金に交換
  • $A_{j-1} / A_j$
    • $j-1$ 日目に金を銀に交換
    • $j$ 日目に銀を金に交換

することと等価である. 金の量を増やすには $A_i > A_{i+1}$ となるところで交換を行えばいい.

void solve() {
    int n;
    cin >> n;
    vector<int> A(n), change(n, 0);
    rep(i, n) cin >> A[i];
    rep(i, n - 1) {
        if (A[i] > A[i + 1]) {
            change[i] ^= 1;
            change[i + 1] ^= 1;
        }
    }
    print(change);
}

XOR をとっているのは同じ日に金を銀に, 銀を金にという交換を連続する操作があった場合に 打ち消し合わせるため.

ARC 129

B - Range Point Distance

問題

$dist(l, r, x) = \max(0, l-x, x-r)$ であるから

\begin{align} \max(dist(L_1, R_1, x)&, \cdots, dist(L_k, R_k, x)) \ &= \max(0, L_1 - x, x - R_1, \cdots, L_k - x, x - R_k) \ &= \max(0, \max(L_1 - x, \cdots, L_k - x), \max(x - R_1, \cdots, x - R_k)) \end{align} である.

\begin{align} &\max(L_1 - x, \cdots, L_k - x) = \max(L_1, \cdots, L_k) - x = A_k - x\ &\max(x - R_1, \cdots, x - R_k) = x + \max(-R_1, \cdots, -R_k) = x - \min(R_1, \cdots, R_k) = x - B_k \end{align} where $A_k = \max(L_1, \cdots, L_k)$ and $B_k = \min(R_1, \cdots, R_k)$.

ABC 234

C - Happy New Year

問題

$K$ を2進数にしたときの 1 となっているところを 2 に置き換えたものが答え

void solve() {
    ll k;
    cin >> k;

    // n: bit が立っている最も大きい idx
    int n = 0;
    rep(i, 60) if ((k >> i) & 1) n = i;

    string ans = "";
    rep(i, n + 1) ans.push_back((k >> i) & 1 ? '2' : '0');
    reverse(all(ans));
    cout << ans << endl;
}

提出コード

D - Prefix K-th Max

$S = \{P_1, \cdots, P_K\}$ となる集合を考える. $K+1$ 番目以降の数は保存しておく必要がないので, $P_{K+1} > \min(S)$ ならば $\min(S)$ を取り除き, $P_{K+1}$ を追加. $P_{K+1} < \min(S)$ ならば $P_{K+1}$ は集合に入れずそのまま捨てる. 以降 $P_N$ まで同様の操作を行って, その時その時の $\min(S)$ を出力する.

ABC 222

D - Between Two Arrays

問題

$dp[i][x]$: $i$ 番目までの数が決まっており, $c_i = x$ のときの場合の数とする.

$c_i = x$ となるときの場合の数は, $c_{i-1} \leq x$ となる場合の数であるから

$\displaystyle dp[i][x] = \sum_{k = a_{i-1}}^x dp[i-1][k]$

これを元に naive に実装すると以下のようになる.

vector<vector<ll>> dp(n, vector<ll>(MAX, 0));
rep2(x, a[0], b[0] + 1) dp[0][x] = 1;
rep2(i, 1, n) {
    rep2(x, a[i], b[i] + 1) {
        rep2(k, a[i - 1], x + 1) {
            dp[i][x] += dp[i - 1][k];
            dp[i][x] %= MOD;
        }
    }
}

ll ans = 0;
rep(i, MAX) {
    ans += dp[n - 1][i];
    ans %= MOD;
}
cout << ans << endl;

しかしこれでは最大で $O(N \times 3000^2) = O(3000^3)$ の計算量がかかり制限時間に待ち合わない. TLE 提出コード

ABC 233

C - Product

問題

$\displaystyle \prod^N_{i=1}L_i \leq 10^5$ より, 考えうるボールの取り出し方を全通り試しても $O(10^5)$ なので全探索する.

$1 \leq a_{i,j} \leq 10^9$ と $N \geq 2$ より, $10^9$ という数が3回以上掛けることがありうるが, 倍精度整数で表現できる整数の範囲はたかだか18桁なので愚直に積を取ると途中でオーバーフローしてしまう. 掛け算をしている途中で $X < \text{(それまでの積)} \times a_{i,j}$ としたいがこのままだと右辺がオーバーフローする可能性があるので, $X / \text{(それまでの積)} < a_{i,j}$ とする.

void dfs(int depth, ll prod) {
    if (depth == N) {
        if (prod == X) ans++;
        return;
    }

    for (ll x : a[depth]) {
        // X < x*prod
        if (X / prod < x) continue;
        dfs(depth+1, prod*x);
    }
}

このオーバーフロー対策を入れないと

ARC 132

A - Permutation Grid

問題

1-index で考え, grid の $A$ と呼ぶことにする.

公式解説の通り, $R$, $C$ の並び方がともに $1, 2, \cdots, n$ である場合は, 上から $r$ 行目, $c$ 行目のマスが $r+c \geq n + 1$ である場合に黒で, それ以外では白となる.

arc132_a1.png

ここで行 $i$, $j$ を入れ替えたとき $C_1, \cdots, C_n$ の結果に影響を与えない. 同様に列 $i$, $j$ を入れ替えたとき $R_1, \cdots, R_n$ の結果に影響を与えない.

$R_i$, $C_i$ が昇順になるように行・列の入れ替えを行ってできる grid を $A^\prime$ としたとき $A$ の $(r, c)$ 成分が $A^\prime$ のどの成分に対応するか調べることでマスが黒か白か判定できる.

$R$, $C$ は $1, \cdots, n$ の順列だから, ソートによって $r$ 行目は $R_r$ 行目に, $c$ 列目は $C_c$ 列目に移動する. よって $R_r + C_c \geq n+1$ のとき黒, それ以外のときは白となる.

ABC 232

A - QQ solver

問題

char で受けて '0' で引くということもできるが, iostream は条件に合うものを最大長マッチでとってくる(らしい)ので下のような書き方でも通る.

int a, b;
char x;
cin >> a >> x >> b;
cout << a*b << endl;

提出コード

C - Graph Isomorphism

問題

高橋くんの番号と青木くんの番号の対応関係 $N!$ 通りをすべて試して, グラフが一致すれば Yes, だめなら No.

提出コード

D - Weak Takahashi

問題

$(1, 1)$ から BFS してステップ数が最大となるものが答え

提出コード

E - Rook Path

問題

$H \times W$ のグリッドを下のような領域に分ける.

  • $S_{0,0} = \{(x, y) | x \neq x_2 \wedge y \neq y_2 \}$ (白の領域)
  • $S_{0,1} = \{(x, y_2) | x \neq x_2\}$ (赤の領域)
  • $S_{1,0} = \{(x_2, y) | y \neq y_2\}$ (青の領域)
  • $S_{1,1} = \{(x_2, y_2)\}$ (緑の領域)

e_grid.png

ABC 231

C - Counting 2

昇順でソートして lower_bound 使って $x_i$ 以上となる index を調べる. 二部探索用途で multiset を使うと遅いので注意.

ref: multiset vs vector

submission code

D - Neighbors

人を node, 隣り合う人の情報を edge としグラフを構築する. node の次数が3以上のとき, またはグラフに cycle がある場合は No, それ以外は Yes を出力する.

submission code: cycle 検出方法ごとの実装

  • Union Find
    • 辺を作る前に両端の node が同じ親を持っているか確認する. 同じだった場合, 辺を張ると cycle を作ってしまうことがわかる.
1
|
2 -- 3

1-2, 2-3 の辺を張った状態で 1, 3 のそれぞれの親を調べると同じになる. その場合, 1-3 に辺を張ると cycle ができることがわかる.

Algorithm

int <-> string

  • to_string(int) -> string
  • to_string(ll) -> string
  • stoi(string) -> int
  • stoll(string) -> ll

priority queue

昇順

#include <iostream>
#include <queue>

int main() {
    // intを要素に持つ優先順位付きキュー.
    // 降順に処理する
    std::priority_queue<int> que;

    // データを追加する
    que.push(3);
    que.push(1);
    que.push(4);

    // 処理順に出力する
    while (!que.empty()) {
        std::cout << que.top() << std::endl;
        que.pop();
    }
}
4
3
1

降順に出力

#include <iostream>
#include <queue>

int main() {
    // intを要素に持つ優先順位付きキュー.
    // 降順に処理する
    std::priority_queue<int, vector<int>, greater<int>> que;

    // データを追加する
    que.push(3);
    que.push(1);
    que.push(4);

    // 降順順に出力する
    while (!que.empty()) {
        std::cout << que.top() << std::endl;
        que.pop();
    }
}
1
3
4

struct で priority queue

Tips

Performance

入出力の高速化

他人のコードをみてたまにやっているのを見かける以下のような書き方は入出力を高速化するためにやっているらしい

ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cout << val << '\n';

ref: https://maspypy.com/library-checker-many-a-b#google_vignette

multiset vs vector

multiset は遅いので二部探索のように使うときは注意する. insert, find, erase の計算量は $\log(N)$ ($N$ は要素数)であるが, 定数項が効いているのかなかなかに遅い.

#include <bits/stdc++.h>
#define all(v) (v).begin(), (v).end()
#define rall(v) (v).rbegin(), (v).rend()
#define rep(i, n) for (int i = 0; i < (n); ++i)
#define rep2(i, k, n) for (int i = (k); i < (n); ++i)
using namespace std;
using ll = long long;
using P = pair<int, int>;
// using P = pair<ll,ll>;

const ll INF = (ll)1e18;
// const int INF = (int)1e9+7;
template <typename T>
void chmin(T& a, T b) {
    a = min(a, b);
}
template <typename T>
void chmax(T& a, T b) {
    a = max(a, b);
}

void solve() {
    int ns[] = {100, 1000, 10000, 100000, 1000000, 10000000};
    cout << "multiset" << endl;
    cout << "size\telapsed time (sec)" << endl;
    for (int n : ns) {
        multiset<int> ms;
        clock_t start = clock();
        rep(i, n) ms.insert(i);
        clock_t end = clock();
        cout << n << "\t" << (double)(end - start) / CLOCKS_PER_SEC << endl;
    }

    cout << endl;
    cout << "vector" << endl;
    cout << "size\telapsed time (sec)" << endl;
    for (int n : ns) {
        vector<int> v(n);
        clock_t start = clock();
        rep(i, n) v[i] = i;
        sort(all(v));
        clock_t end = clock();
        cout << n << "\t" << (double)(end - start) / CLOCKS_PER_SEC << endl;
    }
}

int main() {
    solve();
    return 0;
}
multiset
size    elapsed time (sec)
100     9e-06
1000    6.4e-05
10000   0.000915
100000  0.011328
1000000 0.144224
10000000        2.5446

vector
size    elapsed time (sec)
100     5e-06
1000    1e-05
10000   7.5e-05
100000  0.000858
1000000 0.010139
10000000        0.130072

multiset_vs_vector

ABC 230

E - Fraction Floor Sum

$\displaystyle \sum^{n}_{i=1} \floor{N/i}$ を求める問題. 言い換えると $y \le n / x$, $(1 \le x \le n)$ の範囲にある格子点の数を数える問題.

$$ \sum_{i=1}^{n} \floor{N/i} = 2 \sum_{i=1}^{\floor{\sqrt{n}}} \floor{N/i} - (\floor{\sqrt{n}})^2 $$

計算量は $O(\sqrt{n})$ で解ける.

解説

$y = n/x$ は $y = x$ に関して対称であり, $y = n/x$ と $y = x$ は $(x,y) = (\sqrt{n}, \sqrt{n})$ で交わる.

対称性より図の青い斜線の領域に含まれる格子点の数 ($\sum^{\floor{\sqrt{n}}}_{i = 1}\floor{N/i}$) と, 赤い斜線の領域に含まれる格子点の数は一致する. 2つの領域で共通する部分に含まれる格子点の数は $(\floor{\sqrt{n}})^2$.

したがって,

$$ \begin{aligned} (\mathrm{題意の格子点数}) \\\\ =& (\mathrm{青斜線に含まれる格子点数}) + (\mathrm{赤斜線に含まれる格子点数}) - (共通する領域に含まれる格子点数) \\\\ =& 2\sum^{\floor{\sqrt{n}}}_{i = 1}\floor{ N/i } - (\floor{ \sqrt{n} })^2 \end{aligned} $$

e.png