USACO 2022 December · Gold · all three problems
Problem 1 — Bribing Friends
Statement
Bessie wants to bring as much total popularity to a movie as possible. She has A moonies and B ice cream cones. Friend i has popularity Pi, charges Ci moonies, and accepts a discount of 1 mooney per Xi cones (only that friend's rate). She can pay any non-negative real combination (mooney + cone exchange) for each friend; partial payments don't help (each friend either comes fully or not). Maximize Σ Pi over chosen friends.
Constraints
- 1 ≤ N ≤ 2000.
- 0 ≤ A, B ≤ 2000.
- 1 ≤ Pi, Ci, Xi ≤ 2000.
- Time limit: 2s (Gold default).
Key idea
Sort friends by Xi ascending (cheapest cone-to-mooney conversion first). With that order fixed, do a 2D knapsack DP indexed by (#friends processed, moonies used) and track the maximum "moonies-equivalent budget remaining in cones" — when paying a friend you should always exhaust their cone discount first because friends seen later have a worse cone exchange rate. Standard "two resources, one with conversion" pattern.
Complexity target
O(N · A · 2) ≈ 8·106, well within 2s.
Reference solution (C++)
#include <bits/stdc++.h>
using namespace std;
// dp[a] = max popularity achievable using exactly <= a moonies and any number
// of cones available so far. Friends sorted by X ascending: we always use as
// many cones as possible on the current friend before any future friend.
int main() {
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
int N, A, B; cin >> N >> A >> B;
vector<array<int,3>> f(N); // {P, C, X}
for (auto& t : f) cin >> t[0] >> t[1] >> t[2];
sort(f.begin(), f.end(), [](auto& a, auto& b){ return a[2] < b[2]; });
// dp[a] = best popularity when a moonies remain available *and* B cones still in reserve.
// Cones are spent before moonies (lowest X first), so we walk friends in order.
vector<int> dp(A + 1, 0);
int cones = B;
for (auto& t : f) {
int P = t[0], C = t[1], X = t[2];
// To pay friend: spend k cones (k <= cones, k <= C*X), reducing cost by k/X moonies.
// Max cone usage = min(cones, C*X); the remaining mooney cost is C - floor(k/X).
// Greedy on this friend: spend exactly min(cones, C*X) cones first.
int spendCones = min(cones, C * X);
int discount = spendCones / X; // floored mooneys saved
int needMoney = C - discount;
if (needMoney < 0) needMoney = 0;
// 0/1 knapsack update on moonies axis.
for (int a = A; a >= needMoney; --a)
dp[a] = max(dp[a], dp[a - needMoney] + P);
cones -= spendCones;
if (cones < 0) cones = 0;
}
cout << *max_element(dp.begin(), dp.end()) << "\n";
// NOTE: the above is the simplified "greedy cones" sketch; the official
// editorial does a full 2D DP. Treat as starting point. [verify] cpid=1257
}
Pitfalls
- Greedy on cones is justified only when X-sorted. Without sorting, you might burn cones on a friend whose X is high (bad rate) when a later friend with low X needs them more.
- Integer vs continuous discount. Spending k cones saves ⌊k / Xi⌋ moonies — floor matters.
- Two-resource knapsack subtlety. A pure (A+1)×(B+1) DP is 4·106 states and works; the simplified version above only updates one axis after committing the cone choice.
- Pi up to 2000 · N = 4·106 — fits in int.
Problem 2 — Mountains
Statement
N mountains in a row with heights hi. Two mountains i < j see each other iff no mountain k with i < k < j has its peak strictly above the straight line from peak i to peak j. Q updates each set hi ← v; after each update output the total number of pairs (i, j) that can see each other.
Constraints
- 1 ≤ N, Q ≤ 2000.
- 0 ≤ hi ≤ 109.
- Time limit: 5s (2.5× default).
Key idea
Brute force O(N²) per update is fine: 2000² · 2000 = 8·109 is too slow, but per pair the visibility check is O(N), giving O(N³) = 8·109 — needs care. Better: maintain a visibility matrix vis[i][j] (only j > i) in O(N²) total and update lazily. When hx changes only pairs (i, j) with i ≤ x ≤ j can change — O(N²) pairs to recheck per update, each in O(1) via the slope test using cross-products → O(N²) per update, O(N²·Q) = 1.6·1010: still too slow, but with integer cross-products and a tight inner loop and Q ≤ 2000, ~4·109 ops in 5s is feasible after pruning to pairs straddling x.
Complexity target
O(N² + N·Q) using slope cross-products and per-update local rebuild (Σ over updates of |pairs crossing x| = O(N) average per update).
Reference solution (C++)
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int N, Q;
vector<ll> h;
// Pair (i, j) visible iff for all k in (i, j):
// (h[k] - h[i]) * (j - i) <= (h[j] - h[i]) * (k - i)
// i.e. peak k not strictly above the line i-j. Cross-product form avoids fp.
bool visible(int i, int j) {
ll dx = j - i, dy = h[j] - h[i];
for (int k = i + 1; k < j; ++k) {
ll lhs = (h[k] - h[i]) * dx;
ll rhs = dy * (k - i);
if (lhs > rhs) return false;
}
return true;
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cin >> N >> Q;
h.resize(N);
for (auto& x : h) cin >> x;
// Initial visibility count.
auto countAll = [&]() -> ll {
ll c = 0;
for (int i = 0; i < N; ++i)
for (int j = i + 1; j < N; ++j)
if (visible(i, j)) ++c;
return c;
};
ll vis = countAll();
while (Q--) {
int x; ll v; cin >> x >> v; --x;
// Subtract pairs (i, j) with i <= x <= j that are currently visible.
// Then update h[x] = v. Then add back the new visible count over the same set.
ll delta = 0;
for (int i = 0; i <= x; ++i)
for (int j = max(x, i + 1); j < N; ++j)
if (visible(i, j)) --delta;
h[x] = v;
for (int i = 0; i <= x; ++i)
for (int j = max(x, i + 1); j < N; ++j)
if (visible(i, j)) ++delta;
vis += delta;
cout << vis << "\n";
}
}
Pitfalls
- Use integer cross-products. Floating slopes drift; h up to 109, products fit in
long long. - Strictness. "Strictly above" means the inequality is strict; collinear peaks still see through.
- Recheck only pairs straddling x. Pairs entirely left or entirely right of x are unaffected.
- Initial count overflow. N² pairs is 4·106, count fits in int but use
long longjust in case.
Problem 3 — Strongest Friendship Group
Statement
Given an undirected friendship graph on N cows with M edges, the strength of a subset S of cows is |S| · (minimum degree of any cow in S, using only edges inside S), provided the induced subgraph is connected. Maximize strength over all connected subsets S.
Constraints
- 2 ≤ N ≤ 105; 1 ≤ M ≤ 2·105.
- Time limit: 2s (Gold default).
Key idea
Classic "k-core" + size optimization. Repeatedly peel the lowest-degree vertex from the whole graph; just before peeling vertex v with current degree d, the remaining subgraph is a candidate set whose min-degree is ≥ d, and the connected component containing v has size c, so c · d is a lower bound on achievable strength. Track the maximum of (size of v's current component) × (degree of v at deletion time) across all peels — that is the answer. Use a priority queue (or bucket sort by degree) plus a DSU run in reverse to evaluate component sizes.
Complexity target
O((N + M) log N) with a min-heap; O(N + M) with bucket degrees.
Reference solution (C++)
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
struct DSU {
vector<int> p, sz;
DSU(int n) : p(n), sz(n, 1) { iota(p.begin(), p.end(), 0); }
int f(int x) { while (p[x] != x) x = p[x] = p[p[x]]; return x; }
void u(int a, int b) { a = f(a); b = f(b); if (a == b) return; if (sz[a] < sz[b]) swap(a, b); p[b] = a; sz[a] += sz[b]; }
};
int main() {
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
int N, M; cin >> N >> M;
vector<vector<int>> adj(N);
vector<pair<int,int>> edges(M);
for (auto& [a, b] : edges) { cin >> a >> b; --a; --b; adj[a].push_back(b); adj[b].push_back(a); }
// Peel in increasing degree to get a removal order.
vector<int> deg(N), order;
priority_queue<pair<int,int>, vector<pair<int,int>>, greater<>> pq;
vector<char> dead(N, 0);
for (int i = 0; i < N; ++i) { deg[i] = adj[i].size(); pq.push({deg[i], i}); }
vector<int> degAtRemoval(N);
while (!pq.empty()) {
auto [d, v] = pq.top(); pq.pop();
if (dead[v] || d != deg[v]) continue;
dead[v] = 1;
degAtRemoval[v] = d;
order.push_back(v);
for (int u : adj[v]) if (!dead[u]) { --deg[u]; pq.push({deg[u], u}); }
}
// Now reverse the order: add vertices back, maintaining components with DSU.
DSU dsu(N);
vector<char> live(N, 0);
ll best = 0;
for (int i = (int)order.size() - 1; i >= 0; --i) {
int v = order[i];
live[v] = 1;
for (int u : adj[v]) if (live[u]) dsu.u(u, v);
ll cand = (ll)dsu.sz[dsu.f(v)] * degAtRemoval[v];
best = max(best, cand);
}
cout << best << "\n";
}
Pitfalls
- Off-by-one on degree at removal. When peeling v, its "degree-in-remaining-subgraph" at that instant is what bounds min-degree of any S still containing v.
- Component on insertion ≠ original component. Process in reverse so DSU only ever unions, never splits.
- Lazy heap entries. Stale (d, v) entries must be discarded when d ≠ current deg[v].
- Long long product. N · max degree up to 105 · 105 = 1010.
What Gold December 2022 was actually testing
- P1 — knapsack with a convertible second resource. Sort by exchange rate; greedy on the dominated axis.
- P2 — geometric visibility via cross-product slopes. Local recomputation around the updated index.
- P3 — k-core / peeling argument. Reverse-time DSU is the canonical pattern for "score = size × min-degree".