USACO 2018 December · Platinum · all three problems
Problem 1 — Balance Beam
Statement
Bessie stands on a balance beam at integer positions 0..N+1. At each interior position k ∈ [1, N] she can (a) jump off and collect f(k), or (b) flip a fair coin and move to k−1 or k+1 with probability 1/2 each. Reaching 0 or N+1 pays 0. Bessie plays optimally; for each starting position i ∈ [1, N], output ⌊105 · E[optimal payoff from i]⌋.
Constraints
- 2 ≤ N ≤ 105.
- 0 ≤ f(k) ≤ 109.
- Time limit: 2s.
Key idea
Let g(i) be the optimal expected payoff from position i. Then g(0) = g(N+1) = 0 and on the interior g(i) = max(f(i), (g(i−1) + g(i+1)) / 2). The set of points where it is optimal to stop forms the upper concave envelope of f(·) (and the endpoints 0). For i not on the hull, g is linear between adjacent hull stop-points; otherwise g(i) = f(i). Compute the upper convex hull of {(k, f(k)) : 0 ≤ k ≤ N+1} treating boundary as height 0, then interpolate linearly for non-hull points.
Complexity target
O(N) via a monotonic-stack upper-hull pass.
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; cin >> N;
vector<ll> f(N + 2, 0); // f[0] = f[N+1] = 0
for (int i = 1; i <= N; ++i) cin >> f[i];
// Upper convex hull on (x, f[x]) for x = 0..N+1.
// A point is on the hull iff stopping there beats the linear chord through
// the surrounding hull points.
vector<int> hull;
auto cross = [&](int a, int b, int c) -> long long {
// (b - a) x (c - a): positive => left turn
ll x1 = b - a, y1 = f[b] - f[a];
ll x2 = c - a, y2 = f[c] - f[a];
return x1 * y2 - y1 * x2;
};
for (int i = 0; i <= N + 1; ++i) {
while (hull.size() >= 2 && cross(hull[hull.size()-2], hull.back(), i) >= 0)
hull.pop_back();
hull.push_back(i);
}
// For each i in [1, N], find adjacent hull points and interpolate.
int p = 0;
for (int i = 1; i <= N; ++i) {
while (hull[p + 1] < i) ++p;
int L = hull[p], R = hull[p + 1];
// E[payoff] = f[L] + (f[R] - f[L]) * (i - L) / (R - L)
// Scale by 1e5 and floor.
// Use 128-bit to avoid overflow: f up to 1e9, R-L up to 1e5.
__int128 num = (__int128)f[L] * (R - L) * 100000
+ (__int128)(f[R] - f[L]) * (i - L) * 100000;
__int128 den = (__int128)(R - L) * 100000;
// We want floor(1e5 * (f[L] + (f[R]-f[L])*(i-L)/(R-L)))
__int128 numerator = (__int128)100000 * ( (__int128)f[L] * (R - L)
+ (__int128)(f[R] - f[L]) * (i - L) );
__int128 denominator = (R - L);
ll ans = (ll)(numerator / denominator);
cout << ans << "\n";
}
}
Pitfalls
- It's the upper concave envelope, not the convex hull of all points. Boundary heights are 0 and must be included; otherwise the "stop now vs. linear interpolation" comparison is wrong.
- Floor of (105·E), not round. Use integer arithmetic; floating point loses precision when E has long rational parts.
- Overflow. f up to 109, (R−L) up to 105, scale 105: product is ~1019. Use
__int128or careful staging. - Don't run a 109-iteration value iteration. It converges but TLEs hard.
Problem 2 — Sort It Out
Statement
A permutation p[1..N] of 1..N. Choose a subset S of cow IDs and yell at them repeatedly in increasing ID order until p is sorted; the smallest such S has size N − LIS(p). Among all minimum-size sorting subsets, output the K-th lexicographically smallest one (as a sorted list of IDs, one per line, with the size on the first line).
Subtasks
- 3/16: N ≤ 6 and K = 1.
- +5/16 (8/16 total): K = 1.
- +8/16 (16/16): full constraints.
Constraints
- 1 ≤ N ≤ 105, 1 ≤ K ≤ 1018.
- Guaranteed K does not exceed the count of minimum-size subsets.
- Time limit: 2s.
Key idea
Minimum non-yelled set is a longest increasing subsequence of p (the cows you don't yell at must already be in order). So min |S| = N − LIS. Output set = complement of one LIS. Lex-smallest output of S corresponds to lex-largest LIS by values; enumerate LIS extensions greedily, at each value branch multiplying ways using digit-DP-style counts on the LIS layer counts. Walk the K-th branch.
Complexity target
O(N log N) for the LIS structure plus O(N · L) traversal where L = LIS length, with big-integer-ish K capped at 1018.
Reference solution (C++)
// Sketch — full solution is > 50 lines; this is the LIS + counting skeleton.
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll CAP = (ll)2e18;
int N; ll K;
vector<int> p;
int main() {
ios_base::sync_with_stdio(false); cin.tie(nullptr);
cin >> N >> K;
p.resize(N + 1);
for (int i = 1; i <= N; ++i) cin >> p[i];
// Compute LIS length L via patience sorting; lis[i] = length of LIS ending at i.
vector<int> tails, lis(N + 1, 0);
for (int i = 1; i <= N; ++i) {
auto it = lower_bound(tails.begin(), tails.end(), p[i]);
lis[i] = (it - tails.begin()) + 1;
if (it == tails.end()) tails.push_back(p[i]); else *it = p[i];
}
int L = tails.size();
// Bucket indices by lis-layer, in reverse i order, smallest p[i] first so
// we can greedily pick lex-largest LIS values => lex-smallest complement.
vector<vector<int>> layer(L + 2);
for (int i = 1; i <= N; ++i) layer[lis[i]].push_back(i);
// ways[i] = number of LISes extending from i to the end (going through layers
// lis[i]+1..L), respecting i < j and p[i] < p[j]. Cap at CAP.
vector<ll> ways(N + 2, 0);
auto add = [&](ll& a, ll b){ a = min(CAP, a + b); };
// ways for layer L = 1 each.
for (int i : layer[L]) ways[i] = 1;
for (int lvl = L - 1; lvl >= 1; --lvl)
for (int i : layer[lvl])
for (int j : layer[lvl + 1])
if (j > i && p[j] > p[i]) add(ways[i], ways[j]);
// Choose the K-th LIS by walking layer by layer, picking the smallest p[i]
// (so the complement is lex-smallest) whose ways >= K.
vector<bool> inLIS(N + 2, false);
int prev_i = 0, prev_v = 0;
for (int lvl = 1; lvl <= L; ++lvl) {
// candidates in this layer with index > prev_i and value > prev_v,
// sorted by p ascending (so smaller value picked first => complement small).
vector<int> cand;
for (int i : layer[lvl]) if (i > prev_i && p[i] > prev_v) cand.push_back(i);
sort(cand.begin(), cand.end(), [&](int a, int b){ return p[a] < p[b]; });
for (int i : cand) {
if (ways[i] >= K) { inLIS[i] = true; prev_i = i; prev_v = p[i]; break; }
K -= ways[i];
}
}
// Output complement as sorted values.
vector<int> out;
for (int i = 1; i <= N; ++i) if (!inLIS[i]) out.push_back(p[i]);
sort(out.begin(), out.end());
cout << out.size() << "\n";
for (int v : out) cout << v << "\n";
}
Pitfalls
- Output is the set of yelled-at IDs, sorted ascending by value. Not the indices, the cow IDs (= p[i]).
- Lex order is over the set, not over the LIS. Lex-smallest complement ↔ lex-largest LIS at each branch; alternatively, walk the LIS picking the smallest cow ID whose subtree count ≥ K.
- Cap K-counts. Counts blow past 1018; saturating addition keeps comparisons valid.
- Naïve O(N · L) per layer is too slow at L ≈ N. A BIT keyed by p[i] is needed for full credit; the sketch above is the partial-credit shape.
Problem 3 — The Cow Gathering
Statement
A tree on N nodes plus M ordering constraints of the form "ai must leave before bi". Cows depart one by one; at every moment the remaining cows must form a connected subtree (i.e., the departing cow is always a leaf of the current induced subtree). For each cow i ∈ [1, N], output 1 if she can be the last cow to leave under some valid order respecting all constraints, else 0.
Constraints
- 1 ≤ N, M ≤ 105.
- The N−1 edges form a tree.
- Time limit: 2s.
Key idea
Fix any node r as the candidate "last". Then every other cow leaves before r, which forces every constraint edge a → b to point toward r in the tree rooted at r (otherwise b leaves before its required successor a along the path). Equivalently: root the tree at any node, then for each constraint a → b the path from b to a in the rooted tree determines a set of "forbidden roots". With careful DP on the tree (re-rooting + counting violated constraints per candidate root), a node is a valid last iff its violation count is 0. Topological feasibility itself is checked once by a Kahn-style BFS treating the leaves as removable nodes whose constraints are satisfied.
Complexity target
O((N + M) log N) using LCA + re-rooting; O(N + M) with subtree counting.
Reference solution (C++)
// Sketch — re-rooting violation count. Full solution + LCA is > 80 lines.
#include <bits/stdc++.h>
using namespace std;
int N, M;
vector<vector<int>> G;
vector<int> parent, depth, tin, tout;
int timer = 0;
vector<vector<int>> up; // binary lifting
int LOG;
void dfs(int u, int p) {
parent[u] = p; tin[u] = timer++;
up[0][u] = p;
for (int k = 1; k < LOG; ++k) up[k][u] = up[k-1][ up[k-1][u] < 0 ? u : up[k-1][u] ];
for (int v : G[u]) if (v != p) { depth[v] = depth[u] + 1; dfs(v, u); }
tout[u] = timer++;
}
bool isAnc(int u, int v) { return tin[u] <= tin[v] && tout[v] <= tout[u]; }
int main() {
cin >> N >> M;
G.assign(N + 1, {});
parent.assign(N + 1, 0); depth.assign(N + 1, 0);
tin.assign(N + 1, 0); tout.assign(N + 1, 0);
LOG = 1; while ((1 << LOG) < N) ++LOG;
up.assign(LOG, vector<int>(N + 1, 0));
for (int i = 0; i < N - 1; ++i) {
int a, b; cin >> a >> b;
G[a].push_back(b); G[b].push_back(a);
}
dfs(1, 0);
// For each constraint a -> b ("a leaves before b"), candidate roots r for
// which the constraint is satisfied are exactly the nodes "on b's side" when
// the tree is split by the edge on the path from b toward a. Equivalently:
// r is valid for this constraint iff a is on the path from b to r.
// We accumulate +1 over the valid region using subtree difference arrays.
vector<int> mark(N + 2, 0); // sketch only; full impl uses Euler tour diffs
// ... fill mark via LCA-based subtree increments ...
// A node r is a valid "last" iff every constraint marks it -> total = M.
for (int r = 1; r <= N; ++r)
cout << (mark[r] == M ? 1 : 0) << "\n";
}
Pitfalls
- Topological infeasibility short-circuits everything. If the constraints contain a cycle (in the DAG induced on the tree), no node is valid — answer is all zeros. Detect with Kahn's algorithm first.
- "Removable leaf" semantics. A cow can leave only if she has no unsatisfied outgoing constraints AND she is a leaf in the current induced subtree.
- Re-rooting is non-trivial. Many writeups use the trick: pick any feasible last node r0 via the simulation, then valid roots are exactly the nodes reachable in a particular DAG built on the tree edges.
- Don't simulate per candidate root. N · (N + M) = 1010 is far too slow.
What Platinum December 2018 was actually testing
- P1 — optimal stopping ≡ upper concave envelope. Recognize the random-walk Bellman equation as harmonic interpolation between stop points.
- P2 — LIS + K-th lex enumeration. Combinatorial counting with saturating arithmetic for K up to 1018.
- P3 — tree + DAG of constraints. Re-rooting DP, Kahn's algorithm for feasibility, LCA for constraint-region marking.