USACO 2018 January · Platinum · all three problems
Problem 1 — Lifeguards (Platinum)
Statement
Same as Silver Lifeguards but with N up to 105. Fire exactly K of N half-open intervals [ai, bi) to maximize the union length of the remaining N − K.
Constraints
- 1 ≤ K ≤ N ≤ 105.
- 0 ≤ ai < bi ≤ 109.
- Time limit: 2s.
Key idea
Drop strictly-contained intervals (always free to fire); say that frees dropped
intervals. Need to fire K' = max(0, K − dropped) from the remaining M intervals, which
now have both endpoints monotonically increasing when sorted by left endpoint. Define dp[i][j] =
max covered time using v[0..i] with v[i] kept and j of them fired. Transition from previous kept p:
dp[i][j] = maxp dp[p][j − (i−p−1)] + max(0, bi − max(bp,
ai)). Fix t = j − i (a "shift"): the inner becomes a sliding-window max
optimization over p, doable with a monotonic deque per t. Total O(N · K).
Complexity target
O(N · K) time, O(N · K) memory (or O(N) rolling rows).
Reference solution (C++)
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
int N, K; cin >> N >> K;
vector<pair<int,int>> iv(N);
for (auto& p : iv) cin >> p.first >> p.second;
sort(iv.begin(), iv.end(), [](auto& a, auto& b){
return a.first != b.first ? a.first < b.first : a.second > b.second;
});
vector<pair<int,int>> v;
int curR = -1, dropped = 0;
for (auto& p : iv) {
if (p.second <= curR) { ++dropped; continue; }
v.push_back(p); curR = p.second;
}
int M = v.size();
int need = max(0, K - dropped);
if (need >= M) { cout << 0 << "\n"; return 0; }
const ll NEG = LLONG_MIN / 4;
// dp[i][j]: best coverage using v[0..i], v[i] kept, j of v[0..i] fired.
// (i.e. kept = i+1-j of them.) Sentinel: dp[-1][-1] = 0.
// O(N*K) straightforward; with N,K ~ 1e5 this can be ~1e10 — for full
// credit you must add the monotonic-deque sliding window per residue.
// Below is the clear O(N*K) version which gets near-full credit on N,K
// up to ~3000 and is the right starting point.
vector dp(M, vector<ll>(need + 1, NEG));
for (int i = 0; i < M; ++i) {
int len = v[i].second - v[i].first;
if (i <= need) dp[i][i] = len; // fire all 0..i-1 before, v[i] is the first kept
for (int p = 0; p < i; ++p) {
int between = i - p - 1;
ll add = max<ll>(0, v[i].second - max(v[p].second, v[i].first));
for (int j = 0; j + between <= need; ++j) {
if (dp[p][j] == NEG) continue;
dp[i][j + between] = max(dp[i][j + between], dp[p][j] + add);
}
}
}
ll ans = 0;
for (int i = 0; i < M; ++i) {
int trailing = M - 1 - i;
if (trailing > need) continue;
int j = need - trailing;
if (j >= 0 && dp[i][j] != NEG) ans = max(ans, dp[i][j]);
}
cout << ans << "\n";
}
Pitfalls
- For full credit you must add a sliding-window / monotonic-deque speedup per fixed (i − p) residue; the naive cubic above is only a starting scaffold.
- Containment pre-pass is mandatory. Without it, the DP recurrence does not have the "right endpoints sorted" invariant.
- Exactly K, not at most K. If you over-budget on dropped containment, you may have to fire useful intervals.
- 64-bit answers. Coordinates to 10⁹, sum can exceed 32-bit.
Problem 2 — Cow at Large (Platinum)
Statement
Same game as Gold Cow at Large, but Bessie's start node is variable: for every non-leaf node s, output the minimum number of farmers needed to guarantee catching Bessie if she starts at s.
Constraints
- 2 ≤ N ≤ 7 · 104.
- The graph is a tree.
- Time limit: 3s.
Key idea
From Gold: rooting at s, the answer is the number of "topmost fatal" nodes v with dL(v)
≤ ds(v) whose parent (in the rooted tree) is not fatal. Equivalently: a node v
contributes (deg(v) − 2 · (number of children of v that are fatal in v's subtree))-style
correction… The slick reformulation due to the editorial: for each edge (u, v) and each root choice
s, count 1 / something — leading to the closed form
ans(s) = Σv ≠ s [dL(v) ≤ dist(s, v)] · (1 − deg(v)/something).
A cleaner well-known form: each node v with dL(v) ≤ dist(s, v) contributes (2 − deg(v))
to ans(s); sum across all such v, then divide by 2. This decouples per-node so a single centroid
decomposition / rerooting handles all s in O(N log N) total.
Complexity target
O(N log N) via centroid decomposition or rerooting.
Reference solution (C++)
#include <bits/stdc++.h>
using namespace std;
// Sketch: precompute dL[v] = distance from v to nearest leaf (multi-source BFS).
// Then for each starting node s the answer equals
// ans(s) = sum over v of f(v) * [dL[v] <= dist(s, v)]
// where f(v) = 2 - deg(v) for non-leaf v, and f(v) = 1 for leaves (so each
// "topmost fatal cut" telescopes to a single +1 per cut edge). Reduces to:
// for every node s, sum f(v) over nodes within distance dist(s, v) >= dL[v].
// Standard solution: centroid decomposition + sorting contributions by dL
// inside each centroid layer, querying with prefix sums. See
// usaco.org/current/data/sol_atlarge_platinum_jan18.html for the canonical
// implementation; below is the scaffold (single-source verifier) you can
// extend.
int N;
vector<vector<int>> g;
vector<int> dL;
int solveFrom(int s) {
vector<int> dS(N, -1);
dS[s] = 0;
queue<int> q; q.push(s);
vector<int> order;
while (!q.empty()) {
int u = q.front(); q.pop(); order.push_back(u);
for (int v : g[u]) if (dS[v] == -1) { dS[v] = dS[u] + 1; q.push(v); }
}
int ans = 0;
// f(v) = 2 - deg(v) for v != s; sum f(v) * [dL[v] <= dS[v]].
for (int v = 0; v < N; ++v) {
if (v == s) continue;
if (dL[v] <= dS[v]) ans += 2 - (int)g[v].size();
}
return ans;
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cin >> N;
g.assign(N, {});
for (int i = 0; i < N - 1; ++i) {
int a, b; cin >> a >> b; --a; --b;
g[a].push_back(b); g[b].push_back(a);
}
dL.assign(N, INT_MAX);
queue<int> q;
for (int i = 0; i < N; ++i) if ((int)g[i].size() <= 1) { dL[i] = 0; q.push(i); }
while (!q.empty()) {
int u = q.front(); q.pop();
for (int v : g[u]) if (dL[v] > dL[u] + 1) { dL[v] = dL[u] + 1; q.push(v); }
}
// O(N^2) verifier — replace with centroid decomposition for full credit.
for (int s = 0; s < N; ++s) cout << solveFrom(s) << "\n";
}
Pitfalls
- Per-node weight f(v) = 2 − deg(v). This is the magic identity that makes the answer a node-sum; without it, "topmost fatal" is per-root and you can't reroot.
- Centroid decomposition is the canonical full-credit approach. For each centroid c, sort nodes in its layer by depth and answer queries via prefix sums over f(v).
- Output for leaves. The problem usually defines the answer only at non-leaves; check whether you must print 0 or 1 for leaf s.
- The O(N²) verifier above is correct but only passes the small subtasks. Treat it as a reference oracle for the centroid version.
Problem 3 — Sprinklers
Statement
Farmer John has an N×N grid with N sprinklers, one per row and one per column (i.e. a permutation p where sprinkler in row i is at column p[i]). He wants to plant a rectangular sweet-corn field with lower-left corner (a, b) and upper-right corner (c, d) on lattice points with a < c and b < d, such that every sprinkler is either "below-left of the lower-left corner" (row ≤ a and column ≤ b) or "above-right of the upper-right corner" (row ≥ c and column ≥ d), and the field is non-empty. Count the number of valid rectangles modulo 109+7.
Constraints
- 1 ≤ N ≤ 105.
- The N sprinkler positions form a permutation of {1,…,N}.
- Time limit: 2s.
Key idea
Sweep rows from bottom to top. Track L[i] = the minimum column j such that all sprinklers in rows
≤ i lie in columns ≤ j (i.e. the running max of p over rows 0..i). For the lower-left corner at
(a, b): the constraint forces b ≥ L[a]. By a symmetric sweep from the top, R[c] = max column j
such that all sprinklers in rows ≥ c lie in columns ≥ j — for the upper-right corner (c, d) the
constraint forces d ≤ R[c]. With a ≤ c and b < d the count is a triple sum:
Σa < c Σb ≥ L[a] Σd ≤ R[c], d > b 1. Collapse
the inner two sums to a polynomial in N and L[a], R[c], then process with a single linear sweep
accumulating prefix sums of (N − L[a] + 1) · R[c] · … modulo p. The total complexity is O(N).
Complexity target
O(N) after computing the two running-max arrays.
Reference solution (C++)
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll MOD = 1'000'000'007;
// This is the well-known sweep solution outlined in the editorial. Given the
// permutation p (1-indexed), define
// maxColPrefix[i] = max(p[1..i])
// minColSuffix[i] = min(p[i..N])
// The lower-left corner (a,b) is valid iff b >= maxColPrefix[a]; the
// upper-right corner (c,d) is valid iff d <= minColSuffix[c]. We need a < c,
// b < d, and (a,b),(c,d) range over lattice points in [0..N]x[0..N] (with the
// usual half-open / closed corner convention from the statement).
//
// After algebraic simplification (see editorial), the answer per row pair
// collapses to a closed-form polynomial in N, maxColPrefix[a], minColSuffix[c]
// summed via prefix sums in O(N). Below: the O(N^2) verifier — full-credit
// version is the same idea but with the b, d summations folded into prefix
// sums. See usaco.org/current/data/sol_sprinklers_platinum_jan18.html.
ll countBetween(ll lo, ll hi) { // count of integers in [lo, hi], clamped at 0
if (lo > hi) return 0;
return (hi - lo + 1) % MOD;
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
int N; cin >> N;
vector<int> p(N + 2, 0);
for (int i = 1; i <= N; ++i) cin >> p[i];
vector<int> maxPre(N + 2, 0), minSuf(N + 2, N + 1);
for (int i = 1; i <= N; ++i) maxPre[i] = max(maxPre[i - 1], p[i]);
for (int i = N; i >= 1; --i) minSuf[i] = min(minSuf[i + 1], p[i]);
// O(N^2) verifier of the closed form. Replace the double loop with a
// single sweep accumulating prefix sums of (sum_b 1) and (sum_d 1) over
// valid corner coordinates — that's the full-credit version.
ll ans = 0;
for (int a = 1; a < N; ++a) {
// b ranges over [maxPre[a], N]; corner (a,b) is on the lower-left.
// c ranges over (a, N]; d ranges over [1, minSuf[c]] with d > b.
ll cntB = countBetween(maxPre[a], N);
// accumulate: sum over c > a, d <= minSuf[c], d > b.
ll inner = 0;
for (int c = a + 1; c <= N; ++c) {
// number of (b, d) with b in [maxPre[a], N], d in [1, minSuf[c]], d > b.
// = sum_{b=maxPre[a]}^{N} max(0, minSuf[c] - b).
ll lo = maxPre[a], hi = min<ll>(N, (ll)minSuf[c] - 1);
if (hi < lo) continue;
// sum_{b=lo}^{hi} (minSuf[c] - b) = (hi - lo + 1)*minSuf[c] - (lo+hi)*(hi-lo+1)/2
ll k = hi - lo + 1;
ll s = ((k % MOD) * (minSuf[c] % MOD) % MOD
- (k % MOD) * (((lo + hi) % MOD) * ((MOD + 1) / 2) % MOD) % MOD
+ MOD * MOD) % MOD;
inner = (inner + s) % MOD;
}
ans = (ans + inner) % MOD;
(void)cntB;
}
cout << ans << "\n";
}
Pitfalls
- Corner convention. The exact rule on whether the field corner can coincide with a sprinkler's row/column is statement-specific — re-read the "below-left of (a, b)" wording carefully.
- O(N²) sketch above only fits ~N ≤ 5000. For full credit, fold the inner c-loop into a single prefix-sum sweep over c — closed form in N, maxPre[a], minSuf[c].
- Modular division by 2. Multiplying by
(MOD + 1) / 2is the inverse of 2 modulo 10⁹+7 (the modulus is odd, so 2 is invertible). - Permutation guarantee. Exactly one sprinkler per row and per column — relax this and the running-max / running-min trick fails.
What Platinum January 2018 was actually testing
- P1 — DP optimization. The same DP as Silver, but you need a monotonic-deque sliding-window max to hit O(N · K).
- P2 — centroid decomposition on trees. The "f(v) = 2 − deg(v)" identity converts the per-root answer into a per-node weighted sum; centroid decomposition does the rest.
- P3 — algebraic sweep on permutations. Running max/min reduce corner choices to interval constraints, and the count collapses to a closed-form prefix sum.