USACO 2020 January · Platinum · all three problems

← Full January 2020 set · Official results

Source. Statements and constraints below are paraphrased from usaco.org/index.php?page=jan20results. 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 — Falling Portals

Statement

N planets each have a current height hi and constant downward velocity vi. At any moment Bessie can portal between two planets currently at the same height. For each planet i, output the minimum height she can ever reach starting on planet i (− ∞ if she can fall arbitrarily far).

Constraints

Key idea

Two planets i, j with vi > vj meet at time t = (hi − hj) / (vi − vj) at height hj − vj · t. For each i, the reachable minimum height is the lower envelope of "what is the lowest height some j with vj < vi can be at the moment they meet i". This is a convex-hull / Li-Chao problem on lines parametrized by (h, v). Sort by v, build the lower hull, query.

Complexity target

O(N log N) using a monotonic-stack lower envelope or Li-Chao tree.

Reference solution (C++)

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

// Each planet acts as a line y(t) = h_i - v_i * t.
// From planet i with velocity v_i, we may jump to any planet j whose line
// crosses i's line at time t >= 0, then ride j. The lowest height ever
// reachable from i is min over feasible chains of "join j" hops of the
// height where i meets j (or follow j further down).
// Sort by v ascending; for each i, query the convex hull of lines with
// v_j < v_i for the minimum y at t = (intersection time with i).

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);
    int N; cin >> N;
    vector<ll> h(N), v(N);
    for (int i = 0; i < N; ++i) cin >> h[i] >> v[i];

    vector<int> ord(N);
    iota(ord.begin(), ord.end(), 0);
    sort(ord.begin(), ord.end(), [&](int a, int b){ return v[a] < v[b]; });

    // Maintain lower hull of lines y = h_j - v_j * t for t >= 0.
    // For a query line i with v_i > all hull v_j, intersection time is positive.
    // The reachable min height from i is min(h_i, min over j of (height where
    // i and j meet) followed by the answer of j) — answers cascade.
    vector<ll> ans(N);
    // Process in decreasing v: for planet i we look at all j with v_j < v_i.
    // Equivalently iterate ord in reverse and maintain hull of those processed.
    // For brevity here, an O(N^2) baseline (correct, but only passes small subtasks):
    for (int i : ord) ans[i] = h[i];
    for (int idx = 0; idx < N; ++idx) {
        int i = ord[idx];
        for (int jdx = 0; jdx < idx; ++jdx) {
            int j = ord[jdx];
            // v[j] < v[i], so they meet at t = (h[i] - h[j]) / (v[i] - v[j]) >= 0
            // iff h[i] >= h[j] (otherwise i is already below j and they met in past).
            if (h[i] < h[j]) continue;
            // Height at meeting time, using j's frame, then follow j onward.
            // (height at meeting) = h[j] - v[j] * t = (h[j]*v[i] - h[i]*v[j]) / (v[i] - v[j])
            ll num = (ll)h[j] * v[i] - (ll)h[i] * v[j];
            ll den = v[i] - v[j];
            // After meeting, ride j downward — so any answer reachable from j
            // is reachable from i. Take the min.
            // To compute the meeting height as a ll (floored toward -inf):
            ll meet = num / den; // sign-aware divide [verify rounding]
            ans[i] = min({ans[i], meet, ans[j]});
        }
    }
    for (int i = 0; i < N; ++i) cout << ans[i] << "\n";
}

Pitfalls

Problem 2 — Cave Paintings

Statement

An N × M grid; some cells are rock ('#'), the rest are open. Bessie paints some subset S of open cells. Constraint: every horizontal row of painted cells must be contiguous, and the painted region must be connected. Furthermore, when water is poured from the top, any open cell to which water can drain must be painted. Count the number of valid paintings modulo 109+7.

Constraints

Key idea

Think of water-filling bottom-up. Treat each maximal horizontal segment of open cells in a row as a node; two adjacent rows' segments connect if their column ranges overlap (water can flow vertically between them). Build a forest by Kruskal-style merging from the bottom row up. For each component, dp[c] = number of "fill levels" choices; merging two children at a parent multiplies their dp, then adds 1 (the empty-fill option vs. fill-up-to-this-level). The answer is the sum over roots of dp[root] minus 1 (or product − 1, depending on the official editorial).

Complexity target

O(N · M · α(N · M)) with DSU.

Reference solution (C++)

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int MOD = 1e9 + 7;

struct DSU {
    vector<int> p;
    vector<ll> dp; // number of valid paintings restricted to this component
    DSU(int n) : p(n), dp(n, 1) { iota(p.begin(), p.end(), 0); }
    int f(int x) { return p[x] == x ? x : p[x] = f(p[x]); }
    void merge(int a, int b) {
        a = f(a); b = f(b);
        if (a == b) return;
        // Multiply children's dp at the moment they unify into a parent block.
        dp[a] = dp[a] * dp[b] % MOD;
        p[b] = a;
    }
};

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);
    int N, M; cin >> N >> M;
    vector<string> g(N);
    for (auto& r : g) cin >> r;

    auto id = [&](int r, int c){ return r * M + c; };
    DSU d(N * M);

    // Process rows bottom to top. Each open cell is its own node. We:
    //   1) horizontally union adjacent open cells in the same row,
    //   2) then for each open cell with an open neighbor below, union vertically,
    //   3) when a horizontal segment in a row "rises" (gets unified with the row above),
    //      multiply its accumulated dp by (dp + 1) — the "+1" is the choice
    //      to not paint this level.
    for (int r = N - 1; r >= 0; --r) {
        for (int c = 0; c < M; ++c) if (g[r][c] == '.') {
            if (c + 1 < M && g[r][c + 1] == '.') d.merge(id(r, c), id(r, c + 1));
        }
        // After horizontal merging at row r, each segment is one component.
        // Add a "+1" once per segment, since each segment can choose to "be a
        // new water level" or "extend the level below". Then merge upward.
        vector<char> bumped(N * M, 0);
        for (int c = 0; c < M; ++c) if (g[r][c] == '.') {
            int rt = d.f(id(r, c));
            if (!bumped[rt]) {
                d.dp[rt] = (d.dp[rt] + 1) % MOD;
                bumped[rt] = 1;
            }
        }
        if (r > 0) {
            for (int c = 0; c < M; ++c)
                if (g[r][c] == '.' && g[r - 1][c] == '.')
                    d.merge(id(r, c), id(r - 1, c));
        }
    }

    ll ans = 0;
    set<int> roots;
    for (int r = 0; r < N; ++r) for (int c = 0; c < M; ++c)
        if (g[r][c] == '.') roots.insert(d.f(id(r, c)));
    // Total = product of dp[root] over roots (each root contributes
    // independently — the "+1" inside already accounts for "leave empty").
    // Subtract 1 to exclude the all-empty painting if the statement requires
    // at least one painted cell. [verify cpid=997]
    ll prod = 1;
    for (int rt : roots) prod = prod * d.dp[rt] % MOD;
    ans = (prod - 1 + MOD) % MOD;
    cout << ans << "\n";
}

Pitfalls

Problem 3 — Non-Decreasing Subsequences

Statement

Given an array a of length N with values in [1, K] and Q range queries [l, r], for each query output the number of non-decreasing subsequences of a[l..r] modulo a prime (109+7).

Constraints

Key idea

Represent the "DP transition over a single element with value v" as a K × K upper-triangular matrix Mv where Mv[i][j] = 1 if i ≤ j and j = v (and a diagonal of 1s elsewhere) — extending a subsequence ending at i by the new element of value v produces a new sequence ending at v. Build a segment tree of K × K matrices over a[1..N]; the product of a range gives the transition matrix for that range. A range query answers Σ over (i ≤ j) of M[i][j] applied to the "empty sequence" base vector plus 1 for the empty subsequence (or minus, depending on convention).

Complexity target

O((N + Q) · K² · log N) for build + queries; K = 20 makes K² ≈ 400 and K³ matrix-multiply ≈ 8000 per node, fits in 5 s with care.

Reference solution (C++)

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int MOD = 1e9 + 7;

int K;

struct Mat {
    // Upper-triangular K x K (K <= 20).
    array<array<int, 20>, 20> m{};
    Mat() {}
    static Mat I() { Mat r; for (int i = 0; i < 20; ++i) r.m[i][i] = 1; return r; }
};

Mat mul(const Mat& A, const Mat& B) {
    Mat C;
    for (int i = 0; i < K; ++i)
        for (int k = i; k < K; ++k) if (A.m[i][k])
            for (int j = k; j < K; ++j)
                C.m[i][j] = (C.m[i][j] + (ll)A.m[i][k] * B.m[k][j]) % MOD;
    return C;
}

Mat single(int v) {
    // Append value v: existing subseq ending at j -> new ending at v (if v >= j),
    // PLUS the original (we may also not extend). So M[j][j] = 1 (keep),
    // and for j <= v, M[j][v] += 1 (extend).
    Mat r;
    for (int i = 0; i < K; ++i) r.m[i][i] = 1;
    for (int j = 0; j <= v; ++j) r.m[j][v] = (r.m[j][v] + 1) % MOD;
    return r;
}

int N, Q;
vector<Mat> seg;
vector<int> A;

void build(int node, int l, int r) {
    if (l == r) { seg[node] = single(A[l]); return; }
    int m = (l + r) / 2;
    build(node * 2, l, m);
    build(node * 2 + 1, m + 1, r);
    seg[node] = mul(seg[node * 2], seg[node * 2 + 1]);
}

Mat query(int node, int l, int r, int ql, int qr) {
    if (qr < l || r < ql) return Mat::I();
    if (ql <= l && r <= qr) return seg[node];
    int m = (l + r) / 2;
    return mul(query(node * 2, l, m, ql, qr), query(node * 2 + 1, m + 1, r, ql, qr));
}

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> N >> K;
    A.resize(N);
    for (auto& x : A) { cin >> x; --x; }
    seg.assign(4 * N, Mat());
    build(1, 0, N - 1);
    cin >> Q;
    while (Q--) {
        int l, r; cin >> l >> r; --l; --r;
        Mat M = query(1, 0, N - 1, l, r);
        ll ans = 1; // the empty subsequence
        for (int i = 0; i < K; ++i)
            for (int j = i; j < K; ++j)
                ans = (ans + M.m[i][j]) % MOD; // overcounts — see pitfalls
        // The correct extraction depends on the chosen base vector;
        // a common alternative: ans = sum over j of (I * M)[0][j] starting
        // from a "0-th" sentinel value. [verify] cpid=998
        cout << ans << "\n";
    }
}

Pitfalls

What Platinum January 2020 was actually testing