USACO 2017 December · Silver · all three problems
Problem 1 — My Cow Ate My Homework
Statement
Bessie has scores s1..sN. For each k = 0..N−2, consider the suffix sk+1..sN: drop its single lowest score, then take the average of the remaining scores. Output every value of k that achieves the maximum such average (smallest k first).
Constraints
- 1 ≤ N ≤ 105.
- 0 ≤ si ≤ 10000.
- Time limit: 2s.
Key idea
Process suffixes right-to-left. Maintain running suffix sum S, running minimum M, and suffix length L. For each k (i.e. each starting position from N−1 down to 0), the score after dropping the min is (S − M) / (L − 1). To compare fractions without floats, compare a/b vs c/d via a·d vs c·b in 64-bit. Store all k along with their fractions, find the max fraction, output every k that ties.
Complexity target
O(N) suffix sweep, O(N) compare pass.
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; cin >> N;
vector<int> s(N);
for (auto& x : s) cin >> x;
// For k = 0..N-2 (suffix starts at index k, must have >= 2 elements).
vector<pair<ll,ll>> avg(N - 1); // numerator, denominator
ll sum = 0, mn = LLONG_MAX, len = 0;
for (int i = N - 1; i >= 0; --i) {
sum += s[i]; mn = min(mn, (ll)s[i]); ++len;
if (len >= 2) avg[i] = { sum - mn, len - 1 };
}
// Find max fraction.
pair<ll,ll> best = avg[0];
for (int k = 1; k < N - 1; ++k)
if (avg[k].first * best.second > best.first * avg[k].second)
best = avg[k];
for (int k = 0; k < N - 1; ++k)
if (avg[k].first * best.second == best.first * avg[k].second)
cout << k << "\n";
}
Pitfalls
- Output every k that ties the max, not just one. Sorted ascending (the loop order gives this for free).
- Compare exactly. Floats lose precision; cross-multiply with 64-bit ints.
- Suffix must have ≥ 2 entries. Dropping a min from a single-element suffix is undefined; stop at k = N−2.
- "K" semantics. The problem labels K as the number of dropped initial homeworks; double-check whether k or N−k indexes — paraphrased here as suffix start.
Problem 2 — Milk Measurement
Statement
There are N cows, each starting at 7 gallons/day. There are M events; each event says on day d, cow c changes production by ±x. After processing events in day order, count the number of days on which the set of top producers changes from the previous day (initial state: all N cows tied at 7).
Constraints
- 1 ≤ N ≤ 105, 1 ≤ M ≤ 105.
- 1 ≤ d ≤ 106; days are distinct.
- |x| ≤ 100 (small); production stays non-negative .
- Time limit: 2s.
Key idea
A multiset (or map<int,int,greater> of production → count) lets you read off the
current max and how many cows hold it in O(log N). Also track for each event-affected cow whether it
was a max-holder before and after. The "top set" changes iff (a) the max value itself changed, or
(b) the touched cow's membership in the max group flipped. You don't need to enumerate the set; just
track its identity hash (max value, count, and whether the touched cow is in it).
Simpler implementation: maintain a sorted multiset of productions. For each event, look at the old max, the old production of cow c, and whether cow c was in the max set. Update, look at the new max and whether cow c is in it. If the (max, count, "is c in?") triple changed, the answer increments.
Complexity target
O((N + M) log N) with a multiset / map.
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; cin >> N >> M;
vector<tuple<int,int,int>> ev(M); // day, cow, delta
for (auto& [d, c, x] : ev) cin >> d >> c >> x;
sort(ev.begin(), ev.end());
unordered_map<int, ll> prod; // cow -> production (default 7)
map<ll, int, greater<ll>> freq; // production -> count of cows
freq[7] = N; // all N cows start at 7
auto getProd = [&](int c) {
auto it = prod.find(c);
return it == prod.end() ? 7LL : it->second;
};
int ans = 0;
for (auto& [d, c, x] : ev) {
ll oldP = getProd(c);
ll oldMax = freq.begin()->first;
int oldCnt = freq.begin()->second;
bool oldIn = (oldP == oldMax);
if (--freq[oldP] == 0) freq.erase(oldP);
ll newP = oldP + x; prod[c] = newP;
++freq[newP];
ll newMax = freq.begin()->first;
int newCnt = freq.begin()->second;
bool newIn = (newP == newMax);
if (oldMax != newMax || oldCnt != newCnt || oldIn != newIn) ++ans;
}
cout << ans << "\n";
}
Pitfalls
- Sort events by day. Input is unsorted.
- The "set changed" condition is set-equality of which cows are at the max, not the max value itself. Two cows tied at max, then one drops while max value stays the same, still counts.
- Default production 7. Don't store every cow up front; lazy map.
- Erase zero-count entries from the production-frequency map, else
freq.begin()returns a stale max.
Problem 3 — The Bovine Shuffle
Statement
N cows stand in positions 1..N. The shuffle is the functional mapping a1..aN (not necessarily a permutation — multiple positions can map to the same target). The shuffle is applied infinitely many times. After enough applications, some positions are guaranteed to be empty and some are guaranteed to remain occupied. Count the positions that remain occupied forever.
Constraints
- 1 ≤ N ≤ 105.
- 1 ≤ ai ≤ N (a is a function, not a permutation).
- Time limit: 2s.
Key idea
Think of a as a functional graph: directed edge i → a[i]. After enough steps, every walk lands inside a cycle. The positions that stay occupied forever are exactly the nodes that lie on some cycle of the functional graph. Count cycle nodes: repeatedly remove leaves (in-degree 0), and what's left is the union of cycles.
Complexity target
O(N) — a topological-style peel.
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> a(N + 1), indeg(N + 1, 0);
for (int i = 1; i <= N; ++i) { cin >> a[i]; ++indeg[a[i]]; }
// Peel nodes with indeg 0; they cannot lie on a cycle.
queue<int> q;
for (int i = 1; i <= N; ++i) if (indeg[i] == 0) q.push(i);
int peeled = 0;
while (!q.empty()) {
int u = q.front(); q.pop(); ++peeled;
if (--indeg[a[u]] == 0) q.push(a[u]);
}
cout << (N - peeled) << "\n";
}
Pitfalls
- "a" is a function, not a permutation. Multiple positions can map to the same target; the Bronze version's invariance argument doesn't apply.
- Cycle detection ≠ DFS lowlink here. A simple Kahn-style indegree peel is the cleanest tool.
- Self-loops (a[i] = i) are cycles of length 1. They survive the peel correctly.
- Don't actually simulate ∞ shuffles. The cows never disappear in physical reality — what changes is how many distinct positions stay occupied; "occupied forever" is the cycle membership question, not population count.
What Silver December 2017 was actually testing
- P1 — suffix sweep + exact fraction compare. Maintaining sum/min/length and avoiding floats.
- P2 — multiset / sorted-map for streaming top-1. Plus careful "set changed" semantics.
- P3 — functional graph cycle nodes. Indegree-peel pattern; appears again in Gold/Plat throughout USACO history.