2024 US Open · Bronze
← Back to 2024 US Open · Official results page
Three Bronze problems, 5 hours combined. The flavor this round was unusually heavy on case analysis — Bronze 1 wants careful parsing of boolean expressions, Bronze 2 wants a clean geometry walk, and Bronze 3 is a classic reconstruct-the-permutation puzzle that punishes off-by-one errors.
B1 · Logical Moos
Official statement (cpid=1419)
Statement (paraphrase)
You're given a boolean expression of length N (odd) where tokens alternate between
values (true/false) and operators (AND/OR),
with AND binding tighter than OR. For each of Q queries (l, r, v),
decide whether replacing the subarray [l..r] with the single boolean v
makes the whole expression evaluate to true. Queries are independent.
Constraints
- 1 ≤ N < 2·10⁵ (N odd), 1 ≤ Q ≤ 2·10⁵
- Subtasks: N,Q ≤ 10² · N,Q ≤ 10³ · full
- Time limit: 2 s, memory: 256 MB
Key idea
Split the expression by OR into "AND-blocks." The whole expression is true
iff at least one AND-block evaluates to true (every value in it is true).
Precompute, for each AND-block, whether it is currently true. A query
[l, r] affects at most a contiguous range of AND-blocks: the one containing
l, the one containing r, and every block strictly between them is
"consumed" into the replacement. So pre-compute "is any AND-block outside [L_block, R_block] already true?" with prefix/suffix OR
arrays. Then check the residual left fragment, the residual right fragment, and the
inserted value v.
Complexity
O(N + Q) after one linear preprocess. Each query is O(1) after locating the affected block range with the precomputed token → block index map.
Reference C++
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N, Q;
cin >> N >> Q;
vector<string> tok(N);
for (auto &t : tok) cin >> t;
// Split into AND-blocks by OR. block_id[i] = which block token i belongs to.
vector<int> block_id(N), block_l, block_r;
int b = 0;
block_l.push_back(0);
for (int i = 0; i < N; i++) {
block_id[i] = b;
if (tok[i] == "OR") {
block_r.push_back(i - 1);
b++;
block_l.push_back(i + 1);
block_id[i] = -1; // separator
}
}
block_r.push_back(N - 1);
int B = (int)block_l.size();
// val[b] = AND of all values in block b (operators ignored).
vector<int> val(B, 1);
for (int j = 0; j < B; j++)
for (int i = block_l[j]; i <= block_r[j]; i += 2)
if (tok[i] == "false") val[j] = 0;
// Prefix/suffix OR of val[].
vector<int> pre(B + 2, 0), suf(B + 2, 0);
for (int j = 0; j < B; j++) pre[j + 1] = pre[j] | val[j];
for (int j = B - 1; j >= 0; j--) suf[j] = suf[j + 1] | val[j];
// Per-block prefix AND of value tokens, by token index within block.
// To answer "is left fragment [block_l[B_l], l-1] all-true?" we use a prefix
// "has-false up to index i" array.
vector<int> bad_pref(N + 1, 0);
for (int i = 0; i < N; i++)
bad_pref[i + 1] = bad_pref[i] + (i % 2 == 0 && tok[i] == "false");
string out;
out.reserve(Q);
while (Q--) {
int l, r;
string vs;
cin >> l >> r >> vs;
--l; --r;
int v = (vs == "true");
// Find affected block range.
// Slide l rightward to skip OR token if l lands on one.
int Bl = (tok[l] == "OR") ? block_id[l - 1] + 1 : block_id[l];
int Br = (tok[r] == "OR") ? block_id[r + 1] - 1 : block_id[r];
// Outside blocks contribute their OR.
int outside = pre[Bl] | suf[Br + 1];
if (outside) { out += 'Y'; continue; }
// Inner: left residual (block_l[Bl] .. l-1), value v, right residual (r+1 .. block_r[Br]).
// The merged block evaluates true iff (left all-true) AND v AND (right all-true).
int left_l = block_l[Bl], right_r = block_r[Br];
bool left_ok = (bad_pref[l] - bad_pref[left_l] == 0);
bool right_ok = (bad_pref[right_r + 1] - bad_pref[r + 1] == 0);
out += (left_ok && v && right_ok) ? 'Y' : 'N';
}
cout << out << "\n";
}
Pitfalls
- Query endpoints can land on operator tokens — normalize before reading the block.
- "Outside is already true" short-circuits everything; check it first.
- Operator precedence: AND binds tighter, so split by OR not AND.
B2 · Walking Along a Fence
Official statement (cpid=1420)
Statement (paraphrase)
A simple rectilinear closed polygon has P posts (vertices). N cows each give a start and end point that lie on the fence (on some edge between consecutive posts). Each cow walks along the fence and takes the shorter of the two possible directions. Output the distance each cow walks.
Constraints
- 1 ≤ N ≤ 10⁵, 4 ≤ P ≤ 2·10⁵ (P even), 0 ≤ coords ≤ 1000
- Time limit: 2 s, memory: 256 MB
Key idea
Parameterize the fence by arc length: walk the polygon once and assign each post a
cumulative perimeter offset. For any query point on edge (i, i+1), compute its
offset as offset[i] + distance from post i. The two possible walks have
length |a − b| and perimeter − |a − b|; answer is the min.
Complexity
O(P + N · log P). For each query we binary-search which edge the point lies on (or do O(P) per query for full credit, since N·P ≤ 2·10¹⁰ — actually too slow, so use binary search by sorted edge ranges in x or y).
Reference C++
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N, P;
cin >> N >> P;
vector<int> px(P), py(P);
for (int i = 0; i < P; i++) cin >> px[i] >> py[i];
// Cumulative arc length at each post (closed polygon, edge i -> i+1 mod P).
vector<long long> arc(P + 1, 0);
auto edgeLen = [&](int i) {
int j = (i + 1) % P;
return (long long)(abs(px[i] - px[j]) + abs(py[i] - py[j]));
};
for (int i = 0; i < P; i++) arc[i + 1] = arc[i] + edgeLen(i);
long long peri = arc[P];
// Given a point (x,y) on the fence, find its arc offset.
auto offsetOf = [&](int x, int y) -> long long {
for (int i = 0; i < P; i++) {
int j = (i + 1) % P;
int x1 = px[i], y1 = py[i], x2 = px[j], y2 = py[j];
// axis-aligned: either x1==x2 or y1==y2.
if (x1 == x2 && x == x1 &&
min(y1, y2) <= y && y <= max(y1, y2))
return arc[i] + abs(y - y1);
if (y1 == y2 && y == y1 &&
min(x1, x2) <= x && x <= max(x1, x2))
return arc[i] + abs(x - x1);
}
return -1; // unreachable per problem
};
while (N--) {
int x1, y1, x2, y2;
cin >> x1 >> y1 >> x2 >> y2;
long long a = offsetOf(x1, y1), b = offsetOf(x2, y2);
long long d = llabs(a - b);
cout << min(d, peri - d) << "\n";
}
}
Pitfalls
- Coords up to 1000 with P up to 2·10⁵ — perimeter fits in 32-bit, but use
long longdefensively. - Linear scan in
offsetOfis O(N·P) ≈ 2·10¹⁰ — too slow for full credit. For the full subtask, bucket edges by their fixed coordinate and binary-search the right edge in O(log P). - A query point on a post is on two edges; pick the canonical one (smaller index) to avoid double counting.
B3 · Farmer John's Favorite Permutation
Official statement (cpid=1421)
Statement (paraphrase)
Start with an unknown permutation of 1..N in a deque. Repeatedly: compare the front and back; if front < back, pop the front and write down what is now the new front; otherwise pop the back and write down the new back. You're given the N−1 written values; reconstruct the lexicographically smallest permutation that could produce them, or print −1.
Constraints
- 2 ≤ N ≤ 10⁵ across T test cases (sum of N bounded)
- Subtasks: N ≤ 8 · N ≤ 100 · full
- Time limit: 2 s, memory: 256 MB
Key idea
Process the hints from last to first, reconstructing the deque in reverse. The final deque has 1 element; at each reverse step we know which end (front or back) gained an element and what value is now adjacent. Maintain "what was the front" and "what was the back" as we grow the deque outward. Use the lex-smallest choice when the missing value is ambiguous.
Complexity
O(N) per test case using a deque and a "used value" boolean array.
Reference C++
#include <bits/stdc++.h>
using namespace std;
void solve() {
int N;
cin >> N;
vector<int> h(N - 1);
for (auto &x : h) cin >> x;
// Reverse-simulate. After the last operation the deque is [h[N-2]] (the last written value).
// Walk hints right to left: each hint h[i] was the "new neighbor" after a pop.
// We don't know the popped value, but we know it must be the larger end (if it was
// a front pop, popped < old_back; etc.). Track candidates and pick lex-smallest assignment.
deque<int> dq;
vector<char> used(N + 2, 0);
dq.push_back(h[N - 2]);
used[h[N - 2]] = 1;
// Greedy: at reverse step i (going N-3 .. 0), we must prepend or append a value.
// The current front and back of dq are known. The new value must satisfy:
// - if front comparison made the previous pop happen from front: new_front > current_front
// - if from back: new_back > current_back
// and after the pop, the hint we wrote becomes the new neighbor.
// The deterministic rule: the LARGER of the new front and new back is what was popped.
// Pick the smallest unused value satisfying constraints.
for (int i = N - 3; i >= 0; i--) {
int v = h[i]; // value that became visible at the relevant end after pop
// v must already be present and adjacent to the popped end; if not, try inserting.
bool placed = false;
for (int cand = 1; cand <= N && !placed; cand++) {
if (used[cand]) continue;
// Try as new front: front becomes cand, then pop front -> new front is dq.front() which must equal v
if (dq.front() == v && cand > dq.front()) {
// popped element would be cand only if cand > dq.back() too? Actually the rule
// (pop the larger end) means the popped end equals max(front,back). So cand
// must equal max(cand, dq.back()) i.e. cand >= dq.back().
if (cand >= dq.back()) { dq.push_front(cand); used[cand] = 1; placed = true; break; }
}
if (dq.back() == v && cand > dq.back()) {
if (cand >= dq.front()) { dq.push_back(cand); used[cand] = 1; placed = true; break; }
}
}
if (!placed) { cout << -1 << "\n"; return; }
}
// Fill remaining unused values; lex-smallest means place them at the back in ascending order.
for (int v = 1; v <= N; v++) if (!used[v]) dq.push_back(v);
for (size_t i = 0; i < dq.size(); i++) cout << dq[i] << " \n"[i + 1 == dq.size()];
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T; cin >> T;
while (T--) solve();
}
Pitfalls
- The inner loop above is O(N²) — fine for the partial subtask but you need a smarter "next unused value ≥ x" structure (std::set) for full credit.
- Lex-smallest tiebreaks matter: when both ends are valid, prefer the smaller candidate at the smaller-index end.
- Always check the consistency invariant
front ≠ backwhen the deque has size ≥ 2.