2023 January Silver — three problems
← 2023 January contest index · Official results page
Silver pivots toward the letter-mapping graph in Find and Replace, plus a bigger Air Cownditioning that wants real DP.
Problem 1 · Air Cownditioning II
Official statement (cpid=1284)
Statement (paraphrased)
Same setup as Bronze: N stalls with cooling demands, M air conditioners each covering a range with a power and a cost. Choose a subset minimising total cost while satisfying every stall. Silver lifts the bounds so 2M brute force no longer fits — but M is still small enough for bitmask DP on the set of chosen ACs.
Constraints
- 1 ≤ N ≤ 100, 1 ≤ M ≤ 20
- Time/memory: USACO Silver default (2 s / 256 MB)
Key idea
Bitmask over ACs is still the cleanest framing: 220 ≈ 106 subsets. For each subset, build per-stall cooling with a difference array (O(M)) and total cost; check feasibility in O(N). Total roughly 2M · (N + M) ≈ 108 — tight but acceptable in C++ with arrays not vectors. If M is larger in the real PDF, fall back to ILP-style branch-and-bound or DP on stall index with the set of ACs ending at or after the current stall.
Complexity
O(2M · (N + M)) with bitmask. Memory O(N).
C++ reference
#include <bits/stdc++.h>
using namespace std;
int N, M;
int d[105];
int s[25], t[25], p[25], m[25];
int diffArr[110];
int main() {
scanf("%d %d", &N, &M);
for (int i = 1; i <= N; i++) scanf("%d", &d[i]);
for (int i = 0; i < M; i++) scanf("%d %d %d %d", &s[i], &t[i], &p[i], &m[i]);
int best = INT_MAX;
for (int mask = 0; mask < (1 << M); mask++) {
memset(diffArr, 0, sizeof(int) * (N + 2));
long long cost = 0;
for (int i = 0; i < M; i++) if (mask >> i & 1) {
diffArr[s[i]] += p[i];
diffArr[t[i] + 1] -= p[i];
cost += m[i];
if (cost >= best) goto skip;
}
{
int cool = 0;
for (int j = 1; j <= N; j++) { cool += diffArr[j]; if (cool < d[j]) goto skip; }
best = (int)cost;
}
skip:;
}
printf("%d\n", best);
}
Pitfalls
- Difference array beats per-cell loops at large M.
- Early termination (
cost >= best) gives a big constant-factor speedup. - Don't allocate inside the inner loop. Reuse a fixed array of size N+2.
Problem 2 · Spelling Bee
Official statement (cpid=1285)
Statement (paraphrased)
Given a target string T of length n, and Q query strings, decide for each query whether it can be reduced to T by repeatedly applying the rule: collapse two adjacent equal characters into one of the same character (or some similar "stack reduction" rule).
Constraints
- |T| ≤ 50, query length up to 106, Q up to 100
- Time/memory: USACO Silver default (2 s / 256 MB)
Key idea
Reduce each query with a stack: push next char if it differs from the stack top, else (if the rule collapses equal pairs) pop or merge. The final stack is the canonical reduced form. Compare against the canonical reduction of T.
Complexity
O(sum of |query|) total.
C++ reference
#include <bits/stdc++.h>
using namespace std;
// [verify] exact reduction rule against cpid=1285. Below assumes "collapse two equal
// adjacent characters into one" until no two adjacent characters are equal.
string reduce(const string& s) {
string st;
for (char c : s) {
if (!st.empty() && st.back() == c) {
// merge: leave one copy on the stack (idempotent)
continue;
}
st.push_back(c);
}
return st;
}
int main() {
string T; cin >> T;
int Q; cin >> Q;
string canon = reduce(T);
while (Q--) {
string q; cin >> q;
cout << (reduce(q) == canon ? "YES" : "NO") << "\n";
}
}
Pitfalls
- Use fast I/O (
cin.tie(0)+sync_with_stdio(false)) — query strings are huge. - Make sure the reduction is confluent (different application orders yield the same canonical form) before relying on the stack.
- If the rule is "double a character" rather than "collapse a pair", invert the simulation: expand T to all reachable strings of length ≤ max query.
Problem 3 · Find and Replace
Official statement (cpid=1286)
Statement (paraphrased)
Given strings S and T of equal length over an alphabet Σ, in one operation you pick a letter a and a letter b and globally replace every a in the current string with b. Find the minimum number of operations to turn S into T, or report it is impossible. Silver uses a small alphabet (uppercase + lowercase letters, |Σ| ≤ 52).
Constraints
- 1 ≤ |S| = |T| ≤ 105, |Σ| ≤ 52
- Time/memory: USACO Silver default (2 s / 256 MB)
Key idea
Build a function f : Σ → Σ where f(c) is what every occurrence of c in S must eventually become according to T. If S has an a that maps to two different targets, output −1. Otherwise look at the functional graph of f restricted to letters that actually appear in S. Each weakly-connected component contributes (size − 1) operations if it is a tree, and (size) operations if it contains a cycle — and a cycle additionally requires a "temporary" letter from Σ that is currently unused, costing one extra rename if no such letter exists.
Complexity
O(|S| + |Σ|).
C++ reference
#include <bits/stdc++.h>
using namespace std;
int idx(char c) {
if (c >= 'a' && c <= 'z') return c - 'a';
return 26 + (c - 'A');
}
int main() {
string S, T; cin >> S >> T;
if (S.size() != T.size()) { cout << -1; return 0; }
const int K = 52;
vector<int> f(K, -1);
vector<bool> usedInS(K, false), usedInT(K, false);
for (size_t i = 0; i < S.size(); i++) {
int a = idx(S[i]), b = idx(T[i]);
usedInS[a] = true; usedInT[b] = true;
if (f[a] != -1 && f[a] != b) { cout << -1; return 0; }
f[a] = b;
}
bool hasFree = false;
for (int c = 0; c < K; c++) if (!usedInS[c] && !usedInT[c]) { hasFree = true; break; }
// walk components in the functional graph of f over letters with f[c] != -1 and f[c] != c
vector<int> colour(K, 0); // 0 unseen, 1 in-stack, 2 done
int ans = 0;
for (int start = 0; start < K; start++) {
if (colour[start] != 0 || f[start] == -1 || f[start] == start) continue;
// walk path from start, detect cycle in this component
vector<int> path;
int u = start;
while (u != -1 && colour[u] == 0) { colour[u] = 1; path.push_back(u); u = f[u]; }
bool cycle = (u != -1 && colour[u] == 1); // returned into this path
for (int v : path) colour[v] = 2;
ans += (int)path.size();
if (!cycle) ans--; // tree: size - 1 renames
else if (!hasFree) { cout << -1; return 0; } // [verify] cycles need a scratch letter
}
cout << ans;
}
Pitfalls
- The "no free letter" case in a cycle is the classic edge case; without a scratch register you cannot break the cycle.
- Letters appearing only in T but not S are still part of the alphabet — they can serve as scratch.
- Self-loops (f(c) = c) don't cost anything and aren't part of a non-trivial component.