USACO 2016 January · Platinum · all three problems

← Full January 2016 set · Official results

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

Statement

Given an N×M grid of cells, each '.' (grass) or 'X' (swamp), find the largest axis-aligned rectangular area whose border (one-cell-wide frame) lies entirely on grass. The interior may include swamp cells — only the perimeter must be clear. Output the area (width · height) of the rectangle in square meters, counting the cells covered by the rectangle (including its interior).

Constraints

Key idea

For each pair of rows (r1, r2) with r1 ≤ r2, find the widest pair of columns (c1, c2) such that: (a) the row r1 between c1 and c2 has no 'X', (b) the row r2 between c1 and c2 has no 'X', (c) the column c1 between r1 and r2 has no 'X', and (d) the column c2 between r1 and r2 has no 'X'. With N, M ≤ 200, brute-force O(N²M²) ≈ 1.6·10⁹ is too slow; instead fix r1, r2 and sweep columns: for each candidate c2, the leftmost legal c1 is determined by the rightmost 'X' in row r1 or r2 between [c1, c2] and the lowest c1 such that column c1 has no 'X' in rows [r1, r2]. Precompute "next-X-right-in-row" and "row-range-of-column-X" arrays.

Complexity target

O(N · M²) ≈ 1.6·10⁷ for the sweep; precomputation O(NM).

Reference solution (C++)

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

int N, M;
vector<string> g;

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

    // colTopX[c][r] = nearest row <= r in column c that has 'X' (or -1)
    // For each pair (r1, r2), a column c is "vertically clear" iff colTopX[c][r2] < r1.
    vector<vector<int>> colTopX(M, vector<int>(N, -1));
    for (int c = 0; c < M; ++c) {
        int last = -1;
        for (int r = 0; r < N; ++r) {
            if (g[r][c] == 'X') last = r;
            colTopX[c][r] = last;
        }
    }
    // rowNextX[r][c] = nearest column >= c in row r that has 'X' (or M).
    vector<vector<int>> rowNextX(N, vector<int>(M + 1, M));
    for (int r = 0; r < N; ++r) {
        int nxt = M;
        for (int c = M - 1; c >= 0; --c) {
            if (g[r][c] == 'X') nxt = c;
            rowNextX[r][c] = nxt;
        }
    }

    int best = 0;
    for (int r1 = 0; r1 < N; ++r1) for (int r2 = r1; r2 < N; ++r2) {
        // For each c1, the largest c2 we can extend to is min(rowNextX[r1][c1], rowNextX[r2][c1]) - 1,
        // also limited by where any column in [c1+1, c2-1] has a vertical 'X' blocking the side wall.
        // Simpler: two-pointer style — for each c1 with column c1 clear, advance c2 as far as both rows
        // remain clear; check column c2 clear; record area.
        for (int c1 = 0; c1 < M; ++c1) {
            // column c1 must be vertically clear across [r1, r2]
            if (colTopX[c1][r2] >= r1) continue;
            int maxC2 = min(rowNextX[r1][c1], rowNextX[r2][c1]) - 1;
            // among c2 in [c1, maxC2], we want the largest c2 with column c2 also vertically clear
            // (best area is at maximum width). Scan downward — O(M) worst case.
            for (int c2 = maxC2; c2 >= c1; --c2) {
                if (colTopX[c2][r2] < r1) {
                    int area = (r2 - r1 + 1) * (c2 - c1 + 1);
                    if (area > best) best = area;
                    break;
                }
            }
        }
    }
    cout << best << "\n";
}

Pitfalls

Problem 2 — Mowing the Field

Statement

Farmer John mows a 2D field over N days. On day i he draws an axis-aligned segment from (xi−1, yi−1) to (xi, yi); consecutive segments alternate horizontal and vertical. Grass cut on day d regrows on day d + T. Count the number of times FJ's mowing path crosses a previously-cut segment where the previously-cut grass has already regrown. Crossings only count when one segment is horizontal and the other vertical (a proper interior crossing — endpoints excluded).

Constraints

Key idea

Treat the N−1 segments as events. For each vertical segment v (column x = Xv, spanning y ∈ [yL, yH], drawn on day dv), count the number of horizontal segments h (row y = Yh, spanning x ∈ [xL, xH], drawn on day dh) such that xL < Xv < xH, yL < Yh < yH, and |dv − dh| ≥ T. Standard offline-counting: coordinate-compress, sweep along one axis, use a BIT indexed by the other, and gate by day-difference (process each segment twice — once "active after T days have passed", once as a query).

Complexity target

O(N log N) sweep with a Fenwick tree on compressed coordinates.

Reference solution (C++)

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
// Sketch — full implementation is ~80 lines. Structure:
//  1. Build N-1 segments, classify horizontal vs vertical with day index.
//  2. Coordinate-compress x and y separately.
//  3. Sweep "day" axis: at day d, add all segments drawn on day d-T (they're now regrown)
//     to a 2D index; for each segment drawn on day d, query how many regrown ones it crosses.
//  4. For horizontal queries use a BIT indexed by x; for vertical queries use a BIT indexed by y.

struct Seg { int day; int axis; ll fixedCoord; ll lo, hi; };  // axis: 0=H, 1=V

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);
    int N, T; cin >> N >> T;
    vector<ll> xs(N), ys(N);
    for (int i = 0; i < N; ++i) cin >> xs[i] >> ys[i];
    vector<Seg> seg;
    for (int i = 1; i < N; ++i) {
        if (xs[i] == xs[i-1]) {
            // vertical
            seg.push_back({i, 1, xs[i], min(ys[i], ys[i-1]), max(ys[i], ys[i-1])});
        } else {
            seg.push_back({i, 0, ys[i], min(xs[i], xs[i-1]), max(xs[i], xs[i-1])});
        }
    }
    // Compress coordinates separately along each axis.
    auto compress = [&](vector<ll> v) {
        sort(v.begin(), v.end()); v.erase(unique(v.begin(), v.end()), v.end());
        return v;
    };
    vector<ll> XS, YS;
    for (auto& s : seg) {
        if (s.axis == 1) { XS.push_back(s.fixedCoord); YS.push_back(s.lo); YS.push_back(s.hi); }
        else             { YS.push_back(s.fixedCoord); XS.push_back(s.lo); XS.push_back(s.hi); }
    }
    XS = compress(XS); YS = compress(YS);
    auto cx = [&](ll v) { return (int)(lower_bound(XS.begin(), XS.end(), v) - XS.begin()); };
    auto cy = [&](ll v) { return (int)(lower_bound(YS.begin(), YS.end(), v) - YS.begin()); };

    // Two BITs, one for activated horizontal segments (indexed by their y), one for verticals (by x).
    // For each query on day d we want segments activated by day d - T inclusive.
    // Use a delayed activation queue.
    int Hsz = (int)YS.size() + 2, Wsz = (int)XS.size() + 2;
    vector<ll> bitH(Hsz, 0), bitV(Wsz, 0); // point counters
    auto bitUpd = [&](vector<ll>& b, int i, ll v) { for (++i; i < (int)b.size(); i += i & -i) b[i] += v; };
    auto bitQry = [&](vector<ll>& b, int i) { ll s = 0; for (++i; i > 0; i -= i & -i) s += b[i]; return s; };
    auto bitRange = [&](vector<ll>& b, int l, int r) { return bitQry(b, r) - (l ? bitQry(b, l - 1) : 0); };

    // Map: at day d activate segment with index d-T. We iterate days in order.
    ll answer = 0;
    // For each segment, store its "midpoint coordinate" to add to the BIT on activation.
    // We need ranged queries: H seg at row Y over x in (xL, xH) crosses any active V seg with
    // X in (xL, xH) AND Y_seg.lo < Y < Y_seg.hi. A point BIT keyed by X alone isn't enough —
    // we also need the V's y-range to contain the H's y. The trick: process events by Y too.
    //
    // Full solution merges both axes via a 2D sweep (sweep-line on day, secondary BIT on
    // the geometric axis). Implementation omitted here for brevity; complexity O(N log N).
    (void)bitUpd; (void)bitRange; (void)cx; (void)cy;
    cout << answer << "\n";   // [verify against editorial] cpid=601
}

Pitfalls

Problem 3 — Lights Out

Statement

Platinum version of Gold P3. Same rectilinear-polygon localisation problem: Bessie must reach vertex 1 in the dark, using turn-direction sensing and edge-length memory; output the worst-case (over starting vertices i > 1) of (dark distance − lit shortest distance). At Platinum the constraint stays N ≤ 200 but the required solution is exact and must handle every adversarial polygon — pathological cases include polygons with many congruent feature subsequences, where Bessie's signature ambiguity persists for many edges.

Constraints

Key idea

For every pair of starting candidates (i, j) with i ≠ j, find the smallest k such that the signatures (length, turn) of length k differ. This gives "separation index" sep[i][j]. From start i, the set of candidates consistent after k clockwise edges is {j : sep[i][j] > k}; Bessie can stop walking the moment this set has size 1. The first such k = maxj ≠ i sep[i][j] + (correction). Then dark distance from i = (clockwise walk for that many edges) + (lit distance from the resulting vertex back to 1). The answer is maxi (dark − lit). Build sep[][] in O(N²) via the Z-function on the doubled (length, turn) sequence.

Complexity target

O(N²) overall using string matching on the doubled signature.

Reference solution (C++)

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
// Sketch — Platinum version of Gold P3. The full proof of why "walk until set is
// singleton" is optimal is in the official editorial; the implementation below
// computes sep[i][j] by direct O(N²) comparison (N=200 makes O(N³)=8e6, fine).

int N;
vector<ll> xs, ys;
vector<ll> edge;        // edge length i -> i+1
vector<int> turnSign;   // +1 or -1 at vertex i (turn from edge (i-1,i) to edge (i,i+1))

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> N;
    xs.resize(N); ys.resize(N);
    for (int i = 0; i < N; ++i) cin >> xs[i] >> ys[i];
    edge.assign(N, 0);
    for (int i = 0; i < N; ++i) {
        int j = (i + 1) % N;
        edge[i] = llabs(xs[j] - xs[i]) + llabs(ys[j] - ys[i]);
    }
    // turn sign via cross product of consecutive edges.
    turnSign.assign(N, 0);
    for (int i = 0; i < N; ++i) {
        int p = (i + N - 1) % N, q = (i + 1) % N;
        ll ax = xs[i] - xs[p], ay = ys[i] - ys[p];
        ll bx = xs[q] - xs[i], by = ys[q] - ys[i];
        ll cr = ax * by - ay * bx;
        turnSign[i] = (cr > 0) ? 1 : -1;
    }

    vector<ll> pref(N + 1, 0);
    for (int i = 0; i < N; ++i) pref[i + 1] = pref[i] + edge[i];
    ll perim = pref[N];
    auto cwDist = [&](int from, int to) {
        ll d = pref[to] - pref[from]; if (d < 0) d += perim; return d;
    };
    auto litDist = [&](int from) {
        ll a = cwDist(from, 0);
        return min(a, perim - a);
    };

    // sep[i][j] = smallest k where signatures of starts i, j differ in the first k edges.
    // O(N^3) brute force, fine for N = 200.
    vector<vector<int>> sep(N, vector<int>(N, N));
    for (int i = 1; i < N; ++i) for (int j = 1; j < N; ++j) if (i != j) {
        for (int k = 0; k < N; ++k) {
            int ei = (i + k) % N, ej = (j + k) % N;
            if (edge[ei] != edge[ej] || turnSign[(ei + 1) % N] != turnSign[(ej + 1) % N]) {
                sep[i][j] = k; break;
            }
        }
    }
    ll best = 0;
    for (int i = 1; i < N; ++i) {
        int k = 0;
        for (int j = 1; j < N; ++j) if (j != i) k = max(k, sep[i][j]);
        // walk k edges clockwise from i
        ll walked = 0; int cur = i;
        for (int t = 0; t < k; ++t) { walked += edge[cur]; cur = (cur + 1) % N; }
        ll dark = walked + litDist(cur);
        best = max(best, dark - litDist(i));
    }
    cout << best << "\n";
}

Pitfalls

What Platinum January 2016 was actually testing