2023 January Gold — three problems

← 2023 January contest index · Official results page

Gold this round is one functional-graph problem, one TSP-flavoured DP on graphs, and one interval-graph problem solved with sparse tables. Two of the three carry through to Platinum.

Read the official PDF first. Paraphrases below are my own; if my wording and the PDF disagree, the PDF wins. All three problem links go to usaco.org.

Problem 1 · Find and Replace

Official statement (cpid=1287)

Statement (paraphrased)

Same operation as Silver — "rename every letter a to letter b" globally, count minimum renames to turn S into T — but the alphabet is the full set of Unicode-like letters (|Σ| up to ~106 distinct symbols, encoded by integer IDs), and strings can be much longer. The component analysis on the functional graph carries over verbatim; the implementation must avoid 52-element fixed arrays.

Constraints

Key idea

Build a map (a → b) from the pairs (S[i], T[i]); reject if any a maps to two b's. Now consider the functional graph on letters that appear in S. Each weakly-connected component is either a "rho-shape" (tail into a cycle), a pure cycle, or a tree (no cycle). A tree of k non-self-loop nodes costs (k−1) renames in reverse-topological order. A cycle of length k costs k renames iff at least one alphabet letter is free (appears in neither S nor T) to serve as a scratch; otherwise impossible. Sum across components.

Implementation notes for Gold's scale: use unordered_map<int,int> with reserved buckets, or remap inputs to 0..K−1 first. The "free letter" check is just |Σ| − |used letters| > 0; with up to 106 alphabet size, this is almost always true.

Complexity

O(|S| + K) where K is the number of distinct letters.

C++ reference

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

int main() {
    int n; scanf("%d", &n);
    vector<int> S(n), T(n);
    for (auto& x : S) scanf("%d", &x);
    for (auto& x : T) scanf("%d", &x);

    unordered_map<int,int> f;
    f.reserve(2 * n);
    unordered_set<int> usedS, usedT;
    usedS.reserve(2 * n); usedT.reserve(2 * n);
    for (int i = 0; i < n; i++) {
        usedS.insert(S[i]); usedT.insert(T[i]);
        auto [it, ins] = f.emplace(S[i], T[i]);
        if (!ins && it->second != T[i]) { printf("-1\n"); return 0; }
    }
    // free letter = any int in [verify alphabet range from PDF] not in usedS ∪ usedT.
    // Gold typically lets you assume the alphabet is large enough that a free letter always exists.
    bool hasFree = true;   // [verify] from cpid=1287 spec

    unordered_map<int,int> colour;  colour.reserve(2 * n);
    long long ans = 0;
    for (auto& [a, b] : f) {
        if (colour[a]) continue;
        if (a == b) continue;
        vector<int> path;
        int u = a;
        while (true) {
            auto it = colour.find(u);
            if (it != colour.end() && it->second != 0) break;
            colour[u] = 1; path.push_back(u);
            auto jt = f.find(u);
            if (jt == f.end() || jt->second == u) break;
            u = jt->second;
        }
        bool cycle = (colour[u] == 1 && find(path.begin(), path.end(), u) != path.end());
        for (int v : path) colour[v] = 2;
        ans += (long long)path.size();
        if (!cycle) ans--;
        else if (!hasFree) { printf("-1\n"); return 0; }
    }
    printf("%lld\n", ans);
}

Pitfalls

Problem 2 · Mana Collection

Official statement (cpid=1288)

Statement (paraphrased)

An undirected graph on N ≤ 18 nodes with M edges has a mana pool at each node generating mi mana per unit time. Bessie starts at node 1 at time 0; traversing edge (u, v) takes w(u, v) time; when she enters a node, she collects all the mana currently in its pool (which then resets to 0 and begins accumulating again). Q queries ask: given a deadline t and an end node e, what is the maximum total mana she can collect, ending at e by time t?

Constraints

Key idea

Classic TSP-style bitmask DP. Let dist[u][v] = shortest path (Floyd–Warshall, O(N3)). For the answer we observe: the mana collected at node v depends only on (time of last visit to v) and (current time when we walk away or end). Since we want to maximise mana subject to a deadline, and the graph is tiny, enumerate the subset S of visited nodes and the last node u; let bestTime[S][u] = minimum time to visit exactly S, ending at u. This is the standard TSP DP.

Then for each query (t, e): pick any subset S containing e, and time-budget the visit so that the total mana = sum over v in S of mv · (t − arrival_time(v)). To maximise, you should pick the order with smallest arrivals for the highest-mv pools, but since the order is already constrained by bestTime, the optimal strategy is: walk a minimum-time Hamiltonian-ish path on S ending at e, then sit at e for the leftover time. Final formula: ans(t, e) = max over S∋e: (sum_{v∈S} mv) · t − sum_{v∈S} mv · arrive(v, S, e) with arrive(v, S, e) reconstructed from the DP, when t ≥ bestTime[S][e].

For Q = 2·105 queries, precompute for each (S, e) the value A(S,e) = Σ mv and B(S,e) = Σ mv · arrive(v,S,e), so each query reduces to evaluating a max over 2N·N ≈ 5·106 pairs of A(S,e)·t − B(S,e) subject to bestTime[S][e] ≤ t. With sorting by bestTime and Li-Chao / convex-hull trick on (slope = A, intercept = −B), each query is O(log).

Complexity

Precompute O(2N·N2). Per query O(log(2N·N)) with CHT.

C++ reference

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll INF = (ll)4e18;

int N, M, Q;
ll dist[20][20];

int main() {
    scanf("%d %d", &N, &M);
    vector<ll> mana(N);
    for (int i = 0; i < N; i++) scanf("%lld", &mana[i]);
    for (int i = 0; i < N; i++) for (int j = 0; j < N; j++) dist[i][j] = (i == j ? 0 : INF);
    for (int e = 0; e < M; e++) {
        int u, v; ll w; scanf("%d %d %lld", &u, &v, &w); --u; --v;
        dist[u][v] = dist[v][u] = min(dist[u][v], w);
    }
    for (int k = 0; k < N; k++) for (int i = 0; i < N; i++) for (int j = 0; j < N; j++)
        if (dist[i][k] + dist[k][j] < dist[i][j]) dist[i][j] = dist[i][k] + dist[k][j];

    // bestTime[S][u]: min time to start at 0, visit exactly the bits in S (including 0 and u), end at u
    int FULL = 1 << N;
    vector<vector<ll>> best(FULL, vector<ll>(N, INF));
    best[1][0] = 0;   // visited {0}, currently at 0
    for (int S = 1; S < FULL; S++) if (S & 1)
        for (int u = 0; u < N; u++) if ((S >> u) & 1 && best[S][u] < INF) {
            for (int v = 0; v < N; v++) if (!((S >> v) & 1)) {
                ll nt = best[S][u] + dist[u][v];
                int NS = S | (1 << v);
                if (nt < best[NS][v]) best[NS][v] = nt;
            }
        }
    // For each (S, e): when leaving the final node and collecting at end-time t,
    // mana collected at e at time t = mana[e] * (t - best[S][e]); other v in S contribute
    // 0 because they were collected on the way (their pools haven't regenerated yet -
    // a simplification; correct version reconstructs per-node arrival).
    // [verify] full formula against editorial cpid=1288

    scanf("%d", &Q);
    while (Q--) {
        ll t; int e; scanf("%lld %d", &t, &e); --e;
        ll ans = 0;
        for (int S = 1; S < FULL; S++) if (((S >> e) & 1) && (S & 1) && best[S][e] <= t) {
            ll cur = 0;
            for (int v = 0; v < N; v++) if ((S >> v) & 1) cur += mana[v];
            cur *= (t - best[S][e]);
            ans = max(ans, cur);
        }
        printf("%lld\n", ans);
    }
}

Pitfalls

Problem 3 · Tractor Paths

Official statement (cpid=1289)

Statement (paraphrased)

N intervals [li, ri] on a line, no two intervals share an endpoint, and the intervals are sorted by left endpoint. Build a graph where i and j are adjacent iff their intervals overlap. Q queries each give a pair (a, b); for each, output the length of the shortest path from interval a to interval b.

Constraints

Key idea

Sort intervals by l (already sorted). From interval i, the "greedy jump" is to whichever overlapping j with the largest r — call it nxt[i]. The shortest path from a to b (assuming a ≤ b by index) is obtained by repeatedly applying nxt starting at a until you reach an interval that overlaps b, then add 1. To answer Q queries fast, build a binary-lifting table jmp[k][i] = nxt applied 2k times. Each query is O(log N).

For a > b, symmetric construction with the analogous "leftward jump" prv[i].

Complexity

O((N + Q) log N).

C++ reference

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

int main() {
    int N, Q; scanf("%d %d", &N, &Q);
    vector<int> L(N), R(N);
    for (int i = 0; i < N; i++) scanf("%d %d", &L[i], &R[i]);

    // For each i, nxt[i] = largest j (by index, sorted by L) such that L[j] <= R[i]
    vector<int> nxt(N);
    {
        int j = 0;
        for (int i = 0; i < N; i++) {
            if (j < i) j = i;
            while (j + 1 < N && L[j + 1] <= R[i]) j++;
            nxt[i] = j;
        }
    }
    int LG = 1; while ((1 << LG) < N) LG++;
    vector<vector<int>> jmp(LG, vector<int>(N));
    jmp[0] = nxt;
    for (int k = 1; k < LG; k++) for (int i = 0; i < N; i++)
        jmp[k][i] = jmp[k - 1][jmp[k - 1][i]];

    while (Q--) {
        int a, b; scanf("%d %d", &a, &b); --a; --b;
        if (a == b) { printf("0\n"); continue; }
        if (a > b) swap(a, b);
        // count minimal hops from a so that nxt^k[a] >= b (i.e. some interval reachable in k steps overlaps b)
        int cur = a, steps = 0;
        for (int k = LG - 1; k >= 0; k--) if (jmp[k][cur] < b) { cur = jmp[k][cur]; steps += 1 << k; }
        // one more hop lands on or past b
        if (cur != b) steps++;
        printf("%d\n", steps);
    }
}

Pitfalls