USACO 2015 December · Gold · all three problems
Problem 1 — High Card Low Card (Gold)
Statement
A deck has 2N cards numbered 1…2N (N even). Elsie holds some N of them and plays them in a known order over N rounds; Bessie has the remaining N and can permute. In the first N/2 rounds the higher card wins; in the last N/2 rounds the lower card wins. Output the maximum number of rounds Bessie can win.
Constraints
- 2 ≤ N ≤ 50,000 (N even).
- Time limit: 2s. Memory limit: 256 MB.
Key idea
Because Bessie permutes, the rounds split cleanly: the first N/2 of Elsie's plays go to the "high wins" pool, the last N/2 go to the "low wins" pool. For high-wins, run the standard horse-race greedy on those N/2 cards vs Bessie's hand. For low-wins, mirror: for each Elsie card (descending), use Bessie's largest remaining card < it. Sum the two. The catch: Bessie must commit which cards she'll use in each half — but since both halves are greedy and structurally symmetric, the optimal split is to give the high half her largest k cards (for some k chosen by sorting). In practice, treat the two halves independently using a multiset of Bessie's cards.
Complexity target
O(N log N) sort + multiset operations.
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> elsie(N);
vector<bool> taken(2*N + 1, false);
for (auto& e : elsie) { cin >> e; taken[e] = true; }
// Greedy: assign Bessie's largest cards to high-half rounds, smallest to low-half.
vector<int> bessie;
for (int v = 1; v <= 2*N; ++v) if (!taken[v]) bessie.push_back(v);
sort(bessie.begin(), bessie.end());
int half = N / 2;
// Bessie's "high" pool = top `half` cards; "low" pool = bottom `half`.
multiset<int> hi(bessie.begin() + half, bessie.end());
multiset<int> lo(bessie.begin(), bessie.begin() + half);
int wins = 0;
// First half: high wins. For each Elsie card find smallest hi card > it.
for (int i = 0; i < half; ++i) {
auto it = hi.upper_bound(elsie[i]);
if (it != hi.end()) { ++wins; hi.erase(it); }
else hi.erase(hi.begin());
}
// Second half: low wins. For each Elsie card find largest lo card < it.
for (int i = half; i < N; ++i) {
auto it = lo.lower_bound(elsie[i]); // first >= elsie[i]
if (it != lo.begin()) { --it; ++wins; lo.erase(it); }
else lo.erase(lo.begin());
}
cout << wins << "\n";
}
Pitfalls
- The pool split is the key insight. Bessie should use her biggest cards in the high half and her smallest in the low half — by an exchange argument, any swap can only hurt or break even.
- Strict comparison. Cards are distinct; "higher" and "lower" are strict.
- Elsie's order matters within a half. The greedy operates on the original per-round sequence, not a sorted version of Elsie's half.
- Don't try a single global greedy. The two halves have opposite preferences; mixing them is wrong.
Problem 2 — Fruit Feast
Statement
Bessie's fullness starts at 0, capped at T. She can eat oranges (+A fullness) and lemons (+B fullness) unlimited times. At most once she can drink water, which halves her current fullness (floor). Eating a fruit that would push her over T is forbidden. Output the maximum final fullness she can reach.
Constraints
- 1 ≤ T ≤ 5,000,000.
- 1 ≤ A, B ≤ T.
- Time limit: 2s. Memory limit: 256 MB.
Key idea
Two BFS-style passes over fullness states. reach1[v] = true if v is reachable without ever drinking water; compute by iterating v from 0 up: if reach1[v] then reach1[v+A] and reach1[v+B] (if ≤ T). Then for each v with reach1[v], you may drink water, landing at v/2; mark reach2[v/2] = true. From every reach2 state, propagate +A/+B again. The answer is max v with reach1[v] OR reach2[v].
Complexity target
O(T) time and memory.
Reference solution (C++)
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
int T, A, B; cin >> T >> A >> B;
vector<char> r1(T + 1, 0), r2(T + 1, 0);
r1[0] = 1;
for (int v = 0; v <= T; ++v) {
if (!r1[v]) continue;
if (v + A <= T) r1[v + A] = 1;
if (v + B <= T) r1[v + B] = 1;
}
// Drink water at any reach1 state.
for (int v = 0; v <= T; ++v) if (r1[v]) r2[v / 2] = 1;
for (int v = 0; v <= T; ++v) {
if (!r2[v]) continue;
if (v + A <= T) r2[v + A] = 1;
if (v + B <= T) r2[v + B] = 1;
}
int ans = 0;
for (int v = 0; v <= T; ++v) if (r1[v] || r2[v]) ans = v;
cout << ans << "\n";
}
Pitfalls
- Water is optional. Don't forget to consider not drinking it — that's the r1 pass.
- Halving is floor. 7/2 = 3, not 4. Integer division in C++ matches the spec for non-negatives.
- State space is fullness, not "fruits eaten". Two BFS passes over [0, T] suffice; don't try to enumerate counts.
- Memory. 2 × (T+1) chars = 10 MB at T=5·10⁶; fine inside the 256 MB cap.
Problem 3 — Bessie's Dream
Statement
On an N×M grid Bessie starts at (1,1) and wants to reach (N,M) in the minimum number of moves. Tiles: 0 red (impassable), 1 pink (normal), 2 orange (grants "orange smell"), 3 blue (passable only when she has smell), 4 purple (when entered, slides her one tile further in the same direction repeatedly until she's not on purple; also clears smell). Each move costs 1. Output the minimum moves or -1.
Constraints
- 1 ≤ N, M ≤ 1000.
- Time limit: 2s. Memory limit: 256 MB.
Key idea
BFS on a 3D state (r, c, smell). Smell is a 0/1 bit; orange tiles set it to 1; purple slides clear it. Step cost is uniform → ordinary BFS works. When attempting to step into a purple tile, simulate the slide in the current direction until off purple or off-grid; if the landing tile is illegal (red, or blue without smell) the move is forbidden. Smell flips ON if you end on orange, OFF if you traversed purple.
Complexity target
O(N · M · 2 · 4) edges = ~8·10⁷ in the worst case; feasible in 2s with tight code.
Reference solution (C++)
#include <bits/stdc++.h>
using namespace std;
int N, M;
vector<string> rowbuf;
int g[1005][1005];
int dist[1005][1005][2];
int dr[] = {-1,1,0,0}, dc[] = {0,0,-1,1};
int main() {
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cin >> N >> M;
for (int i = 0; i < N; ++i)
for (int j = 0; j < M; ++j) cin >> g[i][j];
memset(dist, 0x3f, sizeof dist);
// start: (0,0), smell = 0 if pink; if orange, smell = 1.
int s0 = (g[0][0] == 2) ? 1 : 0;
dist[0][0][s0] = 0;
queue<tuple<int,int,int>> q; q.push({0, 0, s0});
while (!q.empty()) {
auto [r, c, s] = q.front(); q.pop();
int d = dist[r][c][s];
for (int k = 0; k < 4; ++k) {
int nr = r + dr[k], nc = c + dc[k], ns = s;
if (nr<0||nc<0||nr>=N||nc>=M) continue;
if (g[nr][nc] == 0) continue; // red: blocked
if (g[nr][nc] == 3 && ns == 0) continue; // blue: need smell
if (g[nr][nc] == 4) { // purple: slide
ns = 0;
while (nr>=0&&nc>=0&&nr<N&&nc<M && g[nr][nc] == 4) { nr += dr[k]; nc += dc[k]; }
if (nr<0||nc<0||nr>=N||nc>=M) continue;
if (g[nr][nc] == 0) continue;
if (g[nr][nc] == 3) continue; // blue after slide with no smell
}
if (g[nr][nc] == 2) ns = 1;
if (d + 1 < dist[nr][nc][ns]) {
dist[nr][nc][ns] = d + 1;
q.push({nr, nc, ns});
}
}
}
int ans = min(dist[N-1][M-1][0], dist[N-1][M-1][1]);
cout << (ans >= 0x3f3f3f3f ? -1 : ans) << "\n";
}
Pitfalls
- Purple costs 1 move total, not one per tile slid. The whole slide is a single step.
- Slide clears smell. Even if you slide over a tile, you can't pick up new smell mid-slide.
- Blue check happens at the landing tile of a slide too. If the slide ends on a blue tile with no smell, the move is illegal.
- State is (r, c, smell) — 2× the memory, not 1×. Forgetting this either OOMs or gives wrong answers.
What Gold December 2015 was actually testing
- P1 — multi-criterion greedy with pool splitting. Two halves of the round have opposite winning rules; assign Bessie's hand accordingly.
- P2 — BFS over a numeric state space. Treat fullness as the BFS node; one optional water move doubles the state.
- P3 — state-aware grid BFS. Smell adds a binary dimension; purple tiles compress into single-move slides.