USACO 2015 December · Platinum · all three problems
← Full December 2015 set · Official results · First-ever Platinum contest.
Problem 1 — Max Flow
Statement
A tree on N stalls with N−1 pipes. K paths of milk are routed, each given as a pair (s, t); one unit of flow runs along the unique tree path from s to t. Output the maximum total flow through any single node.
Constraints
- 2 ≤ N ≤ 50,000.
- 1 ≤ K ≤ 100,000.
- Time limit: 2s. Memory limit: 256 MB.
Key idea
Tree path addition via LCA + node difference array. For each path (s, t) with L = LCA(s, t), diff[s]++, diff[t]++, diff[L]−−, diff[parent(L)]−− (if exists). After all paths, do a post-order DFS: flow[v] = diff[v] + Σ flow[children]. Answer = max flow[v]. Use binary-lifting LCA in O((N + K) log N).
Complexity target
O((N + K) log N).
Reference solution (C++)
#include <bits/stdc++.h>
using namespace std;
const int LG = 17;
int N, K;
vector<int> adj[50005];
int up[50005][LG], depth_[50005], par[50005];
long long diff_[50005], flow_[50005];
int order_[50005], oi = 0;
void bfs_root() { // iterative BFS to fill parents/depth in a topological order
queue<int> q; q.push(1); par[1] = 0; depth_[1] = 0;
vector<char> seen(N+1,0); seen[1] = 1;
while (!q.empty()) {
int v = q.front(); q.pop(); order_[oi++] = v;
for (int u : adj[v]) if (!seen[u]) { seen[u]=1; par[u]=v; depth_[u]=depth_[v]+1; q.push(u); }
}
}
int lca(int a, int b) {
if (depth_[a] < depth_[b]) swap(a, b);
int d = depth_[a] - depth_[b];
for (int k = 0; k < LG; ++k) if (d & (1 << k)) a = up[a][k];
if (a == b) return a;
for (int k = LG-1; k >= 0; --k) if (up[a][k] != up[b][k]) { a = up[a][k]; b = up[b][k]; }
return up[a][0];
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cin >> N >> K;
for (int i = 0; i < N-1; ++i) {
int a, b; cin >> a >> b;
adj[a].push_back(b); adj[b].push_back(a);
}
bfs_root();
for (int v = 1; v <= N; ++v) up[v][0] = par[v];
for (int k = 1; k < LG; ++k)
for (int v = 1; v <= N; ++v) up[v][k] = up[up[v][k-1]][k-1];
while (K--) {
int s, t; cin >> s >> t;
int L = lca(s, t);
diff_[s]++; diff_[t]++; diff_[L]--; if (par[L]) diff_[par[L]]--;
}
// accumulate flow in reverse BFS order (children before parent).
for (int i = oi - 1; i >= 0; --i) {
int v = order_[i];
flow_[v] += diff_[v];
if (par[v]) flow_[par[v]] += flow_[v];
}
long long ans = 0;
for (int v = 1; v <= N; ++v) ans = max(ans, flow_[v]);
cout << ans << "\n";
}
Pitfalls
- Subtract at LCA AND its parent. The standard node-diff for path s→t adds at s and t, subtracts at LCA and parent(LCA). Forgetting parent(LCA) double-counts the LCA's subtree.
- Accumulate in post-order. Reverse BFS order works because parent is visited last.
- Recursive DFS will stack-overflow at N=50,000. Use iterative BFS to record order, then accumulate in reverse.
- K up to 10⁵, N up to 5·10⁴. Per-path LCA must be O(log N); naive walking blows up.
Problem 2 — High Card Low Card (Platinum)
Statement
The Gold version's setup, but Bessie chooses the split: after seeing Elsie's full sequence, she picks a round index k ∈ [0, N] such that rounds 1..k are "high wins" and rounds k+1..N are "low wins". She then permutes her hand optimally. Output the maximum number of rounds she can win across all k.
Constraints
- 2 ≤ N ≤ 50,000.
- Time limit: 2s. Memory limit: 256 MB.
Key idea
Precompute hi[k] = max wins on Elsie's prefix of length k under high-wins rules, using Bessie's "high cards" (top j of her hand for some j). And lo[k] = max wins on Elsie's suffix starting at k+1 under low-wins rules, using Bessie's "low cards". For each split k, Bessie can give the top j cards of her sorted hand to the high half and the rest to the low half — best is to scan and choose. A clean approach: for fixed prefix length k, Bessie's high-pool optimum uses her largest j = k cards; her low-pool optimum uses her remaining N − k cards. Each can be computed in O(N log N) with a sweep using a multiset; then iterate k and report the max sum. Total O(N log N).
Complexity target
O(N log N).
Reference solution (C++)
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
int N; cin >> N;
vector<int> e(N+1);
vector<bool> taken(2*N + 2, false);
for (int i = 1; i <= N; ++i) { cin >> e[i]; taken[e[i]] = true; }
vector<int> b;
for (int v = 1; v <= 2*N; ++v) if (!taken[v]) b.push_back(v);
sort(b.begin(), b.end()); // ascending
// hi[k] = wins on first k rounds (high-wins) using Bessie's top k cards.
// Process Elsie cards in order; for each new round, add Bessie's next-largest card to "available",
// then greedily win if any available card beats the current Elsie card.
vector<int> hi(N+1, 0);
multiset<int> pool;
for (int k = 1; k <= N; ++k) {
pool.insert(b[N - k]); // add k-th largest of Bessie
auto it = pool.upper_bound(e[k]);
hi[k] = hi[k-1];
if (it != pool.end()) { ++hi[k]; pool.erase(it); }
else pool.erase(pool.begin()); // forced to play a card; sacrifice smallest
}
// lo[k] = wins on rounds k+1..N (low-wins) using Bessie's bottom N-k cards.
// Iterate k from N down to 0; for each step add Bessie's next-smallest card.
vector<int> lo(N+2, 0);
pool.clear();
for (int k = N; k >= 1; --k) {
pool.insert(b[N - 1 - (N - k)]); // = b[k-1]
// Round k is "low wins": want largest Bessie card < e[k].
lo[k-1] = lo[k];
auto it = pool.lower_bound(e[k]);
if (it != pool.begin()) { --it; ++lo[k-1]; pool.erase(it); }
else pool.erase(--pool.end());
}
int ans = 0;
for (int k = 0; k <= N; ++k) ans = max(ans, hi[k] + lo[k]);
cout << ans << "\n";
}
Pitfalls
- The split point is global to the round sequence. Bessie cannot mix rule choices round by round; she commits once.
- Pool allocation by sorted position is the trick. Top k cards for the high prefix of length k; remaining for the low suffix. Any other allocation is dominated.
- "Sacrifice the smallest" when no winning card exists. You still have to play a card; the greedy must consume one even when you lose.
- O(N²) brute force across all k is too slow. Aim for O(N log N) by sweeping in both directions with a multiset.
Problem 3 — Counting Haybales
Statement
N fields in a row, each starting with some haybales. Q operations of three types: M a b — print min of haybales in fields [a, b]. P a b c — add c to each field in [a, b]. S a b — print sum of haybales in [a, b].
Constraints
- 1 ≤ N ≤ 200,000.
- 1 ≤ Q ≤ 100,000.
- Initial haybales per field ≤ 100,000. Added per op c ≤ 100,000.
- Time limit: 2s. Memory limit: 256 MB.
Key idea
Lazy-propagation segment tree storing sum and min per node, with a pending "add" lazy tag. Range-add updates both sum (+= add · len) and min (+= add) for the affected node; push the lazy down to children only when needed.
Complexity target
O((N + Q) log N).
Reference solution (C++)
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 200005;
long long sum_[4*MAXN], mn_[4*MAXN], lazy[4*MAXN];
int N;
void build(int node, int l, int r, vector<long long>& a) {
if (l == r) { sum_[node] = mn_[node] = a[l]; return; }
int m = (l+r)/2;
build(node*2, l, m, a); build(node*2+1, m+1, r, a);
sum_[node] = sum_[node*2] + sum_[node*2+1];
mn_[node] = min(mn_[node*2], mn_[node*2+1]);
}
void push(int node, int l, int r) {
if (!lazy[node]) return;
int m = (l+r)/2;
long long v = lazy[node];
sum_[node*2] += v * (m - l + 1); mn_[node*2] += v; lazy[node*2] += v;
sum_[node*2+1] += v * (r - m); mn_[node*2+1] += v; lazy[node*2+1] += v;
lazy[node] = 0;
}
void update(int node, int l, int r, int ql, int qr, long long v) {
if (qr < l || r < ql) return;
if (ql <= l && r <= qr) {
sum_[node] += v * (r - l + 1); mn_[node] += v; lazy[node] += v; return;
}
push(node, l, r); int m = (l+r)/2;
update(node*2, l, m, ql, qr, v); update(node*2+1, m+1, r, ql, qr, v);
sum_[node] = sum_[node*2] + sum_[node*2+1];
mn_[node] = min(mn_[node*2], mn_[node*2+1]);
}
pair<long long,long long> query(int node, int l, int r, int ql, int qr) {
if (qr < l || r < ql) return {0, LLONG_MAX};
if (ql <= l && r <= qr) return {sum_[node], mn_[node]};
push(node, l, r); int m = (l+r)/2;
auto A = query(node*2, l, m, ql, qr);
auto B = query(node*2+1, m+1, r, ql, qr);
return {A.first + B.first, min(A.second, B.second)};
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
int Q; cin >> N >> Q;
vector<long long> a(N+1);
for (int i = 1; i <= N; ++i) cin >> a[i];
build(1, 1, N, a);
while (Q--) {
char op; int x, y; cin >> op >> x >> y;
if (op == 'M') cout << query(1, 1, N, x, y).second << "\n";
else if (op == 'S') cout << query(1, 1, N, x, y).first << "\n";
else { long long c; cin >> c; update(1, 1, N, x, y, c); }
}
}
Pitfalls
- Sums can exceed int. Worst case ≈ 200,000 fields × (10⁵ initial + 10⁵·10⁵ added) — use long long throughout.
- Lazy must update both sum and min. Sum gets v·len; min and lazy get v. A common bug is forgetting to push to children before recursing.
- Op parsing. The first token is a single character (M/P/S); read as char, not string.
- Don't reach for BIT. A Fenwick tree can do range-add + range-sum but not range-min; segment tree is the simpler unified solution.
What Platinum December 2015 was actually testing
- P1 — tree LCA + difference arrays. Path-add then aggregate to find max node load.
- P2 — optimal split greedy. Pool-allocate Bessie's cards by rank to the two halves; sweep in both directions.
- P3 — segment tree with lazy propagation. Range add, range sum, range min — the textbook trifecta that made this a fitting first-Platinum P3.