USACO 2016 US Open · Platinum · all three problems
Problem 1 — 262144
Statement
The Platinum upgrade of Gold's 248. You are given a sequence of N positive integers; you may repeatedly merge two adjacent equal values v, v into a single tile v+1. Output the maximum tile value that can ever appear in the sequence during such merges.
Constraints
- 1 ≤ N ≤ 262 144.
- 1 ≤ ai ≤ 40.
- Time limit: 2s.
Key idea
The Gold O(N3) interval DP dies at N = 262144. Re-parameterize by the value instead of
the interval length. Define R[v][i] = the smallest index j such that the subarray
a[i .. j−1] collapses to a single tile of value v (or 0 / sentinel if impossible). Recurrence:
R[v][i] = R[v-1][ R[v-1][i] ], because a tile of value v is made by two adjacent tiles of
value v−1, the left one occupying [i, R[v-1][i]) and the right one starting where the left ends.
Base: R[a[i]][i] = i+1. The maximum v for which any R[v][i] is well-defined is
the answer. v ranges over [1, 40 + log2 N] ≤ 58, so the table is
58 × N ≈ 1.5·107.
Complexity target
O(N · (log N + max ai)) 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 N; cin >> N;
vector<int> a(N);
for (auto& x : a) cin >> x;
const int VMAX = 60; // a_i up to 40, plus log2(262144) = 18
// R[v][i] = right endpoint (exclusive) of a v-tile starting at i, or 0 if impossible.
vector<vector<int>> R(VMAX + 1, vector<int>(N + 1, 0));
for (int i = 0; i < N; ++i) R[a[i]][i] = i + 1;
int ans = 0;
for (int i = 0; i < N; ++i) ans = max(ans, a[i]);
for (int v = 2; v <= VMAX; ++v) {
for (int i = 0; i < N; ++i) {
if (R[v][i]) { ans = max(ans, v); continue; }
int mid = R[v-1][i];
if (mid && mid <= N && R[v-1][mid]) {
R[v][i] = R[v-1][mid];
ans = max(ans, v);
}
}
}
cout << ans << "\n";
}
Pitfalls
- VMAX is not 40. Merges keep raising the value — with N up to 262144 you can grow up to ~log2 N = 18 extra levels above 40. Set VMAX = 58–60 to be safe.
- Memory. A 60 × 262145
inttable is ~60 MB. If the judge memory limit is tight, storeRas two rolling rows or compress toint32;shortwon't fit N. - Initialize
R[a[i]][i] = i+1, notR[a[i]][i] = 1. The table stores absolute right endpoints, not lengths. - Don't compute via the Gold-style interval DP. O(N2) is 7·1010; only the value-indexed DP fits.
Problem 2 — Bull in a China Shop
Statement
A glass figurine pictured on an N×M grid (each cell colored or empty) has been shattered into K pieces. Each piece is given as a small R×C colored sub-grid with its own connected non-empty cells. Count the number of unordered triples (i, j, k), i < j < k, of pieces that — after independently translating, rotating by 0/90/180/270 degrees, and/or reflecting each piece — tile the original figurine exactly: each colored cell of the original is covered by exactly one of the three pieces, with no overlaps and no cells outside the figurine.
Constraints
- 3 ≤ N, M ≤ 500.
- 1 ≤ R, C ≤ 100 per piece.
- 4 ≤ K ≤ 100.
- Time limit: 2s.
Key idea
For each piece, generate its 8 orientations (4 rotations × 2 reflections); normalize each to a canonical form by translating its bounding box to the origin and serializing the colored cell set. Then for every piece and every translation that fits inside the original figurine, check whether the colored cells of the (oriented, translated) piece exactly equal a subset of the original picture's colored cells that also matches by color. Hash each such "(piece, placement)" as the set of original-cell indices it covers; key the hash by a sorted tuple or a 64-bit polynomial hash over those indices.
Now enumerate triples: for each ordered pair (i, j) of placements with disjoint covers and same color match, look up "the complement set of original cells" in a hash map keyed on placements. The inclusion-exclusion is the implementation trap; the mathematical idea is "two placements determine the third uniquely if the answer exists."
Complexity target
Roughly O(K · 8 · N · M) to enumerate piece placements (~4·107); triple-counting is the dominant cost — a hash on covered-cell sets keeps it polynomial.
Reference solution (C++)
// Skeleton: full Platinum solution; condensed to the key data flow.
#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long u64;
int N, M, K;
vector<string> orig; // N rows, M cols; '.' = empty, others = color
vector<vector<string>> pcs; // K pieces, each its own RxC grid
// Generate the 8 dihedral orientations of a grid g.
vector<vector<string>> eight(const vector<string>& g) {
vector<vector<string>> out;
auto rot = [](const vector<string>& a){
int r=a.size(), c=a[0].size();
vector<string> b(c, string(r,'.'));
for(int i=0;i<r;++i) for(int j=0;j<c;++j) b[j][r-1-i]=a[i][j];
return b;
};
auto flip = [](vector<string> a){
for(auto& s : a) reverse(s.begin(), s.end()); return a;
};
auto cur = g;
for (int t = 0; t < 4; ++t) { out.push_back(cur); cur = rot(cur); }
cur = flip(g);
for (int t = 0; t < 4; ++t) { out.push_back(cur); cur = rot(cur); }
return out;
}
// For each piece, enumerate all placements over the original grid that match color exactly.
// Hash the set of covered original-cell indices via a Zobrist-style XOR table.
vector<u64> zob; // size N*M, random 64-bit per cell
vector<vector<u64>> placements; // per piece: list of placement hashes
vector<vector<u64>> piecesByHash;
void enumeratePiece(int p) {
set<u64> seen;
for (auto& ori : eight(pcs[p])) {
int pr = ori.size(), pc = ori[0].size();
for (int dr = 0; dr + pr <= N; ++dr)
for (int dc = 0; dc + pc <= M; ++dc) {
u64 h = 0; bool ok = true;
for (int i = 0; i < pr && ok; ++i)
for (int j = 0; j < pc && ok; ++j) {
if (ori[i][j] == '.') continue;
if (orig[dr+i][dc+j] != ori[i][j]) { ok = false; break; }
h ^= zob[(dr+i)*M + dc+j];
}
if (ok && seen.insert(h).second) placements[p].push_back(h);
}
}
}
u64 FULL = 0; // XOR of all colored original cells
int main() {
// ... read N, M, orig; read K and pieces ...
// build zob; compute FULL over colored cells of orig;
// enumeratePiece(p) for each p; collect.
// For each ordered pair (i, j), i<j, and each h_i, h_j with (h_i ^ h_j) leaving FULL ^ h_i ^ h_j as a valid placement of some k>j:
// ans += count of placements of any piece k>j with hash == FULL ^ h_i ^ h_j.
// (Need disjointness check beyond hash; use bitset of covered cells for verification on candidate hits.)
cout << "see editorial for full implementation\n";
}
Pitfalls
- Color match, not just shape. Each cell carries a color; two pieces fit only if their colored cells match the original's colors exactly, not merely shape.
- Disjointness via hash + verify. Zobrist XOR detects covers that sum to FULL, but two placements can XOR-cancel cells they share. After a hash hit, verify with a real bitset to rule out false positives.
- Symmetric orientations. A piece may map to itself under some rotation/flip; deduplicate placements with a
seton the placement hash to avoid over-counting. - Skeleton only. This is one of the hardest 2016 Open problems; the runnable code above is a skeleton — verify against the editorial before committing the full implementation.
Problem 3 — Landscaping
Statement
There are N flowerbeds in a row. Bed i currently holds Ai units of dirt and FJ wants it to hold Bi units. He has three operations: (1) buy and place one unit of dirt at any bed for cost X; (2) remove and dispose of one unit of dirt from any bed for cost Y; (3) move one unit from bed i to bed j for cost Z · |i − j|. Find the minimum total cost.
Constraints
- 1 ≤ N ≤ 100 000.
- 0 ≤ Ai, Bi ≤ 10.
- 0 ≤ X, Y ≤ 108; 0 ≤ Z ≤ 1000.
- Time limit: 2s.
Key idea
Decompose each flowerbed into individual dirt units. Each unit needed (where Bi > current supply at i) is a "demand" of cost X if bought externally. Each unit in surplus is a "supply" of cost Y if shipped away. A demand at position j can also be met by transporting a surplus at position i for cost Z · |i − j|. This is exactly the 1D min-cost-matching problem.
Sweep left to right. Maintain a priority queue of unmatched surplus units keyed by "effective price" at
the current sweep position: when a surplus unit at position p is reached, push it with cost −Y (we
save Y by not disposing) plus the running Z penalty for shifting it rightward. When a demand at position p
arrives, either buy it for X, or pop the cheapest surplus matching (which costs queuedCost + Z · p).
Use the standard "regret" trick: push the popped supply back with a corrected cost equal to
2 · (matched cost) − previous so future demands can rebind it.
Complexity target
O((total dirt) log (total dirt)) ≈ (10 · N) log (10 N) ≈ 107.
Reference solution (C++)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
// Each "event" is a unit of supply (type=+1) or demand (type=-1) at position p.
// Demand can be filled by: (a) buying for X, or (b) transporting a surplus
// from position q at cost Z*|p-q| - Y (we save Y by not disposing of the surplus).
// Sweep left to right. Push each supply unit into a min-heap keyed by
// effective offset cost; when a demand arrives, match against the cheapest
// available supply if it beats X.
int main() {
ios_base::sync_with_stdio(false); cin.tie(nullptr);
int N; ll X, Y, Z;
cin >> N >> X >> Y >> Z;
vector<pair<int,int>> events; // (position, +1 supply / -1 demand)
for (int i = 0; i < N; ++i) {
int A, B; cin >> A >> B;
int diff = A - B;
if (diff > 0) for (int k = 0; k < diff; ++k) events.push_back({i, +1});
else if (diff < 0) for (int k = 0; k < -diff; ++k) events.push_back({i, -1});
}
// Heap of supplies keyed by (queued cost), interpretation:
// matching at sweep position p costs (queuedCost + Z*p - Y) [we save -Y disposal].
// Equivalently store key = (Z * supply_position - Y - 2*Z*supply_position) trick is overkill here;
// we use a regret-style min-heap with a "double push" on match to allow rebinding.
priority_queue<ll, vector<ll>, greater<ll>> supplyHeap; // stores match cost paid so far
priority_queue<ll, vector<ll>, greater<ll>> demandHeap; // for symmetry: rebindable demand keys
ll ans = 0;
for (auto& e : events) {
ll p = e.first;
if (e.second == +1) {
// supply unit at p: its eventual cost contribution if matched later at q>=p
// is Z*(q - p) - Y; the "queued cost" we store is Z*(-p) - Y.
supplyHeap.push(Z * (-p) - Y);
} else {
// demand at p: candidate match cost = top_supply + Z*p; compare with X (buy fresh).
ll buyCost = X;
ll matchCost = supplyHeap.empty() ? LLONG_MAX
: supplyHeap.top() + Z * p;
if (matchCost < buyCost) {
ans += matchCost;
supplyHeap.pop();
// regret: allow this supply to be "un-matched" in favor of a cheaper future demand.
supplyHeap.push(-matchCost + Z * (-p) - Y);
} else {
ans += buyCost;
// regret slot for the bought demand (let future supply attach for credit).
demandHeap.push(-buyCost - Z * p);
}
}
}
cout << ans << "\n";
}
Pitfalls
- Decompose by units, not by beds. A bed with surplus 7 is 7 independent supply events. Total events ≤ 10 N = 106, which fits.
- Regret trick. A supply matched to a demand may later be a better match for a closer future demand — you push back a "reverse cost" so the future demand can rebind, paying the differential. Skipping the rebind gives a wrong answer on adversarial cases.
- Don't forget the "do nothing" no-cost path. Beds with Ai = Bi generate zero events; don't accidentally add them.
- Alternative: min-cost flow on a line with a slope-trick / convex hull formulation also works and is what the official editorial may present. The priority-queue version above is the standard "Aliens-trick-adjacent" 1D matching.
- Use 64-bit everywhere. Costs blow up: X up to 108, ~106 events, products easily overflow 32-bit.
What Platinum 2016 US Open was actually testing
- P1 — reparameterized DP. 262144 is the canonical "you index by value, not by interval length" exercise; one of the most copied tricks in subsequent USACO sets.
- P2 — geometric hashing under symmetries. Zobrist-style covers + dihedral orientations + disjointness verification. A heavy implementation problem.
- P3 — 1D min-cost matching. Either via slope-trick / convex flow on a line, or via a regret-augmented priority queue. The exemplar problem for both techniques.