USACO 2018 January · Silver · all three problems
Problem 1 — Lifeguards (Silver)
Statement
N lifeguards each cover a half-open time interval [ai, bi). Farmer John must fire exactly K of them. Maximize the total time during which at least one of the remaining N−K lifeguards is on duty.
Constraints
- 1 ≤ K ≤ N ≤ 100.
- 0 ≤ ai < bi ≤ 109.
- Time limit: 2s.
Key idea
First, discard any interval strictly contained in another — keeping the bigger one is always at least as good (firing it only ever costs more coverage). After that the remaining intervals, sorted by left endpoint, also have increasing right endpoints. Now run a DP: dp[i][j] = maximum covered time using intervals from the first i (sorted) of which we kept some subset, having fired j so far, with interval i kept. Transition: from a previous kept interval p, the marginal coverage is bi − max(bp, ai), and (i − p − 1) intervals between them are fired.
Complexity target
O(N² · K). With N ≤ 100, fine in any constant.
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, K; cin >> N >> K;
vector<pair<int,int>> iv(N);
for (auto& p : iv) cin >> p.first >> p.second;
// Sort by left ascending, right descending — then drop any interval whose
// right endpoint is <= the running max right (it's contained in a previous).
sort(iv.begin(), iv.end(), [](auto& a, auto& b){
return a.first != b.first ? a.first < b.first : a.second > b.second;
});
vector<pair<int,int>> v;
int curR = -1;
int dropped = 0;
for (auto& p : iv) {
if (p.second <= curR) { ++dropped; continue; } // contained
v.push_back(p); curR = p.second;
}
int M = v.size();
// We must fire K total. We've already "freely" discarded `dropped` contained
// ones; we still need to fire (K - dropped) more from v. If that's < 0 we
// were forced to fire useful intervals — clamp to 0; if > M-1 we can't keep
// even one, answer is 0.
int need = max(0, K - dropped);
if (need >= M) { cout << 0 << "\n"; return 0; }
// dp[i][j] = best coverage using v[0..i], firing exactly j of v[0..i],
// with v[i] kept. Sentinel v[-1] = (0,0).
// Final answer = max over i, j = need, plus we fire all v[i+1..M-1] (which
// forces those to be in the "fired" budget).
const ll NEG = LLONG_MIN / 4;
vector dp(M, vector<ll>(need + 1, NEG));
for (int i = 0; i < M; ++i) {
// option A: v[i] is the first kept interval — fire all i before it.
if (i <= need) dp[i][i] = v[i].second - v[i].first;
// option B: extend from some previous kept p < i, firing i-p-1 between.
for (int p = 0; p < i; ++p) {
ll add = v[i].second - max(v[p].second, v[i].first);
if (add < 0) add = 0;
int between = i - p - 1;
for (int j = 0; j + between <= need; ++j) {
if (dp[p][j] == NEG) continue;
dp[i][j + between] = max(dp[i][j + between], dp[p][j] + add);
}
}
}
ll ans = 0;
for (int i = 0; i < M; ++i) {
int trailing = M - 1 - i; // these are fired
if (trailing > need) continue;
int j = need - trailing;
if (j >= 0 && j <= need && dp[i][j] != NEG)
ans = max(ans, dp[i][j]);
}
cout << ans << "\n";
}
Pitfalls
- Contained intervals. A strictly-contained interval is always optimal to drop first; it never adds coverage.
- "Exactly K", not "at most K". If you under-fire you must fire useful intervals on top of the free contained ones.
- Overlap math. Marginal coverage from keeping the next interval is
bi − max(bp, ai), clamped at 0. - Coordinates up to 10⁹ — use
long longfor sums.
Problem 2 — Rental Service
Statement
Farmer John has N cows producing mi gallons of milk each per day. He has M milk-store contracts: store j buys up to qj gallons at pj cents per gallon (cumulative capacity, can sell to many stores). He also has R neighbors who will rent a whole cow for rk cents per day. Each cow either sells all its milk or is rented out — never both. Maximize total daily revenue.
Constraints
- 1 ≤ N, M, R ≤ 105.
- 1 ≤ mi, qj, pj, rk ≤ 106.
- Time limit: 2s.
Key idea
Sort cows by milk descending, stores by price descending, renters by price descending. For each "split" k ∈ [0, N] meaning "rent the top k highest-paying renters out, milk the remaining N−k biggest-milk cows", revenue = (sum of top k renter prices) + (top milk filling top stores). The "top milk filling top stores" function is monotone non-increasing in k (you lose your largest cow each step), so a single linear sweep with a pointer into the milk/store stream is enough.
Complexity target
O((N + M + R) log) for sorts, O(N + M) for the sweep.
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, M, R;
cin >> N >> M >> R;
vector<ll> cows(N);
for (auto& x : cows) cin >> x;
vector<pair<ll,ll>> stores(M); // (price, qty)
for (auto& p : stores) cin >> p.second >> p.first;
vector<ll> rents(R);
for (auto& x : rents) cin >> x;
sort(cows.rbegin(), cows.rend());
sort(stores.rbegin(), stores.rend()); // by price desc
sort(rents.rbegin(), rents.rend());
// milkRev[k] = revenue selling milk from the FIRST k cows (highest milk).
// We'll then evaluate: pick top j renters, milk the remaining N-j highest-milk
// cows = first N-j cows of the sorted list. So milkRev[N-j] is needed.
vector<ll> milkRev(N + 1, 0);
int si = 0;
ll remain = (si < M ? stores[si].second : 0);
for (int i = 0; i < N; ++i) {
ll have = cows[i], rev = 0;
while (have > 0 && si < M) {
ll take = min(have, remain);
rev += take * stores[si].first;
have -= take; remain -= take;
if (remain == 0) {
++si;
remain = (si < M ? stores[si].second : 0);
}
}
milkRev[i + 1] = milkRev[i] + rev;
}
// rentPrefix[j] = sum of top j renter prices.
vector<ll> rentPrefix(R + 1, 0);
for (int i = 0; i < R; ++i) rentPrefix[i + 1] = rentPrefix[i] + rents[i];
ll ans = 0;
for (int j = 0; j <= min(N, R); ++j) {
ll rev = rentPrefix[j] + milkRev[N - j];
ans = max(ans, rev);
}
cout << ans << "\n";
}
Pitfalls
- Cow input order matters for revenue. A higher-producing cow can fill more expensive store slots first — sort by milk descending before pricing.
- Renting replaces the highest-milk cow. Because we lose top-down, only the prefix milkRev[N−j] matters; the j renters are the top j prices.
- Store input is sometimes "q p" not "p q" — check the field order on the statement page.
- 64-bit revenue. Up to ~105 · 106 · 106 theoretically — use
long longeverywhere.
Problem 3 — MooTube (Silver)
Statement
Bessie's MooTube has N videos connected by N−1 weighted edges (a tree). Two videos are "K-similar" if every edge on the path between them has weight ≥ K. For each of Q queries (K, v) output the number of videos K-similar to v (excluding v itself).
Constraints
- 2 ≤ N ≤ 105, 1 ≤ Q ≤ 105.
- Edge weights and query K up to 109.
- Time limit: 2s.
Key idea
Offline. Sort edges by weight descending. Sort queries by K descending. Sweep: while the next edge has weight ≥ current K, union its endpoints (DSU with size). Then answer = size of v's component − 1.
Complexity target
O((N + Q) log (N + Q)) for sorts, O((N + Q) α(N)) for DSU.
Reference solution (C++)
#include <bits/stdc++.h>
using namespace std;
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 (x != p[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, Q; cin >> N >> Q;
vector<tuple<int,int,int>> edges(N - 1); // (w, a, b)
for (auto& [w,a,b] : edges) { cin >> a >> b >> w; --a; --b; }
sort(edges.begin(), edges.end(), greater<>());
vector<tuple<int,int,int>> queries(Q); // (K, v, idx)
for (int i = 0; i < Q; ++i) {
int k, v; cin >> k >> v; --v;
queries[i] = {k, v, i};
}
sort(queries.begin(), queries.end(), greater<>());
DSU dsu(N);
vector<int> ans(Q);
int ei = 0;
for (auto& [k, v, idx] : queries) {
while (ei < (int)edges.size() && get<0>(edges[ei]) >= k) {
dsu.u(get<1>(edges[ei]), get<2>(edges[ei]));
++ei;
}
ans[idx] = dsu.sz[dsu.f(v)] - 1;
}
for (int x : ans) cout << x << "\n";
}
Pitfalls
- Offline ordering. You can't easily delete edges; sort by descending weight and add.
- Answer excludes v itself. Subtract 1 from the component size.
- Don't BFS per query. Q · N = 1010, hopeless.
- 1-indexed input. USACO almost always uses 1-based node IDs.
What Silver January 2018 was actually testing
- P1 — interval DP with "contained" pruning. Recognize the dominance and run the standard "keep some, fire the rest" DP.
- P2 — multi-source greedy with sweep. Two sorted streams (cows × stores) plus a prefix over renters; iterate the split.
- P3 — offline DSU with sorted thresholds. Canonical "queries on minimum/maximum edge along path" trick.