USACO 2021 December · Platinum · all three problems

← Full December 2021 set · Official results

Source. Statements and constraints below are paraphrased from usaco.org/index.php?page=dec21results. Each problem heading links to the canonical statement on usaco.org. Code is my own reference C++ — cross-check the official editorial before trusting it under contest pressure.

Problem 1 — Tickets

Statement

There are N fields and K tickets. Ticket i lets you, at cost ci, travel from any field in the interval [ai, bi] to a designated field pi. You want every field f to compute the minimum total cost to reach a starting set S (or vice versa). Output that min cost per field (or −1 if unreachable).

Subtask structure (typical)

Constraints

Key idea

Materialize each ticket as a virtual node vi with cost-ci edge from vi to pi, and zero-cost edges from every node in [ai, bi] to vi. The interval-to-node edge fan-in is implemented with a segment tree whose internal nodes route into vi. Total nodes O(N + K log N), edges O(N + K log N). Run Dijkstra from the source set. Distances are answers for the original N nodes.

Complexity target

O((N + K log N) log(N + K log N)) for Dijkstra with a heap.

Reference solution (C++)

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

// Segment tree on graph: leaves 0..N-1 are real nodes; internal nodes route
// edges INTO their children with weight 0. To attach a ticket reading
// [a,b] -> v with weight c, add 0-weight edges from each seg-tree node
// covering [a,b] to v (this lets any node in [a,b] reach v at cost 0).
struct SegGraph {
    int N, T;                       // T = total nodes (leaves + internal)
    vector<vector<pair<int, ll>>> g;
    SegGraph(int n) : N(n) {
        T = 1; while (T < N) T <<= 1;
        g.assign(2 * T, {});
        // Each internal node points down to its two children with weight 0.
        for (int v = 1; v < T; ++v) {
            g[v].push_back({2 * v,     0});
            g[v].push_back({2 * v + 1, 0});
        }
    }
    int leaf(int i) const { return T + i; }   // real node id in graph
    void addIntervalEdge(int l, int r, int dst, ll w) {
        // edge from any node in [l,r] to dst with weight w: add a 0-edge from
        // each seg-tree segment covering [l,r] to a helper that forwards to
        // dst with weight w. We collapse the helper into a direct seg->dst
        // by paying w only once: route via a single intermediate.
        static int next = -1; if (next == -1) next = 2 * T;
        int mid = next++; g.push_back({});
        g[mid].push_back({dst, w});
        function<void(int,int,int)> rec = [&](int v, int lo, int hi) {
            if (r < lo || hi < l) return;
            if (l <= lo && hi <= r) { g[v].push_back({mid, 0}); return; }
            int m = (lo + hi) / 2;
            rec(2 * v, lo, m); rec(2 * v + 1, m + 1, hi);
        };
        rec(1, 0, T - 1);
    }
    vector<ll> dijkstra(int src) {
        vector<ll> d(g.size(), LLONG_MAX);
        priority_queue<pair<ll,int>, vector<pair<ll,int>>, greater<>> pq;
        d[src] = 0; pq.push({0, src});
        while (!pq.empty()) {
            auto [du, u] = pq.top(); pq.pop();
            if (du > d[u]) continue;
            for (auto [v, w] : g[u]) if (du + w < d[v]) { d[v] = du + w; pq.push({d[v], v}); }
        }
        return d;
    }
};

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);
    int N, K; cin >> N >> K;
    SegGraph G(N);
    int src = G.leaf(0);   // [verify start node convention] cpid=1164
    for (int i = 0; i < K; ++i) {
        int a, b, p; ll c; cin >> a >> b >> p >> c;
        G.addIntervalEdge(a - 1, b - 1, G.leaf(p - 1), c);
    }
    auto d = G.dijkstra(src);
    for (int i = 0; i < N; ++i) cout << (d[G.leaf(i)] == LLONG_MAX ? -1 : d[G.leaf(i)]) << "\n";
}

Pitfalls

Problem 2 — Paired Up

Statement

Platinum-scale variant of the Gold problem. N entries each describe a contiguous block of cows of one breed with a given weight, so the total number of cows can be up to 1018. Pair Holsteins with Guernseys whose weights differ by ≤ K. Maximize total weight of paired cows.

Subtask structure (typical)

Constraints

Key idea

Sort blocks by weight. Sweep with a DP that tracks the count of "open" Holsteins and Guernseys still inside the K-window. With block sizes up to 1018 you cannot enumerate individual cows; instead, transitions per block come in 3 forms (pair as many as possible with opposite open; absorb extra count as new open; let some expire). Each transition takes O(1) per block using a difference on min(opensOfOther, blockSize). Maintain the answer in a segment tree keyed by weight.

Complexity target

O(N log N · log W) or O(N² log N) depending on which subtask you target.

Reference solution (C++)

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

// Block sweep: sort by weight, maintain a "queue" of opens per breed within
// the K-window. When a new block arrives we can pair min(blockSize, oppositeOpen)
// cows; the rest of the block becomes open and waits to be paired with a future
// opposite block. When the window slides past an open, that mass is lost.
//
// At Platinum scale we operate on (weight, breed, count) tuples, not individuals.
int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);
    int N; ll K; cin >> N >> K;
    struct B { ll w, n; char b; };
    vector<B> v(N);
    for (auto& x : v) cin >> x.w >> x.n >> x.b;     // [verify input order] cpid=1165
    sort(v.begin(), v.end(), [](auto& a, auto& b){ return a.w < b.w; });

    deque<pair<ll, ll>> openH, openG;       // (weight, count) FIFO
    ll ans = 0;
    auto expire = [&](deque<pair<ll,ll>>& dq, ll curW) {
        while (!dq.empty() && curW - dq.front().first > K) dq.pop_front();
    };
    for (auto& [w, n, b] : v) {
        expire(openH, w); expire(openG, w);
        auto& same  = (b == 'H' ? openH : openG);
        auto& other = (b == 'H' ? openG : openH);
        ll remaining = n;
        // Pair greedily with oldest opposite opens (FIFO preserves window validity).
        while (remaining > 0 && !other.empty()) {
            auto& [ow, oc] = other.front();
            ll take = min(remaining, oc);
            ans += take * (w + ow);
            oc -= take; remaining -= take;
            if (oc == 0) other.pop_front();
        }
        if (remaining > 0) same.push_back({w, remaining});
    }
    cout << ans << "\n";
}

Pitfalls

Problem 3 — HILO

Statement

The Platinum variant of the Gold HILO. You're given a specific permutation of [1..N] (Bessie's guess order). For each prefix length k (or each starting position), output the expected number of HI→LO transitions over the random hidden X, taken modulo 109+7.

Subtask structure (typical)

Constraints

Key idea

Process the permutation positions one by one. For each newly revealed value v, maintain an order-stat structure of seen values so we can, in O(log N), find the "neighbors of v" in the seen set. A HI→LO transition involving X happens exactly when in the permutation order, the predecessor of X among already-seen values switches from a value > X to one < X. For each X this contributes a count determined by how many times its lower-vs-upper neighbor flips as the sweep proceeds. Aggregate over X using a BIT keyed by value.

Complexity target

O(N log N) using order-statistic trees / Fenwick trees.

Reference solution (C++)

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll MOD = 1'000'000'007;

ll pw(ll a, ll e) { ll r = 1; a %= MOD; while (e) { if (e&1) r = r*a%MOD; a = a*a%MOD; e >>= 1; } return r; }

// Sketch only: maintain a Fenwick by value. For each newly revealed v, find
// the largest seen value < v and smallest seen value > v in O(log N) via the
// Fenwick. Each insertion potentially flips the "active predecessor sign" for
// the X strictly between the new neighbors, contributing to that X's HI->LO
// count. Sum contributions weighted by 1/N (uniform X).
int N;
struct BIT { vector<int> t; int n;
    BIT(int n) : t(n + 1, 0), n(n) {}
    void upd(int i, int v) { for (; i <= n; i += i & -i) t[i] += v; }
    int qry(int i) { int s = 0; for (; i > 0; i -= i & -i) s += t[i]; return s; }
    int kth(int k) { int x = 0, lg = __lg(n);
        for (int p = 1 << lg; p; p >>= 1) if (x + p <= n && t[x + p] < k) { x += p; k -= t[x]; }
        return x + 1;
    }
};

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> N;
    vector<int> perm(N); for (auto& x : perm) cin >> x;
    BIT bit(N);
    vector<ll> contrib(N + 2, 0);     // contribution per X
    for (int i = 0; i < N; ++i) {
        int v = perm[i];
        int rank = bit.qry(v - 1);
        int hiNeighbor = (rank == bit.qry(N)) ? N + 1 : bit.kth(rank + 1);
        int loNeighbor = (rank == 0)          ? 0      : bit.kth(rank);
        // Each X strictly between loNeighbor and hiNeighbor gets one flip event
        // when v lands between them. This is a sketch — the full editorial
        // tracks sign of the most recent neighbor explicitly. [verify cpid=1166]
        for (int x = loNeighbor + 1; x < hiNeighbor; ++x) ++contrib[x]; // O(N^2) worst case
        bit.upd(v, 1);
    }
    ll total = 0;
    for (int x = 1; x <= N; ++x) total = (total + contrib[x]) % MOD;
    ll ans = total % MOD * pw((ll)N, MOD - 2) % MOD;
    cout << ans << "\n";
}

Pitfalls

What Platinum December 2021 was actually testing