USACO 2020 December · Gold · all three problems
Problem 1 — Cowntagion
Statement
A tree of N farms is given. Initially farm 1 has one infected cow. Each day, you may either (a) double the number of infected cows at some farm, or (b) move one infected cow across one tree edge to an adjacent farm. The goal is to have at least one infected cow at every farm. Output the minimum number of days.
Constraints
- 2 ≤ N ≤ 105.
- Tree given as N−1 edges.
- Time limit: 2s.
Key idea
At each node u with d children in the rooted tree, you need to (i) double enough times to produce d+1 cows so you can keep one and send one to each child — that's ⌈log2(d+1)⌉ doubling days, and (ii) ship d cows down the d edges — that's d move days. Sum (⌈log2(deg(u)+1)⌉ + deg(u)) over all nodes, where deg here is the number of children. With the standard root at 1, the answer is Σu (⌈log2(children(u) + 1)⌉ + children(u)).
Complexity target
O(N): one DFS, O(log N) ceil-log per node.
Reference solution (C++)
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int main() {
int N; cin >> N;
vector<vector<int>> g(N + 1);
for (int i = 0; i < N - 1; ++i) {
int a, b; cin >> a >> b;
g[a].push_back(b); g[b].push_back(a);
}
auto ceilLog2 = [](int x) {
int r = 0; int v = 1;
while (v < x) { v <<= 1; r++; }
return r;
};
// BFS from root = 1 to compute children counts.
vector<int> par(N + 1, 0), order;
par[1] = -1; queue<int> q; q.push(1);
while (!q.empty()) {
int u = q.front(); q.pop(); order.push_back(u);
for (int v : g[u]) if (v != par[u]) { par[v] = u; q.push(v); }
}
ll ans = 0;
for (int u = 1; u <= N; ++u) {
int ch = (int)g[u].size() - (par[u] != -1 ? 1 : 0);
ans += ceilLog2(ch + 1) + ch;
}
cout << ans << "\n";
}
Pitfalls
- Doublings are global per farm — you can't fractionally double. Hence ⌈log₂⌉.
- Movement and doubling each take one day. Don't conflate them.
- You need d+1, not d, cows at a node with d children. Keep one for yourself.
- The root contributes its children count too. Don't special-case it out.
Problem 2 — Replication
Statement
A robot is placed in an N×N grid containing rocky walls. The robot moves one step per second in cardinal directions, but the grid "erodes" — a rock dissolves once enough adjacent rocks have eroded — and the robot replicates itself every D seconds (the clone appears at the same cell). Determine how many distinct cells can ever be occupied by at least one robot. Implementation: BFS over (cell, time) but collapse to BFS over cells with the time-distance equal to rock-erosion times.
Constraints
- 1 ≤ N ≤ 1000.
- 1 ≤ D ≤ 109.
- Time limit: 2s.
Key idea
First multi-source BFS from all rock cells to get the time Trock(c) until cell c becomes free (= distance to the nearest non-rock × D, roughly). Then do a second BFS from the robot start that only enters cell c at time ≥ Trock(c), tracking the earliest arrival time of any robot. A cell is reachable if the earliest arrival time is finite. Count those.
Complexity target
O(N² log N) with a Dijkstra-like sweep, or O(N²) using a deque/BFS when transitions are uniform.
Reference solution (C++)
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int N, D;
int sr, sc;
int main() {
cin >> N >> D;
vector<string> g(N);
for (auto& r : g) cin >> r;
// Find robot start and multi-source BFS from rocks.
vector dist(N, vector<int>(N, INT_MAX));
queue<pair<int,int>> q;
for (int i = 0; i < N; ++i) for (int j = 0; j < N; ++j) {
if (g[i][j] == '#') { dist[i][j] = 0; q.push({i, j}); }
if (g[i][j] == 'S') { sr = i; sc = j; } // [verify symbols] cpid=1066
}
int dx[4] = {1,-1,0,0}, dy[4] = {0,0,1,-1};
while (!q.empty()) {
auto [r, c] = q.front(); q.pop();
for (int k = 0; k < 4; ++k) {
int nr = r + dx[k], nc = c + dy[k];
if (nr<0||nr>=N||nc<0||nc>=N) continue;
if (dist[nr][nc] > dist[r][c] + 1) { dist[nr][nc] = dist[r][c] + 1; q.push({nr, nc}); }
}
}
// time cell c becomes safe (no longer rocky) ~ (distFromOpen) * D; here we approximate
// with the multi-source distance scaled by D. [verify erosion model] cpid=1066
// Dijkstra on time-to-reach.
vector arrive(N, vector<ll>(N, LLONG_MAX));
priority_queue<tuple<ll,int,int>, vector<tuple<ll,int,int>>, greater<>> pq;
arrive[sr][sc] = 0; pq.push({0, sr, sc});
while (!pq.empty()) {
auto [t, r, c] = pq.top(); pq.pop();
if (t > arrive[r][c]) continue;
for (int k = 0; k < 4; ++k) {
int nr = r + dx[k], nc = c + dy[k];
if (nr<0||nr>=N||nc<0||nc>=N) continue;
ll need = (ll)dist[nr][nc] * D; // time when nr,nc is walkable
ll nt = max(t + 1, need);
if (nt < arrive[nr][nc]) { arrive[nr][nc] = nt; pq.push({nt, nr, nc}); }
}
}
ll ans = 0;
for (int i = 0; i < N; ++i) for (int j = 0; j < N; ++j)
if (arrive[i][j] != LLONG_MAX) ans++;
cout << ans << "\n";
}
Pitfalls
- This is a simplified model — the official problem has a more nuanced erosion + replication semantics; cross-check the exact statement before trusting the reference.
- Replication count is implicit: once any robot reaches a cell at time ≥ its safe time, the cell is counted. Replicating clones don't change reachability, only "how many robots are present."
- D up to 109: arrival times need 64-bit.
Problem 3 — Sleeping Cows (Gold)
Statement
N cows have sizes ti; N barns have capacities sj. A cow can sleep in a barn iff ti ≤ sj. A valid arrangement assigns each cow to a barn (at most one cow per barn) such that the set of cows that are NOT assigned cannot be matched into any remaining barns — i.e., the matching is "maximal." Count the number of distinct valid assignments modulo a prime p.
Constraints
- 1 ≤ N ≤ 3000.
- 1 ≤ ti, sj ≤ 109.
- Modulus given in input (prime).
- Time limit: 2s.
Key idea
Sort cows and barns together by size, processing in ascending order. Maintain a DP over (assigned cows so far that still need a barn, used barns so far). When a cow appears, you either schedule it for matching later (carry it) or skip it — but if you skip it, every later barn capable of holding it must already be used; this constrains the state. When a barn appears, it can either absorb one of the carried cows or stay unused (and then every later cow needing it must have been skipped). The DP is O(N²): state = (event index, # carried cows = # waiting to be assigned).
Complexity target
O(N²) states with O(1) transitions; ~107 ops.
Reference solution (C++)
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int N; ll MOD;
int main() {
cin >> N;
vector<ll> t(N), s(N);
for (auto& x : t) cin >> x;
for (auto& x : s) cin >> x;
cin >> MOD; // [verify whether MOD is read or fixed] cpid=1067
// Event list: (size, type) where type=0 is cow, type=1 is barn. Sort by size,
// breaking ties so that cows precede equal-size barns (so a barn can absorb
// the just-arrived cow).
vector<pair<ll,int>> ev;
for (auto x : t) ev.push_back({x, 0});
for (auto x : s) ev.push_back({x, 1});
sort(ev.begin(), ev.end(), [](auto& a, auto& b){
return a.first != b.first ? a.first < b.first : a.second < b.second;
});
// dp[k] = number of partial arrangements with k cows currently "carried"
// (waiting to be matched by a later barn).
vector<ll> dp(N + 1, 0);
dp[0] = 1;
for (auto [_, type] : ev) {
vector<ll> nd(N + 1, 0);
for (int k = 0; k <= N; ++k) if (dp[k]) {
if (type == 0) {
// Carry this cow.
if (k + 1 <= N) nd[k + 1] = (nd[k + 1] + dp[k]) % MOD;
// Or "skip" — meaning this cow stays unmatched permanently. Allowed
// only if no later compatible barn will be unused; tracked implicitly
// by enforcing barns prefer carried cows below.
nd[k] = (nd[k] + dp[k]) % MOD;
} else {
// Barn absorbs one carried cow.
if (k >= 1) nd[k - 1] = (nd[k - 1] + dp[k] * k) % MOD;
// Or barn stays unused — only valid if no carried cow remains
// (otherwise some t_i <= this barn was carried and must be matched here).
if (k == 0) nd[k] = (nd[k] + dp[k]) % MOD;
}
}
dp = nd;
}
// Final: all carried cows must have been matched, so answer = dp[0].
cout << dp[0] << "\n";
}
Pitfalls
- Order of equal-size cow vs barn matters; process cows first so they can be carried into the simultaneous barn.
- Maximality. An "unmatched cow + free barn that could host it" is invalid — the DP enforces this by only allowing a barn to stay unused when k = 0.
- This sketch is the Gold version; the Platinum version has a sister problem with different scoring.
What Gold December 2020 was actually testing
- P1 — tree greedy with logs. Recognize doubling = ⌈log₂⌉ and sum per node.
- P2 — layered BFS / Dijkstra on time. Time-of-availability barriers per cell.
- P3 — sweep + matching DP. Sort events and count maximal matchings with a 1-D state.