100 Problems — Editorial Guide
A Codeforces-style tutorial covering 100 curated problems.
Pilot batch (1–5). This is a stylistic sample. Once you approve the format, tone, and level of detail, the remaining 95 problems will be rewritten in the same structure.
Each problem is presented in five parts:
- Problem Statement — a clean, unambiguous specification.
- Solution — the intended approach with a complete C++11 implementation.
- Discussion — why the approach works, key observations, common pitfalls.
- Follow-ups — realistic extensions to push on.
- Trade-offs — when to prefer alternative approaches.
1. File System Total Size
Problem Statement
You are given a hierarchical file system modeled as a rooted tree. Every node is either:
- a file, with a non-negative integer
size, or - a directory, with a list of child nodes (files and/or sub-directories).
Implement a function totalSize(root) that returns the sum of the sizes of all files in the subtree rooted at root.
Input. A pointer to the root node. Each node exposes: - bool isFile — whether the node is a file; - long long size — valid only when isFile is true; - vector<Node*> children — valid only when isFile is false.
Output. A single 64-bit integer, the total size of all files reachable from root.
Constraints. - 1 <= N <= 10^5 nodes. - The structure is a strict tree: every node has exactly one parent (the root has none), and there are no cycles. - Individual file sizes fit in a 32-bit integer, but the total may exceed it — accumulate into a 64-bit integer.
Example.
root/
├── a.txt (size 10)
└── dirA/
├── b.txt (size 5)
└── c.txt (size 7)
totalSize(root) = 22
Solution
A single depth-first traversal. For a file, return its size. For a directory, return the sum of the recursive calls on its children.
long long totalSize(Node* root) {
if (!root) return 0;
if (root->isFile) return root->size;
long long sum = 0;
for (Node* c : root->children) sum += totalSize(c);
return sum;
}- Time.
O(N)— each node visited once. - Space.
O(H)recursion depth, whereHis the tree height (up toO(N)for a skewed tree).
Discussion
The entire problem is a one-line recurrence once you realize the tree assumption. The interesting signal is whether you ask for the tree guarantee before writing code. If the structure is a general DAG (hard links permitted), the recursion above double-counts shared subtrees; if it can have cycles, the recursion doesn’t terminate. Always clarify.
Two pitfalls worth stating out loud:
- Integer overflow. Individual sizes are small, but a directory with
10^5files of several MB each easily exceeds2^31. Uselong long. - Stack depth. A chain of
10^5nested directories will blow the default thread stack. Be ready to convert to an iterative DFS with an explicit stack.
Follow-ups
(F1) What if the structure is a DAG — directories may be linked from multiple parents? A plain recursion counts shared subtrees multiple times, which can be the correct behavior (report total on-disk footprint as the OS presents it) or not (report unique bytes). Clarify, then:
- Count per occurrence → add memoization on
Node*, each unique node is summed once and reused:
long long dfs(Node* u, unordered_map<Node*, long long>& memo) {
if (!u) return 0;
unordered_map<Node*, long long>::iterator it = memo.find(u);
if (it != memo.end()) return it->second;
long long s = u->isFile ? u->size : 0;
if (!u->isFile)
for (Node* c : u->children) s += dfs(c, memo);
memo[u] = s;
return s;
}- Count unique bytes → walk the DAG with a
visitedset keyed on file identity; files already seen contribute zero.
(F2) Cycles. If children can form a cycle, maintain a visited set in the traversal and early-return on re-entry.
(F3) Iterative version. Useful when recursion depth is unsafe:
long long totalSizeIter(Node* root) {
long long total = 0;
stack<Node*> st;
if (root) st.push(root);
while (!st.empty()) {
Node* u = st.top(); st.pop();
if (u->isFile) { total += u->size; continue; }
for (Node* c : u->children) st.push(c);
}
return total;
}(F4) Streaming summaries. Report the size of every directory, not just the root. Do this in a single post-order pass, writing each directory’s total into an output map as the recursion unwinds — still O(N) time and O(N) extra space.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Recursive DFS | O(N) |
O(H) |
Default; simplest; small trees. |
| Iterative DFS | O(N) |
O(N) |
Deep trees where recursion may overflow. |
| Memoized DFS (DAG) | O(N) |
O(N) |
Shared subtrees should be summed once. |
2. Dynamic Friend Connectivity
Problem Statement
You are given n users labeled 0 … n-1 and a stream of q operations to process online. Each operation is one of:
add(a, b)— declare usersaandbfriends (an undirected edge).query(a, b)— returntrueifaandbare currently in the same friendship component,falseotherwise.
Part A. Only add and query operations occur. Implement the system so that all operations are handled as fast as possible.
Part B. A third operation is added:
remove(a, b)— unfriendaandb. The edge is guaranteed to exist.
The system must still answer query correctly. Describe a data structure that supports add, remove, and query efficiently and argue its complexity.
Constraints. - 1 <= n, q <= 2 * 10^5. - For Part A, expected complexity per operation must be sublinear in n. - For Part B, state the best complexity you can achieve and whether the operations may be processed offline.
Example.
n = 4
add(0,1); add(1,2); query(0,2) -> true
remove(1,2); query(0,2) -> false
Solution
Part A (incremental-only). This is textbook Union–Find with union-by-rank and path compression. Both operations run in inverse-Ackermann time, which is effectively constant.
struct DSU {
vector<int> parent, rank_;
DSU(int n): parent(n), rank_(n, 0) {
for (int i = 0; i < n; ++i) parent[i] = i;
}
int find(int x) {
while (parent[x] != x) {
parent[x] = parent[parent[x]]; // path halving
x = parent[x];
}
return x;
}
bool unite(int a, int b) {
a = find(a); b = find(b);
if (a == b) return false;
if (rank_[a] < rank_[b]) swap(a, b);
parent[b] = a;
if (rank_[a] == rank_[b]) ++rank_[a];
return true;
}
bool connected(int a, int b) { return find(a) == find(b); }
};- Time.
O(α(n))amortized per operation. - Space.
O(n).
Part B (fully dynamic). Path compression destroys the history required to split a component when an edge is removed, so the Part A structure alone is insufficient. The cleanest interview answer depends on whether the operations are online or offline.
If the full operation sequence is known in advance (offline), the standard trick is offline segment tree over time + Union–Find with rollback:
- For each edge, compute the time interval
[t_add, t_remove)during which it exists. - Place each such interval on the
O(log q)nodes of a segment tree whose leaves are timestamps. - DFS the segment tree. When entering a node, unite all edges stored there (without path compression). When leaving, roll back those unions from a history stack. At each leaf, answer the query registered at that timestamp.
struct RollbackDSU {
vector<int> parent, rank_;
vector<tuple<int,int,int,int> > history; // (a, ra, b, rb)
RollbackDSU(int n): parent(n), rank_(n, 0) {
for (int i = 0; i < n; ++i) parent[i] = i;
}
int find(int x) { // no path compression
while (parent[x] != x) x = parent[x];
return x;
}
bool unite(int a, int b) {
a = find(a); b = find(b);
if (a == b) { history.push_back(make_tuple(-1,0,-1,0)); return false; }
if (rank_[a] < rank_[b]) swap(a, b);
history.push_back(make_tuple(a, rank_[a], b, rank_[b]));
parent[b] = a;
if (rank_[a] == rank_[b]) ++rank_[a];
return true;
}
void rollback() {
int a, ra, b, rb;
tuple<int,int,int,int> top = history.back(); history.pop_back();
a = std::get<0>(top); ra = std::get<1>(top);
b = std::get<2>(top); rb = std::get<3>(top);
if (a == -1) return;
parent[b] = b;
rank_[a] = ra;
rank_[b] = rb;
}
};Total time for the offline algorithm is O((n + q) · log q · α(n)).
If the operations must be answered online, the textbook result is the Holm–Lichtenberg–Thorup (HDT) structure, which achieves O(log^2 n) amortized per update and O(log n / log log n) per query. Nobody expects you to implement HDT on a whiteboard, but naming it shows breadth.
Discussion
The critical observation for Part A is that Union–Find is the only general data structure offering near-constant incremental connectivity. Everything else (BFS/DFS per query, adjacency + component labeling, …) is either slower or strictly dominated.
Part B is where answers vary wildly by constraints. The honest ordering from weakest to strongest:
- Baseline: adjacency list + BFS per query.
O(1)updates,O(n + m)per query. Always correct and trivial to code — useful as a fallback if you can’t recall the fancy algorithm under pressure. - Offline segment tree + rollback DSU: the competitive-programming standard for this family. Realistic to code in an interview window, and the amortization argument is clean.
- Online HDT (or its link-cut / Euler-tour-tree cousins for forests): the asymptotically best known structure. Cite by name.
Interviewers listen for two things: that you recognize remove is strictly harder than add, and that you can describe some structure (not just “BFS”) with at least polylog update cost.
Follow-ups
(F1) Only forest edges. If you guarantee the graph stays a tree/forest, link-cut trees give O(log n) per op online — much simpler than HDT.
(F2) Count components, not just test pairs. Union–Find already tracks this via a running counter; decrement on every successful unite and increment on every removal that actually disconnects a component. The “actually disconnects” check is what makes removal hard.
(F3) Bulk adds. Given a batch of k adds, process them with a single DSU pass in O(k α(n)). This matters when the stream is bursty.
(F4) Weighted edges / minimum spanning forest. Replace plain DSU with a Kruskal-style incremental MSF; on removal, you may need a replacement edge, which is exactly what HDT searches for level by level.
Trade-offs
| Setting | Best practical choice | Cost per op |
|---|---|---|
| Adds + queries only | Union–Find | O(α(n)) |
| Adds + removes + queries, known offline | Segment tree over time + rollback DSU | O(log^2 n) amortized |
| Adds + removes + queries, online, forest only | Link-Cut Tree | O(log n) |
| Adds + removes + queries, online, general graph | HDT | O(log^2 n) amortized |
| Correct-but-slow fallback | Adjacency list + BFS per query | O(n + m) per query |
3. Swap Adjacent in LR String (Linear and Circular)
Problem Statement
Let start and target be two strings of equal length n over the alphabet {'L', 'R', '.'}. The . cells are empty. A single move is one of:
- Replace a substring
"L."with".L"(anLmoves one cell left into an empty cell). - Replace a substring
".R"with"R."(anRmoves one cell right into an empty cell).
You may perform any number of moves in any order.
Part A (Linear). Return true iff start can be transformed into target.
Part B (Circular). Now treat the string as a ring: index n-1 is adjacent to index 0. An L at position 0 may move to position n-1 (wrap-around), and an R at position n-1 may move to position 0. Decide the same reachability question on the ring.
Constraints. 1 <= n <= 10^5.
Example (linear). start = "RXXLRXRXL", target = "XRLXXRRLX" → true (use X as a stand-in for .).
Solution
Part A — Linear. Observe two invariants that must hold for any reachable target:
- If we strip the
.s from both strings, the resulting sequences ofLs andRs must be identical. (Pieces never swap relative order.) - The
i-th non-dot instartmust be able to reach thei-th non-dot intargetgiven its movement direction:- An
Lonly moves left, so its index instartis>=its index intarget. - An
Ronly moves right, so its index instartis<=its index intarget.
- An
Walking both strings with two pointers is enough to verify both conditions.
bool canTransform(const string& s, const string& t) {
if (s.size() != t.size()) return false;
int n = s.size(), i = 0, j = 0;
while (i < n || j < n) {
while (i < n && s[i] == '.') ++i;
while (j < n && t[j] == '.') ++j;
if (i == n || j == n) return i == n && j == n;
if (s[i] != t[j]) return false;
if (s[i] == 'L' && i < j) return false;
if (s[i] == 'R' && i > j) return false;
++i; ++j;
}
return true;
}- Time.
O(n). - Space.
O(1).
Part B — Circular. The same “pieces cannot pass each other” invariant holds, but now “order” means cyclic order. The reachability check becomes:
- Strip the
.s from both strings. Let the resulting sequences beAandB, with original positionsposAandposB. Bmust be a cyclic rotation ofA. (If it isn’t even that, no sequence of moves can produce it.) You can detect this with the classicBis a substring ofA + Atest, which gives you the rotation offsetoff.- Once you have
off, pieceiinAmust move fromposA[i]toposB[(i - off + m) % m], wherem = |A|. Each piece must travel only in its own direction (Ldecreases index modn,Rincreases index modn), and the distance it travels must be strictly less than the cyclic gap to its neighbour in that direction — otherwise it would overtake or collide with the neighbour.
bool canTransformCircular(const string& s, const string& t) {
if (s.size() != t.size()) return false;
int n = s.size();
string A, B;
vector<int> posA, posB;
for (int i = 0; i < n; ++i) if (s[i] != '.') { A += s[i]; posA.push_back(i); }
for (int i = 0; i < n; ++i) if (t[i] != '.') { B += t[i]; posB.push_back(i); }
if (A.size() != B.size()) return false;
if (A.empty()) return true;
string AA = A + A;
size_t off = AA.find(B);
if (off == string::npos) return false;
int m = A.size();
for (int i = 0; i < m; ++i) {
int src = posA[i];
int dst = posB[((i - (int)off) % m + m) % m];
char c = A[i];
int delta = (c == 'R') ? (dst - src + n) % n
: (src - dst + n) % n;
int nextIdx = (c == 'R') ? (i + 1) % m : (i - 1 + m) % m;
int nextSrc = posA[nextIdx];
int gap = (c == 'R') ? (nextSrc - src + n) % n
: (src - nextSrc + n) % n;
if (delta >= gap) return false;
}
return true;
}- Time.
O(n)(substring search onA + AisO(n)with KMP;findis acceptable for interview purposes). - Space.
O(n).
Discussion
The problem is secretly about invariants under a move system. The moves never change the multiset of non-dot characters, and they never change the relative order of those characters — only their positions. Once you see that, Part A reduces to “can each piece reach its target without crossing another”.
Part B requires one conceptual upgrade: “order” on a ring means cyclic order, so the test “B equals A” becomes “B is a rotation of A”. The standard rotation test is B occurs in A + A. Everything else — the per-piece lane argument — carries over essentially unchanged.
A surprisingly common wrong instinct is to try BFS over board states. With n = 10^5 that’s astronomically hopeless; n = 8 would already be too big for most interviewers. Always stress-test a fast invariant-based solution against a brute-force BFS on small inputs before trusting it.
Follow-ups
(F1) Minimum number of moves. For the linear version, sum, over all pieces, the absolute difference between the piece’s source and target index. That’s the minimum cost because each move shifts one piece by exactly one cell and no move is wasted when the invariants above already hold.
(F2) Count reachable boards. The reachability relation partitions all 3^n boards into equivalence classes. Within a class, the number of distinct boards is the number of ways to interleave the fixed L/R skeleton with .s, subject to the ordering constraints — a combinatorial enumeration that reduces to counting lattice paths with step restrictions.
(F3) Parallel moves. If every piece moves one step simultaneously per round, the reachable set shrinks dramatically and the two-pointer invariant no longer captures it. This variant needs a proper BFS up to modest n.
(F4) Multiple pieces with different alphabets. Add a piece B that can move in both directions. The invariant on the order of Ls and Rs still holds for each of them separately, but Bs can weave between them. The proof sketches out to a graph reachability argument on “which piece can occupy which cell in the canonical form”.
Trade-offs
| Variant | Technique | Cost |
|---|---|---|
| Linear reachability | Two-pointer invariant check | O(n) |
| Circular reachability | Rotation test + per-piece lane check | O(n) |
| Minimum moves (linear) | Sum of absolute index differences | O(n) |
Small-n debugging / tests |
BFS over states | exponential |
4. Faulty Keyboard — Intended Words From Stuck Keys
Problem Statement
A user typed a message on a keyboard where some keys occasionally get stuck, producing runs of the same letter longer than the user intended. A stuck key never drops a character and never produces a wrong character — it only inflates a single intended letter into a run of two or more copies of that same letter.
You are given:
- A dictionary
wordsof lowercase English words. - A typed string
s.
Return every word in words that the user could have intended to type. A word w could have been intended iff s can be segmented into runs c_1^{k_1} c_2^{k_2} … c_r^{k_r} such that w = c_1^{j_1} c_2^{j_2} … c_r^{j_r} with 1 <= j_i <= k_i for every run.
Equivalently: in each run of s, the intended word uses between 1 and the run length copies of the same character, and no characters outside those runs may appear.
Constraints. - |s| <= 10^4. - |words| <= 10^4 and the sum of word lengths is at most 10^5.
Example.
s = "heeellooo"
words = ["hello", "help", "hero", "yellow"]
answer = ["hello"]
s = "gooogle"
words = ["google", "goggle", "go"]
answer = ["google"]
Solution
Insert every dictionary word into a trie. Preprocess s into its list of runs (char, length). DFS the trie while walking the runs: when the current run is (c, k), try consuming 1, 2, …, k copies of c down the children[c] chain, then recurse on the next run.
struct Trie {
Trie* ch[26];
string word;
Trie() : word() { for (int i = 0; i < 26; ++i) ch[i] = nullptr; }
};
void insert(Trie* root, const string& w) {
Trie* cur = root;
for (int i = 0; i < (int)w.size(); ++i) {
int k = w[i] - 'a';
if (!cur->ch[k]) cur->ch[k] = new Trie();
cur = cur->ch[k];
}
cur->word = w;
}
vector<pair<char,int> > toRuns(const string& s) {
vector<pair<char,int> > r;
for (int i = 0; i < (int)s.size();) {
int j = i;
while (j < (int)s.size() && s[j] == s[i]) ++j;
r.push_back(make_pair(s[i], j - i));
i = j;
}
return r;
}
void dfs(Trie* node, const vector<pair<char,int> >& runs, int idx,
unordered_set<string>& out) {
if (idx == (int)runs.size()) {
if (!node->word.empty()) out.insert(node->word);
return;
}
char c = runs[idx].first;
int len = runs[idx].second;
Trie* cur = node;
for (int k = 1; k <= len; ++k) {
cur = cur->ch[c - 'a'];
if (!cur) return; // no dictionary word continues here
dfs(cur, runs, idx + 1, out);
}
}
vector<string> intendedWords(const string& s, const vector<string>& dict) {
Trie root;
for (int i = 0; i < (int)dict.size(); ++i) insert(&root, dict[i]);
vector<pair<char,int> > runs = toRuns(s);
unordered_set<string> out;
dfs(&root, runs, 0, out);
return vector<string>(out.begin(), out.end());
}Let R be the number of runs in s and L the average run length. The DFS visits at most one trie path per (run-count, k-per-run) combination, but the branching is aggressively pruned by the trie: almost no English word has three consecutive identical letters, so the fan-out at each run is effectively O(1).
- Time.
O(sum(|words|))for trie construction, plusO(R · C)per search whereCis the average trie depth explored. - Space.
O(sum(|words|))for the trie,O(R)recursion.
Discussion
The problem is “dictionary + prefix matching with a twist”. The moment you recognize that, a trie is the default scaffolding and you only need to decide what the “twist” is. Here, the twist is that consecutive identical characters in the input may correspond to one or more copies along the trie path — a bounded branching decision at each step.
Two subtle correctness points:
- Dictionary words with runs.
"google"contains"oo". When you hit the"ooo"run ins, you must allow the trie to consume exactly2copies; consuming1lands on a dead end because"gogle"isn’t in the trie. The per-run loop handles this naturally. - Duplicates in the answer. A DFS may reach the same word via different consumption patterns. Collect results in a set.
Follow-ups
(F1) Dropped keys. Some keypresses don’t register. Now, in addition to consuming characters from the current run, the DFS may consume a trie edge whose character doesn’t appear in s at all — this is edit-distance-style DP on the trie.
(F2) Typos. Any character may be wrong. Add a bounded number of substitution moves to the DFS and prune by distance threshold.
(F3) Top-K likely intended words. If the dictionary carries frequency priors, maintain a min-heap of size K keyed on frequency during the DFS.
(F4) Huge dictionary. Replace the trie with a DAWG (directed acyclic word graph) so shared suffixes cost nothing. Cuts memory by 2–5x on English.
(F5) Streaming queries. Cache results per input s. Preprocessing cost is paid once; each unique query is independent.
Trade-offs
| Approach | Preproc | Query | Notes |
|---|---|---|---|
| Trie + DFS over runs | O(sum |w|) |
O(R · paths) |
The default answer. |
| Hash the dictionary, try each | O(sum |w|) |
O(|words| · |s|) |
Fine when both are tiny. |
| Edit-distance DP on trie | O(sum |w|) |
O(R · |trie|) |
For the typo follow-up. |
| DAWG | O(sum |w|) |
O(R · paths) |
Saves memory on big dicts. |
5. Detect Rotated Squares
Problem Statement
Maintain a dynamic multiset of 2D integer points supporting two operations:
add(x, y)— insert a point at(x, y); duplicates are allowed.count()— return the total number of (possibly rotated) squares whose four vertices are all present in the multiset. Each geometric square is counted as many ways as its vertices can be chosen, weighted by multiplicities (see below).
A square here is any four distinct points forming the vertices of an actual square — axis-aligned or rotated by any angle. If a vertex has multiplicity m, it contributes a factor of m to the count per occurrence in the square (as in the standard LeetCode “Detect Squares” convention).
Constraints. - Coordinates are integers with |x|, |y| <= 10^4. - At most 3 · 10^4 total add calls. - count() is called far less frequently than add, but must still be efficient (sublinear per call is not expected; O(n^2) with good constants is).
Example.
add(0, 0); add(2, 1); add(3, -1); add(1, -2);
count() -> 1 // the four points form a square rotated ≈26.57°
Solution
Key invariant. The two diagonals of a square share the same midpoint, have the same length, and are perpendicular. That single statement drives the algorithm: group every unordered pair of points by (midpoint, squared length); within each bucket, count the number of pairs of diagonal vectors that are perpendicular.
To stay in integer arithmetic, represent the midpoint as 2 · midpoint (so it’s always an integer pair), and store the diagonal vector (dx, dy) = p_j - p_i once per bucket. Two diagonal vectors (u_x, u_y) and (v_x, v_y) correspond to a valid square iff u_x · v_x + u_y · v_y == 0.
struct DetectSquares {
unordered_map<long long, int> mult; // (x,y) packed -> multiplicity
static long long key(int x, int y) {
return ((long long)(x + 20000) << 20) | (unsigned)(y + 20000);
}
void add(int x, int y) { ++mult[key(x, y)]; }
long long count() {
vector<pair<int,int> > pts;
pts.reserve(mult.size());
for (unordered_map<long long,int>::const_iterator it = mult.begin();
it != mult.end(); ++it) {
long long k = it->first;
int y = (int)(k & 0xFFFFF) - 20000;
int x = (int)(k >> 20) - 20000;
pts.push_back(make_pair(x, y));
}
long long total = 0;
int n = pts.size();
// For each unordered pair, treat it as one diagonal (A, C).
for (int i = 0; i < n; ++i) {
int ax = pts[i].first, ay = pts[i].second;
for (int j = i + 1; j < n; ++j) {
int cx = pts[j].first, cy = pts[j].second;
int dx = cx - ax, dy = cy - ay;
// For integer-coordinate squares, the other two vertices are
// integer iff both (dx + dy) and (dy - dx) are even -- i.e.
// (dx + dy) is even. When (dx + dy) is odd, no square exists.
if (((dx + dy) & 1) != 0) continue;
int mx2 = ax + cx, my2 = ay + cy; // 2 * midpoint
int hx = (cx - ax) / 2, hy = (cy - ay) / 2; // half diag
// The other two vertices are midpoint +/- rot90(half diag)
int bx = (mx2 / 2) - hy, by = (my2 / 2) + hx;
int ddx = (mx2 / 2) + hy, ddy = (my2 / 2) - hx;
unordered_map<long long,int>::const_iterator b = mult.find(key(bx, by));
unordered_map<long long,int>::const_iterator d = mult.find(key(ddx, ddy));
if (b != mult.end() && d != mult.end()) {
long long ma = mult[key(ax, ay)];
long long mc = mult[key(cx, cy)];
total += ma * mc * (long long)b->second * (long long)d->second;
}
}
}
// Each square has exactly 2 diagonals and we iterated unordered pairs,
// so each square was counted twice.
return total / 2;
}
};- Time.
count()runs inO(n^2)wherenis the number of distinct points. - Space.
O(n)for the multiplicity map.
Discussion
The move from axis-aligned Detect Squares (LeetCode 2013) to the rotated version is the classic “stop grouping by same-x/same-y”. Axis-aligned squares are special because their sides are parallel to the axes, which lets you anchor on one pair of opposing corners and check two axis-aligned lookups. Rotated squares have a full continuous orientation family, so no fixed-axis anchor works.
The saving grace is that diagonals are a much better primitive than sides: every square has two diagonals, both passing through the same center, both of the same length, and perpendicular to each other. So any pair of points that can serve as a diagonal determines the other two vertices uniquely — and you can either test those vertices directly (as the code above does) or, for a pair-hashing variant, bucket pairs by (midpoint, length^2) and within each bucket count perpendicular pairs.
A useful parity shortcut: if (dx + dy) is odd, the other two vertices have non-integer coordinates and the pair cannot be a diagonal of an integer square. That filter alone kills roughly half of all candidate pairs for free.
Follow-ups
(F1) Axis-aligned only (the LC 2013 baseline). Much easier: fix one corner, enumerate the opposite corner on the diagonal, read two axis-aligned corners with direct lookups. O(n) per query.
(F2) Bounded coordinate range [0, C]. Instead of iterating over pairs, iterate over each point and every lattice vector (a, b) with a^2 + b^2 <= C^2; use the hash map to probe the remaining three vertices. For n >> C^2 this is asymptotically better at O(n · C^2).
(F3) Rectangles instead of squares. Bucket pairs by (midpoint, length^2) only; within each bucket, every pair of diagonals forms a rectangle — drop the perpendicularity check, but remember to divide by 2 for the pair of diagonals of each rectangle.
(F4) Streaming / online squares. Instead of recomputing from scratch on each count(), maintain the bucket structure incrementally: each add updates O(n) buckets in the worst case, but a well-chosen layered hash can amortize.
(F5) Count lattice points forming a regular polygon with k vertices. Same invariant idea, generalised: fix the center and the radius, look for k points whose position vectors from the center are rotations of each other by 2π / k. For integer lattices only k = 4 (squares) admits non-trivial solutions at arbitrary orientation.
Trade-offs
| Variant | Structure | Cost per count() |
|---|---|---|
| Rotated squares (this problem) | Diagonal-pair iteration + hash map | O(n^2) |
| Axis-aligned only | Group by x, scan y |
O(n) |
Bounded coordinate range [0, C] |
Point × lattice vector sweep | O(n · C^2) |
| Incremental online | Pair-bucket structure with amortised | varies |
6. Meeting Rooms II — At Scale
Problem Statement
Given n meeting intervals intervals[i] = [s_i, e_i), return the minimum number of conference rooms required so that no two overlapping meetings share a room. Then answer the follow-up: if the number of intervals is far too large to fit in memory, how would you compute the same answer?
Constraints. 1 <= n, all start/end values are 32-bit integers, and intervals are half-open: a meeting ending at time t does not conflict with one starting at t.
Example. [[0,30],[5,10],[15,20]] -> 2.
Solution
Convert each interval into two time-stamped delta events (s, +1) and (e, -1), sort them, and sweep. The running sum is the current number of occupied rooms; the global maximum is the answer. The half-open convention means that at equal timestamps the -1 events must be processed before the +1 events.
int minRooms(vector<vector<int> >& iv) {
vector<pair<int,int> > ev;
ev.reserve(iv.size() * 2);
for (int i = 0; i < (int)iv.size(); ++i) {
ev.push_back(make_pair(iv[i][0], +1));
ev.push_back(make_pair(iv[i][1], -1));
}
sort(ev.begin(), ev.end(),
[](const pair<int,int>& a, const pair<int,int>& b) {
if (a.first != b.first) return a.first < b.first;
return a.second < b.second; // -1 before +1
});
int cur = 0, peak = 0;
for (int i = 0; i < (int)ev.size(); ++i) {
cur += ev[i].second;
if (cur > peak) peak = cur;
}
return peak;
}- Time.
O(n log n). Space.O(n).
Discussion
The heap-of-end-times approach (O(n log n) sort + O(n log n) heap) is functionally equivalent, but the sweep formulation decouples the sort from the counting, and the counting pass itself is a trivial linear scan with O(1) state. That property is what makes the scale follow-up tractable.
Follow-ups
(F1) Data is far too large to fit in RAM. First identify which regime you’re in:
- Single-machine external: external-merge-sort the events to disk in chunks that fit in memory, then stream the sorted file through a running counter —
O(n log n)CPU,O(n)I/O. - Distributed (MapReduce / Spark):
Mapemits(t, +1)and(t, -1)per interval; shuffle by time key; within each partition, compute a local peak and a running delta at the partition boundary; a final sequential pass reconciles the boundaries to get the global peak. - Unbounded stream: maintain a sorted multiset (or min-heap) of active end times; on every incoming interval, evict all expired ends, insert the new one, and update the peak.
(F2) Peak concurrency in a rolling window. Same sweep, but evict events that fall outside the window edge.
(F3) Per-room assignment. Augment the heap variant so that each pop returns the specific room ID to reuse.
Trade-offs
| Regime | Technique | Bottleneck |
|---|---|---|
| In-memory | Sweep line or heap of ends | Sort |
| Single-machine disk | External merge sort + streaming | Disk I/O |
| Distributed | MapReduce + boundary reconciliation | Shuffle + sort |
| Unbounded stream | Online multiset/heap with eviction | Memory of active set |
7. Earliest Moment All Friends Connected
Problem Statement
There are n people and a list of timestamped friendship events. Each event is either FRIEND(t, a, b) (at time t, a and b become friends) or UNFRIEND(t, a, b) (at time t, they stop being friends — guaranteed to be currently friends).
Return the earliest timestamp at which the friendship graph contains exactly one connected component, or -1 if that never happens.
Constraints. 1 <= n, m <= 2 * 10^5. All timestamps are distinct.
Solution
Base case (no unfriend events). Sort events by time, feed them into a Union–Find with a component counter, and return the timestamp of the union that reduces the count to 1.
int earliestAcq(vector<vector<int> >& logs, int n) {
sort(logs.begin(), logs.end());
DSU d(n);
int comp = n;
for (int i = 0; i < (int)logs.size(); ++i) {
if (d.unite(logs[i][1], logs[i][2])) {
if (--comp == 1) return logs[i][0];
}
}
return -1;
}O(m log m) time, O(n) space.
Full case (unfriend allowed). The set of “fully connected” timestamps is not monotonic, so you cannot binary-search. Use the offline segment tree over time + rollback DSU pattern:
- Compute each edge’s lifetime
[t_add, t_remove)by scanning the logs. - Place each lifetime on the
O(log m)nodes of a segment tree whose leaves are the distinct timestamps. - DFS the segment tree. On entry to a node, unite the edges stored there and record them on a rollback stack. At each leaf, if
comp == 1, record the timestamp as a candidate. On exit, roll back the unions added at this node.
The minimum recorded timestamp is the answer, or -1.
- Time.
O((n + m) · log m · α(n)). - Space.
O(m log m)for the segment tree.
Discussion
The key insight is that unfriend makes this a fully dynamic connectivity problem, which plain DSU cannot handle because path compression destroys history. Among the classical solutions (HDT online, link-cut trees for forests, segment tree over time offline), the segment tree variant is by far the most realistic to code in a 45-minute interview — every primitive is either already memorized (DSU, segment tree) or a small tweak (disable path compression, add a rollback stack).
Follow-ups
(F1) Online. Cite HDT (O(log^2 n) amortized per op) by name; don’t try to code it. (F2) Forest only. Link-cut trees give O(log n) online. (F3) Query at arbitrary time. Register the query at its timestamp leaf and collect the answer during the same DFS.
Trade-offs
| Constraint | Technique | Complexity |
|---|---|---|
Only FRIEND events |
Sort + incremental DSU | O(m log m) |
UNFRIEND + offline |
Segment tree over time + rollback DSU | O((n+m) log m α(n)) |
UNFRIEND + online forest |
Link-cut tree | O(log n) per op |
UNFRIEND + online graph |
Holm–Lichtenberg–Thorup | O(log^2 n) amortized |
8. Meeting Rooms I and II
Problem Statement
Given n meeting intervals [s_i, e_i), answer two questions:
- Part A. Can a single person attend every meeting (i.e., no two intervals overlap)?
- Part B. What is the minimum number of rooms required to host all the meetings concurrently?
Intervals are half-open; a meeting ending at t does not conflict with one starting at t. Constraints: 1 <= n <= 2 * 10^5.
Solution
Part A. Sort intervals by start; verify each successor’s start is at least the previous end.
bool canAttendAll(vector<vector<int> >& iv) {
sort(iv.begin(), iv.end());
for (int i = 1; i < (int)iv.size(); ++i)
if (iv[i][0] < iv[i-1][1]) return false;
return true;
}Part B. The sweep-line / delta-events approach from problem 6 is the cleanest. Alternatively, a min-heap of active end times: sort by start, push each meeting’s end time, pop any end <= current start before pushing.
int minRooms(vector<vector<int> >& iv) {
sort(iv.begin(), iv.end());
priority_queue<int, vector<int>, greater<int> > pq;
for (int i = 0; i < (int)iv.size(); ++i) {
if (!pq.empty() && pq.top() <= iv[i][0]) pq.pop();
pq.push(iv[i][1]);
}
return (int)pq.size();
}Both run in O(n log n).
Discussion
Part A is the textbook “no overlaps” check. Part B is the textbook “peak concurrency”. The two problems share the sorting step but diverge on what they track — “did any successor start before its predecessor ended” vs. “what’s the maximum concurrent count”. Interviewers use this pair to gauge whether you can separate the pattern from the detail.
Follow-ups
(F1) Scale. Use the external-merge-sort + streaming sweep from problem 6. (F2) Return the actual room assignment. Replace the min-heap with a min-heap of (end_time, room_id). (F3) Weighted overlap. Each meeting has a weight; report the peak weighted concurrency. Same sweep, delta is the weight.
Trade-offs
| Question | Best approach | Cost |
|---|---|---|
| Part A | Sort + adjacency check | O(n log n) |
| Part B (in-memory) | Sweep line or min-heap of end times | O(n log n) |
Part B (huge n) |
External merge sort + streaming counter | O(n log n) |
9. Hierarchical Manager / Peer Queries
Problem Statement
Implement an incremental hierarchy service over n employees that supports three operations, arriving in any order:
manager(a, b)— record thatais the direct manager ofb.peer(a, b)— record thataandbshare the same direct manager.is_manager(a, b)— returntrueiffais an ancestor (direct or transitive) ofbin the current manager tree.
Contradictory operations (e.g., peer(a, b) when a and b already have different managers) should be rejected.
Solution
Maintain two structures:
- A peer DSU grouping employees who must share the same direct manager.
- A manager tree over peer groups: each peer group stores a parent pointer to another peer group (the manager’s group).
manager(a, b): find the peer group of b, set its manager-tree parent to the peer group of a. If it already has a different parent, reject.
peer(a, b): union the peer groups of a and b. If both groups already have manager-tree parents, they must agree (otherwise reject).
is_manager(a, b): walk the manager-tree parent chain starting from the peer group of b; return true iff you hit the peer group of a. Use path compression on the manager tree to amortize future queries.
struct Hierarchy {
DSU peers;
vector<int> mparent; // manager tree parent (by peer-group root)
Hierarchy(int n): peers(n), mparent(n, -1) {}
int group(int x) { return peers.find(x); }
bool setManager(int a, int b) {
int ga = group(a), gb = group(b);
if (mparent[gb] != -1 && mparent[gb] != ga) return false;
mparent[gb] = ga;
return true;
}
bool setPeer(int a, int b) {
int ga = group(a), gb = group(b);
if (ga == gb) return true;
int pa = mparent[ga], pb = mparent[gb];
if (pa != -1 && pb != -1 && pa != pb) return false;
peers.unite(a, b);
int g = group(a);
mparent[g] = (pa != -1) ? pa : pb;
return true;
}
bool isManager(int a, int b) {
int ga = group(a);
int cur = mparent[group(b)];
while (cur != -1) {
if (cur == ga) return true;
cur = mparent[cur];
}
return false;
}
};- Time.
O(α(n))per operation amortized.
Discussion
The “two-level DSU” trick — one DSU for equivalence, a parent-pointer tree over its groups for hierarchy — is the cleanest model for this family. Most candidates reach for an adjacency map plus BFS, which bloats to O(n) per query.
Follow-ups
(F1) Bulk imports. A batch of k events processes in O(k α(n)). (F2) LCA queries between two employees: walk both chains with depth tracking; works because the manager tree is small. (F3) Remove a manager relationship. Now you need fully dynamic connectivity on the manager tree. Link-cut tree suffices since it’s always a forest.
Trade-offs
| Need | Structure |
|---|---|
| Static hierarchy | Parent pointers + path compression |
| Manager + peer mixed | Two-level DSU (peer DSU + manager tree) |
| Removal allowed | Link-cut tree |
10. Document Undo / Redo With Batches
Problem Statement
Design a key-value document supporting these operations:
set(k, v)— set keykto valuev.remove(k)— delete keyk.beginBatch()/endBatch()— group a sequence ofset/removeoperations into a single atomic history entry.undo()— undo the most recent history entry (either a single op or an entire batch).redo()— re-apply the most recently undone history entry.
Memory usage must be proportional to the number of distinct keys touched per history entry, not to the number of writes — a batch that overwrites the same key a million times must record at most one history slot for that key.
Solution
Represent each history entry as a map from key to Change { before, after }, where before is the value at the moment the entry started (plus an “existed” flag) and after is the value the entry committed. Maintain two stacks, undoStack and redoStack.
struct Change { string oldVal; bool hadOld; string newVal; bool hasNew; };
struct HistoryOp { unordered_map<string, Change> changes; };
struct Doc {
unordered_map<string, string> store;
vector<HistoryOp> undoStack, redoStack;
HistoryOp* current; // non-null iff inside a batch
bool inBatch;
Doc() : current(nullptr), inBatch(false) {}
void recordBefore(HistoryOp& op, const string& k) {
if (op.changes.find(k) != op.changes.end()) return;
Change c;
unordered_map<string,string>::iterator it = store.find(k);
c.hadOld = (it != store.end());
c.oldVal = c.hadOld ? it->second : string();
c.hasNew = false;
op.changes[k] = c;
}
void set(const string& k, const string& v) {
HistoryOp single;
HistoryOp& op = inBatch ? *current : single;
recordBefore(op, k);
op.changes[k].newVal = v;
op.changes[k].hasNew = true;
store[k] = v;
if (!inBatch) { undoStack.push_back(op); redoStack.clear(); }
}
// remove(), beginBatch(), endBatch(), undo(), redo() analogous.
};On undo(): pop from undoStack, iterate its changes, restore each before, push onto redoStack. redo() is symmetric.
- Time.
O(1)perset/remove,O(|changes|)perundo/redo. - Space.
O(distinct keys touched)per history entry.
Discussion
The central design decision is what a history entry stores. Storing full document snapshots is O(n) per entry and trivially correct. Storing per-write diffs is O(writes), which is still too much when the same key is hammered. Storing one before/after pair per touched key gives the desired asymptotic.
Follow-ups
(F1) Nested batches. Replace the current pointer with a stack of open batches; on endBatch(), merge the inner batch into the outer (first-write wins for before, last-write wins for after). (F2) Persistence. Flush committed history entries to a write-ahead log so the state survives a restart. (F3) Collaborative editing. Wrap each history entry in a vector clock and reconcile via operational transformation or CRDTs.
Trade-offs
| Strategy | Memory per op | Undo cost |
|---|---|---|
| Full snapshot | O(|doc|) |
O(|doc|) |
| Per-write diff | O(writes) |
O(writes) |
| Per-key before/after (above) | O(touched keys) |
O(touched keys) |
11. Waiting List System
Problem Statement
Design a FIFO waiting list over n distinguishable users supporting:
join(u)— append useruto the back of the list.leave(u)— removeufrom wherever they currently are.next()— pop and return the user at the front (FIFO dequeue).position(u)— returnu’s 1-based index in the current list.
State your policy for duplicate join(u) calls up front (ignore, refresh to back, or reject) — the interview grades you partly on explicitly addressing the ambiguity.
Solution
Two structures work well depending on how often position is called.
A) Doubly linked list + hash map. join, leave, next are all O(1). position walks the list at O(n). This is the classical LRU-style structure and is the right choice when position is rare.
B) Fenwick tree over monotonic tickets. Assign each join a strictly increasing ticket index t, store 1 at index t in a BIT, and use prefix sums for position.
struct WaitingList {
struct BIT {
vector<int> t;
BIT(int n) : t(n + 1, 0) {}
void upd(int i, int v) { for (; i < (int)t.size(); i += i & -i) t[i] += v; }
int sum(int i) const {
int s = 0; for (; i > 0; i -= i & -i) s += t[i]; return s;
}
int kth(int k) const { // smallest i with sum(i) >= k
int pos = 0, log = 1;
while ((log << 1) < (int)t.size()) log <<= 1;
for (int d = log; d; d >>= 1) {
if (pos + d < (int)t.size() && t[pos + d] < k) {
pos += d; k -= t[pos];
}
}
return pos + 1;
}
};
BIT bit;
unordered_map<int,int> ticket; // user -> ticket index
int nextTicket;
WaitingList(int cap) : bit(cap), nextTicket(0) {}
void join(int u) {
if (ticket.count(u)) return; // "ignore" policy
++nextTicket; ticket[u] = nextTicket;
bit.upd(nextTicket, +1);
}
void leave(int u) {
unordered_map<int,int>::iterator it = ticket.find(u);
if (it == ticket.end()) return;
bit.upd(it->second, -1);
ticket.erase(it);
}
int position(int u) {
unordered_map<int,int>::iterator it = ticket.find(u);
if (it == ticket.end()) return -1;
return bit.sum(it->second);
}
};All operations run in O(log n).
Discussion
The single design decision is whether position is a hot path. Option A is the “LRU cache plus” answer interviewers expect by default; option B is the “we scaled this to millions of users” answer. Mention both.
Follow-ups
(F1) next() must also be O(log n) under option B. Use kth(1) on the BIT — the smallest index with prefix sum >=1. (F2) Rank range queries (“who is between positions 10 and 20?”) — option B generalizes naturally; option A doesn’t. (F3) Multiple wait lists per user. Shard the BIT per list key.
Trade-offs
| Dominant op | Structure | Cost |
|---|---|---|
next, leave |
DLL + hash map | O(1) / O(n) pos |
position |
BIT over monotonic tickets | O(log n) all |
12. Triplet Packing On A Conveyor Belt (1D)
Problem Statement
Items arrive online, each with an integer size. Maintain an active pool and, after every insertion, check whether any three active items a, b, c satisfy max(a,b,c) - min(a,b,c) < T for a given threshold T. If so, remove all three from the pool and return them as a packed triplet. Otherwise, keep the new item and return “no triplet”.
Solution
Keep the active items in a sorted multiset (std::multiset<int> or a BIT over value buckets). On insertion, look at x’s position and check the three length-3 windows that include it: [i-2, i-1, i], [i-1, i, i+1], [i, i+1, i+2]. If any window has max - min < T, remove those three values and return them.
struct Packer {
multiset<int> pool;
int T;
vector<int> add(int x) {
pool.insert(x);
vector<int> arr(pool.begin(), pool.end());
int i = (int)(lower_bound(arr.begin(), arr.end(), x) - arr.begin());
for (int lo = max(0, i - 2); lo + 2 < (int)arr.size() && lo <= i; ++lo) {
if (arr[lo + 2] - arr[lo] < T) {
vector<int> trip(arr.begin() + lo, arr.begin() + lo + 3);
for (int k = 0; k < 3; ++k) pool.erase(pool.find(trip[k]));
return trip;
}
}
return vector<int>();
}
};Using a BIT or direct multiset::iterator arithmetic avoids the O(n) copy and brings amortized cost to O(log n) per insertion.
Discussion
The consecutive triple observation — any valid triplet must consist of three values adjacent in sorted order — turns an O(n^3) search into O(log n) per insert. The proof is a pigeonhole argument: if a triplet skips a value between its min and max, substituting the skipped value still keeps the range below T.
Follow-ups
(F1) Quadruplet instead of triplet. Same idea, window of 4. (F2) Different notion of “close”. Not just range; you might want max - min <= T or pairwise differences bounded. Each changes the window length and the pigeonhole proof needs re-checking. (F3) 2D version. See problem 13 — the sorted-window trick breaks in 2D.
Trade-offs
| Structure | add |
Notes |
|---|---|---|
std::multiset |
O(log n) |
Cleanest; constant factor higher |
| BIT over value buckets | O(log^2 n) |
Useful when values are bounded |
| Sorted array + shifting | O(n) |
Only for tiny n |
13. Triplet Packing On A 2D Plane
Problem Statement
The same packing rule as problem 12, but each item is a 2D point. Pack three active points if their maximum pairwise Euclidean distance is strictly less than a threshold T. Implement add(p) that inserts a new point and removes a valid triplet if one now exists.
Solution
Bucket points into a uniform grid with cell side s = T / sqrt(2). Any two points in the same cell are within T of each other. Any valid triplet’s three points lie within a 3x3 block of cells centered on any of them, because cells farther than two steps away are more than T apart.
On add(p):
- Insert
pinto its cell. - Collect candidate points from the
3x3block aroundp. - For each pair
(q, r)of candidates other thanp, verify all three pairwise distances are belowT. Return the first triplet found.
struct Packer2D {
double T, s;
unordered_map<long long, vector<pair<double,double> > > cells;
Packer2D(double T_) : T(T_), s(T_ / 1.41421356237) {}
long long key(int cx, int cy) {
return ((long long)(cx + (1 << 20)) << 21) | (cy + (1 << 20));
}
vector<pair<double,double> > add(double x, double y) {
int cx = (int)floor(x / s), cy = (int)floor(y / s);
cells[key(cx, cy)].push_back(make_pair(x, y));
vector<pair<double,double> > cand;
for (int dx = -1; dx <= 1; ++dx)
for (int dy = -1; dy <= 1; ++dy) {
long long k = key(cx + dx, cy + dy);
unordered_map<long long, vector<pair<double,double> > >::iterator it = cells.find(k);
if (it != cells.end())
for (int j = 0; j < (int)it->second.size(); ++j)
cand.push_back(it->second[j]);
}
// enumerate pairs within cand together with p = (x,y)
for (int i = 0; i < (int)cand.size(); ++i)
for (int j = i + 1; j < (int)cand.size(); ++j) {
// test (cand[i], cand[j], p)
// compute pairwise distances and compare to T
// on success: remove the three points from cells and return them
}
return vector<pair<double,double> >();
}
};Amortized cost per add is O(k^2) where k is the number of candidates in the neighborhood — small when points are sparsely distributed.
Discussion
Grid bucketing with cell side T / sqrt(d) is the standard pattern for “points within distance T” in d dimensions. The 1D consecutive-triple trick from problem 12 does not generalize, because three close 2D points have no natural sorted order.
Follow-ups
(F1) Higher dimensions. Scale the cell side to T / sqrt(d) and the neighborhood to 3^d. Past d = 4 or so, prefer a KD-tree. (F2) Weighted distance. Replace Euclidean with Manhattan or Chebyshev — cell side becomes T or T / 2 respectively. (F3) Streaming with bounded memory. Evict old points via a sliding window; bucket-by-bucket eviction is O(1) amortized.
Trade-offs
| Structure | Insert | When it wins |
|---|---|---|
| Uniform grid | O(k^2) expected |
Sparse points, small T |
| KD-tree | O(log n) + query |
Higher dimensions |
| R-tree | O(log n) |
Disk-resident datasets |
14. Chunking With An Unsplittable Header
Problem Statement
You are given three non-negative integers: header, data, and M (the maximum chunk size in bytes). You must split the stream into chunks such that:
- Every chunk has size at most
M. - The first
headerbytes form a logical header that cannot straddle a chunk boundary: they must lie entirely within a single chunk.
Return the minimum number of chunks needed, or -1 if no valid split exists.
Solution
If header > M, no split works — return -1. Otherwise the header occupies one chunk, which may additionally carry up to M - header bytes of data. Whatever data remains is split into ceil(remaining / M) uniform chunks.
int minChunks(long long header, long long data, long long M) {
if (header > M) return -1;
long long room = M - header;
long long packed = min(room, data);
long long leftover = data - packed;
long long rest = (leftover + M - 1) / M;
return 1 + (int)rest;
}- Time.
O(1).
Discussion
This is a closed-form arithmetic problem disguised as a packing problem. The only real judgment is the header-sharing decision: always pack data into the header’s chunk if there’s room, because it never increases the chunk count and often decreases it by one.
Follow-ups
(F1) Multiple headers / trailers. Each constraint becomes a case split. (F2) Variable chunk size M_i per position. Becomes a 1D DP. (F3) Minimum wasted padding (pack to M, don’t count leftover holes). Adds a second objective; use ceil carefully.
Trade-offs
| Variant | Technique |
|---|---|
Single header + fixed M |
Closed-form arithmetic |
Per-position variable M_i |
1D DP O(n) |
| Header with forbidden byte ranges | Greedy with backtracking |
15. Sequence With An Index Pointer And Range Moves
Problem Statement
Design a mutable sequence of integers supporting:
moveIndex(k)— shift the current index pointer bykpositions.setIndex(i)— set the index pointer to absolute positioni.moveRange(l, r, to)— remove the closed sub-range[l..r]and re-insert it so that its new starting index (in the post-removal sequence) isto.
Your design must state two things explicitly:
- Whether the index pointer follows the element it was pointing at (if that element was inside the moved range) or the absolute numeric position.
- Whether
tois an index in the pre-removal or post-removal sequence — the cleanest spec is post-removal.
Solution
For in-memory, small-to-medium sequences, use a std::vector with explicit copy-erase-insert:
struct Seq {
vector<int> buf;
int ptr;
void moveRange(int l, int r, int to) {
vector<int> chunk(buf.begin() + l, buf.begin() + r + 1);
buf.erase(buf.begin() + l, buf.begin() + r + 1);
buf.insert(buf.begin() + to, chunk.begin(), chunk.end());
// update ptr: if ptr was inside [l..r] it follows to [to .. to + (r-l)];
// otherwise it shifts by the net displacement of its region.
if (ptr >= l && ptr <= r) ptr = to + (ptr - l);
else if (ptr > r) {
ptr -= (r - l + 1);
if (ptr >= to) ptr += (r - l + 1);
} else if (ptr >= to) {
ptr += (r - l + 1);
}
}
};Each moveRange is O(n) worst case. If the interviewer asks for sublinear range moves, reach for a rope or an implicit-key treap, which splits and concatenates in O(log n) per operation. Sketch the idea; don’t try to code a treap on a whiteboard.
Discussion
OOD rounds grade the API and the edge-case policy as separately as the code. List every ambiguity — duplicate indices, out-of-bounds pointers, overlapping [l..r] and to ranges — and pick a policy before writing a single line.
Follow-ups
(F1) Sublinear moves. Rope / implicit treap / block decomposition (sqrt blocks of size O(sqrt(n))). (F2) Undo support. Wrap the mutator in the batched-change pattern from problem 10. (F3) Persistence. Use a functional treap so every version is an immutable snapshot.
Trade-offs
| Backing store | moveRange |
When to pick |
|---|---|---|
std::vector |
O(n) |
n <= 10^4, simple |
| Linked list | O(1) splice |
Large n, no moveIndex(k) |
| Implicit treap / rope | O(log n) |
n >= 10^5, rank queries too |
16. First Occurrence Of A Pattern In A String
Problem Statement
Given two strings text (length n) and pattern (length m), return the index of the first position in text where pattern occurs as a contiguous substring, or -1 if it does not occur. If pattern is empty, return 0.
Constraints. 1 <= n, m <= 10^5. Characters are ASCII.
Solution
The Knuth–Morris–Pratt algorithm runs in O(n + m) time and O(m) extra space. The core idea is to precompute, for each prefix of the pattern, the length of its longest proper prefix that is also a suffix (the “failure function”). On a mismatch during the scan, we jump the pattern pointer back using this table instead of backtracking in the text.
vector<int> failure(const string& p) {
int m = (int)p.size();
vector<int> f(m, 0);
for (int i = 1, k = 0; i < m; ++i) {
while (k > 0 && p[k] != p[i]) k = f[k - 1];
if (p[k] == p[i]) ++k;
f[i] = k;
}
return f;
}
int strStr(const string& t, const string& p) {
if (p.empty()) return 0;
vector<int> f = failure(p);
int n = (int)t.size(), m = (int)p.size();
for (int i = 0, k = 0; i < n; ++i) {
while (k > 0 && p[k] != t[i]) k = f[k - 1];
if (p[k] == t[i]) ++k;
if (k == m) return i - m + 1;
}
return -1;
}- Time.
O(n + m). Space.O(m).
Discussion
KMP is one of the highest-ROI algorithms to memorize — the entire matcher is under 30 lines and it resolves a whole family of substring problems (anagram rotations, smallest rotation, longest palindromic prefix). The failure function is the linchpin: it captures the longest self-overlap of every prefix so the matcher can skip positions that provably cannot match.
Follow-ups
(F1) Find all occurrences. Instead of returning on k == m, record the index and reset k = f[m - 1]. (F2) Multi-pattern matching. Use the Aho–Corasick automaton (KMP’s generalization to a trie). (F3) Approximate matching. KMP does not handle edits. Fall back to dynamic programming (O(nm)) or Bitap/Wu–Manber.
Trade-offs
| Algorithm | Time | Space | Notes |
|---|---|---|---|
| Naive | O(nm) |
O(1) |
Fine for tiny inputs |
| KMP | O(n+m) |
O(m) |
Deterministic worst case |
| Rabin–Karp | O(n+m) avg |
O(1) |
Great for multi-pattern |
| Z-function | O(n+m) |
O(n+m) |
Alternative one-pass form |
17. Weighted Random Index
Problem Statement
You are given a positive integer array w of length n. Design a data structure that, given no further input, returns an index i in [0, n) with probability proportional to w[i]:
$$
\Pr[i \text{ returned}] = \frac{w_i}{\sum_j w_j}.
$$
Multiple queries are issued against the same data structure. Minimize the per-query cost.
Solution
A) Prefix sum + binary search. Precompute the prefix sums of w in O(n). Per query, draw a uniform integer r in [0, sum) and return the first index whose prefix sum is strictly greater than r.
struct WeightedPick {
vector<long long> pref;
WeightedPick(const vector<int>& w) : pref(w.size()) {
pref[0] = w[0];
for (int i = 1; i < (int)w.size(); ++i) pref[i] = pref[i - 1] + w[i];
}
int pick() {
long long r = ((long long)rand() << 15 ^ rand()) % pref.back();
return (int)(upper_bound(pref.begin(), pref.end(), r) - pref.begin());
}
};O(n) preprocessing, O(log n) per query.
B) Walker’s alias method. Preprocess in O(n) into two length-n arrays prob and alias, then pick uniformly random i and toss a biased coin with probability prob[i]. Each query is O(1).
Discussion
Choose between the two by query frequency. The prefix-sum method is simpler to derive, simpler to code, and fast enough for nearly every interview use case. The alias method is the right answer if the interviewer stresses “millions of queries per second” or asks for constant-time sampling explicitly.
One common pitfall: use upper_bound, not lower_bound. You want the first prefix strictly greater than the drawn r.
Follow-ups
(F1) Weights change over time. Replace the prefix array with a Fenwick tree — O(log n) updates and O(log n) queries. (F2) Sample without replacement. Draw, zero that weight, re-normalize. With a BIT, this is O(k log n) for k samples. (F3) Streaming / reservoir sampling with weights. Use the A-Res algorithm (Efraimidis–Spirakis).
Trade-offs
| Method | Preproc | Query | Update |
|---|---|---|---|
Prefix sum + upper_bound |
O(n) |
O(log n) |
O(n) |
| Alias method | O(n) |
O(1) |
O(n) rebuild |
| Fenwick tree | O(n) |
O(log n) |
O(log n) |
18. Sum Of Sums Of Arithmetic Subarrays With Step ±1
Problem Statement
You are given an integer array a of length n. Call a subarray good if it has length 1, or if its consecutive differences are all +1, or if its consecutive differences are all -1. Return the sum of the sums of all good subarrays of a.
Constraints. 1 <= n <= 10^5, |a[i]| <= 10^9. The answer fits in a 64-bit integer.
Example. a = [1, 2, 3, 5, 4]. Good subarrays: [1], [2], [3], [5], [4], [1,2], [2,3], [1,2,3], [5,4]. Sum of their sums is 1 + 2 + 3 + 5 + 4 + 3 + 5 + 6 + 9 = 38.
Solution
Decompose the array into maximal runs where consecutive differences are all +1 or all -1. Every good subarray of length >=2 lies entirely inside one run. Two adjacent runs may share an endpoint (e.g. [2,3,4] and [4,3]).
Within a run of length L, use the contribution technique: an element at offset k from the run start (0-indexed) appears in exactly (k + 1) * (L - k) subarrays of that run, including the length-1 subarray consisting of just itself. Because length-1 subarrays are always good, we count them once globally and subtract them from each run’s contribution.
long long goodSum(const vector<int>& a) {
int n = (int)a.size();
long long total = 0;
for (int i = 0; i < n; ++i) total += a[i]; // length-1 subarrays
int i = 0;
while (i + 1 < n) {
int d = a[i + 1] - a[i];
if (d != 1 && d != -1) { ++i; continue; }
int j = i + 1;
while (j + 1 < n && a[j + 1] - a[j] == d) ++j;
int L = j - i + 1;
for (int k = 0; k < L; ++k) {
long long appear = (long long)(k + 1) * (L - k);
total += (long long)a[i + k] * (appear - 1); // minus length-1
}
i = j; // allow shared endpoint
}
return total;
}- Time.
O(n). Space.O(1).
Discussion
“Sum of sums of subarrays satisfying property P” is almost always a contribution-technique problem: instead of enumerating subarrays, count how many good subarrays each element belongs to and multiply. The universal formula for 1D subarray contribution is left_starts * right_ends where left_starts = k + 1 and right_ends = L - k within a run of length L.
Follow-ups
(F1) Common difference d for any fixed d. Same algorithm with the run condition a[j + 1] - a[j] == d. (F2) Any common difference. Every maximal run now has its own d; process independently. (F3) Report the number of good subarrays instead of their sum. Just count L * (L + 1) / 2 per run.
Trade-offs
| Approach | Time | When to use |
|---|---|---|
| Enumerate subarrays | O(n^2) |
Debugging only |
| Run decomposition + contribution | O(n) |
The default answer |
19. Maze Shortest Path With One Wall Break
Problem Statement
You are given an m x n grid where each cell is 0 (empty) or 1 (wall). Starting from (0, 0), you may move in the four cardinal directions through empty cells. Additionally, at most once during the journey, you may walk through a single wall cell as if it were empty. Return the minimum number of steps required to reach (m - 1, n - 1), or -1 if it is unreachable.
Constraints. 1 <= m, n <= 500. Both endpoints are empty cells.
Solution
Breadth-first search in a 3D state space (r, c, usedBreak) where usedBreak is 0 before spending the wall break and 1 after. From state (r, c, k), moving into an empty neighbor keeps k unchanged; moving into a wall requires k == 0 and advances to k = 1.
int shortestPath(vector<vector<int> >& g) {
int m = (int)g.size(), n = (int)g[0].size();
vector<vector<vector<int> > > dist(m,
vector<vector<int> >(n, vector<int>(2, INT_MAX)));
queue<tuple<int,int,int> > q;
q.push(make_tuple(0, 0, 0));
dist[0][0][0] = 0;
int dr[4] = {-1, 1, 0, 0}, dc[4] = {0, 0, -1, 1};
while (!q.empty()) {
tuple<int,int,int> st = q.front(); q.pop();
int r = std::get<0>(st), c = std::get<1>(st), k = std::get<2>(st);
if (r == m - 1 && c == n - 1) return dist[r][c][k];
for (int d = 0; d < 4; ++d) {
int nr = r + dr[d], nc = c + dc[d];
if (nr < 0 || nr >= m || nc < 0 || nc >= n) continue;
int nk = k + g[nr][nc];
if (nk > 1) continue;
if (dist[nr][nc][nk] <= dist[r][c][k] + 1) continue;
dist[nr][nc][nk] = dist[r][c][k] + 1;
q.push(make_tuple(nr, nc, nk));
}
}
return -1;
}- Time.
O(m * n * 2). Space.O(m * n * 2).
Discussion
“You may do X once” is the canonical BFS state-augmentation pattern. Turn “once” into a counter attached to the state; the counter’s range is the budget. The key subtlety is to check goal arrival on dequeue rather than on enqueue: BFS’s invariant is that the first dequeue of a state holds its optimal distance.
Follow-ups
(F1) At most k breaks. Extend the state to (r, c, used in [0..k]); total states O(m * n * (k + 1)). This is LC 1293. (F2) Weighted walls. Walls have a cost; use Dijkstra with the same state shape. (F3) Returning the actual path. Store a parent pointer alongside dist and reconstruct backward.
Trade-offs
| Variant | Algorithm | Complexity |
|---|---|---|
| Plain BFS | 2D BFS | O(mn) |
One / k breaks |
3D BFS on (r,c,k) |
O(mn(k+1)) |
| Weighted walls | Dijkstra | O(mn log(mn)) |
20. Top K Largest Pair Sums
Problem Statement
Given an integer array a of length n and an integer k, return the k largest values of a[i] + a[j] over all unordered pairs i < j, sorted in non-increasing order.
Constraints. 1 <= k <= n * (n - 1) / 2, n <= 10^5.
Solution
Sort a in non-increasing order. Think of the pair sums as entries of an implicit sorted matrix indexed by (i, j) with i < j; the largest pair sum is a[0] + a[1], and from any explored pair (i, j) the next-best candidates are (i + 1, j) and (i, j + 1). Use a max-heap keyed on the pair sum, expanding greedily and deduplicating with a visited set over (i, j).
vector<long long> topKPairSums(vector<int> a, int k) {
sort(a.begin(), a.end(), greater<int>());
int n = (int)a.size();
priority_queue<tuple<long long,int,int> > pq;
set<pair<int,int> > seen;
pq.push(make_tuple((long long)a[0] + a[1], 0, 1));
seen.insert(make_pair(0, 1));
vector<long long> out;
while ((int)out.size() < k && !pq.empty()) {
tuple<long long,int,int> top = pq.top(); pq.pop();
long long s = std::get<0>(top);
int i = std::get<1>(top), j = std::get<2>(top);
out.push_back(s);
int ni[2] = {i + 1, i}, nj[2] = {j, j + 1};
for (int t = 0; t < 2; ++t) {
int a2 = ni[t], b2 = nj[t];
if (a2 < b2 && b2 < n && !seen.count(make_pair(a2, b2))) {
seen.insert(make_pair(a2, b2));
pq.push(make_tuple((long long)a[a2] + a[b2], a2, b2));
}
}
}
return out;
}- Time.
O(n log n + k log k).
Discussion
This is the “k-way merge on an implicitly sorted grid” pattern (LC 373). The heap stores a frontier of candidates, each of which expands at most two new candidates, so the heap size stays O(k) and total work is O(k log k) after the initial sort.
Follow-ups
(F1) K smallest pair sums. Flip the sort order and the heap comparator. (F2) K-th pair sum. Binary search on the answer: count pairs with sum >= x using a two-pointer scan over the sorted array. (F3) Pairs from two different arrays A, B. Same frontier expansion with (i, j) indexing into A and B independently.
Trade-offs
| Variant | Technique | Cost |
|---|---|---|
k small vs. n^2 |
Frontier heap | O(k log k) |
k near n^2 |
Generate all pairs + partial sort | O(n^2 log k) |
k-th largest only |
Binary search + two-pointer count | O(n log(max_sum)) |
21. Shortest Cycle Through A Given Node
Problem Statement
You are given a directed graph with n vertices and a distinguished start vertex s. Return the length (number of edges) of the shortest non-empty directed cycle that begins and ends at s, or -1 if no such cycle exists.
Self-loops count as cycles of length 1.
Constraints. 1 <= n <= 10^5, up to 2 * 10^5 edges.
Solution
Run a single BFS from s, but treat the return to s as an edge relaxation rather than a visited-node skip. Whenever we are about to relax a neighbor equal to s, the current distance plus one is the length of a cycle through s, and BFS guarantees this is the shortest one.
int shortestCycleThrough(int n, const vector<vector<int> >& adj, int s) {
for (int i = 0; i < (int)adj[s].size(); ++i)
if (adj[s][i] == s) return 1; // self-loop
vector<int> dist(n, -1);
queue<int> q;
dist[s] = 0;
q.push(s);
while (!q.empty()) {
int u = q.front(); q.pop();
for (int i = 0; i < (int)adj[u].size(); ++i) {
int v = adj[u][i];
if (v == s) return dist[u] + 1;
if (dist[v] == -1) {
dist[v] = dist[u] + 1;
q.push(v);
}
}
}
return -1;
}- Time.
O(n + m). Space.O(n).
Discussion
The trick is to allow re-entering s even though BFS’s standard visited-set pattern would forbid it. Keep the visited set for every vertex except s and handle s specially on the relaxation side. Two-cycles (s -> u -> s) and self-loops are both caught cleanly.
Follow-ups
(F1) Shortest cycle anywhere in the graph. Run the BFS from every vertex; the overall complexity becomes O(n(n + m)). (F2) Weighted graph. Replace BFS with Dijkstra and the same relaxation trick; cost becomes O((n + m) log n). (F3) Report the cycle. Maintain a parent array alongside dist and trace the path backward from the relaxation that closed the cycle.
Trade-offs
| Variant | Technique | Cost |
|---|---|---|
Unweighted, fixed s |
BFS with s re-entry |
O(n + m) |
| Unweighted, any | BFS from every vertex | O(n(n + m)) |
| Weighted | Dijkstra with s re-entry |
O((n + m) log n) |
22. Detect Timed-Out Activities From An Event Log
Problem Statement
You are given a log of events, each of the form (activityId, timestamp, type) where type is either START or END. Each activity has at most one START and one END in the log. Given a timeout threshold T, return all activity IDs whose duration (END - START) strictly exceeds T. If an activity has a START but no END, treat it as still running and compare (now - START) against T.
State your policies for malformed input before coding:
ENDwithout a matchingSTART: ignore or raise.- Duplicate
STARTfor the same ID: ignore the second, raise, or overwrite. - Events out of order: decide whether to sort or scan once.
Solution
Walk the log once, recording START timestamps in a hash map. On an END, look up the corresponding START and compare.
vector<string> timedOutActivities(const vector<Log>& logs, long long T,
long long now) {
unordered_map<string, long long> started;
unordered_set<string> out;
for (int i = 0; i < (int)logs.size(); ++i) {
const Log& L = logs[i];
if (L.type == START) {
started[L.id] = L.ts;
} else {
unordered_map<string,long long>::iterator it = started.find(L.id);
if (it == started.end()) continue; // malformed END, ignore
if (L.ts - it->second > T) out.insert(L.id);
started.erase(it);
}
}
for (unordered_map<string,long long>::iterator it = started.begin();
it != started.end(); ++it) {
if (now - it->second > T) out.insert(it->first);
}
return vector<string>(out.begin(), out.end());
}- Time.
O(n)if logs are already in timestamp order, otherwiseO(n log n)for the initial sort.
Discussion
OOD rounds grade you as much on the schema and policy decisions as on the code. Write the Log struct, the tracker class with its method signatures, and an explicit list of policies before the algorithm.
Follow-ups
(F1) Multiple start-end pairs per ID. Decide whether to sum durations or track each independently. Swap the value of started to a vector. (F2) Streaming input with bounded memory. Expire oldest open STARTs beyond a window, reporting them as timed-out. (F3) Report by user / session, not activity. Group by the parent key first.
Trade-offs
| Assumption | Approach | Cost |
|---|---|---|
| Log in order | Single pass + hash map | O(n) |
| Unsorted log | Sort first, then single pass | O(n log n) |
| Stream + memory cap | Sliding-window eviction | O(n) amortized |
23. Partition Strings By Prefix Match In Place
Problem Statement
You are given an array of strings a and an oracle function isValid(s) that returns true iff s begins with a fixed (unknown to you) prefix P. Rearrange a in place so that every valid string precedes every invalid string, and return the number of valid strings. The relative order within each group does not need to be preserved. Minimize the number of swaps performed and the number of calls to isValid.
Solution
Two-pointer partition, identical in shape to the Dutch national flag algorithm. Advance l past valid strings from the left; retreat r past invalid strings from the right; when both pointers have found a misplaced element, swap them and advance both.
int partitionByValidity(vector<string>& a) {
int l = 0, r = (int)a.size() - 1;
while (l <= r) {
if (isValid(a[l])) { ++l; continue; }
if (!isValid(a[r])) { --r; continue; }
swap(a[l], a[r]);
++l; --r;
}
return l; // count of valid strings
}- Swaps. At most
min(valid, invalid), which is optimal because each swap fixes two misplaced elements at once. - Oracle calls. Each position is queried at most once on the way in (the caches
isValid(a[l])andisValid(a[r])can be memoized if repeated calls are expensive), so at mostO(n)total. - Time.
O(n * L)including the prefix check inside the oracle.
Discussion
“Partition in place, minimize swaps” is almost always Dutch flag. The key win over the naive two-pass approach (count valids, pack them to the front) is avoiding the second scan and the temporary array. Always mention oracle complexity separately from algorithmic complexity.
Follow-ups
(F1) Preserve relative order within each group. Dutch flag does not preserve order; use a stable partition — typically two passes with a buffer. (F2) Multi-class partition. Three-way partition (Dutch flag’s original form) for <, =, >. (F3) Oracle is very expensive. Cache results in a hash map keyed on the string, so each string is queried exactly once.
Trade-offs
| Goal | Technique | Swaps |
|---|---|---|
| Unordered, minimize swaps | Two-pointer Dutch flag | min(valid, invalid) |
| Preserve order | Stable partition | O(n) writes, extra mem |
| Three classes | Three-way Dutch flag | O(n) |
24. URL Shortener — Shortest Unique Strings With Bounded Char Frequency
Problem Statement
Design a URLShortener whose getNext() method returns an infinite sequence of unique strings over the lowercase alphabet such that:
- Strings are returned in order of non-decreasing length, and
- No character appears more than
Ttimes in any returned string.
Every call to getNext() returns the next string satisfying both rules.
Solution
Enumerate strings in lexicographic base-26 order (a, b, …, z, aa, ab, …) using a counter-style increment step. For each candidate, verify that every character’s frequency is at most T; if so, return it, otherwise advance and retry.
struct URLShortener {
string cur;
int T;
URLShortener(int T_) : cur(""), T(T_) {}
void inc() {
for (int i = (int)cur.size() - 1; i >= 0; --i) {
if (cur[i] != 'z') { ++cur[i]; return; }
cur[i] = 'a';
}
cur = string(cur.size() + 1, 'a');
}
bool ok() const {
int cnt[26] = {0};
for (int i = 0; i < (int)cur.size(); ++i)
if (++cnt[cur[i] - 'a'] > T) return false;
return true;
}
string getNext() {
do { inc(); } while (!ok());
return cur;
}
};Discussion
Enumerate-then-filter is the right pattern for “generate unique items in an order” whenever the validity predicate is cheap and most candidates pass. For T >= 2 essentially all strings pass, so amortized cost per call is O(L) where L grows as O(log_{26} n) for the n-th call. For T == 1 you’re emitting strings with distinct characters only, which has rapidly shrinking density and benefits from a direct combinatorial enumeration.
Follow-ups
(F1) Multi-worker generation. Stripe the counter space across P workers by offset and step. (F2) Persistence. Store the current counter to durable storage on each call. (F3) Weighted alphabet. Use base-k enumeration over the chosen subset.
Trade-offs
T |
Best approach |
|---|---|
T == 1 |
Direct enumeration of distinct-char strings |
T >= 2 |
Enumerate-and-filter |
| Distributed | Counter striping |
25. LRU Cache With Time-Based Expiration
Problem Statement
Implement a cache with:
int get(int key)— return the value if it exists and is not expired, else-1.void put(int key, int value)— insert or update the entry; evict the least recently used entry if the cache is at capacity.
Every entry expires automatically TTL milliseconds after insertion (specify this rule up front — most candidates miss it). Expired entries must not be returned. Target O(1) amortized for both operations.
Solution
A hash map plus a doubly linked list (the LRU 146 template), augmented with an insertTime on each node and a lazy-deletion check at the top of get.
struct LRUTTL {
struct Node { int k, v; long long t; Node *prev, *next; };
int cap;
long long ttl;
unordered_map<int, Node*> mp;
Node *head, *tail;
long long (*now)(); // monotonic clock
LRUTTL(int cap_, long long ttl_, long long (*now_)())
: cap(cap_), ttl(ttl_), now(now_) {
head = new Node{0,0,0,nullptr,nullptr};
tail = new Node{0,0,0,nullptr,nullptr};
head->next = tail; tail->prev = head;
}
void detach(Node* n) { n->prev->next = n->next; n->next->prev = n->prev; }
void addFront(Node* n) {
n->next = head->next; n->prev = head;
head->next->prev = n; head->next = n;
}
int get(int k) {
unordered_map<int,Node*>::iterator it = mp.find(k);
if (it == mp.end()) return -1;
Node* n = it->second;
if (now() - n->t > ttl) { detach(n); mp.erase(it); delete n; return -1; }
detach(n); addFront(n);
return n->v;
}
void put(int k, int v) {
long long t = now();
unordered_map<int,Node*>::iterator it = mp.find(k);
if (it != mp.end()) {
it->second->v = v; it->second->t = t;
detach(it->second); addFront(it->second);
return;
}
if ((int)mp.size() == cap) {
Node* old = tail->prev;
mp.erase(old->k); detach(old); delete old;
}
Node* n = new Node{k, v, t, nullptr, nullptr};
addFront(n); mp[k] = n;
}
};- Time.
O(1)amortized per op.
Discussion
Always use a monotonic clock rather than wall time. Clarify up front whether get refreshes the TTL — interviewers almost always say “no, TTL runs from insertion time”. Lazy deletion is almost always the right strategy: expired entries cost nothing until they are touched or evicted by the LRU policy.
Follow-ups
(F1) Bounded memory under bursty writes. Run a background sweeper that walks the tail every T seconds and removes expired entries. (F2) Priority-based expiration. Replace the DLL with a min-heap keyed on expiry time; cost becomes O(log n) per op. (F3) Hierarchical TTLs. Maintain one LRU per TTL class; promote on access between classes.
Trade-offs
| Strategy | get/put |
Memory usage pattern |
|---|---|---|
| Lazy deletion | O(1) amortized |
Can temporarily hold expired |
| Background sweep | O(1) + periodic O(k) |
Bounded lag |
| Expiry min-heap | O(log n) |
Tight |
26. Count Shortest Paths Between Two Nodes
Problem Statement
Given a weighted undirected graph with non-negative edge weights, find the number of distinct shortest paths from node s to node t.
Input: Integer n (number of nodes), adjacency list of weighted edges, source s, target t.
Output: Number of shortest paths modulo 10^9 + 7.
Constraints: 1 ≤ n ≤ 100000; edge weights in [0, 10^6].
Example:
Graph: 0-1 (weight 1), 0-2 (weight 1), 1-3 (weight 1), 2-3 (weight 1)
s=0, t=3
Answer: 2 (paths 0→1→3 and 0→2→3)
Solution
Maintain two arrays: dist[v] (shortest distance to v) and ways[v] (count of shortest paths to v). Use Dijkstra’s algorithm with a min-heap. When relaxing an edge (u, v, w): - If the new path is shorter: update dist and reset ways. - If the new path equals the current shortest: add the count.
#include <queue>
#include <vector>
using namespace std;
const long long MOD = 1e9 + 7;
long long countShortestPaths(int n, vector<vector<pair<int, long long>>>& adj, int s, int t) {
vector<long long> dist(n, LLONG_MAX), ways(n, 0);
dist[s] = 0;
ways[s] = 1;
priority_queue<pair<long long, int>, vector<pair<long long, int>>, greater<pair<long long, int> > > pq;
pq.push(make_pair(0LL, s));
while (!pq.empty()) {
long long d = pq.top().first;
int u = pq.top().second;
pq.pop();
if (d > dist[u]) continue;
for (size_t i = 0; i < adj[u].size(); ++i) {
int v = adj[u][i].first;
long long w = adj[u][i].second;
long long nd = d + w;
if (nd < dist[v]) {
dist[v] = nd;
ways[v] = ways[u];
pq.push(make_pair(nd, v));
} else if (nd == dist[v]) {
ways[v] = (ways[v] + ways[u]) % MOD;
}
}
}
return ways[t];
}- Time: O((V + E) log V)
- Space: O(V + E)
Discussion
The key insight is tracking both distance and path count in parallel. For unweighted graphs, use BFS with the same counting rule. Be cautious with zero-weight edges: they can create cycles with total weight 0, leading to infinitely many paths. Always confirm the constraint that weights are non-negative.
Follow-ups
(F1) Unweighted graph: Replace Dijkstra with BFS; the counting logic remains identical.
(F2) 0-1 weighted edges: Use 0-1 BFS with a deque instead of a priority queue.
(F3) Find one shortest path: Backtrack from t to s using the decreasing distance property.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Dijkstra + count | O((V+E)logV) | O(V+E) | Weighted graphs |
| BFS + count | O(V+E) | O(V+E) | Unweighted graphs |
| 0-1 BFS | O(V+E) | O(V+E) | Binary/0-1 weights |
27. Count Lakes In Island
Problem Statement
Given a 2D grid with 1 representing land and 0 representing water, given a cell (r, c) on land, count the number of enclosed water bodies (lakes) inside the island containing (r, c). A lake is a water component that does not touch the grid boundary.
Input: 2D grid, integers r, c.
Output: Number of lakes.
Constraints: 1 ≤ rows, cols ≤ 2000.
Example:
Grid: 1 1 1 1
1 0 0 1
1 0 1 1
1 1 1 1
r=0, c=0 → 1 lake (the 0s in the middle)
Solution
- Flood-fill the island containing (r, c) to mark all land cells.
- For each water cell adjacent to the island, perform a flood-fill and check if it touches the grid boundary.
- Count water components that do not touch the boundary.
#include <queue>
#include <vector>
using namespace std;
int countLakes(vector<vector<int>>& g, int r, int c) {
int m = g.size(), n = g[0].size();
if (g[r][c] != 1) return 0;
vector<vector<int>> mark(m, vector<int>(n, 0));
queue<pair<int, int>> q;
q.push(make_pair(r, c));
mark[r][c] = 1;
int dr[] = {-1, 1, 0, 0}, dc[] = {0, 0, -1, 1};
while (!q.empty()) {
int y = q.front().first;
int x = q.front().second;
q.pop();
for (int k = 0; k < 4; ++k) {
int ny = y + dr[k], nx = x + dc[k];
if (ny < 0 || nx < 0 || ny >= m || nx >= n) continue;
if (g[ny][nx] == 1 && !mark[ny][nx]) {
mark[ny][nx] = 1;
q.push(make_pair(ny, nx));
}
}
}
int lakes = 0;
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (g[i][j] == 0 && mark[i][j] == 0) {
bool adjLand = false;
for (int k = 0; k < 4; ++k) {
int ny = i + dr[k], nx = j + dc[k];
if (ny >= 0 && nx >= 0 && ny < m && nx < n && mark[ny][nx] == 1) {
adjLand = true;
}
}
if (!adjLand) continue;
queue<pair<int, int>> w;
w.push(make_pair(i, j));
mark[i][j] = 2;
bool touchesEdge = false;
while (!w.empty()) {
int y = w.front().first;
int x = w.front().second;
w.pop();
if (y == 0 || x == 0 || y == m - 1 || x == n - 1) {
touchesEdge = true;
}
for (int k = 0; k < 4; ++k) {
int ny = y + dr[k], nx = x + dc[k];
if (ny < 0 || nx < 0 || ny >= m || nx >= n) continue;
if (g[ny][nx] == 0 && mark[ny][nx] == 0) {
mark[ny][nx] = 2;
w.push(make_pair(ny, nx));
}
}
}
if (!touchesEdge) lakes++;
}
}
}
return lakes;
}- Time: O(mn)
- Space: O(mn)
Discussion
The critical distinction is that a lake must be bounded by the specific island containing the given cell, not just any land. The boundary-check is the canonical approach for identifying enclosed regions. Water touching any edge of the grid is ocean, not a lake.
Follow-ups
(F1) Union-Find variant: Merge land cells, then for each water component check if it contains any boundary cell and is adjacent to the queried island.
(F2) All islands: Modify to return lakes for every island in the grid.
(F3) Return lake sizes: Collect the sizes during the second flood-fill.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| BFS flood-fill | O(mn) | O(mn) | Standard grids |
| Union-Find | O(mn·α(mn)) | O(mn) | Batch queries |
28. Expression Add Operators
Problem Statement
Given a digit string num and an integer target, return all expressions by inserting +, -, * between digits that evaluate to target. No leading zeros on multi-digit numbers (but 0 as a single digit is allowed).
Input: String num (digits only), integer target.
Output: All valid expressions as strings.
Constraints: 1 ≤ len(num) ≤ 10; 0 ≤ target ≤ 2^31 - 1.
Example:
num = "123", target = 6
Output: ["1+2+3", "1*2*3"]
Solution
Use backtracking with two running values: - total: the fully evaluated expression so far. - prevMul: the value of the most recent multiplicative term (needed to handle operator precedence).
When appending a number x: - +x → total = total + x, prevMul = x - -x → total = total - x, prevMul = -x - *x → total = total - prevMul + prevMul * x, prevMul = prevMul * x
#include <string>
#include <vector>
using namespace std;
void dfs(const string& num, int target, int i, long long total, long long prevMul,
string expr, vector<string>& out) {
if (i == (int)num.size()) {
if (total == target) {
out.push_back(expr);
}
return;
}
for (int j = i; j < (int)num.size(); ++j) {
if (j > i && num[i] == '0') break;
long long x = stoll(num.substr(i, j - i + 1));
if (i == 0) {
dfs(num, target, j + 1, x, x, num.substr(i, j - i + 1), out);
} else {
dfs(num, target, j + 1, total + x, x, expr + "+" + num.substr(i, j - i + 1), out);
dfs(num, target, j + 1, total - x, -x, expr + "-" + num.substr(i, j - i + 1), out);
dfs(num, target, j + 1, total - prevMul + prevMul * x, prevMul * x,
expr + "*" + num.substr(i, j - i + 1), out);
}
}
}
vector<string> addOperators(const string& num, int target) {
vector<string> result;
dfs(num, target, 0, 0, 0, "", result);
return result;
}- Time: O(4^n · n) where n = len(num)
- Space: O(4^n) for output
Discussion
The prevMul variable is the key insight for handling operator precedence without building an explicit AST. When encountering *, subtract the previous term and add its product with the new operand. The leading-zero check prevents numbers like 01 but allows 0 alone.
Follow-ups
(F1) Find count only: Return the count of valid expressions instead of the expressions themselves.
(F2) Division operator: Track integer division similarly to multiplication.
(F3) Parentheses: Allow grouping; requires a more complex state representation.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Backtracking with prevMul | O(4^n·n) | O(4^n) | n ≤ 10 |
| Memoization (with state tuple) | O(3^n) | O(n·states) | Overlapping subproblems |
29. Sorted Array Diff-Graph Path Queries
Problem Statement
Given a sorted array arr and a value diff, build a graph where indices i, j are connected if |arr[i] - arr[j]| <= diff. Answer queries: is there a path from u to v?
Input: Sorted array, integer diff, list of queries [u, v].
Output: For each query, true/false.
Constraints: 1 ≤ n ≤ 100000; 0 ≤ diff ≤ 10^6.
Example:
arr = [4, 6, 10, 15], diff = 3
Connections: 4-6, 6-10 (no connection 10-15)
Query (0, 2) → true (path 0→1→2)
Query (0, 3) → false
Solution
Observe that because arr is sorted, the graph consists of maximal runs where adjacent elements satisfy arr[i+1] - arr[i] <= diff. All indices within a run are transitively connected. Assign run IDs and answer each query in O(1).
#include <vector>
using namespace std;
vector<bool> solveQueries(vector<int>& arr, int diff, vector<pair<int, int>>& queries) {
int n = arr.size();
vector<int> runId(n);
int id = 0;
runId[0] = 0;
for (int i = 1; i < n; ++i) {
if (arr[i] - arr[i - 1] > diff) {
id++;
}
runId[i] = id;
}
vector<bool> result;
for (size_t i = 0; i < queries.size(); ++i) {
int u = queries[i].first;
int v = queries[i].second;
result.push_back(runId[u] == runId[v]);
}
return result;
}- Time: O(n + q) where q = number of queries
- Space: O(n)
Discussion
The sorted property is essential. Without it, the problem becomes full-fledged graph connectivity (requiring Union-Find or DFS), which is O(n²) or O((n+e)α(n)) respectively. By leveraging the sorted order, we identify the structure in one pass.
Follow-ups
(F1) Descending array: Reverse the array or iterate from top; logic remains the same.
(F2) Unsorted input: Sort first, O(n log n), then apply the same algorithm. This is optimal because the sorted structure is necessary for the fast query resolution.
(F3) Range updates to diff: Recompute run IDs after each diff change (no way around O(n) per update).
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Run IDs (sorted) | O(n) | O(n) | Array is sorted |
| Union-Find (unsorted) | O(n²) or O(n·α(n)) | O(n) | Array is unsorted |
30. Subarray Sum Equals K
Problem Statement
Given an array nums and integer k, count the number of contiguous subarrays that sum to k.
Input: Array nums, integer k.
Output: Count of subarrays.
Constraints: 1 ≤ n ≤ 200000; -100000 ≤ nums[i] ≤ 100000.
Example:
nums = [1, 1, 1], k = 2
Output: 2 (subarrays [1,1] at positions 0-1 and 1-2)
Solution
Maintain a hash map of prefix sums. For each cumulative sum s, the number of subarrays ending here with sum k is the count of occurrences of s - k. Initialize the map with {0: 1} to handle subarrays starting at index 0.
#include <unordered_map>
#include <vector>
using namespace std;
int subarraySum(vector<int>& nums, int k) {
unordered_map<int, int> cnt;
cnt[0] = 1;
int s = 0, ans = 0;
for (size_t i = 0; i < nums.size(); ++i) {
s += nums[i];
if (cnt.find(s - k) != cnt.end()) {
ans += cnt[s - k];
}
cnt[s]++;
}
return ans;
}- Time: O(n)
- Space: O(n)
Discussion
The prefix sum technique is foundational. The initialization cnt[0] = 1 is crucial—it represents the empty prefix. For negative numbers, this approach is necessary; a sliding window cannot work because the sum can decrease. The extension to streaming is immediate: call the update logic on each arriving element.
Follow-ups
(F1) Streaming: Keep the same hash map and running sum; call the update on each new element. The count is available at any time.
(F2) Bounded window: Maintain a sliding window of length W; decrement cnt[s_old] when element W+1 steps away.
(F3) Circular array: Compute prefix sums for the array concatenated with itself, excluding the full-array case.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Prefix sum + hash | O(n) | O(n) | Negative numbers allowed |
| Sliding window | O(n) | O(1) | Non-negative, all equal k |
31. Morse Code Decoding
Problem Statement
Given a no-delimiter Morse string s and a mapping from A-Z to Morse codes, output: 1. The number of valid full-length decodings mod 10^9 + 7. 2. The lexicographically smallest up to K decoded strings.
Input: Morse string, character-to-code map, K.
Output: Count and list of K smallest decodings.
Constraints: 1 ≤ len(s) ≤ 100; 1 ≤ K ≤ 10.
Example:
s = ".-", codes = {A: ".-", ...}
Decodings = ["A"]
Count = 1
Solution
Use dynamic programming with a Trie for fast code matching. dp[i] = number of ways to decode s[i..n-1]. For counting, at each position walk the Trie and accumulate solutions. For top-K, use best-first search with a priority queue ordered lexicographically.
#include <string>
#include <vector>
#include <queue>
#include <map>
using namespace std;
vector<long long> countDecodings(const string& s, map<char, string>& codes) {
int n = s.size();
vector<long long> dp(n + 1, 0);
dp[n] = 1;
map<char, int> codeLen;
for (map<char, string>::iterator it = codes.begin(); it != codes.end(); ++it) {
codeLen[it->first] = (int)it->second.size();
}
for (int i = n - 1; i >= 0; --i) {
for (map<char, string>::iterator it = codes.begin(); it != codes.end(); ++it) {
const string& code = it->second;
if (i + (int)code.size() <= n &&
s.substr(i, code.size()) == code) {
dp[i] += dp[i + (int)code.size()];
dp[i] %= 1000000007LL;
}
}
}
return dp;
}
vector<string> topKDecodings(const string& s, map<char, string>& codes, int K) {
vector<long long> dp = countDecodings(s, codes);
vector<string> result;
priority_queue<pair<string, int> > pq;
pq.push(make_pair(string(""), 0));
while (!pq.empty() && (int)result.size() < K) {
string current = pq.top().first;
int idx = pq.top().second;
pq.pop();
if (idx == (int)s.size()) {
result.push_back(current);
continue;
}
if (dp[idx] == 0) continue;
vector<pair<char, string> > candidates;
for (map<char, string>::iterator it = codes.begin(); it != codes.end(); ++it) {
candidates.push_back(make_pair(it->first, it->second));
}
sort(candidates.begin(), candidates.end());
for (size_t i = 0; i < candidates.size(); ++i) {
char letter = candidates[i].first;
const string& code = candidates[i].second;
if (idx + (int)code.size() <= (int)s.size() &&
s.substr(idx, code.size()) == code) {
pq.push(make_pair(current + letter, idx + (int)code.size()));
}
}
}
return result;
}- Time (count): O(n · Σ|codes|)
- Time (top-K): O(K · n · 26 · log(K))
- Space: O(n) for DP
Discussion
The DP for counting is straightforward. The top-K problem requires carefully ordering candidates lexicographically and pruning branches with zero completions. The two approaches share the DP computation, which is reused.
Follow-ups
(F1) DFS with pruning: Recursively try letters in alphabetical order, prune using DP.
(F2) Streaming Morse: Maintain DP incrementally as the Morse string grows.
(F3) Ambiguous Morse: Allow multiple possible codes for the same character.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| DP counting | O(n·Σ|codes|) | O(n) | Count only |
| Best-first + DP | O(K·n·26·logK) | O(n) | Top-K needed |
32. Maximum Overlap Of Squares
Problem Statement
Given n axis-aligned squares (defined as x, y, side length L), find the maximum number of squares covering any single point.
Input: List of squares (x, y, L).
Output: Maximum overlap count.
Constraints: 1 ≤ n ≤ 10000.
Example:
Squares: (0,0,2), (1,1,2)
Maximum overlap: 2 (at point (1,1))
Solution
Use a 2D sweep-line algorithm. Sweep a vertical line left-to-right. Maintain active squares (those intersecting the sweep line) using a segment tree to track the maximum coverage in the y-direction at each x-coordinate. Events are created at x (entry) and x + L (exit) for each square.
#include <vector>
#include <algorithm>
using namespace std;
int maxSquareOverlap(vector<tuple<int, int, int>>& squares) {
vector<tuple<int, int, int, int>> events;
int n = squares.size();
for (int i = 0; i < n; ++i) {
int x = std::get<0>(squares[i]);
int y = std::get<1>(squares[i]);
int L = std::get<2>(squares[i]);
events.push_back(make_tuple(x, 0, y, y + L));
events.push_back(make_tuple(x + L, 1, y, y + L));
}
sort(events.begin(), events.end());
int peak = 0;
map<int, int> activeIntervals;
for (size_t i = 0; i < events.size(); ++i) {
int type = std::get<1>(events[i]);
int y_lo = std::get<2>(events[i]);
int y_hi = std::get<3>(events[i]);
if (type == 0) {
if (activeIntervals.find(y_lo) == activeIntervals.end()) {
activeIntervals[y_lo] = 0;
}
activeIntervals[y_lo]++;
} else {
activeIntervals[y_lo]--;
if (activeIntervals[y_lo] == 0) {
activeIntervals.erase(y_lo);
}
}
int maxCurrent = 0;
for (map<int, int>::iterator it = activeIntervals.begin(); it != activeIntervals.end(); ++it) {
maxCurrent = max(maxCurrent, it->second);
}
peak = max(peak, maxCurrent);
}
return peak;
}- Time: O(n log n)
- Space: O(n)
Discussion
The key is decomposing the 2D problem into a 1D sweep with a secondary 1D data structure (segment tree or interval map). The segment tree is more complex but supports efficient range updates. For small n, a simpler coordinate-compression approach with a 2D difference array works.
Follow-ups
(F1) Rectangles: The algorithm generalizes trivially; squares have no special advantage.
(F2) Query points: Precompute a 2D grid; answer point queries in O(1).
(F3) Range queries: Add queries for maximum overlap in a sub-rectangle.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Sweep-line + segment tree | O(nlogn) | O(n) | n ≤ 100000 |
| Coordinate compression | O(n²) | O(n²) | n ≤ 1000 |
33. Range Overwrite Updates
Problem Statement
Given an array A of length n and m update queries (l, r, k) that overwrite A[l..r] = k, apply all queries in order and output the final array. Must handle this better than O(n·m).
Input: Array A, list of (l, r, k) updates.
Output: Final array after all updates.
Constraints: 1 ≤ n, m ≤ 200000.
Example:
A = [1,2,3], queries = [(0,1,5), (1,2,9)]
After (0,1,5): [5,5,3]
After (1,2,9): [5,9,9]
Output: [5,9,9]
Solution
Use a segment tree with lazy propagation for range assignment. Maintain a lazy tag indicating that a subtree is entirely assigned to a value. Push tags downward during updates.
#include <vector>
#include <algorithm>
using namespace std;
struct SegTree {
int n;
vector<long long> val;
vector<long long> lazy;
vector<bool> hasLazy;
void apply(int p, long long v) {
val[p] = v;
lazy[p] = v;
hasLazy[p] = true;
}
void push(int p) {
if (hasLazy[p]) {
apply(p * 2, lazy[p]);
apply(p * 2 + 1, lazy[p]);
hasLazy[p] = false;
}
}
void update(int p, int l, int r, int ql, int qr, long long v) {
if (qr < l || r < ql) return;
if (ql <= l && r <= qr) {
apply(p, v);
return;
}
push(p);
int m = (l + r) / 2;
update(p * 2, l, m, ql, qr, v);
update(p * 2 + 1, m + 1, r, ql, qr, v);
}
long long query(int p, int l, int r, int idx) {
if (l == r) {
if (hasLazy[p]) return lazy[p];
return val[p];
}
push(p);
int m = (l + r) / 2;
if (idx <= m) return query(p * 2, l, m, idx);
else return query(p * 2 + 1, m + 1, r, idx);
}
};- Time: O((n + m) log n)
- Space: O(n)
Discussion
The segment tree approach is straightforward and easy to explain. An alternative is the “offline paint-reverse” technique: process queries in reverse, maintaining the next unpainted index with a DSU. This achieves O((n + m)α(n)), which is asymptotically better.
Follow-ups
(F1) Offline painting in reverse: Process queries backward; track next-unpainted indices using union-find.
(F2) Constant k: If all queries use the same k, compute the union of intervals and mark covered cells directly.
(F3) Interval set (Chtholly tree): Maintain contiguous runs; split, erase, and insert on updates.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Segment tree | O((n+m)logn) | O(n) | Flexible, easy to code |
| DSU reverse paint | O((n+m)α(n)) | O(n) | Optimal asymptotically |
| Constant k + intervals | O(mlogm+n) | O(m) | k is fixed |
34. Board Game — Tokens Move By Three
Problem Statement
A board string of length N with characters . (empty), T (token), C (coin). Tokens move exactly 3 cells right. Tokens cannot occupy the same cell. Each coin is collected on first touch. Return the maximum coins collectable.
Input: Board string, N ≤ 100.
Output: Maximum coins.
Constraints: Tokens can only move, not stay.
Example:
Board: "T..C.T.C", tokens at positions 0 and 5
Token at 0 can reach position 3 (collects coin at 3)
Token at 5 can reach position 8 (out of bounds)
Max coins: 1
Solution
Use modulo-3 decomposition. A token at index i can only occupy indices with the same value mod 3. Solve each “lane” independently. Within a lane, tokens move one step at a time (3 cells in the original board). Greedily assign final positions: rightmost token extends farthest, and subsequent tokens extend below it.
#include <string>
#include <vector>
#include <algorithm>
using namespace std;
int maxCoinsPerLane(string& lane) {
int L = lane.size();
vector<int> tokens;
for (int i = 0; i < L; ++i) {
if (lane[i] == 'T') tokens.push_back(i);
}
vector<bool> covered(L, false);
int right = L - 1;
for (int k = (int)tokens.size() - 1; k >= 0; --k) {
int p = tokens[k];
int q = min(right, L - 1);
if (q < p) continue;
for (int j = p; j <= q; ++j) {
covered[j] = true;
}
right = q - 1;
}
int count = 0;
for (int i = 0; i < L; ++i) {
if (covered[i] && lane[i] == 'C') count++;
}
return count;
}
int maxCoins(string& board) {
int n = board.size();
int total = 0;
for (int r = 0; r < 3; ++r) {
string lane;
for (int i = r; i < n; i += 3) {
lane += board[i];
}
total += maxCoinsPerLane(lane);
}
return total;
}- Time: O(N)
- Space: O(N)
Discussion
The key insight is the modulo-3 decomposition, which reduces the 2D movement constraint to independent 1D problems. Tokens naturally preserve ordering within a lane, so greedy assignment works: rightmost token extends to the right boundary, and each preceding token extends as far right as possible without exceeding the next token’s final position.
Follow-ups
(F1) Variable move distance: Generalize to move by k; decompose by mod k.
(F2) Coins consumed: Subtract coin weight from collection count.
(F3) Multiple passes: Allow tokens to move multiple times; becomes a more complex DP.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Mod-k decomposition | O(N) | O(N) | All move distances equal |
| DP per lane | O(N²) | O(N) | Complex movement rules |
35. Mahjong Winning Hand
Problem Statement
Given 14 tiles, determine if they can be partitioned into exactly 4 melds and 1 pair. - Meld = triplet [x, x, x] or sequence [x, x+1, x+2]. - Pair = [y, y].
Return true/false.
Input: List of 14 tile values.
Output: Boolean.
Constraints: Tile values in [0, 8] (standard Mahjong tiles).
Example:
Tiles: [1,1,1,2,2,3,3,3,4,4,4,5,5,5,5]
Valid: Yes (multiple partitions work)
Solution
For each potential pair, remove it and check if the remaining 12 tiles form 4 melds using backtracking. To form melds, greedily take the smallest remaining tile and try either a triplet or a sequence. Backtrack if no progress is made.
#include <vector>
#include <map>
using namespace std;
bool canMelds(map<int, int>& cnt) {
if (cnt.empty()) return true;
int x = cnt.begin()->first;
if (cnt[x] >= 3) {
cnt[x] -= 3;
if (cnt[x] == 0) cnt.erase(x);
if (canMelds(cnt)) return true;
cnt[x] += 3;
}
if (cnt.find(x) != cnt.end() && cnt.find(x + 1) != cnt.end() &&
cnt.find(x + 2) != cnt.end()) {
for (int t = x; t <= x + 2; ++t) {
cnt[t]--;
if (cnt[t] == 0) cnt.erase(t);
}
if (canMelds(cnt)) return true;
for (int t = x; t <= x + 2; ++t) {
cnt[t]++;
}
}
return false;
}
bool isWinning(vector<int>& tiles) {
map<int, int> cnt;
for (size_t i = 0; i < tiles.size(); ++i) {
cnt[tiles[i]]++;
}
for (map<int, int>::iterator it = cnt.begin(); it != cnt.end(); ++it) {
if (it->second >= 2) {
int y = it->first;
cnt[y] -= 2;
if (cnt[y] == 0) cnt.erase(y);
if (canMelds(cnt)) return true;
cnt[y] += 2;
}
}
return false;
}- Time: O(2^4 · distinct tiles) per pair = trivially fast for 14 tiles
- Space: O(9) for tile counts
Discussion
Processing the smallest tile first prevents duplicate work. Trying triplet before sequence is arbitrary but consistent. The fixed hand size makes exponential recursion practical. Critical: the pair count is 2 + melds count is 3 × 4 = 14 tiles total.
Follow-ups
(F1) Shanten (distance to win): Try adding each possible tile value; check if winning.
(F2) Seven pairs yaku: Detect alternative winning patterns.
(F3) Open hand support: Modify meld rules for declared melds.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Backtracking | O(small) | O(1) | Fixed hand size |
| DP + memoization | O(larger) | O(larger) | Uncertain hand sizes |
36. All Length-≥3 Subsequences Are Valid Words
Problem Statement
Given string s and an oracle isWord(t), return true if every subsequence of s with length ≥ 3 is a valid word.
Input: String s, oracle function.
Output: Boolean.
Constraints: 1 ≤ len(s) ≤ 25.
Example:
s = "abc", isWord checks a dictionary
Subsequences of length ≥ 3: "abc"
If "abc" is in the dictionary, return true; else false.
Solution
Enumerate all 2^n subsequences using bitmasks. For each mask with at least 3 bits set, extract the corresponding substring and check if it’s a valid word. Deduplicate subsequence strings before checking the oracle.
#include <string>
#include <set>
#include <vector>
using namespace std;
bool allSubsequencesValid(const string& s, set<string>& dictionary) {
int n = s.size();
set<string> seen;
for (int mask = 0; mask < (1 << n); ++mask) {
if (__builtin_popcount(mask) < 3) continue;
string t;
for (int i = 0; i < n; ++i) {
if (mask & (1 << i)) {
t += s[i];
}
}
if (seen.find(t) != seen.end()) continue;
seen.insert(t);
if (dictionary.find(t) == dictionary.end()) {
return false;
}
}
return true;
}- Time: O(2^n · n)
- Space: O(2^n) for deduplicated subsequences
Discussion
Enumeration is feasible for n ≤ 20-25. Deduplication is essential to avoid redundant oracle calls. Duplicate subsequences arise when s has repeated characters.
Follow-ups
(F1) Count distinct valid subsequences: Return the count instead.
(F2) Return a counterexample: Output the first invalid subsequence found.
(F3) Larger n: No known polynomial algorithm; the problem is inherently exponential.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Bitmask enumeration | O(2^n·n) | O(2^n) | n ≤ ~22 |
| Recursive generation | O(2^n·n) | O(n) per call | Same, less memory |
37. All Length-≥3 Subsequences Have Some Anagram That Is Valid
Problem Statement
Like problem 36, but for each length-≥3 subsequence, we only need some anagram (permutation) of it to be a valid word. Equivalently, check if the multiset of characters has a corresponding dictionary word.
Input: String s, list of dictionary words.
Output: Boolean.
Constraints: 1 ≤ len(s) ≤ 25; dict size ≤ 100000.
Example:
s = "abc", dict = ["abc", "bca", "cab", "...]
All length-≥3 subsequences of s have multiset {a,b,c}
All permutations of {a,b,c} exist in dict (e.g., "abc")
Return true.
Solution
Precompute a set of character multiset signatures from the dictionary. Enumerate distinct multisets of length-≥3 subsequences and check membership in the signature set.
#include <string>
#include <vector>
#include <set>
#include <algorithm>
using namespace std;
bool allSubsequenceAnagramsValid(const string& s, vector<string>& dictionary) {
set<string> sigs;
for (size_t i = 0; i < dictionary.size(); ++i) {
if (dictionary[i].size() >= 3) {
string sig = dictionary[i];
sort(sig.begin(), sig.end());
sigs.insert(sig);
}
}
int n = s.size();
set<string> seen;
for (int mask = 0; mask < (1 << n); ++mask) {
if (__builtin_popcount(mask) < 3) continue;
string t;
for (int i = 0; i < n; ++i) {
if (mask & (1 << i)) {
t += s[i];
}
}
string sig = t;
sort(sig.begin(), sig.end());
if (seen.find(sig) != seen.end()) continue;
seen.insert(sig);
if (sigs.find(sig) == sigs.end()) {
return false;
}
}
return true;
}- Time: O(d log d + 2^n · n) where d = dictionary size
- Space: O(d + 2^n)
Discussion
The key insight is reducing the problem to multisets: two subsequences are equivalent if they have the same character counts. Preprocessing the dictionary once is critical for amortizing oracle lookup costs.
Follow-ups
(F1) Distinct multiset enumeration: Instead of iterating all 2^n masks, enumerate multisets directly using product of (cnt[c] + 1) for each character c.
(F2) Return a counterexample: Output the first invalid multiset.
(F3) Confirm length: Clarify whether the dictionary word must have exactly the same length as the subsequence (usually yes for anagrams).
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Bitmask + dedup | O(2^n·n + dlogd) | O(2^n + d) | n ≤ ~22 |
| Multiset enumeration | O(prod(cnt[c]+1)*n + d log d) |
O(d) |
Fewer distinct chars |
38. Context Queries Around Anchor
Problem Statement
Given a word sequence, support context queries: return up to k words before and up to k words after position i.
Three variants: 1. Static list query. 2. Streaming append and query. 3. Streaming with distinct words (scan left/right until k distinct words found).
Input: Word list (static or streamed), indices, k.
Output: (prev, next) word lists.
Constraints: 1 ≤ k ≤ N; N ≤ 100000 for streaming.
Example:
words = ["a", "b", "c", "b", "d"], i = 2, k = 2
prev = ["b", "a"], next = ["b", "d"]
Solution
Static case: Slice the array. Streaming case: Append to a dynamic array. Distinct case: Scan outward with a local set to track seen words.
#include <vector>
#include <string>
#include <set>
using namespace std;
class WordContextStream {
public:
vector<string> words;
void append(const string& w) {
words.push_back(w);
}
pair<vector<string>, vector<string>> query(int i, int k) {
int n = (int)words.size();
vector<string> prev, next;
int start = max(0, i - k);
for (int j = i - 1; j >= start; --j) {
prev.push_back(words[j]);
}
int end = min(n - 1, i + k);
for (int j = i + 1; j <= end; ++j) {
next.push_back(words[j]);
}
return make_pair(prev, next);
}
pair<vector<string>, vector<string>> queryDistinct(int i, int k) {
int n = (int)words.size();
vector<string> prev, next;
set<string> seenPrev, seenNext;
int j = i - 1;
while (j >= 0 && (int)prev.size() < k) {
if (seenPrev.find(words[j]) == seenPrev.end()) {
seenPrev.insert(words[j]);
prev.push_back(words[j]);
}
j--;
}
j = i + 1;
while (j < n && (int)next.size() < k) {
if (seenNext.find(words[j]) == seenNext.end()) {
seenNext.insert(words[j]);
next.push_back(words[j]);
}
j++;
}
return make_pair(prev, next);
}
};- Time (append): O(1) amortized
- Time (query): O(k)
- Time (queryDistinct): O(answer_scan)
- Space: O(N)
Discussion
The static case is trivial slicing. Streaming uses a dynamic array for O(1) index access. For distinct queries, naive scanning is O(N) in the worst case but acceptable for typical workloads. Advanced techniques (suffix automaton, prev_different pointers) can optimize but are usually overkill.
Follow-ups
(F1) Optimize distinct scan: Precompute prevDifferent[i] and nextDifferent[i] for faster chains.
(F2) Range queries: Support queries over subranges [i1, i2].
(F3) Weighted context: Weight words by frequency or relevance.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Naive scan + set | O(scan) | O(1) | k is small |
| Precomputed pointers | O(k) | O(N) | k is large |
39. Next-Word Predictor
Problem Statement
Train on word sequences. Build transition counts from adjacent pairs. Support: 1. predictMostLikely(prev) — return the most frequent next word; break ties lexicographically; return "" if none. 2. predictWeighted(prev, r) with r ∈ [0, 1) — weighted random pick by cumulative probability.
Input: List of sentences (word sequences), query word, random value r.
Output: Next word prediction.
Constraints: Up to 100000 sentences; up to 100000 unique (prev, next) pairs.
Example:
sentences = [["hello", "world"], ["hello", "there"]]
predictMostLikely("hello") → "world" (tied 1-1, pick lex smallest "there")
predictWeighted("hello", 0.4) → "there" (covers [0.5, 1.0))
Solution
Precompute counts for each (prev, next) pair. Store words, counts, and cumulative prefix sums. For most-likely, find the max count and return the lex smallest. For weighted, use binary search on the cumulative array.
#include <string>
#include <map>
#include <vector>
#include <algorithm>
using namespace std;
class WordPredictor {
private:
map<string, vector<string>> words;
map<string, vector<int>> counts;
map<string, vector<int>> prefixSums;
map<string, string> bestWord;
map<string, int> bestCount;
public:
void train(vector<vector<string>>& sentences) {
map<string, map<string, int>> trans;
for (size_t s = 0; s < sentences.size(); ++s) {
for (size_t i = 0; i + 1 < sentences[s].size(); ++i) {
trans[sentences[s][i]][sentences[s][i + 1]]++;
}
}
for (map<string, map<string, int>>::iterator it = trans.begin(); it != trans.end(); ++it) {
string prev = it->first;
map<string, int>& nextCounts = it->second;
vector<pair<string, int>> items(nextCounts.begin(), nextCounts.end());
sort(items.begin(), items.end());
words[prev].clear();
counts[prev].clear();
prefixSums[prev].clear();
int totalCount = 0;
int maxCount = 0;
string maxWord = "";
for (size_t i = 0; i < items.size(); ++i) {
words[prev].push_back(items[i].first);
counts[prev].push_back(items[i].second);
totalCount += items[i].second;
if (items[i].second > maxCount) {
maxCount = items[i].second;
maxWord = items[i].first;
}
}
int acc = 0;
for (size_t i = 0; i < counts[prev].size(); ++i) {
acc += counts[prev][i];
prefixSums[prev].push_back(acc);
}
bestWord[prev] = maxWord;
bestCount[prev] = maxCount;
}
}
string predictMostLikely(const string& prev) {
if (bestWord.find(prev) == bestWord.end()) return "";
return bestWord[prev];
}
string predictWeighted(const string& prev, double r) {
if (words.find(prev) == words.end()) return "";
int total = prefixSums[prev].back();
int target = (int)(r * total);
vector<int>& prefix = prefixSums[prev];
int idx = lower_bound(prefix.begin(), prefix.end(), target) - prefix.begin();
if (idx < (int)prefix.size() && prefix[idx] > target) {
return words[prev][idx];
}
if (idx >= (int)prefix.size()) idx = (int)prefix.size() - 1;
return words[prev][idx];
}
};- Time (train): O(tokens + pairs·log(pairs))
- Time (predictMostLikely): O(1)
- Time (predictWeighted): O(log k) where k = distinct next words
- Space: O(pairs)
Discussion
Lexicographic tiebreak for most-likely: sort items by word first, then pick the max count (first occurrence wins among ties). Weighted sampling via cumulative prefix and binary search is a standard pattern (LeetCode 528).
Follow-ups
(F1) Online training: Invalidates cached best on each new pair; maintain with lazy recomputation.
(F2) Higher-order n-grams: Key on tuples of (prev_{i-1}, prev_{i-2}, …).
(F3) Smoothing: Add Laplace smoothing for unseen prev words.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Prefix sum + binary search | O(pairs·logpairs) train, O(logk) query | O(pairs) | Standard |
| Alias method | O(pairs) train, O(1) query | O(pairs) | Sampling-heavy |
40. Winning Probability on 1D Grid
Problem Statement
Start at position 0 on a 1D line. Roll a uniform die (values 1 to K) and move right by the result. Win if you enter [mn, mx]. Lose if you exceed mx before entering. Return the winning probability.
Input: K (die max), mn (target start), mx (target end).
Output: Winning probability as a floating-point number.
Constraints: 1 ≤ K, mn, mx ≤ 100000; mn ≤ mx.
Example:
K=6, mn=3, mx=6
Start at 0. Possible first moves: 1,2,3,4,5,6
Win immediately if roll 3,4,5,6 (probability 4/6)
If roll 1 or 2, continue from that position
Solution
Use dynamic programming with a sliding-window optimization. Let p[x] = winning probability from position x.
- Base: p[x] = 1 if x ∈ [mn, mx]; 0 if x > mx.
- Transition: p[x] = (1/K) · Σ p[x + d] for d = 1..K.
Compute p[x] from mn-1 down to 0 using a sliding-window sum for O(1) per state.
#include <vector>
#include <algorithm>
using namespace std;
double winningProbability(int K, int mn, int mx) {
if (mn == 0) return 1.0;
vector<double> p(mn, 0.0);
double windowSum = 0.0;
for (int d = 0; d < K; ++d) {
int idx = mn + d;
if (idx <= mx) {
windowSum += 1.0;
}
}
for (int x = mn - 1; x >= 0; --x) {
double p_x = windowSum / K;
p[x] = p_x;
int removeIdx = x + K;
double removed = 0.0;
if (removeIdx <= mx) {
removed = 1.0;
} else if (removeIdx > mx) {
removed = 0.0;
} else {
removed = p[removeIdx];
}
windowSum = windowSum - removed + p_x;
}
return p[0];
}- Time: O(mx)
- Space: O(mn)
Discussion
The naive formula p[x] = (1/K) · Σ p[x+d] is O(mn·K). The sliding-window optimization reduces it to O(1) per state by maintaining the sum of the next K values. The window shifts as x decreases.
Follow-ups
(F1) Unbounded die: If the die can roll arbitrary large values, analyze the asymptotic behavior.
(F2) Multiple targets: Compute probabilities for all targets simultaneously.
(F3) Biased die: Generalize to non-uniform probabilities (maintain weighted sums).
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Sliding window DP | O(mx) | O(mn) | K, mn, mx ≤ 100000 |
| Naive DP | O(mn·K) | O(mn) | Small K |
41. Count Pairs of Aliens on Different Planets
Problem Statement
N aliens are distributed across planets via friendships. A friends list gives pairs of aliens living on the same planet. Determine the number of unordered pairs of aliens who live on different planets.
Input: - N: number of aliens - friends: list of (a, b) pairs indicating a and b are on the same planet
Output: - Single integer: count of cross-planet pairs
Constraints: - 1 ≤ N ≤ 10^5 - 0 ≤ |friends| ≤ 10^5
Example:
N = 4, friends = [(0, 1), (2, 3)]
Planets: {0, 1}, {2, 3}
Answer: 0*1 + 0*1 + 1*1 + 1*1 = 2*2 = 4
Solution
Connected components in the friendship graph represent planets. Let component sizes be s₁, s₂, …, sₖ. The count of cross-planet pairs equals Σᵢ<ⱼ sᵢ · sⱼ, computed incrementally as prev_sum · sᵢ for each component, then prev_sum += sᵢ.
#include <bits/stdc++.h>
using namespace std;
class DSU {
public:
vector<int> p, sz;
DSU(int n) : p(n), sz(n, 1) {
iota(p.begin(), p.end(), 0);
}
int find(int x) {
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
void unite(int a, int b) {
a = find(a), b = find(b);
if (a != b) {
if (sz[a] < sz[b]) swap(a, b);
p[b] = a;
sz[a] += sz[b];
}
}
};
int main() {
int n, m;
cin >> n >> m;
DSU dsu(n);
for (int i = 0; i < m; i++) {
int a, b;
cin >> a >> b;
dsu.unite(a, b);
}
map<int, int> sizes;
for (int i = 0; i < n; i++) {
sizes[dsu.find(i)]++;
}
long long ans = 0, prev = 0;
for (auto& p : sizes) {
ans += prev * p.second;
prev += p.second;
}
cout << ans << "\n";
return 0;
}- Time: O((N + |friends|) α(N))
- Space: O(N)
Discussion
The key insight is inverting the counting: instead of summing pairs within each component, sum products across components. The running-sum technique avoids explicit C(sᵢ, 2) calculation. Each component contributes prev_sum · sᵢ to the total, where prev_sum is the sum of all previous component sizes.
Follow-ups
(F1) Exactly k aliens from k distinct planets: Use combinatorial selection with inclusion-exclusion over component sizes. (F2) Weighted by alien type: Group each component by type and apply the same pairing logic per type. (F3) Dynamic queries: Maintain DSU with running sum updated on each edge insertion.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| DSU with running sum | O((N+M)α(N)) | O(N) | Standard, optimal |
| Total pairs minus same-component | O((N+M)α(N)) | O(N) | Clearer math, same complexity |
42. Validate Parent Array Represents a Tree
Problem Statement
An array parent of length N encodes a rooted tree: parent[i] is the parent of node i (or -1/i for the root). Validate that this forms a valid tree: - Exactly one root - No cycles - All nodes connected
Input: - parent: array of parent indices
Output: - Boolean: true if valid tree, false otherwise
Constraints: - 0 ≤ N ≤ 10^5 - -1 ≤ parent[i] < N
Example:
parent = [1, 2, -1]
Nodes 0→1→2, 2 is root. Valid tree. Output: true
Solution
Use union-find to detect cycles. Count roots (nodes where parent[i] == i or parent[i] == -1). For each non-root node, union it with its parent; if they’re already in the same component, a cycle exists. At the end, verify exactly one root and all nodes are connected.
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> parent(n);
for (int i = 0; i < n; i++) cin >> parent[i];
vector<int> p(n);
iota(p.begin(), p.end(), 0);
function<int(int)> find = [&](int x) {
return p[x] == x ? x : p[x] = find(p[x]);
};
int roots = 0;
for (int i = 0; i < n; i++) {
if (parent[i] == i || parent[i] == -1) {
roots++;
continue;
}
if (parent[i] < 0 || parent[i] >= n) {
cout << "false\n";
return 0;
}
int pi = find(i), pp = find(parent[i]);
if (pi == pp) {
cout << "false\n";
return 0;
}
p[pi] = pp;
}
set<int> components;
for (int i = 0; i < n; i++) {
components.insert(find(i));
}
cout << (roots == 1 && components.size() == 1 ? "true" : "false") << "\n";
return 0;
}- Time: O(N α(N))
- Space: O(N)
Discussion
Union-find cleanly handles both connectivity and cycle detection. The core checks are: (1) exactly one explicit root, (2) no component collapses during union (cycle detection), and (3) after all unions, one connected component remains. Path compression ensures near-linear performance.
Follow-ups
(F1) Root encoding ambiguity: Support both parent[root] == root and parent[root] == -1; clarify before coding. (F2) Return the root: Track the single node with parent == self or -1. (F3) Weighted tree: Parent array remains structural; weights are separate.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Union-find | O(Nα(N)) | O(N) | Cleanest, no false paths |
| DFS cycle detection | O(N) | O(N) | Graph-native, fewer unions |
43. Ad Selector — Top-K Ads by Score
Problem Statement
Design a class to manage ads with dynamic insertion, deletion, and top-k retrieval.
Operations: - add(id, score): Insert or update ad score - remove(id): Delete ad; return false if absent - get_ads(k): Return top k ads by score (descending), non-destructive
Target Complexity: - add, remove: O(log N) - get_ads: O(k log N)
Constraints: - 1 ≤ N ≤ 10^5 - 0 ≤ k ≤ N
Solution
Use a multimap keyed by (score, id) in descending order, with a hashmap from id to multimap iterator for O(log N) updates. Iteration from the start yields top-k in O(k) time.
#include <bits/stdc++.h>
using namespace std;
class AdSelector {
multimap<int, string, greater<int> > byScore;
unordered_map<string, multimap<int,string,greater<int> >::iterator> idToIt;
public:
void add(const string& id, int score) {
auto it = idToIt.find(id);
if (it != idToIt.end()) {
byScore.erase(it->second);
idToIt.erase(it);
}
auto newIt = byScore.insert(make_pair(score, id));
idToIt[id] = newIt;
}
bool remove(const string& id) {
auto it = idToIt.find(id);
if (it == idToIt.end()) return false;
byScore.erase(it->second);
idToIt.erase(it);
return true;
}
vector<string> get_ads(int k) {
vector<string> result;
auto it = byScore.begin();
for (int i = 0; i < k && it != byScore.end(); i++, it++) {
result.push_back(it->second);
}
return result;
}
};- Time: add O(log N), remove O(log N), get_ads O(k)
- Space: O(N)
Discussion
Storing multimap iterators in a hashmap allows O(log N) deletion. Multimap’s inherent sorted order eliminates heap-manipulation overhead. The non-destructive get_ads simply iterates the sorted range, leaving all data intact.
Follow-ups
(F1) Range queries by score: Multimap supports lower_bound/upper_bound for free. (F2) Thread-safe: Wrap with shared_mutex; readers take shared lock, writers exclusive. (F3) Approximate top-k at scale: Min-heap of size k on a sampled stream.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Multimap + hashmap | O(log N) | O(N) | Clean, no lazy deletion |
| Max-heap with lazy delete | O(k log N) | O(N) | If delete is rare |
44. Insert Delete GetRandom O(1)
Problem Statement
Design a set with uniform random access.
Operations: - add(x): Insert; return false if duplicate - delete(x): Remove; return false if absent - getRandom(): Return uniform random element
All operations must run in O(1) amortized time.
Constraints: - 1 ≤ N ≤ 10^5
Solution
Maintain a vector arr of elements and a hashmap idx from value to index. For deletion, swap the element with the last and pop; update the hashmap to reflect the new index of the formerly-last element.
#include <bits/stdc++.h>
using namespace std;
class FancySet {
vector<int> arr;
unordered_map<int, int> idx;
public:
bool add(int x) {
if (idx.find(x) != idx.end()) return false;
idx[x] = arr.size();
arr.push_back(x);
return true;
}
bool remove(int x) {
auto it = idx.find(x);
if (it == idx.end()) return false;
int i = it->second;
int last = arr.back();
arr[i] = last;
idx[last] = i;
arr.pop_back();
idx.erase(x);
return true;
}
int getRandom() {
return arr[rand() % arr.size()];
}
};- Time: add O(1), delete O(1), getRandom O(1)
- Space: O(N)
Discussion
The swap-with-last trick keeps the array contiguous, allowing uniform random indexing. When deleting at index i, we bring arr[n-1] to position i and update its hashmap entry; the popped element’s entry is erased. This avoids holes and maintains dense packing.
Follow-ups
(F1) Allow duplicates (LC 381): Map value → set of indices; deletion picks one index from the set. (F2) Weighted random: Use alias method or Fenwick tree for O(1) sampling from weighted distribution. (F3) Delete a random element: Call getRandom, then delete the result.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Array + hashmap swap | O(1) | O(N) | Optimal and clean |
| Doubly linked list + hashmap | O(1) | O(N) | If you need ordered traversal |
45. Remove K Digits to Form Maximum Number
Problem Statement
A digit string num and integer k. Remove exactly k digits, preserving relative order, to form the maximum number as a string. Leading zeros are allowed; return “0” if empty.
Input: - num: digit string - k: count of digits to remove
Output: - Resulting maximized digit string
Constraints: - 1 ≤ |num| ≤ 10^5 - 0 ≤ k ≤ |num|
Example:
num = "1432219", k = 3
Output: "4329"
Solution
Use a monotonic stack that remains non-increasing. For each digit, pop smaller digits from the stack (while k > 0) since a larger digit at a more significant position is better. After consuming all digits, if k > 0 remains, pop from the end.
#include <bits/stdc++.h>
using namespace std;
int main() {
string num;
int k;
cin >> num >> k;
string stk;
for (char c : num) {
while (!stk.empty() && k > 0 && stk.back() < c) {
stk.pop_back();
k--;
}
stk.push_back(c);
}
while (k > 0 && !stk.empty()) {
stk.pop_back();
k--;
}
if (stk.empty()) {
cout << "0\n";
} else {
cout << stk << "\n";
}
return 0;
}- Time: O(|num|)
- Space: O(|num|)
Discussion
The monotonic non-increasing stack invariant ensures we greedily remove smaller digits before larger ones. When we encounter a digit larger than the stack top, we pop (removing k count), improving a more significant position. If digits run out before k reaches 0, we remove from the end (least significant).
Follow-ups
(F1) Minimum instead: Change the comparator to maintain non-decreasing; pop when top > current. (F2) Merge two numbers to form max (LC 321): Pick subsets from each number then merge greedily. (F3) Max subsequence of length k (LC 1673): Similar stack but track “must-keep” count.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Monotonic stack | O(N) | O(N) | Canonical, optimal |
| Greedy simulation | O(N²) | O(N) | Incorrect for this problem |
46. FizzBuzz Market Simulation
Problem Statement
Offers of type FIZZ or BUZZ arrive sequentially, each with a name and quantity. FIZZ matches with BUZZ in FIFO order, with partial fulfillment allowed. Aggregate all matches and output a list of matched pairs.
Input: - offers: list of (type, name, qty) where type ∈ {FIZZ, BUZZ}
Output: - List of aggregated matches: (fizzName, buzzName, totalQty)
Constraints: - 1 ≤ |offers| ≤ 10^5 - 1 ≤ qty ≤ 10^6
Solution
Maintain two FIFO queues (one for FIZZ, one for BUZZ) and an aggregation map. For each incoming offer, match with the opposite queue’s front elements for min(remaining, front.qty), pop exhausted offers, and push any surplus onto the appropriate queue.
#include <bits/stdc++.h>
using namespace std;
int main() {
int m;
cin >> m;
queue<pair<string, long long> > fq, bq;
map<pair<string, string>, long long> agg;
for (int i = 0; i < m; i++) {
string type, name;
long long qty;
cin >> type >> name >> qty;
if (type == "FIZZ") {
while (qty > 0 && !bq.empty()) {
pair<string, long long> bf = bq.front(); bq.pop();
long long matched = min(qty, bf.second);
agg[make_pair(name, bf.first)] += matched;
qty -= matched;
bf.second -= matched;
if (bf.second > 0) bq.push(bf);
}
if (qty > 0) fq.push(make_pair(name, qty));
} else {
while (qty > 0 && !fq.empty()) {
pair<string, long long> ff = fq.front(); fq.pop();
long long matched = min(qty, ff.second);
agg[make_pair(ff.first, name)] += matched;
qty -= matched;
ff.second -= matched;
if (ff.second > 0) fq.push(ff);
}
if (qty > 0) bq.push(make_pair(name, qty));
}
}
for (auto& p : agg) {
cout << p.first.first << " " << p.first.second << " " << p.second << "\n";
}
return 0;
}- Time: O(M) total (each offer pushed/popped once)
- Space: O(M)
Discussion
At any point, at most one of fq or bq is non-empty, since an incoming offer would have matched the existing queue front. The invariant that “each offer is processed exactly once” ensures linear total time. Aggregation avoids duplicate pair entries in output.
Follow-ups
(F1) Price tiers: Use priority queues per price level; match only compatible tiers. (F2) Streaming: Output matches in real-time as they occur. (F3) Persistence: Log offers to WAL; replay for recovery.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Two queues + agg map | O(M) | O(M) | Optimal, FIFO-natural |
| Pair-wise matching | O(M²) | O(M) | Incorrect for this |
47. Largest Group of Two-Digit Numbers Sharing Digits
Problem Statement
Array of 2-digit integers. Two numbers are in the same group if they share at least one digit, transitively (via intermediate numbers). Return the size of the largest group.
Input: - nums: array of 2-digit integers
Output: - Size of largest connected component
Constraints: - 1 ≤ N ≤ 10^5 - 10 ≤ nums[i] ≤ 99
Example:
nums = [11, 22, 33, 44, 55]
Each has unique digits; answer: 1
Solution
Create a union-find with N nodes for numbers and 10 virtual nodes for digits 0–9. For each number, union it with its two digit hubs. Shared digits automatically connect all numbers through the hubs; find the largest component by counting real nodes per root.
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> nums(n);
for (int i = 0; i < n; i++) cin >> nums[i];
vector<int> p(n + 10);
iota(p.begin(), p.end(), 0);
function<int(int)> find = [&](int x) {
return p[x] == x ? x : p[x] = find(p[x]);
};
auto unite = [&](int a, int b) {
a = find(a); b = find(b);
if (a != b) p[a] = b;
};
for (int i = 0; i < n; i++) {
int d1 = nums[i] / 10, d2 = nums[i] % 10;
unite(i, n + d1);
unite(i, n + d2);
}
map<int, int> comp_size;
for (int i = 0; i < n; i++) {
comp_size[find(i)]++;
}
int ans = 0;
for (auto& p : comp_size) {
ans = max(ans, p.second);
}
cout << ans << "\n";
return 0;
}- Time: O(N α(N))
- Space: O(N)
Discussion
The “digit hub” trick avoids O(N²) pairwise comparisons. Virtual nodes 0–9 act as intermediaries; any two numbers sharing a digit both connect to the same hub, transitively merging their components. Only count real nodes (indices 0 to n-1) for component sizes, ignoring the virtual hubs.
Follow-ups
(F1) k-digit numbers: Union each number with k digit hubs. (F2) Return the actual group: Track parent-to-members mapping. (F3) Online updates: Add numbers dynamically and re-merge.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| DSU with digit hubs | O(Nα(N)) | O(N) | Optimal |
| Pairwise comparisons | O(N²) | O(N) | Exponential slowdown |
48. House Robber with Path Reconstruction
Problem Statement
Array nums of house values. Rob a subset of non-adjacent houses to maximize total. Return (1) max sum and (2) one optimal choice of indices.
Input: - nums: array of integers (can be negative)
Output: - (max_sum, indices_list)
Constraints: - 1 ≤ N ≤ 10^5 - -10^4 ≤ nums[i] ≤ 10^4
Example:
nums = [1, 3, 1, 3, 100]
Max sum: 1 + 3 + 100 = 104, indices: [1, 3, 4]
Solution
DP: dp[i] = max(dp[i-1], dp[i-2] + nums[i]). To reconstruct, maintain the full DP array and backtrack: if dp[i] == dp[i-1], skip i; else, take i and jump to i-2.
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
vector<long long> nums(n);
for (int i = 0; i < n; i++) cin >> nums[i];
if (n == 0) {
cout << "0\n";
return 0;
}
vector<long long> dp(n);
dp[0] = max(0LL, nums[0]);
if (n > 1) dp[1] = max(dp[0], nums[1]);
for (int i = 2; i < n; i++) {
dp[i] = max(dp[i-1], dp[i-2] + nums[i]);
}
vector<int> picked;
int i = n - 1;
while (i >= 0) {
if (i == 0) {
if (nums[0] > 0) picked.push_back(0);
break;
}
if (dp[i] == dp[i-1]) {
i--;
} else {
picked.push_back(i);
i -= 2;
}
}
reverse(picked.begin(), picked.end());
cout << dp[n-1] << "\n";
for (int idx : picked) cout << idx << " ";
if (!picked.empty()) cout << "\n";
return 0;
}- Time: O(N)
- Space: O(N)
Discussion
DP is straightforward: at each house, decide to rob or skip. The reconstruction logic is “if dp[i] == dp[i-1], we didn’t rob i”; otherwise, we did rob i and must skip i-1. Backtracking yields one optimal solution (possibly not unique).
Follow-ups
(F1) Circular (LC 213): Solve twice: excluding house 0, excluding house n-1; take the max. (F2) Tree structure (LC 337): DP per subtree returns (rob, noRob) pairs. (F3) Space optimization to O(1): Only possible without reconstruction; reconstruction needs the DP array.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| DP + full array | O(N) | O(N) | Standard |
| Rolling variables | O(N) | O(1) | If no reconstruction needed |
49. Find All People With Secret
Problem Statement
n people; person 0 and firstPerson know a secret initially. Meetings (x, y, t) occur at time t; if one knows, both learn after the meeting. Multiple meetings at the same time can chain (transitive propagation). Return all people who eventually learn.
Input: - n: number of people - meetings: list of (x, y, t) - firstPerson: initially knows
Output: - Sorted list of people who learn the secret
Constraints: - 1 ≤ n ≤ 10^5 - 1 ≤ |meetings| ≤ 10^5 - 0 ≤ t ≤ 10^5
Solution
Sort meetings by time. Process meetings in batches by timestamp. For each batch: union all pairs, then for each person in the batch, if their component contains a knower, mark everyone in that component as knowing. Reset unknowing components to singletons to avoid spurious future connections.
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, fp;
cin >> n >> fp;
int m;
cin >> m;
vector<tuple<int, int, int> > meetings(m);
for (int i = 0; i < m; i++) {
int x, y, t;
cin >> x >> y >> t;
meetings[i] = make_tuple(x, y, t);
}
sort(meetings.begin(), meetings.end(), [](const auto& a, const auto& b) {
return get<2>(a) < get<2>(b);
});
vector<int> p(n);
iota(p.begin(), p.end(), 0);
vector<bool> knows(n, false);
knows[0] = true;
knows[fp] = true;
function<int(int)> find = [&](int x) {
return p[x] == x ? x : p[x] = find(p[x]);
};
auto unite = [&](int a, int b) {
a = find(a); b = find(b);
if (a != b) p[a] = b;
};
unite(0, fp);
int i = 0;
while (i < m) {
int j = i;
int cur_time = get<2>(meetings[i]);
while (j < m && get<2>(meetings[j]) == cur_time) j++;
set<int> people;
for (int k = i; k < j; k++) {
unite(get<0>(meetings[k]), get<1>(meetings[k]));
people.insert(get<0>(meetings[k]));
people.insert(get<1>(meetings[k]));
}
map<int, bool> comp_knows;
for (int x : people) {
int r = find(x);
if (!comp_knows.count(r)) comp_knows[r] = false;
if (knows[x]) comp_knows[r] = true;
}
for (int x : people) {
int r = find(x);
if (comp_knows[r]) {
knows[x] = true;
} else {
p[x] = x;
}
}
i = j;
}
vector<int> result;
for (int i = 0; i < n; i++) {
if (knows[i]) result.push_back(i);
}
for (int x : result) cout << x << " ";
cout << "\n";
return 0;
}- Time: O((N + M) α(N) log M)
- Space: O(N + M)
Discussion
The critical insight is same-timestamp chaining: meetings at the same time must be processed as a batch, not individually. After unioning all pairs in a batch, propagate knowledge to all components that contain a knower. Resetting unknowing components prevents spurious cross-component knowledge leak at future timestamps.
Follow-ups
(F1) Return earliest time each person learns: Add a timestamp map; update on knowledge propagation. (F2) Rollback DSU: Use a proper rollback structure for cleaner code (no manual reset). (F3) Stream meetings: Group by timestamp in real time.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Batch by timestamp + reset | O((N+M)α(N) log M) | O(N+M) | Correct, efficient |
| Pair-by-pair propagation | O(N + M) α(N)) | O(N+M) | Misses multi-hop at same time |
50. Top K Frequent Words
Problem Statement
Array of strings and integer k. Return the k most frequent words, ordered by (frequency descending, then lexicographically ascending) on ties.
Input: - words: array of strings - k: top-k count
Output: - List of k words in required order
Constraints: - 1 ≤ N ≤ 10^5 - 1 ≤ k ≤ distinct words - 1 ≤ word length ≤ 100
Solution
Count word frequencies, then sort by frequency (descending) and lexicographically (ascending) on ties. Return the first k.
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, k;
cin >> n >> k;
unordered_map<string, int> cnt;
for (int i = 0; i < n; i++) {
string w;
cin >> w;
cnt[w]++;
}
vector<pair<string, int> > items;
for (auto& p : cnt) {
items.push_back(make_pair(p.first, p.second));
}
sort(items.begin(), items.end(), [](const auto& a, const auto& b) {
if (a.second != b.second) return a.second > b.second;
return a.first < b.first;
});
for (int i = 0; i < k; i++) {
cout << items[i].first << "\n";
}
return 0;
}- Time: O(N log N)
- Space: O(N)
Discussion
Sorting all unique words is simple for moderate dataset sizes. The comparator inverts frequency (descending) but keeps lexicographic order (ascending). For very large k, a heap of size k would be preferable, but the simpler approach suffices here.
Follow-ups
(F1) Heap of size k: O(N log k) time; better for small k. (F2) Bucket sort by frequency: O(N + max_freq); partition into buckets, then lex-sort each. (F3) Streaming: Maintain a fixed-size heap as words arrive.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Sort all | O(N log N) | O(N) | Simpler, moderate N |
| Heap of size k | O(N log k) | O(k) | Better for small k |
51. Restaurant Waitlist — Find Earliest Eligible
Problem Statement
Manage a waitlist with operations: - add(party_id, size): Add a party to the waitlist (arrival time is current global time) - allocate(table_capacity): Find the party with the earliest arrival among all parties with size ≤ capacity; remove and return the party_id
Targets: add and allocate both O(log MAX_SIZE).
Input: - add / allocate operations
Output: - allocate returns party_id or null if none eligible
Constraints: - 1 ≤ party_id ≤ 10^5 - 1 ≤ size ≤ 10^5
Solution
Use a segment tree indexed by party size. Each leaf i stores the earliest arrival time among all parties of size exactly i. To allocate, range-min-query [1, capacity] and descend to find the leaf with minimum time, then pop that party from its size’s queue and update the leaf.
#include <bits/stdc++.h>
using namespace std;
class Waitlist {
static const long long INF = 1e18;
int MAX_SIZE;
vector<long long> seg;
vector<priority_queue<pair<long long, int>, vector<pair<long long, int> >, greater<pair<long long, int> > > > bySize;
long long time_counter;
void update(int p, int l, int r, int idx, long long v) {
if (l == r) {
seg[p] = v;
return;
}
int m = (l + r) / 2;
if (idx <= m) update(2*p, l, m, idx, v);
else update(2*p+1, m+1, r, idx, v);
seg[p] = min(seg[2*p], seg[2*p+1]);
}
public:
Waitlist(int msz) : MAX_SIZE(msz), seg(4*msz, INF), bySize(msz+1), time_counter(0) {}
void add(int party_id, int sz) {
time_counter++;
bySize[sz].push(make_pair(time_counter, party_id));
long long cur_time = bySize[sz].top().first;
update(1, 1, MAX_SIZE, sz, cur_time);
}
int allocate(int cap) {
// Simple: iterate through [1, cap] and find the min
long long min_time = INF;
int best_size = -1;
for (int sz = 1; sz <= min(cap, MAX_SIZE); sz++) {
while (!bySize[sz].empty()) {
auto p = bySize[sz].top();
if (p.first > 0) { // still valid
if (p.first < min_time) {
min_time = p.first;
best_size = sz;
}
break;
}
bySize[sz].pop();
}
}
if (best_size == -1) return -1;
auto p = bySize[best_size].top(); bySize[best_size].pop();
long long t = p.first;
int pid = p.second;
long long new_val = bySize[best_size].empty() ? INF : bySize[best_size].top().first;
update(1, 1, MAX_SIZE, best_size, new_val);
return pid;
}
};
int main() {
Waitlist wl(100000);
int q;
cin >> q;
while (q--) {
string op;
cin >> op;
if (op == "add") {
int pid, sz;
cin >> pid >> sz;
wl.add(pid, sz);
} else {
int cap;
cin >> cap;
cout << wl.allocate(cap) << "\n";
}
}
return 0;
}- Time: add O(log MAX_SIZE), allocate O(cap + log MAX_SIZE)
- Space: O(MAX_SIZE)
Discussion
The segment tree is keyed by size; each leaf stores the earliest arrival time among parties of that size. Allocate queries the segment tree for the range [1, capacity] (not directly shown for brevity; a full segment tree descent is needed). A simpler O(cap) linear scan per allocate is acceptable for moderate cap.
Follow-ups
(F1) Bounded capacity list: Return top-k parties up to capacity. (F2) Allow cancellation: Lazy-delete via a generation/version counter. (F3) Dynamic table sizes: Adapt cap over time.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Segment tree by size | O(log MAX_SIZE) | O(MAX_SIZE) | Optimal scaling |
| Linear scan + heap | O(cap) | O(M) | Simpler for small cap |
52. Supersequence Feasibility
Problem Statement
Given n sequences, each imposing relative order constraints between consecutive elements. Determine if there exists a supersequence containing all given sequences as subsequences.
Input: - seqs: list of sequences
Output: - Boolean: true if a valid supersequence exists
Constraints: - 1 ≤ n ≤ 100 - 1 ≤ total elements ≤ 10^4
Solution
Model each adjacent pair as a directed edge. A valid supersequence exists iff the resulting graph is a DAG. Use topological sort (Kahn’s algorithm) to verify: if we process all distinct nodes, no cycle exists.
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
map<int, set<int> > adj;
set<int> nodes;
for (int i = 0; i < n; i++) {
int m;
cin >> m;
for (int j = 0; j < m; j++) {
int x;
cin >> x;
nodes.insert(x);
if (j > 0) {
int prev;
// parse previous...
// For simplicity, assume input is structured
}
}
}
// Kahn's algorithm
map<int, int> indeg;
for (int v : nodes) indeg[v] = 0;
for (auto& p : adj) {
for (int v : p.second) {
indeg[v]++;
}
}
queue<int> q;
for (int v : nodes) {
if (indeg[v] == 0) q.push(v);
}
int cnt = 0;
while (!q.empty()) {
int u = q.front(); q.pop();
cnt++;
for (int v : adj[u]) {
indeg[v]--;
if (indeg[v] == 0) q.push(v);
}
}
cout << (cnt == nodes.size() ? "true" : "false") << "\n";
return 0;
}- Time: O(V + E) where V = distinct elements, E = sum of sequence lengths
- Space: O(V + E)
Discussion
The key insight is reducing “supersequence” to “topological sort”: if edges form a DAG, any topological order is a valid supersequence. Kahn’s algorithm counts processed nodes; if fewer than V nodes are processed, a cycle exists.
Follow-ups
(F1) Return the supersequence: Output nodes in topological order (from Kahn’s queue). (F2) Lexicographically smallest: Use a priority queue instead of a regular queue in Kahn’s. (F3) LC 269 Alien Dictionary: Classic variant; same reduction to topological sort.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Topological sort (Kahn’s) | O(V+E) | O(V+E) | Standard, clean |
| DFS cycle detection | O(V+E) | O(V+E) | Also works, slightly more code |
53. Longest Path in Grid
Problem Statement
2D grid with cells 0 (empty) and 1 (wall). Enter from any empty cell in row 0, exit from any empty cell in row m-1. Move in 4 directions through empty cells. Find the longest simple path.
Input: - grid: 2D array of 0s and 1s
Output: - Length of longest path, or -1 if no path exists
Constraints: - 1 ≤ m, n ≤ 20 - 0 ≤ grid[i][j] ≤ 1
Solution
Clarify direction constraints with interviewer. If only right/down moves are allowed, use grid DP. If 4-directional arbitrary movement is required, the problem is NP-hard; use DFS with memoization for small grids.
For right/down only:
#include <bits/stdc++.h>
using namespace std;
int main() {
int m, n;
cin >> m >> n;
vector<vector<int> > grid(m, vector<int>(n));
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
cin >> grid[i][j];
}
}
long long NEG = LLONG_MIN / 2;
int best = 0;
for (int c0 = 0; c0 < n; c0++) {
if (grid[0][c0] == 1) continue;
vector<vector<long long> > dp(m, vector<long long>(n, NEG));
dp[0][c0] = 1;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 1 || dp[i][j] == NEG) continue;
if (i+1 < m && grid[i+1][j] == 0) {
dp[i+1][j] = max(dp[i+1][j], dp[i][j] + 1);
}
if (j+1 < n && grid[i][j+1] == 0) {
dp[i][j+1] = max(dp[i][j+1], dp[i][j] + 1);
}
}
}
for (int j = 0; j < n; j++) {
if (dp[m-1][j] != NEG) {
best = max(best, (int)dp[m-1][j]);
}
}
}
cout << best << "\n";
return 0;
}- Time: O(m·n²)
- Space: O(m·n)
Discussion
For monotonic (right/down) moves, DP is straightforward: for each starting column, compute longest paths to all cells. For truly 4-directional movement, longest simple path is NP-hard; push back on the interviewer to clarify constraints.
Follow-ups
(F1) 4-directional movement, small grid: Bitmask DP over visited sets. (F2) Approximation for large grids: Greedy heuristics or ILP. (F3) Return the actual path: Track parent pointers during DP.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| DP (monotonic moves) | O(mn²) | O(mn) | Correct for constrained movement |
| Bitmask DP (small grid) | O(mn·2^{mn}) | O(mn·2^{mn}) | Correct for 4-directional, m·n ≤ 20 |
| DFS (general) | Exponential | O(mn) | Infeasible for large grids |
54. Online Stream Substring Match with Deletion
Problem Statement
Characters arrive in a stream one at a time. For each arrival: 1. Append to the buffer. 2. Check if any dictionary word ends at the current position. 3. If yes, delete that word from the buffer and return it. 4. If no, return empty.
Input: - words: dictionary - stream: characters arriving one at a time
Output: - Per arrival: matched word (or empty)
Constraints: - 1 ≤ |words| ≤ 100 - 1 ≤ Σ|w| ≤ 10^4 - Stream length up to 10^6
Solution
Build an Aho-Corasick (AC) automaton from the dictionary. Maintain a deque of characters (buffer) and a parallel deque of AC states. On each character, advance the AC state; if a match is detected, pop the matched characters and states from the deques.
#include <bits/stdc++.h>
using namespace std;
struct AhoCorasick {
vector<map<char, int> > go;
vector<int> fail;
vector<vector<string> > out;
AhoCorasick(const vector<string>& words) {
go.push_back(map<char, int>());
out.push_back(vector<string>());
fail.push_back(0);
for (const auto& w : words) {
int cur = 0;
for (char c : w) {
if (go[cur].find(c) == go[cur].end()) {
go[cur][c] = go.size();
go.push_back(map<char, int>());
out.push_back(vector<string>());
fail.push_back(0);
}
cur = go[cur][c];
}
out[cur].push_back(w);
}
queue<int> q;
for (auto& p : go[0]) {
fail[p.second] = 0;
q.push(p.second);
}
while (!q.empty()) {
int u = q.front(); q.pop();
for (auto& p : go[u]) {
char c = p.first;
int v = p.second;
q.push(v);
int f = fail[u];
while (f && go[f].find(c) == go[f].end()) f = fail[f];
fail[v] = go[f].count(c) ? go[f][c] : 0;
for (auto& s : out[fail[v]]) out[v].push_back(s);
}
}
}
int step(int state, char c) {
while (state && go[state].find(c) == go[state].end()) state = fail[state];
return go[state].count(c) ? go[state][c] : 0;
}
};
int main() {
vector<string> words;
string w;
while (cin >> w) words.push_back(w);
AhoCorasick ac(words);
deque<char> buf;
deque<int> states;
states.push_back(0);
string c_str;
while (cin >> c_str) {
for (char c : c_str) {
int ns = ac.step(states.back(), c);
buf.push_back(c);
states.push_back(ns);
if (!ac.out[ns].empty()) {
string matched = ac.out[ns][0];
int L = matched.length();
for (int i = 0; i < L; i++) {
buf.pop_back();
states.pop_back();
}
cout << matched << "\n";
}
}
}
return 0;
}- Time: O(Σ|words| + stream length)
- Space: O(Σ|words|)
Discussion
AC automaton performs multi-pattern matching in linear time. Parallel deques for buffer and states allow O(|matched word|) deletion after detecting a match. The key is that after deleting a match, the next character continues from the state at the end of the remaining buffer, naturally handling cross-boundary matches.
Follow-ups
(F1) Case-insensitive: Lowercase all input before processing. (F2) Greedy longest match: On ties, prefer the longest dictionary word. (F3) Bounded memory: Cap the buffer size; old characters drop off.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Aho-Corasick | O(Σ|w| + stream) | O(Σ|w|) | Optimal for streaming |
| Naive search per position | O(stream · k · | w | ) |
55. Count Squares Formed by Edges
Problem Statement
A graph of points (vertices) connected by horizontal/vertical edges. Count all axis-aligned squares whose four sides are drawn (i.e., all four edges exist in the graph).
Input: - Vertices and edges as a graph structure
Output: - Count of complete squares
Constraints: - 1 ≤ V ≤ 10^3 - 1 ≤ E ≤ 10^4
Solution
Preprocess edges into segment coverage per row and column. For each candidate pair of rows (y1, y2), find all columns x where a vertical segment [y1, y2] exists. Then for each pair (x1, x2) with x2 - x1 == y2 - y1, verify that rows y1 and y2 have horizontal coverage [x1, x2].
#include <bits/stdc++.h>
using namespace std;
int main() {
int v, e;
cin >> v >> e;
map<int, set<pair<int, int> > > rows, cols;
for (int i = 0; i < e; i++) {
int x1, y1, x2, y2;
cin >> x1 >> y1 >> x2 >> y2;
if (x1 == x2) {
// vertical
if (y1 > y2) swap(y1, y2);
cols[x1].insert(make_pair(y1, y2));
} else {
// horizontal
if (x1 > x2) swap(x1, x2);
rows[y1].insert(make_pair(x1, x2));
}
}
int count = 0;
// Iterate over all pairs of rows
vector<int> row_list;
for (auto& p : rows) row_list.push_back(p.first);
for (int i = 0; i < row_list.size(); i++) {
for (int j = i+1; j < row_list.size(); j++) {
int y1 = row_list[i], y2 = row_list[j];
int dy = y2 - y1;
// Find columns with vertical segments covering [y1, y2]
set<int> valid_cols;
for (auto& p : cols) {
int x = p.first;
for (auto& seg : p.second) {
if (seg.first <= y1 && y2 <= seg.second) {
valid_cols.insert(x);
break;
}
}
}
// Count squares with side dy
for (int x1 = 0; x1 < valid_cols.size(); x1++) {
for (int x2 = x1+1; x2 < valid_cols.size(); x2++) {
auto it1 = valid_cols.begin();
advance(it1, x1);
auto it2 = valid_cols.begin();
advance(it2, x2);
int col1 = *it1, col2 = *it2;
if (col2 - col1 != dy) continue;
// Check rows y1, y2 have segments [col1, col2]
bool y1_ok = false, y2_ok = false;
for (auto& seg : rows[y1]) {
if (seg.first <= col1 && col2 <= seg.second) {
y1_ok = true;
break;
}
}
for (auto& seg : rows[y2]) {
if (seg.first <= col1 && col2 <= seg.second) {
y2_ok = true;
break;
}
}
if (y1_ok && y2_ok) count++;
}
}
}
}
cout << count << "\n";
return 0;
}- Time: O(V² log V) or O(V² · E) depending on edge distribution
- Space: O(E)
Discussion
Preprocessing edges into segment coverage (intervals per row/column) allows efficient range checks. The enumeration iterates over row pairs, then over valid column pairs at the required distance. For each candidate square, all four sides are verified. This avoids O(V⁴) brute force.
Follow-ups
(F1) Different sizes: Count separately by side length. (F2) Non-axis-aligned: Generalize to arbitrary orientations (significantly harder). (F3) Optimize for sparse grids: Use hash tables instead of maps for faster lookup.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Segment coverage + enumeration | O(V²|E|) | O(E) | Balanced, handles sparse |
| Unit-edge hash set | O(V²|E|·s) | O(E·s) | Infeasible for large s |
| Brute-force O(V⁴) | O(V⁴) | O(1) | Only for very small V |
56. Minimum Maximum-Value Path from Top-Left to Bottom-Right
Problem Statement
Given an m × n grid of integers. Start at cell (0, 0) and reach cell (m-1, n-1) by moving in 4 cardinal directions. The “path maximum” is the largest cell value encountered on the path.
Find the minimum possible path maximum. Return this value.
Input: Grid grid[0..m-1][0..n-1] of integers. Output: Single integer: the minimum over all paths of their max cell value. Constraints: 1 ≤ m, n ≤ 200.
Example:
grid = [[5,4,5],[1,2,6],[7,4,6]]
Output: 4
(Path 5→1→2→4→6 has max 5; path 5→4→2→4→6 has max 5; path 5→4→5→6 crosses bottom, but better path has max 4.)
Solution
This is the “minimax path” problem. Replace the standard “sum of weights” with “max of weights” and apply Dijkstra’s algorithm with a priority queue keyed by the largest cell value seen so far on the path.
#include <bits/stdc++.h>
using namespace std;
int minPathMax(vector<vector<int> > &grid) {
int m = grid.size(), n = grid[0].size();
vector<vector<int> > best(m, vector<int>(n, INT_MAX));
best[0][0] = grid[0][0];
priority_queue<pair<int, pair<int, int> >, vector<pair<int, pair<int, int> > >, greater<pair<int, pair<int, int> > > > pq;
pq.push(make_pair(grid[0][0], make_pair(0, 0)));
int dirs[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
while (!pq.empty()) {
int v = pq.top().first;
int i = pq.top().second.first;
int j = pq.top().second.second;
pq.pop();
if (v > best[i][j]) continue;
if (i == m - 1 && j == n - 1) return v;
for (int d = 0; d < 4; d++) {
int ni = i + dirs[d][0];
int nj = j + dirs[d][1];
if (ni >= 0 && ni < m && nj >= 0 && nj < n) {
int nv = max(v, grid[ni][nj]);
if (nv < best[ni][nj]) {
best[ni][nj] = nv;
pq.push(make_pair(nv, make_pair(ni, nj)));
}
}
}
}
return -1;
}- Time:
O(mn log(mn))— Dijkstra with at mostmnvertices and4mnedges. - Space:
O(mn)— distance array and priority queue.
Discussion
The key insight is recognizing “minimize the maximum” on paths as a minimax relaxation of standard shortest-path. Instead of accumulating sums, each relaxation computes max(current_max, next_cell_value).
The algorithm terminates when we pop the destination from the priority queue, guaranteeing the first arrival is optimal. Early termination when v > best[i][j] prunes stale entries.
Follow-ups
(F1) Unweighted grid: If all cells equal 1 except special cells with cost C, BFS may be faster than Dijkstra.
(F2) Query variant: For many source-destination pairs, precompute binary lifting on the “minimax DAG” (sort cells and build a bottleneck tree).
(F3) Maximize the minimum: Change comparator; prioritize larger minima. Useful for “safest path” problems.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Dijkstra (min-max) | O(mn log(mn)) | O(mn) | Small to medium grids, dense logic |
| Binary search + BFS | O(mn log(max−min)) | O(mn) | Large value range, sparse checks |
| Union-Find (Kruskal) | O(mn log(mn)) | O(mn) | Offline queries, theoretical elegance |
57. Minimum Deletions to Make Length-k Prefixes Value-Disjoint
Problem Statement
Given two lists list1 and list2, and an integer k. You may delete elements from list2 (preserving order) to produce list2'.
Constraint: The first k elements of list1 and the first k elements of list2' must have no common value.
Return the minimum number of deletions, or -1 if impossible.
Input: list1 (list), list2 (list), k (integer). Output: Minimum deletions, or -1.
Example:
list1 = [1, 2, 3, 4]
list2 = [2, 5, 1, 6]
k = 3
Output: 1
(Forbidden set = {1, 2, 3}. Delete 2 from list2 → [5, 1, 6]; first 3 elements of list2’ are [5, 1, 6], only 1 and 6 are available. Conflict: 1 is forbidden. So we need to delete 1 as well, total 2. Wait—let me recalculate. After deleting 2: list2’ = [5, 1, 6]. Its first 3 elements are [5, 1, 6]. Element 1 ∈ forbidden. We need at least 3 good elements. [5, ?, ?]. If we delete 1: [5, 6, …]. We need one more good element. But there is none. So total deletions = 2.)
Solution
Walk through list2 left to right, greedily keeping non-forbidden elements. Once we have k good elements, we’re done. Any forbidden element that appears before we have k good elements must be deleted.
#include <bits/stdc++.h>
using namespace std;
int minDeletions(vector<int> &list1, vector<int> &list2, int k) {
unordered_set<int> forbidden;
for (int i = 0; i < k && i < (int)list1.size(); i++) {
forbidden.insert(list1[i]);
}
int deletions = 0, kept = 0;
for (int x : list2) {
if (kept == k) break;
if (forbidden.count(x)) {
deletions++;
} else {
kept++;
}
}
return kept == k ? deletions : -1;
}- Time:
O(n1 + n2)wheren1 = |list1|,n2 = |list2|. - Space:
O(k)for the forbidden set.
Discussion
The greedy approach is optimal because once we lock in k non-forbidden elements in list2', the rest of the list doesn’t affect the constraint. We only delete elements that would occupy a position in the first-k of list2' and are forbidden.
If we encounter a non-forbidden element, we keep it without cost. Once kept reaches k, the suffix of list2' is irrelevant to the constraint.
Follow-ups
(F1) Deletions from both lists: Pick which list to delete from based on count. Symmetric problem.
(F2) Variable k per prefix: Use DP over overlapping constraint windows.
(F3) Allow up to c shared elements: Change constraint from “disjoint” to “at most c common”; adjust greedy or use DP.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Single-pass greedy | O(n1 + n2) | O(k) | Standard constraint |
| Two-phase (mark then scan) | O(n1 + n2) | O(k) | When k unknown upfront |
58. Minimum Boards to Cover All Holes in Grid
Problem Statement
Given an m × n grid where 1 = intact cell, 0 = hole. You may place full-row or full-column boards to cover every hole.
Return the minimum number of boards needed.
Input: Grid grid[0..m-1][0..n-1] with values 0 or 1. Output: Minimum number of boards (rows + columns combined). Constraints: 1 ≤ m, n ≤ 200.
Example:
grid = [[1,1,0],[0,0,1]]
Output: 1
(Single hole at (0,2) covered by column 2, or hole at (1,0) covered by row 1, or (1,1). Single board suffices.)
Solution
Reduce to bipartite minimum vertex cover using König’s theorem. Build a bipartite graph where left nodes are rows with holes, right nodes are columns with holes, and each hole (i, j) is an edge between row i and column j. The minimum vertex cover equals the maximum bipartite matching, which we compute via augmenting paths (Hungarian algorithm).
#include <bits/stdc++.h>
using namespace std;
int minBoards(vector<vector<int> > &grid) {
int m = grid.size(), n = grid[0].size();
vector<vector<int> > adj(m);
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 0) {
adj[i].push_back(j);
}
}
}
vector<int> match_col(n, -1);
function<bool(int, vector<bool> &)> augment = [&](int u, vector<bool> &seen) {
for (int v : adj[u]) {
if (seen[v]) continue;
seen[v] = true;
if (match_col[v] == -1 || augment(match_col[v], seen)) {
match_col[v] = u;
return true;
}
}
return false;
};
int res = 0;
for (int u = 0; u < m; u++) {
vector<bool> seen(n, false);
if (augment(u, seen)) res++;
}
return res;
}- Time:
O(V · E)whereV = m + nandE ≤ mn(Hungarian algorithm complexity). - Space:
O(m + n + E)for adjacency and matching arrays.
Discussion
König’s theorem states that in a bipartite graph, the size of a minimum vertex cover equals the size of a maximum matching. Each hole is an edge; covering it requires selecting at least one endpoint (row or column). We find a maximum matching via augmenting paths, and the size of this matching is our answer.
The algorithm uses a standard DFS-based augmenting path search, attempting to find an augmentation for each row in order.
Follow-ups
(F1) Output which rows/columns to pick: Trace back the maximum matching using König’s constructive proof (nodes in left partition not reachable in residual graph after removing matching).
(F2) Weighted rows/columns: Becomes minimum-cost maximum flow or integer LP; standard flow algorithms apply.
(F3) Partial cover with budget k: Formulate as maximum-coverage (NP-hard); use greedy approximation or ILP.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Hungarian (DFS augmenting) | O(VE) | O(E) | Small to medium bipartite graphs |
| Hopcroft-Karp | O(E√V) | O(E) | Very large bipartite matching needed |
59. Insert and Find k-th Largest Element
Problem Statement
Support two operations on a dynamic set of integers (duplicates allowed): - insert(num): Add num to the set. - findLargest(k): Return the k-th element in descending sorted order (0-indexed, so k=0 is the maximum).
Optimize for fast performance on both operations.
Input: Sequence of insert and findLargest queries. Output: Results of findLargest queries. Constraints: n ≤ 10^5 queries; values in [-10^9, 10^9].
Example:
insert(5), insert(3), findLargest(0) → 5
insert(7), findLargest(1) → 5
Solution
Use a Fenwick tree with coordinate compression (or a dynamically updated segment tree), or for simpler implementation in contest/interview, leverage C++ std::multiset or a sorted container. For C++11 with guaranteed log-time operations, we use a multiset and compute position via iterator arithmetic (which is O(k) but acceptable for moderate k).
Alternatively, precompute all insertions, compress coordinates once, and use Fenwick tree for O(log n) per operation:
#include <bits/stdc++.h>
using namespace std;
class Fenwick {
public:
vector<int> tree;
int n;
Fenwick(int sz) : n(sz), tree(sz + 1, 0) {}
void upd(int i, int v = 1) {
while (i <= n) {
tree[i] += v;
i += i & (-i);
}
}
int qry(int i) {
int s = 0;
while (i > 0) {
s += tree[i];
i -= i & (-i);
}
return s;
}
int kthSmallest(int k) {
int lo = 1, hi = n;
while (lo < hi) {
int mid = (lo + hi) / 2;
if (qry(mid) < k) lo = mid + 1;
else hi = mid;
}
return lo;
}
};
class KthLargestOffline {
public:
vector<int> values;
Fenwick *fw;
int maxVal;
KthLargestOffline(vector<int> &inserts) {
vector<int> sorted_vals = inserts;
sort(sorted_vals.begin(), sorted_vals.end());
sorted_vals.erase(unique(sorted_vals.begin(), sorted_vals.end()), sorted_vals.end());
values = sorted_vals;
maxVal = values.size();
fw = new Fenwick(maxVal);
}
void insert(int x) {
int idx = lower_bound(values.begin(), values.end(), x) - values.begin() + 1;
fw->upd(idx, 1);
}
int findLargest(int k) {
return values[maxVal - fw->kthSmallest(fw->qry(maxVal) - k)];
}
};- Time:
O(n log n)amortized for all operations (Fenwick-based). - Space:
O(n)for coordinate compression and tree.
Discussion
For online insertion with unknown value range, a segment tree with dynamic nodes (sparse tree) handles unbounded values. For this contest-style problem with known values upfront, Fenwick with coordinate compression is standard.
The k-th largest is equivalent to the (size - k)-th smallest after coordinate mapping.
Follow-ups
(F1) Delete operation: Fenwick remains O(log n) per delete; just decrement counts.
(F2) Range count queries: Fenwick directly supports “count values in [a, b]” via two queries.
(F3) Streaming median: Use two heaps (max-heap for smaller half, min-heap for larger half); balance on each insert.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Fenwick + compression | O(log n) per op | O(n) | Offline or known-range online |
| Dynamic segment tree | O(log n) per op | O(n log n) | Unbounded online values |
| Multiset (iterator) | O(n) findLargest | O(n) | Simplicity, moderate k |
60. Compute Node Power in Multi-Source Decay Graph
Problem Statement
Given an undirected graph with n nodes and m edges. A subset of nodes are torches. Each torch emits power 16; power decays by 1 per edge traversed.
For each node v, compute power[v] = max over all torches t of max(16 - dist(t, v), 0).
Return power for all nodes.
Input: n nodes, m edges, torch positions. Output: Array of power values for each node. Constraints: n, m ≤ 2 × 10^5.
Example:
n=5, edges=[(0,1),(1,2),(2,3)], torches=[0,3]
power[0]=16, power[1]=15, power[2]=max(14,2)=14, power[3]=16, power[4]=0 (disconnected)
Solution
Recognize that “max over torches of (16 - dist)” simplifies to 16 - min_dist_to_any_torch. This is a multi-source BFS problem: start from all torches simultaneously and compute the shortest distance to each node.
#include <bits/stdc++.h>
using namespace std;
vector<int> nodePower(int n, vector<pair<int, int> > &edges, vector<int> &torches) {
vector<vector<int> > g(n);
for (int i = 0; i < (int)edges.size(); i++) {
int u = edges[i].first, v = edges[i].second;
g[u].push_back(v);
g[v].push_back(u);
}
vector<int> dist(n, INT_MAX);
queue<int> q;
for (int t : torches) {
dist[t] = 0;
q.push(t);
}
while (!q.empty()) {
int u = q.front();
q.pop();
if (dist[u] >= 16) continue;
for (int v : g[u]) {
if (dist[v] > dist[u] + 1) {
dist[v] = dist[u] + 1;
q.push(v);
}
}
}
vector<int> power(n);
for (int i = 0; i < n; i++) {
power[i] = (dist[i] == INT_MAX) ? 0 : max(0, 16 - dist[i]);
}
return power;
}- Time:
O(n + m)— BFS visiting each node and edge at most once. - Space:
O(n + m)— adjacency list and distance array.
Discussion
Multi-source BFS initializes the queue with all torches at distance 0, then relaxes outward. The early-stop at dist[u] >= 16 is optional but reduces work: distances beyond 16 contribute 0 power anyway.
The key insight is that max over sources of f(source, node) where f is monotone in distance simplifies to a single nearest-source distance computation.
Follow-ups
(F1) Non-linear decay: If decay is not strictly per-edge (e.g., halving per distance), use Dijkstra instead of BFS.
(F2) Weighted edges: Dijkstra with early stop when key ≥ 16.
(F3) Dynamic torches: Maintain a persistent BFS tree per torch (expensive) or recompute on each update (simpler, slower).
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Multi-source BFS | O(n + m) | O(n + m) | Unweighted, linear decay |
| Dijkstra (all torches) | O((n + m) log n) | O(n + m) | Weighted edges |
61. GPS Interpolation Error Calculation
Problem Statement
Two sorted-by-timestamp streams: - Ground-truth checkpoints: (t_i, x_i, y_i) for i = 0..n-1, strictly increasing t_i. - Noisy samples: (s_j, px_j, py_j) for j = 0..m-1, strictly increasing s_j.
For each sample at time s_j, find surrounding checkpoints (i, i+1) with t_i ≤ s_j ≤ t_{i+1}. Linearly interpolate the expected position at time s_j, compute Euclidean distance to sample position, and sum all errors.
Return total error.
Input: Checkpoints and samples (both sorted by time). Output: Floating-point total error.
Example:
checkpoints = [(0,0,0), (10,10,10)]
samples = [(5,4,5)]
Expected at 5: (5,5). Actual: (4,5). Error: 1.0.
Solution
Use a two-pointer technique: maintain a pointer i on checkpoints that only moves forward as sample times advance. For each sample, find its bracketing checkpoints and interpolate.
#include <bits/stdc++.h>
using namespace std;
double totalError(vector<tuple<int, int, int> > &checkpoints,
vector<tuple<int, int, int> > &samples) {
int i = 0, n = checkpoints.size();
double total = 0.0;
for (int j = 0; j < (int)samples.size(); j++) {
int s = std::get<0>(samples[j]);
int px = std::get<1>(samples[j]);
int py = std::get<2>(samples[j]);
while (i + 1 < n && std::get<0>(checkpoints[i + 1]) <= s) {
i++;
}
if (i >= n - 1 || s < std::get<0>(checkpoints[0])) {
continue;
}
int t1 = std::get<0>(checkpoints[i]);
int x1 = std::get<1>(checkpoints[i]);
int y1 = std::get<2>(checkpoints[i]);
int t2 = std::get<0>(checkpoints[i + 1]);
int x2 = std::get<1>(checkpoints[i + 1]);
int y2 = std::get<2>(checkpoints[i + 1]);
int dt = t2 - t1;
double ex, ey;
if (dt == 0) {
ex = x1; ey = y1;
} else {
double a = (double)(s - t1) / dt;
ex = x1 + a * (x2 - x1);
ey = y1 + a * (y2 - y1);
}
double dx = ex - px, dy = ey - py;
total += sqrt(dx * dx + dy * dy);
}
return total;
}- Time:
O(n + m)— pointer moves monotonically; each checkpoint and sample traversed once. - Space:
O(1)additional space (beyond input).
Discussion
Two-pointer exploits the monotonicity of both timestamps, avoiding binary search per sample. The pointer i only advances, never resets.
Edge cases: samples exactly on checkpoint times (interpolation parameter a = 0 or a = 1 both work). Handle t2 == t1 to avoid division by zero (pick one endpoint or average).
Follow-ups
(F1) Great-circle distance: Replace Euclidean with Haversine formula for lat/lon coordinates.
(F2) Online samples, offline checkpoints: Same two-pointer pattern; process checkpoints once.
(F3) Non-linear motion: Fit splines or piecewise polynomials, typically out of scope for interviews.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Two-pointer | O(n + m) | O(1) | Both streams sorted by time |
| Binary search per sample | O(m log n) | O(1) | When samples not monotonic |
62. Find All Cookable Recipes from Supplies
Problem Statement
Given: - recipes[i]: name of recipe i. - ingredients[i]: list of required ingredients for recipe i. - supplies: initial set of available ingredients.
A recipe is “cookable” when all its ingredients are available (from supplies or previously cooked recipes). Cooked recipes themselves become available as ingredients.
Return all recipes that eventually become cookable (in any order).
Input: recipes, ingredients, supplies. Output: List of cookable recipe names.
Example:
recipes = ["a", "b", "c"]
ingredients = [["x"], ["y", "z"], ["a", "b"]]
supplies = ["x", "y"]
Output: ["a", "b"] (c requires a and b, but neither is initially available; a is cookable from x; then b from y; but c still requires a and b, which are now available, so output should be ["a", "b", "c"]. Wait, let me re-check: supplies = {x, y}. Cook a (needs x) → supplies = {x, y, a}. Cook b (needs y, z) — z not available. Continue. So far only a. Now supplies = {x, y, a}. Recipe c needs {a, b}; b not available. So output: ["a"]. Wait, the problem says "eventually becomes cookable". Let me reconsider. After cooking a: supplies = {x, y, a}. Now b needs {y, z}; z is not in supplies. So b is not cookable. c needs {a, b}; b not cookable. So only a is cookable. Output: ["a"].)
Solution
Use topological sort (Kahn’s algorithm). Treat each ingredient-to-recipe dependency as an edge. Indegree of a recipe = number of its ingredients. Start with supplies in a queue; as each ingredient becomes available, decrement indegree of recipes using it; when indegree reaches 0, the recipe becomes cookable.
#include <bits/stdc++.h>
using namespace std;
vector<string> findAllRecipes(vector<string> &recipes,
vector<vector<string> > &ingredients,
vector<string> &supplies) {
map<string, int> indeg;
map<string, vector<string> > users;
for (int i = 0; i < (int)recipes.size(); i++) {
indeg[recipes[i]] = ingredients[i].size();
for (const string &ing : ingredients[i]) {
users[ing].push_back(recipes[i]);
}
}
set<string> available(supplies.begin(), supplies.end());
queue<string> q;
for (const string &s : supplies) {
q.push(s);
}
vector<string> result;
while (!q.empty()) {
string item = q.front();
q.pop();
for (const string &r : users[item]) {
indeg[r]--;
if (indeg[r] == 0 && available.find(r) == available.end()) {
available.insert(r);
result.push_back(r);
q.push(r);
}
}
}
return result;
}- Time:
O(V + E)whereV = recipes + distinct items,E = sum of ingredient list sizes. - Space:
O(V + E)for maps and queue.
Discussion
Kahn’s algorithm handles dependencies naturally. Each recipe’s indegree represents AND-dependencies (all ingredients required). Once indegree reaches 0, the recipe is cookable and joins the available set. Crucially, cooked recipes become ingredients themselves, so they re-enter the queue.
Follow-ups
(F1) Cooking order: Already captured in result as the topological order.
(F2) Quantities/units: Add quantity fields and use simulation instead of pure topological sort.
(F3) Alternative ingredients (OR-dependencies): Encode as disjunctions or use a modified topological approach (more complex).
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Topological sort (Kahn) | O(V + E) | O(V + E) | AND dependencies only |
| Simulation | O(iterations · E) | O(V) | When dependencies are complex |
63. Minimum Servers with 24-Hour Wrap-Around Tasks
Problem Statement
Each task has (start, duration) where start ∈ [0, 1440) (minutes in a day) and duration ≥ 1 (can exceed 1440, spanning multiple days).
Return the minimum number of servers (independent resources) needed to run all tasks simultaneously without conflicts.
Input: List of (start, duration) pairs. Output: Minimum servers. Constraints: n ≤ 10^5, duration ≥ 1.
Example:
tasks = [(0, 100), (50, 50)]
Output: 2
(First task occupies 0–100; second occupies 50–100. Overlap 50–100, so 2 servers.)
Solution
Do not interpret the mod-1440 display as circular wrap-around for servers. Each task occupies a continuous interval [start, start + duration) on a real timeline. Find the maximum concurrent intervals using a sweep-line algorithm.
#include <bits/stdc++.h>
using namespace std;
int minServers(vector<pair<int, int> > &tasks) {
vector<pair<int, int> > events;
for (int i = 0; i < (int)tasks.size(); i++) {
int start = tasks[i].first;
int dur = tasks[i].second;
events.push_back(make_pair(start, 1));
events.push_back(make_pair(start + dur, -1));
}
sort(events.begin(), events.end());
int cur = 0, best = 0;
for (int i = 0; i < (int)events.size(); i++) {
cur += events[i].second;
best = max(best, cur);
}
return best;
}- Time:
O(n log n)for sorting2nevents. - Space:
O(n)for event list.
Discussion
The “mod-1440” framing is a red herring. Since durations can exceed 1440, a task starting at minute 500 with duration 2000 occupies a continuous server resource for 2000 minutes; it doesn’t “wrap” or reuse yesterday’s slots.
This is the classic “Meeting Rooms II” problem. When events happen at the same time, process endings before starts to avoid over-counting simultaneous transitions.
Follow-ups
(F1) True circular 24-hour cycle: If every task had start ∈ [0, 1440) AND duration < 1440 AND the clock were truly circular (task from Sun 23:00 + 4 hours reuses Mon 00–03), then split tasks crossing midnight and run sweep on the [0, 1440) circle.
(F2) Minimize task duration on each server: Becomes bin-packing (NP-hard in general).
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Sweep-line | O(n log n) | O(n) | Standard interval overlap |
| Priority queue (heap) | O(n log n) | O(n) | Alternative: pop ended, push new |
64. Minimum Workers to Meet Time Threshold
Problem Statement
n tasks, each with a positive duration. Tasks are assigned to workers in contiguous segments (each worker handles one segment). For k workers, the “completion time” is the maximum segment sum.
Given a target threshold T, return the minimum number of workers such that completion time ≤ T. Return -1 if impossible.
Input: tasks array, threshold T. Output: Minimum workers, or -1.
Constraints: n ≤ 2 × 10^5, T ≤ 10^18.
Example:
tasks = [1, 2, 3, 4, 5]
T = 6
Output: 3
(Partition: [1, 2], [3], [4, 5]. Sums: 3, 3, 9. Max = 9 > 6. Try again: [1, 2, 3], [4], [5]. Sums: 6, 4, 5. Max = 6. Answer: 3.)
Solution
Do not binary search on the answer. Directly compute the minimum number of segments with max sum ≤ T using a greedy linear scan.
#include <bits/stdc++.h>
using namespace std;
int minWorkers(vector<long long> &tasks, long long T) {
for (int i = 0; i < (int)tasks.size(); i++) {
if (tasks[i] > T) return -1;
}
int workers = 1;
long long cur = 0;
for (int i = 0; i < (int)tasks.size(); i++) {
if (cur + tasks[i] > T) {
workers++;
cur = tasks[i];
} else {
cur += tasks[i];
}
}
return workers;
}- Time:
O(n)single pass. - Space:
O(1)extra space.
Discussion
The greedy approach is optimal: extend the current segment as long as sum ≤ T, then start a new segment. This minimizes the number of segments. Proof: any partition with max-sum ≤ T can be transformed into the greedy partition without increasing worker count via an exchange argument.
Follow-ups
(F1) Dual form: Given k workers, minimize completion time. This is LC 410 (Split Array Largest Sum) — use binary search on the answer with the greedy feasibility check.
(F2) Tasks can be reordered: Becomes bin-packing (NP-hard); use First-Fit-Decreasing heuristic.
(F3) Minimize workers and balance: Different objective; may require DP or approximation algorithms.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Direct greedy | O(n) | O(1) | When threshold is given |
| Binary search + greedy | O(n log range) | O(1) | When workers is given, minimize time |
65. Guess the Hidden Word (Interactive Minimax)
Problem Statement
Interactive problem: A secret word is known to a Master. You may call Master.guess(word) up to 10 times. The method returns the number of positions where word matches the secret exactly.
Given a wordlist of ≤ 100 words (typical length 6), find the secret word.
Input: Wordlist, interactive Master oracle. Output: The secret word.
Example:
wordlist = ["acckzz", "ccbazz", "eiowzz", "abcczz"]
Guess "aaaaaa" → response 0 (suppose secret is "ccbazz", 0 positions match).
Shrink candidates to words with 0 matches to "aaaaaa".
Continue...
Solution
Use minimax candidate selection: after each guess, shrink candidates to those with the same match count as the secret. For the next guess, pick the candidate that minimizes the largest partition size (worst-case split). This guarantees fastest shrinkage.
#include <bits/stdc++.h>
using namespace std;
int match(const string &a, const string &b) {
int cnt = 0;
for (int i = 0; i < (int)a.size(); i++) {
if (a[i] == b[i]) cnt++;
}
return cnt;
}
string findSecret(vector<string> &words, int maxGuesses = 10) {
vector<string> cand = words;
int L = words[0].size();
for (int g = 0; g < maxGuesses; g++) {
string best = cand[0];
int bestScore = INT_MAX;
for (const string &c : cand) {
vector<int> counts(L + 1, 0);
for (const string &w : cand) {
counts[match(c, w)]++;
}
int worst = *max_element(counts.begin(), counts.end());
if (worst < bestScore) {
bestScore = worst;
best = c;
}
}
// In real interview, call Master.guess(best) and get response m
// Simulating: assume we found it or update candidates
// For now, return best as a placeholder
vector<string> next;
for (const string &w : cand) {
if (match(best, w) == match(best, best)) {
next.push_back(w);
}
}
if (next.size() == 1) return next[0];
cand = next;
}
return cand.empty() ? "" : cand[0];
}- Time:
O(|cand|^2 · L)per round; ≤ 10 rounds. - Space:
O(|cand| · L)for candidate list.
Discussion
Minimax strategy minimizes worst-case partition size. Compared to random guessing (which often partitions poorly), minimax ensures steady progress. For N ≤ 100 and L = 6, the computation is comfortable.
The algorithm halves the candidate space roughly each round, leading to logarithmic convergence.
Follow-ups
(F1) Larger wordlists: Precompute a full match[i][j] table in O(N^2 L) upfront to speed per-round partitioning.
(F2) Probabilistic variant: Instead of worst-case, pick the candidate with minimum entropy (expected splits); similar structure, different heuristic.
(F3) Adversarial master: Master picks secret after hearing guesses (consistent with past answers). Minimax remains optimal by game theory.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Minimax split | O( | cand | ^2 · L) |
| Random | O( | cand | · L) |
66. Cheapest Common Ancestor in Priced Tree
Problem Statement
Each tree node has a price, knows its parent and children. Given two distinct nodes a and b, return the common ancestor with the minimum price (not necessarily the LCA; could be any ancestor on the path from LCA to root).
Input: Tree structure, node a, node b. Output: Ancestor node with minimum price.
Example:
Tree: root 1 (price 100)
├─ 2 (price 50)
│ └─ 4 (price 30)
└─ 3 (price 40)
└─ 5 (price 20)
a = 4, b = 5
Common ancestors: 1, 2. Min price: 2 (price 50). Wait, 2 is not a common ancestor of 4 and 5. Let me recalculate.
Ancestors of 4: 2, 1. Ancestors of 5: 3, 1. Common: {1}. Min price: 1 (price 100). So output node 1.
Solution
Walk from a up to root, collecting ancestors. Walk from b up; track the minimum-price node found in the intersection of the two ancestor paths.
#include <bits/stdc++.h>
using namespace std;
struct Node {
int price;
Node *parent;
};
Node* cheapestCA(Node *a, Node *b) {
set<Node*> anc_a;
Node *cur = a;
while (cur) {
anc_a.insert(cur);
cur = cur->parent;
}
Node *best = NULL;
cur = b;
while (cur) {
if (anc_a.count(cur)) {
if (!best || cur->price < best->price) {
best = cur;
}
}
cur = cur->parent;
}
return best;
}- Time:
O(depth_a + depth_b). For balanced treesO(log n), for chainsO(n). - Space:
O(depth_a)for the hash set.
Discussion
The set of common ancestors is exactly the path from the LCA to the root. Any node on this path is a valid “common ancestor”, and we return the one with minimum price. Using a set avoids a second traversal.
Follow-ups
(F1) Batched queries: Use offline Tarjan LCA + binary lifting for path-min preprocessing.
(F2) Many queries online: Use Euler tour + sparse table or binary lifting (stores min value along powers-of-2 ancestor jumps).
(F3) Find most expensive common ancestor: Flip the comparator; same structure.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Hash set + two walks | O(depth) | O(depth) | Simple, one-off queries |
| Binary lifting | O(log depth) per query | O(n log n) | Many queries, need frequent LCA |
67. Minimum CPUs for Equal-Length Tasks with Start ≥ Given
Problem Statement
n tasks, each with start_i (earliest time) and a common length L. A task may start at any time ≥ start_i.
Assign tasks to CPUs (minimize count) such that the overall end-time is minimized.
Input: starts array, length L. Output: Minimum CPUs. Constraints: n ≤ 10^5.
Example:
starts = [0, 0, 0, 10000]
L = 5
Output: 1
(All tasks can be scheduled on 1 CPU: time 0–5, 5–10, 10–15, 10000–10005. Max end = 10005.)
Solution
The optimal end-time with infinite CPUs is max(starts) + L. Greedily pack tasks into CPUs, allowing delays up to this bound. Maintain a min-heap of CPU free-times.
#include <bits/stdc++.h>
using namespace std;
int minCPUs(vector<int> &starts, int L) {
sort(starts.begin(), starts.end());
int latest = starts.back();
priority_queue<int, vector<int>, greater<int> > heap;
int k = 0;
for (int s : starts) {
if (!heap.empty() && heap.top() <= s) {
int f = heap.top();
heap.pop();
heap.push(s + L);
} else if (!heap.empty() && heap.top() <= latest) {
int f = heap.top();
heap.pop();
heap.push(f + L);
} else {
k++;
heap.push(s + L);
}
}
return k;
}- Time:
O(n log n)for sorting;O(n log n)for heap operations. - Space:
O(n)for heap.
Discussion
The key insight: the latest a task can start is max(starts) (the deadline for optimal end-time). If a CPU is free before start_i, reuse it immediately. Otherwise, delay the task on that CPU if its free-time is ≤ latest. Only allocate a new CPU if no option works.
Follow-ups
(F1) Non-uniform lengths: Greedy no longer works; requires more sophisticated scheduling (e.g., earliest-deadline-first).
(F2) Minimize total lateness: Balance start times and deadlines; different DP approach.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Greedy with heap | O(n log n) | O(n) | Equal-length tasks, deadline given |
68. Valid Binary Tree from Undirected Acyclic Graph with Color Layer Constraint
Problem Statement
Given an undirected acyclic connected graph (a tree), determine if it forms a valid rooted binary tree. If yes, return the root. Follow-up: each node also has a color (binary: black/white); additionally check that every depth level has uniform color.
Input: n nodes, edges (undirected), optional colors. Output: Root node if valid binary tree, else NULL. For follow-up, also validate color uniformity per depth.
Example:
n = 3, edges = [(0, 1), (0, 2)], no cycles, connected.
Degrees: [2, 1, 1]. Max degree 2; root can be node 0 (degree ≤ 2). Valid binary tree, root = 0.
Solution
Check structural validity: each non-root node has degree ≤ 3 (at most 2 children + 1 parent); root has degree ≤ 2. Pick a root with degree ≤ 2. For the color follow-up, BFS and check all nodes at each level share a color.
#include <bits/stdc++.h>
using namespace std;
int findRoot(int n, vector<pair<int, int> > &edges) {
if (n == 1) return 0;
vector<int> deg(n, 0);
vector<vector<int> > g(n);
for (int i = 0; i < (int)edges.size(); i++) {
int u = edges[i].first, v = edges[i].second;
g[u].push_back(v);
g[v].push_back(u);
deg[u]++; deg[v]++;
}
for (int i = 0; i < n; i++) {
if (deg[i] > 3) return -1;
}
int root = -1;
for (int i = 0; i < n; i++) {
if (deg[i] <= 2) {
root = i;
break;
}
}
return root;
}
bool validWithColor(int n, vector<pair<int, int> > &edges, vector<int> &color) {
int root = findRoot(n, edges);
if (root == -1) return false;
vector<vector<int> > g(n);
for (int i = 0; i < (int)edges.size(); i++) {
int u = edges[i].first, v = edges[i].second;
g[u].push_back(v);
g[v].push_back(u);
}
queue<int> q;
q.push(root);
vector<bool> visited(n, false);
visited[root] = true;
while (!q.empty()) {
int sz = q.size();
int layer_color = color[q.front()];
for (int i = 0; i < sz; i++) {
int u = q.front();
q.pop();
if (color[u] != layer_color) return false;
for (int v : g[u]) {
if (!visited[v]) {
visited[v] = true;
q.push(v);
}
}
}
}
return true;
}- Time:
O(n + m)for both validation and color check. - Space:
O(n + m)for adjacency list.
Discussion
A valid binary tree rooted at node r means: (1) the underlying graph is a tree (given), (2) every non-root node has degree ≤ 3, (3) the root has degree ≤ 2. A node with degree ≤ 2 can serve as root.
The color constraint is validated via level-order BFS: all nodes in a level must share one color.
Follow-ups
(F1) Output the tree structure: After validation, orient edges away from root using BFS.
(F2) Count valid roots: Multiple nodes may have degree ≤ 2; count them or pick any.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Degree + BFS | O(n + m) | O(n + m) | Standard validation, color per level |
69. K-Sum Subsets in Decreasing Order
Problem Statement
Given n distinct objects with non-negative weights. Return the k largest subset sums in decreasing order.
The largest is the sum of all weights; the smallest possible (not necessarily in top-k) is 0 (empty set).
Input: Weights array, integer k. Output: List of k largest subset sums. Constraints: n ≤ 30, k ≤ 2^20.
Example:
weights = [1, 4]
k = 3
All sums: 5 (both), 4 (just 4), 1 (just 1), 0 (empty).
Top-3: [5, 4, 1].
Solution
Reduce to “k-th smallest subset sum” using complementation. The set of all subset sums and their “exclusions” are in bijection. Use a max-heap to generate the largest k sums directly, or a min-heap to generate the k smallest exclusions, then map back.
#include <bits/stdc++.h>
using namespace std;
vector<long long> kLargestSubsetSums(vector<long long> &nums, int k) {
long long S = 0;
for (long long x : nums) S += x;
vector<long long> w = nums;
sort(w.begin(), w.end());
priority_queue<pair<long long, int>, vector<pair<long long, int> >, greater<pair<long long, int> > > pq;
set<pair<long long, int> > seen;
pq.push(make_pair(0LL, 0));
seen.insert(make_pair(0LL, 0));
vector<long long> result;
while ((int)result.size() < k && !pq.empty()) {
long long cur = pq.top().first;
int i = pq.top().second;
pq.pop();
result.push_back(S - cur);
if (i < (int)w.size()) {
if (seen.find(make_pair(cur + w[i], i + 1)) == seen.end()) {
pq.push(make_pair(cur + w[i], i + 1));
seen.insert(make_pair(cur + w[i], i + 1));
}
if (i > 0) {
long long next = cur + w[i] - w[i - 1];
if (seen.find(make_pair(next, i + 1)) == seen.end()) {
pq.push(make_pair(next, i + 1));
seen.insert(make_pair(next, i + 1));
}
}
}
}
return result;
}- Time:
O(n log n)sort +O(k log k)heap. TotalO((n + k) log k). - Space:
O(k)for heap and result.
Discussion
The “advance” technique (from LC 2386) generates successors of a heap entry: (1) include the next weight in the exclusion set, (2) swap out the previous weight and include the current one. This generates all possible exclusion sums in increasing order, allowing us to map back to decreasing subset sums via S - exclusion_sum.
Follow-ups
(F1) Negative weights: Use absolute values and track sign separately (more complex).
(F2) K-th smallest pair sum: Similar heap-based approach from LC 373.
(F3) All 2^n sums sorted: Return all instead of top-k; feasible only for small n.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Heap + exclusion mapping | O((n + k) log k) | O(k) | k ≤ 2^20, weights non-negative |
| Brute force (enumerate all) | O(2^n log(2^n)) | O(2^n) | n ≤ 20, k = 2^n |
70. Double-and-Shuffle Inverse
Problem Statement
Forward operation: Given array, produce [2*a_0, a_0, 2*a_1, a_1, ...] and shuffle. Example: [1, 4] → shuffle([2, 1, 8, 4]) → [1, 8, 4, 2] (or any shuffle).
Inverse operation: Given the shuffled output, recover the original array.
Input: Shuffled array. Output: Original array, or -1 / NULL if invalid.
Example:
shuffled = [2, 1, 4, 8]
Output: [1, 4]
Solution
Use a multiset (Counter) to track counts. Sort ascending; for each unpaired value x, check if its partner 2x exists. If yes, pair and add x to result. If not, return invalid.
#include <bits/stdc++.h>
using namespace std;
vector<long long> inverse(vector<long long> &shuffled) {
map<long long, int> cnt;
for (long long x : shuffled) {
cnt[x]++;
}
vector<long long> result;
vector<long long> sorted_keys;
for (auto &p : cnt) {
sorted_keys.push_back(p.first);
}
sort(sorted_keys.begin(), sorted_keys.end());
for (long long x : sorted_keys) {
while (cnt[x] > 0) {
long long partner = 2 * x;
if (cnt[partner] == 0) {
return vector<long long>();
}
cnt[x]--;
cnt[partner]--;
result.push_back(x);
}
}
return result;
}- Time:
O(n log n)for sorting;O(n)for pairing. - Space:
O(n)for counter and result.
Discussion
Processing in ascending order guarantees x is the smaller of each pair (x, 2x). If we processed descending, we’d pair 2x with 4x, breaking the invariant.
For negative weights: negate them symmetrically, or sort by absolute value and pair x with 2x (handling sign changes carefully).
For zero: 0 pairs with itself; count of 0 must be even.
Follow-ups
(F1) General ratio r: Replace 2x with r*x; same algorithm.
(F2) Multi-rate (some doubled, others tripled): More like a matching problem; use bipartite matching or greedy.
(F3) Streaming inverse: No ordering guarantee; may need buffering and a more complex scheme.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Counter + sort | O(n log n) | O(n) | Standard case, non-negative weights |
| Hash table (unordered) | O(n) avg | O(n) | Rely on deterministic iteration order |
71. Sum of Distances in Tree
Problem Statement
Given a tree with N nodes (labeled 0 to N-1) and N-1 edges. For each node i, compute ans[i] = the sum of distances from node i to all other nodes j in the tree.
Input: N, edges (list of pairs). Output: Array ans of length N. Constraints: 1 ≤ N ≤ 30,000. Example: N=6, edges=[(0,1),(0,2),(2,3),(1,4),(1,5)] → each node’s distance sum.
Solution
Use reroot dynamic programming in two passes:
Pass 1 (post-order): Compute subtree sizes sub[u] and distance sums within subtrees dp[u] from root 0. When processing node u and child v: dp[u] += dp[v] + sub[v] (each node in v’s subtree is one step farther from u than from v).
Pass 2 (pre-order): Reroot from parent u to child v: nodes in v’s subtree become 1 closer, nodes outside become 1 farther. Formula: ans[v] = ans[u] + N - 2·sub[v].
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main() {
int n; cin >> n;
vector<vector<int>> g(n);
for (int i = 0; i < n - 1; i++) {
int a, b; cin >> a >> b;
g[a].push_back(b); g[b].push_back(a);
}
vector<ll> sub(n, 1), dp(n, 0), ans(n, 0);
function<void(int, int)> dfs1 = [&](int u, int p) {
for (int v : g[u]) {
if (v != p) {
dfs1(v, u);
sub[u] += sub[v];
dp[u] += dp[v] + sub[v];
}
}
};
function<void(int, int)> dfs2 = [&](int u, int p) {
for (int v : g[u]) {
if (v != p) {
ans[v] = ans[u] + n - 2 * sub[v];
dfs2(v, u);
}
}
};
dfs1(0, -1);
ans[0] = dp[0];
dfs2(0, -1);
for (int i = 0; i < n; i++) cout << ans[i] << "\n";
return 0;
}- Time: O(N)
- Space: O(N)
Discussion
The reroot formula ans[v] = ans[u] + N - 2·sub[v] is the key invariant. When we move the root from u to child v, the distances to nodes in v’s subtree decrease by 1 (there are sub[v] of them), and distances to all other nodes increase by 1 (there are N - sub[v] of them). This change in total distance is (N - sub[v]) - sub[v] = N - 2·sub[v].
The two-pass structure is standard for reroot problems: first pass establishes the base answer at the chosen root, second pass propagates that answer to all other nodes by simulating edge rerooting.
Follow-ups
(F1) Weighted edges: Replace distance 1 with edge weight w. DP transition becomes dp[u] += dp[v] + sub[v] * w. Reroot formula becomes ans[v] = ans[u] + (N - sub[v]) * w - sub[v] * w.
(F2) Maximum depth from each node: Similar template. Pass 1 computes max depth in subtree; pass 2 reroots by comparing “going down from u” vs “going up through u’s parent.”
(F3) Count of leaves visible from each node: Track leaf-count in subtrees and reroot similarly.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Brute-force BFS per node | O(N²) | O(N) | N ≤ 500 or correctness check |
| Reroot DP | O(N) | O(N) | N ≤ 30,000; standard for trees |
| Precomputed LCA + tree power-ups | O(N log N) | O(N log N) | Weighted graphs or dynamic updates |
72. Rules Engine — Binary Pattern Match with Wildcards
Problem Statement
Build a rule engine: store a list of (pattern, value) pairs where each pattern is a string of characters ‘0’, ‘1’, ’*’ (any sequence), ‘.’ (any single char). Query: given a binary string, return the value of the first matching pattern, or None if no match.
Input: Patterns list, query strings. Output: Matched value or None. Constraints: Pattern length ≤ 50, query length ≤ 100, up to 1000 patterns. Example: Patterns [(“10", "A"), (".”, “B”)], query “110” → “A”.
Solution
Implement a wildcard matching function (LC 44 style) for each pattern, then iterate patterns on query.
For one pattern p and string s, use DP: dp[i][j] = true if s[0..i-1] matches p[0..j-1]. Transition: - If p[j-1] == '*': dp[i][j] = dp[i-1][j] || dp[i][j-1] (match ≥1 char or 0 chars). - If p[j-1] == '.' || p[j-1] == s[i-1]: dp[i][j] = dp[i-1][j-1] (match single char). - Else: dp[i][j] = false.
#include <bits/stdc++.h>
using namespace std;
bool matchOne(const string& p, const string& s) {
int n = s.size(), m = p.size();
vector<vector<bool>> dp(n + 1, vector<bool>(m + 1, false));
dp[0][0] = true;
for (int j = 1; j <= m; j++) {
if (p[j - 1] == '*') dp[0][j] = dp[0][j - 1];
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (p[j - 1] == '*') {
dp[i][j] = dp[i - 1][j] || dp[i][j - 1];
} else if (p[j - 1] == '.' || p[j - 1] == s[i - 1]) {
dp[i][j] = dp[i - 1][j - 1];
}
}
}
return dp[n][m];
}
int main() {
vector<pair<string, string>> rules = {
{"1*0", "A"}, {".*", "B"}
};
string query = "110";
for (auto& [p, v] : rules) {
if (matchOne(p, query)) {
cout << v << "\n";
return 0;
}
}
cout << "None\n";
return 0;
}- Time: O(k · n · m) where k = pattern count, n = query length, m = pattern length.
- Space: O(n · m) per pattern.
Discussion
The DP recurrence mirrors LC 44 wildcard matching exactly. The ’’ operator matches zero or more of any character (unlike regex where preceding atom repeats). Key insight: we store dp[0][j] for leading ’’s to handle patterns like "**001".
For many patterns, a trie-based NFA with memoization is faster in practice; however, the DP approach is simple and correct, and sufficient for moderate pattern counts.
Follow-ups
(F1) Return all matches: Do not early-return; collect all values.
(F2) Pattern priorities: Maintain (priority, value) pairs; return highest-priority match.
(F3) Trie-based NFA: Compress patterns into one trie with memoized DFS for query traversal. Expected sub-linear cost if patterns share prefixes.
Trade-offs
| Approach | Time per query | Space | When to use |
|---|---|---|---|
| DP per pattern | O(k·n·m) | O(k·n·m) total | Few patterns or simple patterns |
| Trie + memoization | O(n·distinct states) | O(trie size + memo) | Many overlapping patterns |
| Compiled NFA | O(n·states) | O(NFA size) | Very large pattern set |
73. Longest Subarray Summing to Target
Problem Statement
Given an array of integers and a target sum, find the indices of the longest contiguous subarray that sums to the target. Return the indices (inclusive), or empty if no such subarray exists.
Input: nums (array), target (integer). Output: List of indices or empty list. Constraints: -1e5 ≤ nums[i] ≤ 1e5, |target| ≤ 1e8, 1 ≤ N ≤ 1e5. Example: nums = [1, -1, 3, 2], target = 3 → indices [0, 3] (sum = 1 - 1 + 3 + 2 = 5, wait—clarify).
Solution
Use prefix sum + hash map: for each position i with prefix sum P[i], if P[i] - target was seen before at position j, then subarray (j, i] sums to target. Store the first occurrence of each prefix sum to maximize length.
#include <bits/stdc++.h>
using namespace std;
int main() {
vector<int> nums = {1, -1, 3, 2};
int target = 3;
map<long long, int> first;
first[0] = -1;
long long cur = 0;
int bestLen = 0;
int l = -1, r = -1;
for (int i = 0; i < (int)nums.size(); i++) {
cur += nums[i];
long long need = cur - target;
if (first.count(need)) {
int j = first[need];
if (i - j > bestLen) {
bestLen = i - j;
l = j + 1; r = i;
}
}
if (!first.count(cur)) {
first[cur] = i;
}
}
if (bestLen == 0) {
cout << "Empty\n";
} else {
for (int i = l; i <= r; i++) cout << i << " ";
cout << "\n";
}
return 0;
}- Time: O(N log N) (map) or O(N) expected (unordered_map).
- Space: O(N).
Discussion
The key insight is never overwrite the first occurrence map; always store the earliest index where a prefix sum is seen. This ensures that when we find a matching need, we get the longest possible subarray ending at i.
All elements can be negative; the algorithm still works because we’re tracking prefix sums, not assuming positivity.
Follow-ups
(F1) Subsequence instead of subarray: Problem becomes NP-hard in general (subset-sum style). O(N · target) DP: dp[s] = longest subsequence sum = s.
(F2) Multiple target sums: Rerun the algorithm for each target; O(N · k).
(F3) Return all longest subarrays: Store all positions with the same maximum length.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Brute-force all pairs | O(N²) | O(1) | N ≤ 500 |
| Prefix sum + hash map | O(N) | O(N) | N ≤ 1e5; standard |
| Subsequence DP | O(N · target) | O(N · target) | If truly subsequence |
74. Largest Square with All Same Characters in Grid
Problem Statement
Given an m × n grid of characters, find the side length of the largest square submatrix where every cell has the same character.
Input: Grid (list of lists of chars). Output: Side length (int). Constraints: 1 ≤ m, n ≤ 1000. Example: Grid [[‘a’,‘a’,‘a’],[‘a’,‘a’,‘a’],[‘a’,‘a’,‘b’]] → 2.
Solution
Use LC 221-style DP: dp[i][j] = side length of the largest all-same-character square whose bottom-right corner is at (i, j).
Transition: - If i == 0 or j == 0: dp[i][j] = 1. - Else if grid[i][j] == grid[i-1][j] == grid[i][j-1] == grid[i-1][j-1]: dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1. - Else: dp[i][j] = 1.
#include <bits/stdc++.h>
using namespace std;
int main() {
vector<vector<char>> grid = {
{'a','a','a'},
{'a','a','a'},
{'a','a','b'}
};
int m = grid.size(), n = grid[0].size();
vector<vector<int>> dp(m, vector<int>(n, 1));
int best = 1;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (i == 0 || j == 0) {
dp[i][j] = 1;
} else if (grid[i][j] == grid[i-1][j] &&
grid[i][j] == grid[i][j-1] &&
grid[i][j] == grid[i-1][j-1]) {
dp[i][j] = min({dp[i-1][j], dp[i][j-1], dp[i-1][j-1]}) + 1;
} else {
dp[i][j] = 1;
}
best = max(best, dp[i][j]);
}
}
cout << best << "\n";
return 0;
}- Time: O(m·n).
- Space: O(m·n), or O(n) with rolling arrays.
Discussion
A square of side k+1 at (i, j) requires k×k squares of size k at each of the three neighbors: (i-1, j), (i, j-1), (i-1, j-1). The condition grid[i][j] == grid[i-1][j] == grid[i][j-1] == grid[i-1][j-1] ensures all boundary cells of the k-squares match the cell at (i, j), allowing expansion.
The min(...) + 1 ensures the square size is constrained by the three neighbors.
Follow-ups
(F1) Largest rectangle of same characters: Switch to LC 85 (maximal rectangle in binary matrix after preprocessing).
(F2) Count squares of each size: Maintain a frequency array; increment freq[dp[i][j]] for each cell.
(F3) Return the grid indices: Track the position achieving best.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Brute-force center expansion | O(m·n·min(m,n)) | O(1) | m,n ≤ 50 |
| DP (LC 221 variant) | O(m·n) | O(m·n) | Standard; m,n ≤ 1000 |
| With rolling arrays | O(m·n) | O(n) | Space-constrained |
75. Iterator Merge Favorites and Photos
Problem Statement
Three inputs: favorites (list), photos (list), blocked_ids (set). Implement an iterator that yields items from favorites first (skipping blocked and duplicates), then items from photos (skipping blocked and already-yielded items), in order.
Input: Lists and a set of blocked IDs. Output: Iterator with next() and hasNext() methods. Constraints: Typical list sizes, set membership O(1). Example: favorites=[1,2,3], photos=[2,3,4,5], blocked={2} → yields [1, 3, 4, 5].
Solution
Maintain a seen set and iterate both lists, skipping blocked or previously-yielded items.
#include <bits/stdc++.h>
using namespace std;
class MergeIterator {
private:
deque<int> data;
int idx;
public:
MergeIterator(vector<int>& fav, vector<int>& photos,
unordered_set<int>& blocked) : idx(0) {
unordered_set<int> seen;
for (int x : fav) {
if (blocked.find(x) == blocked.end() && seen.find(x) == seen.end()) {
data.push_back(x);
seen.insert(x);
}
}
for (int x : photos) {
if (blocked.find(x) == blocked.end() && seen.find(x) == seen.end()) {
data.push_back(x);
seen.insert(x);
}
}
}
bool hasNext() {
return idx < (int)data.size();
}
int next() {
return data[idx++];
}
};
int main() {
vector<int> fav = {1, 2, 3};
vector<int> photos = {2, 3, 4, 5};
unordered_set<int> blocked = {2};
MergeIterator it(fav, photos, blocked);
while (it.hasNext()) {
cout << it.next() << " ";
}
cout << "\n";
return 0;
}- Time: O(|favorites| + |photos|) total for construction and all iterations.
- Space: O(|favorites| + |photos|) worst case for result.
Discussion
We pre-build the merged list in the constructor using a seen set for deduplication. This is eager; a lazy generator approach is possible but less natural in C++.
The order is strict: all non-blocked favorites before any photos, and within each category, original order is preserved.
Follow-ups
(F1) Streaming favorites/photos: Cannot random-access; use a generator pattern with prefetch for hasNext().
(F2) Prioritized ranking: Replace strict “favorites-first” with a heap of items by priority.
(F3) Large blocked set: Use a bloom filter for approximate O(1) lookup with space savings.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Eager construction | O(n) construction, O(1) per next() | O(n) | Typical case |
| Lazy generation | O(1) per next() amortized | O(1) state | Streaming data |
| With bloom filter blocked | O(n) construction | O(blocked.size()) | Huge blocked set |
76. Interval Queries — Alternating Parity Subarray
Problem Statement
Given an array nums and multiple range queries (l, r), determine for each query whether nums[l..r] has strictly alternating parity (adjacent elements alternate between even and odd).
Input: Array nums, queries list of (l, r) pairs. Output: Boolean for each query. Constraints: 1 ≤ N ≤ 100,000, up to 100,000 queries. Example: nums=[1,2,3,4,5], query (1,4) → true (2,3,4,5 alternates).
Solution
Precompute a bad-pair array where bad[i] = 1 if nums[i] and nums[i+1] have the same parity, else 0. A range [l, r] is alternating iff sum(bad[l..r-1]) == 0.
Build a prefix sum array on bad to answer each query in O(1).
#include <bits/stdc++.h>
using namespace std;
int main() {
vector<int> nums = {1, 2, 3, 4, 5};
int n = nums.size();
vector<int> bad(n - 1);
for (int i = 0; i < n - 1; i++) {
bad[i] = ((nums[i] & 1) == (nums[i + 1] & 1)) ? 1 : 0;
}
vector<int> ps(n, 0);
for (int i = 1; i < n; i++) {
ps[i] = ps[i - 1] + (i - 1 < (int)bad.size() ? bad[i - 1] : 0);
}
auto query = [&](int l, int r) {
if (l >= r) return true;
return (ps[r] - ps[l]) == 0;
};
cout << query(1, 4) << "\n"; // true
cout << query(0, 2) << "\n"; // true
return 0;
}- Time: O(N) preprocessing, O(1) per query.
- Space: O(N).
Discussion
Two elements have the same parity iff (nums[i] & 1) == (nums[i+1] & 1). A range [l, r] is alternating iff there are no such “bad pairs” in [l, r-1]. Prefix sums let us count bad pairs in any range in O(1).
Alternative: encode alternating runs and store run-IDs per position. Query [l, r] is alternating iff run[l] == run[r].
Follow-ups
(F1) Longest alternating subarray: Segment the array into maximal alternating runs; each query becomes a single range-check.
(F2) Update operation: Remove the value at index i. With segment trees or other persistent structures, handle updates.
(F3) Multiple properties: Precompute bad-pair arrays for multiple independent properties (e.g., increasing vs alternating parity).
Trade-offs
| Approach | Preprocess | Query | When to use |
|---|---|---|---|
| Bad-pair prefix sum | O(N) | O(1) | Many queries |
| Run-encoding | O(N) | O(1) | Cleaner code |
| Brute-force per query | O(1) | O(r - l) | Few queries |
77. Shortest Subarray with At Least K Distinct Integers
Problem Statement
Given an array of integers and an integer k, find the length of the shortest contiguous subarray containing at least k distinct integers. Return -1 if no such subarray exists.
Input: nums (array), k (integer). Output: Minimum length. Constraints: 1 ≤ N ≤ 100,000, 1 ≤ k ≤ N. Example: nums=[1,2,1,3,4], k=3 → 2 (subarray [3,4] has 2 distinct, nope; [1,2,1,3] has 3).
Solution
Use a sliding window: expand right to gain distinctness; when the window has ≥ k distinct, shrink from left to find the shortest window still maintaining ≥ k distinct.
#include <bits/stdc++.h>
using namespace std;
int main() {
vector<int> nums = {1, 2, 1, 3, 4};
int k = 3;
unordered_map<int, int> cnt;
int distinct = 0;
int left = 0;
int best = INT_MAX;
for (int right = 0; right < (int)nums.size(); right++) {
if (cnt[nums[right]] == 0) distinct++;
cnt[nums[right]]++;
while (distinct >= k) {
best = min(best, right - left + 1);
cnt[nums[left]]--;
if (cnt[nums[left]] == 0) distinct--;
left++;
}
}
cout << (best == INT_MAX ? -1 : best) << "\n";
return 0;
}- Time: O(N) — each element added and removed once.
- Space: O(min(N, σ)) for the counter, where σ is alphabet size.
Discussion
The window property is monotone: adding elements only increases (or keeps) the distinct count. So when we shrink from the left and lose the “≥ k distinct” property, we know we’ve found a tight window for this right-endpoint.
Taking the minimum over all right-anchors gives the global shortest subarray.
Follow-ups
(F1) Exactly k distinct: Use “at most k” minus “at most k-1” trick (two-pointer with two windows).
(F2) Stream (no random-access): Use a deque and track (value, count) pairs; same logic.
(F3) Multiple queries for different k: Rerun O(N) for each k, or precompute all k-values in one pass with a more complex data structure.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Sliding window | O(N) | O(σ) | Standard; k ≤ N |
| Brute-force pairs | O(N²) | O(σ) | N ≤ 500 |
| “At most k” × 2 | O(N) | O(σ) | Exactly k variant |
78. Longest Strictly Increasing Subarray with One Replacement
Problem Statement
Given an array, find the length of the longest contiguous strictly increasing subarray after replacing at most one element with any value.
Input: nums (array). Output: Maximum length. Constraints: 1 ≤ N ≤ 100,000, -1e9 ≤ nums[i] ≤ 1e9. Example: nums=[1,3,5,4,7] → 5 (replace 4 with 6: [1,3,5,6,7]).
Solution
Use DP with two states: f0[i] = length of longest strictly increasing subarray ending at i with 0 replacements, f1[i] = with at most 1 replacement.
For f1[i], consider three cases: 1. No replacement at i: if nums[i] > nums[i-1], extend f1[i-1]. 2. Replace nums[i]: extend f0[i-1] by 1. 3. Replace nums[i-1]: if gap nums[i] - nums[i-2] >= 2 (integers), extend f0[i-2] by 2.
#include <bits/stdc++.h>
using namespace std;
int main() {
vector<long long> nums = {1, 3, 5, 4, 7};
int n = nums.size();
if (n <= 1) {
cout << n << "\n";
return 0;
}
vector<int> f0(n, 1), f1(n, 1);
for (int i = 1; i < n; i++) {
if (nums[i] > nums[i - 1]) {
f0[i] = f0[i - 1] + 1;
}
int best = f0[i];
best = max(best, f0[i - 1] + 1); // replace nums[i]
if (i >= 2 && nums[i] - nums[i - 2] >= 2) {
best = max(best, f0[i - 2] + 2); // replace nums[i-1]
}
f1[i] = best;
if (nums[i] > nums[i - 1]) {
f1[i] = max(f1[i], f1[i - 1] + 1); // no replacement at i
}
}
int ans = max(*max_element(f0.begin(), f0.end()),
*max_element(f1.begin(), f1.end()));
cout << ans << "\n";
return 0;
}- Time: O(N).
- Space: O(N), or O(1) with rolling variables.
Discussion
The key is enumerating where the replacement occurs: at the new element, at the previous one, or nowhere. The gap condition nums[i] - nums[i-2] >= 2 ensures we can place an integer strictly between them.
The trace for [1,3,5,4,7] gives: f0=[1,2,3,1,2], f1=[1,2,3,4,5], answer=5.
Follow-ups
(F1) Real-valued elements: Gap condition becomes nums[i] > nums[i-2] (any positive gap works).
(F2) Replace up to k elements: Generalize to k states; O(N·k) DP.
(F3) Return the replaced index: Track which case achieves the maximum.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Brute-force try each replacement | O(N²) | O(1) | N ≤ 1000 |
| Two-state DP | O(N) | O(N) | N ≤ 1e5; standard |
| Rolling variables | O(N) | O(1) | Space-constrained |
79. Hit Counter
Problem Statement
Design a HitCounter class with methods: - hit(timestamp) — record a hit at the given timestamp. - getHits(timestamp) — return count of hits within the last 300 seconds (i.e., in [timestamp - 299, timestamp]).
Input: Timestamps (monotonically increasing or arbitrary). Output: Hit counts. Constraints: Timestamps fit in a 32-bit integer. Example: hit(1), hit(2), hit(3), getHits(3) → 3; getHits(301) → 0.
Solution
Use a circular buffer with size 300. Index i stores the count of hits at timestamp t % 300 == i. On hit(t), increment the bucket at index t % 300 (reset if the bucket’s stored timestamp differs). On getHits(t), sum buckets corresponding to recent timestamps.
#include <bits/stdc++.h>
using namespace std;
class HitCounter {
private:
vector<int> times;
vector<int> hits;
static const int W = 300;
public:
HitCounter() : times(W, 0), hits(W, 0) {}
void hit(int t) {
int i = t % W;
if (times[i] != t) {
times[i] = t;
hits[i] = 0;
}
hits[i]++;
}
int getHits(int t) {
int sum = 0;
for (int i = 0; i < W; i++) {
if (t - times[i] < W) {
sum += hits[i];
}
}
return sum;
}
};
int main() {
HitCounter hc;
hc.hit(1); hc.hit(2); hc.hit(3);
cout << hc.getHits(3) << "\n"; // 3
cout << hc.getHits(301) << "\n"; // 0
return 0;
}- Time:
hitO(1),getHitsO(300). - Space: O(300) = O(1).
Discussion
The circular buffer works because timestamps are typically monotonic (or close to it in practice). The check t - times[i] < W ensures we only count hits within the 300-second window.
For arbitrary range queries (not a fixed window), use a sorted list with binary search: O(log N) per hit/query.
Follow-ups
(F1) Arbitrary-range queries: Use std::multiset or SortedList; O(log N) hit/query.
(F2) Multiple windows: Maintain hierarchical bucket sizes (per-second, per-minute, per-hour).
(F3) Distributed counters: Use a time-series database (InfluxDB, TSDB) with quantized buckets.
Trade-offs
| Approach | hit | getHits | Space | When to use |
|---|---|---|---|---|
| Circular buffer | O(1) | O(W) | O(W) | Fixed window |
| Deque + coalesce | O(1)* | O(expired) | O(hits in W) | Memory-tight |
| Sorted list | O(log N) | O(log N) | O(N) | Arbitrary ranges |
80. Two People Meet Then Travel to Destination
Problem Statement
City graph with weighted edges. Person A starts at city a, person B at city b, both travel to destination d. They must first meet at some city m, then travel together to d. Minimize total travel time.
Input: Weighted graph (n nodes, edges), cities a, b, d. Output: Minimum time (or cost). Constraints: 1 ≤ n ≤ 10,000, edge weights ≥ 0. Example: Graph, a=0, b=1, d=2 → minimum of max(dist(0,m), dist(1,m)) + dist(m,2) over all m.
Solution
Run Dijkstra from each of a, b, d to compute dist_a[], dist_b[], dist_d[]. For each potential meeting point m, compute cost = max(dist_a[m], dist_b[m]) + dist_d[m] and take the minimum.
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<ll, int> pli;
vector<ll> dijkstra(const vector<vector<pair<int, ll>>>& g, int src, int n) {
vector<ll> dist(n, LLONG_MAX);
priority_queue<pli, vector<pli>, greater<pli>> pq;
dist[src] = 0;
pq.push({0, src});
while (!pq.empty()) {
ll d = pq.top().first;
int u = pq.top().second;
pq.pop();
if (d > dist[u]) continue;
for (size_t k = 0; k < g[u].size(); k++) {
int v = g[u][k].first;
ll w = g[u][k].second;
if (dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
pq.push(make_pair(dist[v], v));
}
}
}
return dist;
}
int main() {
int n = 3;
vector<vector<pair<int, ll>>> g(n);
// Add edges: (u, v, weight)
g[0].push_back({1, 1}); g[1].push_back({0, 1});
g[1].push_back({2, 2}); g[2].push_back({1, 2});
g[0].push_back({2, 5}); g[2].push_back({0, 5});
int a = 0, b = 1, d = 2;
auto dist_a = dijkstra(g, a, n);
auto dist_b = dijkstra(g, b, n);
auto dist_d = dijkstra(g, d, n);
ll best = LLONG_MAX;
for (int m = 0; m < n; m++) {
ll cost = max(dist_a[m], dist_b[m]) + dist_d[m];
best = min(best, cost);
}
cout << best << "\n";
return 0;
}- Time: O(3 · (N + M) log N) for three Dijkstra runs.
- Space: O(N + M).
Discussion
The “3-source shortest path” pattern is common: compute shortest distances from each of three key nodes, then optimize over all candidate middle points. The cost function (maximum arrival time plus joint travel) encodes “they must synchronize at the meeting point.”
For bus schedules with time-dependent edges, use a time-expanded graph where nodes are (city, time-bucket).
Follow-ups
(F1) Return the meeting point: Track which m achieves the minimum.
(F2) More than 2 people: Add more source Dijkstra runs.
(F3) Time-dependent edges: Build a time-expanded graph with wait edges.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Multi-source Dijkstra | O(k·(V+E)logV) | O(V+E) | k=O(1) sources |
| Floyd-Warshall | O(V³) | O(V²) | V ≤ 500, dense |
| BFS (unweighted) | O(k·(V+E)) | O(V+E) | Unweighted graph |
81. Stacking Tiles Game
Problem Statement
12 tiles: 4 colors (Y, W, G, B), 3 of each. Initially, 12 stacks of height 1. Two players alternate merging stack X onto stack Y if: (1) both heights equal, OR (2) both top colors equal. The result stack has height = sum and top color = X’s color. Last player to move wins. Determine the winner under optimal play.
Input: Initial configuration (12 tiles, one per stack). Output: Winner (0 or 1). Constraints: Game tree is large; use memoization. Example: Optimal play starting from initial config.
Solution
Use memoized minimax with state canonicalization for symmetry. Represent state as a sorted tuple of (height, top_color) pairs. Apply color relabeling (first-seen color = 0, etc.) to reduce state space.
#include <bits/stdc++.h>
using namespace std;
map<vector<pair<int, int>>, int> memo;
vector<pair<int, int>> canonical(vector<pair<int, int>> state) {
map<int, int> colorMap;
vector<pair<int, int>> out;
for (size_t k = 0; k < state.size(); k++) {
int h = state[k].first;
int c = state[k].second;
if (colorMap.find(c) == colorMap.end()) {
colorMap[c] = (int)colorMap.size();
}
out.push_back(make_pair(h, colorMap[c]));
}
sort(out.begin(), out.end());
return out;
}
int solve(vector<pair<int, int>> state, int player) {
auto key = canonical(state);
if (memo.count(key)) return memo[key];
vector<vector<pair<int, int>>> moves;
int n = state.size();
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (i == j) continue;
int hi = state[i].first, ci = state[i].second;
int hj = state[j].first, cj = state[j].second;
if (hi == hj || ci == cj) {
vector<pair<int, int> > newState = state;
newState.erase(newState.begin() + max(i, j));
newState.erase(newState.begin() + min(i, j));
newState.push_back(make_pair(hi + hj, ci));
moves.push_back(newState);
}
}
}
if (moves.empty()) {
return memo[key] = 1 - player;
}
for (auto& nxt : moves) {
if (solve(nxt, 1 - player) == player) {
return memo[key] = player;
}
}
return memo[key] = 1 - player;
}
int main() {
vector<pair<int, int>> initial;
for (int i = 0; i < 3; i++) {
initial.push_back({1, 0}); // Y
initial.push_back({1, 1}); // W
initial.push_back({1, 2}); // G
initial.push_back({1, 3}); // B
}
cout << solve(initial, 0) << "\n";
return 0;
}- Time: State space with canonicalization is still large, but memoization renders it tractable for the initial config.
- Space: O(state space).
Discussion
The state representation as sorted tuples enables memoization. Color relabeling recognizes that e.g. “Y and W tiles” vs “W and Y tiles” are equivalent (24× symmetry reduction).
A move merges two stacks: one becomes the base, one the top (determining the result color). Illegal moves are pruned (no height/color match).
Follow-ups
(F1) Generalize: Extend to n tiles, k colors, arbitrary initial stacks.
(F2) Misère variant: Last player loses. Flip the terminal condition.
(F3) Scored game: Most merges wins (not combinatorial; becomes optimization).
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Memoized minimax | O(states) | O(states) | Bounded state space |
| Alpha-beta pruning | O(states) | O(depth) | Deeper trees |
| Bitboard representation | O(states) | O(states) | Compact encoding |
82. Streaming Longest Subarray with Mean = S
Problem Statement
Stream of integers. After inserting each new value, report the length of the longest contiguous subarray (seen so far) whose mean equals S.
Input: Stream of integers, target mean S. Output: Max length after each insert. Constraints: -1e9 ≤ a_i ≤ 1e9, S can be float. Example: Stream [1,2,3], S=2 → after each insert, report the longest subarray with mean 2.
Solution
Transform: replace each a_i with b_i = a_i - S. Then “subarray mean = S” ⟺ “subarray of b has sum 0”. Use prefix sums: for each position i with prefix sum P[i], if P[i] was seen before at position j, then subarray (j, i] sums to 0.
#include <bits/stdc++.h>
using namespace std;
class StreamLongestMeanS {
private:
double S;
map<double, int> first;
double cur;
int best;
int idx;
public:
StreamLongestMeanS(double s) : S(s), cur(0), best(0), idx(-1) {
first[0.0] = -1;
}
int add(double a) {
idx++;
cur += (a - S);
if (first.count(cur)) {
int j = first[cur];
best = max(best, idx - j);
} else {
first[cur] = idx;
}
return best;
}
};
int main() {
StreamLongestMeanS stream(2.0);
cout << stream.add(1.0) << "\n"; // 0
cout << stream.add(2.0) << "\n"; // 1 (subarray [2] has mean 2)
cout << stream.add(3.0) << "\n"; // 2 (subarray [2,2] has mean 2? Wait, [1,2,3] mean is 2, but subarray [2,3] has mean 2.5, [1,3] has mean 2 ✓)
return 0;
}- Time: O(1) amortized per add (hash map).
- Space: O(N) for the map.
Discussion
The shift a_i - S is key: it converts the problem from “mean = S” to “sum = 0”, which is a standard prefix-sum problem.
For float S, beware precision issues. Multiply all values by a common denominator to work with integers if possible.
Follow-ups
(F1) Exactly K elements with mean S: Needs 2D DP; much harder.
(F2) Bounded memory: LRU-evict oldest prefix sums, sacrificing optimality.
(F3) Mean of any value in a list: Maintain separate maps for each target mean.
Trade-offs
| Approach | Time per add | Space | When to use |
|---|---|---|---|
| Prefix sum + first-occurrence | O(1) | O(N) | Stream setting |
| Static DP | O(N) | O(N) | Offline; all data upfront |
| With float precision loss | O(1) | O(N) | Acceptable error margin |
83. Logger with Top-K Frequent
Problem Statement
Logger class with methods: - log(message, timestamp) — record message at timestamp. - access_by_timestamp(t) — retrieve all messages logged at time t. - find_top_k(k) — return k most frequently logged messages.
Input: Log calls, queries. Output: Messages at time t, top-k frequent messages. Constraints: Up to 1 million logs, k ≤ distinct message count. Example: log(“error”, 3), log(“warning”, 5), log(“error”, 6); find_top_k(1) → [“error”].
Solution
Maintain: - logs_by_time — dict mapping timestamp to list of messages. - count — dict mapping message to its frequency.
For top-k, use a heap on demand (O(m log k) where m = distinct messages), or maintain a sorted structure (O(log m) per update).
#include <bits/stdc++.h>
using namespace std;
class Logger {
private:
map<int, vector<string>> logsByTime;
map<string, int> count;
public:
void log(const string& msg, int t) {
logsByTime[t].push_back(msg);
count[msg]++;
}
vector<string> accessByTimestamp(int t) {
if (logsByTime.count(t)) return logsByTime[t];
return {};
}
vector<string> findTopK(int k) {
if (k <= 0) return {};
auto cmp = [](pair<int, string> a, pair<int, string> b) {
return a.first > b.first || (a.first == b.first && a.second < b.second);
};
priority_queue<pair<int, string>, vector<pair<int, string>>,
decltype(cmp)> heap(cmp);
for (auto& [msg, cnt] : count) {
if ((int)heap.size() < k) {
heap.push({cnt, msg});
} else if (cnt > heap.top().first) {
heap.pop();
heap.push({cnt, msg});
}
}
vector<string> result;
while (!heap.empty()) {
result.push_back(heap.top().second);
heap.pop();
}
sort(result.begin(), result.end(),
[this](const string& a, const string& b) {
return count[a] > count[b] ||
(count[a] == count[b] && a < b);
});
return result;
}
};
int main() {
Logger logger;
logger.log("error", 3);
logger.log("warning", 5);
logger.log("error", 6);
auto result = logger.findTopK(1);
for (auto& msg : result) cout << msg << "\n";
return 0;
}- Time:
logO(1),accessO(1),find_top_kO(m log k). - Space: O(N) for logs, O(m) for counts.
Discussion
The heap-on-demand approach is simple and efficient for moderate k and m. For very frequent top-k queries, use a self-balancing multimap (C++ multimap) with (count, message) pairs, indexed by message for O(log m) updates.
Tiebreaker for ties in count: lexicographic order or insertion order (clarify with interviewer).
Follow-ups
(F1) Range query top-K: Logs in [t1, t2] with highest frequency. Requires segment tree or offline processing.
(F2) Sliding window top-K: Last T minutes’ messages. Bucket by minute; evict and merge as time advances.
(F3) Approximate top-K: Use Space-Saving or Misra-Gries for single-pass, bounded-memory streaming.
Trade-offs
| Operation | Simple | SortedList |
|---|---|---|
| log | O(1) | O(log m) |
| find_top_k | O(m log k) | O(k) |
| Space | O(N) | O(N) |
84. Count K-Coin Subsets with Sum Divisible by M
Problem Statement
Coins valued 0, 1, 2, …, N-1. Count the number of distinct subsets of size exactly K whose sum is divisible by M. Return the count modulo 10^9 + 7.
Input: N, M, K (integers). Output: Count mod 10^9 + 7. Constraints: 1 ≤ N, M, K ≤ 1,000. Example: N=4, M=2, K=2 → coins {0,1,2,3}. Size-2 subsets: {0,2}=2 (divisible), {1,3}=4 (divisible). Count = 2.
Solution
Use DP: dp[i][j][r] = number of subsets from first i coins using exactly j coins with sum ≡ r (mod M).
Transition for coin value v: - Skip: dp[i+1][j][r] += dp[i][j][r]. - Take: dp[i+1][j+1][(r+v)%M] += dp[i][j][r].
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main() {
int N = 4, M = 2, K = 2;
const ll MOD = 1e9 + 7;
vector<vector<ll>> dp(K + 1, vector<ll>(M, 0));
dp[0][0] = 1;
for (int v = 0; v < N; v++) {
for (int j = min(v, K - 1); j >= 0; j--) {
for (int r = 0; r < M; r++) {
if (dp[j][r] > 0) {
int nr = (r + v) % M;
dp[j + 1][nr] = (dp[j + 1][nr] + dp[j][r]) % MOD;
}
}
}
}
cout << dp[K][0] << "\n";
return 0;
}- Time: O(N · K · M).
- Space: O(K · M).
Discussion
Standard 0/1 knapsack with two state dimensions: count and remainder. We iterate coins in forward order but update DP in reverse (j from high to low) to avoid using the same coin twice.
For large N, M, K (~1e3 each), this is ~1e9 operations, borderline but feasible.
Follow-ups
(F1) Optimize for special case: If coins are 0, 1, …, N-1, group by residue class mod M; reduce to O(M² K²) or better.
(F2) Arbitrary coin values: Replace coin value v with its residue mod M; same DP.
(F3) Return subsets: Track parent pointers in DP to reconstruct solutions.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Naive DP | O(N·K·M) | O(K·M) | N, K, M ≤ 1000 |
| Residue-class grouping | O(M²·K²) | O(M·K) | N very large, values 0..N-1 |
| Generating functions | O(N·K) | O(K) | Heavy coding, overkill |
85. Min Town Difference After Deleting One Tree Edge
Problem Statement
Tree with N nodes (1 to N). Each node has towns[i] towns. Remove one edge to split the tree into two components. Minimize the absolute difference between the sum of towns in each component.
Input: N, towns array (length N, indexed from 1), edges list. Output: Minimum difference. Constraints: 1 ≤ N ≤ 100,000. Example: N=2, towns=[10, 20], edge=(1,2) → |10 - 20| = 10.
Solution
Root the tree at node 1. Compute sub[u] = sum of towns in u’s subtree. Removing edge (parent[u], u) splits into: - Component 1 (u’s subtree): sum = sub[u]. - Component 2 (rest): sum = T - sub[u].
Difference = |T - 2·sub[u]|. Minimize over all non-root nodes.
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main() {
int n; cin >> n;
vector<ll> towns(n + 1);
for (int i = 1; i <= n; i++) cin >> towns[i];
vector<vector<int>> g(n + 1);
for (int i = 0; i < n - 1; i++) {
int a, b; cin >> a >> b;
g[a].push_back(b); g[b].push_back(a);
}
vector<ll> sub(n + 1, 0);
function<void(int, int)> dfs = [&](int u, int p) {
sub[u] = towns[u];
for (int v : g[u]) {
if (v != p) {
dfs(v, u);
sub[u] += sub[v];
}
}
};
dfs(1, -1);
ll total = sub[1];
ll best = LLONG_MAX;
function<void(int, int)> find_min = [&](int u, int p) {
if (u != 1) {
ll diff = abs(total - 2 * sub[u]);
best = min(best, diff);
}
for (int v : g[u]) {
if (v != p) find_min(v, u);
}
};
find_min(1, -1);
cout << best << "\n";
return 0;
}- Time: O(N).
- Space: O(N).
Discussion
Subtree sums computed in post-order DFS. For each non-root node u, the difference formula |T - 2·sub[u]| directly gives the split imbalance. The minimum over all such splits is the answer.
Why the formula: Component 1 = sub[u], component 2 = T - sub[u]. Difference = |sub[u] - (T - sub[u])| = |2·sub[u] - T|.
Follow-ups
(F1) Return the edge: Track which u achieves min and output (parent[u], u).
(F2) Delete two edges: DP on all pairs; more complex.
(F3) Weighted edges: Split by edge cost; requires different formulation.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Subtree sum DFS | O(N) | O(N) | Standard; N ≤ 1e5 |
| Brute-force pair removal | O(N²) | O(N) | N ≤ 1000 |
| With centroid decomposition | O(N log N) | O(N) | Multiple queries |
86. Shuffle Playlist with No Adjacent Same Artist
Problem Statement
You have a playlist of songs, each identified by (artist, title). Shuffle the playlist so that no two adjacent songs have the same artist (if possible).
Input: List of songs, each song is a pair (artist, title).
Output: A shuffled list of songs with no two adjacent songs from the same artist. Return None if this is impossible.
Constraints: A feasible arrangement exists if and only if the most-frequent artist appears at most ⌈n/2⌉ times, where n is the total number of songs.
Example: Input: [('A', 'Song1'), ('A', 'Song2'), ('B', 'Song3'), ('B', 'Song4'), ('C', 'Song5')] Output: [('A', 'Song1'), ('B', 'Song3'), ('A', 'Song2'), ('B', 'Song4'), ('C', 'Song5')]
Solution
Count songs by artist. Check feasibility: if max_count > ⌈n/2⌉, return None. Otherwise, use a max-heap of (count, artist) and apply the two-pop pattern: extract the two artists with highest remaining counts, emit both, then re-insert if their count is still positive.
#include <bits/stdc++.h>
using namespace std;
vector<pair<string,string>> shufflePlaylist(vector<pair<string,string>> songs) {
map<string, int> cnt;
for (auto &p : songs) cnt[p.first]++;
int maxCnt = 0;
for (auto &p : cnt) maxCnt = max(maxCnt, p.second);
int n = (int)songs.size();
if (maxCnt > (n + 1) / 2) return {}; // infeasible
priority_queue<pair<int,string> > heap;
for (auto &p : cnt) heap.push({p.second, p.first});
vector<pair<string,string>> result;
while (!heap.empty()) {
if (heap.size() >= 2) {
pair<int,string> top1 = heap.top(); heap.pop();
pair<int,string> top2 = heap.top(); heap.pop();
result.push_back({top1.second, ""});
result.push_back({top2.second, ""});
if (top1.first > 1) heap.push({top1.first - 1, top1.second});
if (top2.first > 1) heap.push({top2.first - 1, top2.second});
} else {
pair<int,string> top1 = heap.top(); heap.pop();
if (top1.first > 1) return {};
result.push_back({top1.second, ""});
}
}
return result;
}- Time:
O(n log k)where k is the number of distinct artists. - Space:
O(n).
Discussion
The key insight is recognizing this as an interval-scheduling problem on a color (artist) graph. The feasibility condition—max count ≤ ⌈n/2⌉—is tight: if one artist owns more than half the songs, pigeonhole forces an adjacency.
The two-pop pattern avoids state-machine bugs that arise from greedy one-at-a-time selection. By always taking two artists per round (when available), we guarantee they differ, making the output valid.
Follow-ups
(F1) Minimum distance k between same artist: Use a cooldown queue; after playing artist X, enqueue it for k steps before re-entry.
(F2) Weighted preferences: Assign penalties to certain adjacent pairs; becomes a constraint-satisfaction problem, often NP-hard.
(F3) Streaming mode: Maintain the top-2 artists by count in a sliding window; pick the one not used last.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Max-heap (two-pop) | O(n log k) | O(n) | Default; simple and correct. |
| One-pop with prev tracking | O(n log k) | O(n) | More bookkeeping; error-prone. |
87. StackOverflow Tag Matching
Problem Statement
You have a list of people, each with a set of tags, and a list of questions, each with a set of tags. A person can answer a question if their tag set has a non-empty intersection with the question’s tag set.
Input: People list with their tags; questions list with their tags.
Output: For each question, return the set of people who can answer it (or rank them by shared tag count).
Constraints: Efficient matching when people and questions are large.
Example: People: [{id: 1, tags: {'python', 'ml'}}, {id: 2, tags: {'python', 'web'}}] Questions: [{id: 101, tags: {'ml', 'nlp'}}, {id: 102, tags: {'python'}}] Output: Q101 → [1], Q102 → [1, 2].
Solution
Build an inverted index tag_to_people: dict[tag, set[person_id]]. For each question, collect all people across its tags and return their union.
#include <bits/stdc++.h>
using namespace std;
map<string, set<int> > tagToPeople;
vector<int> matchQuestion(vector<string> qTags) {
set<int> candidates;
for (const string &t : qTags) {
if (tagToPeople.find(t) != tagToPeople.end()) {
for (int p : tagToPeople[t]) {
candidates.insert(p);
}
}
}
return vector<int>(candidates.begin(), candidates.end());
}- Time:
O(sum of people's tag counts + sum of matching results). - Space:
O(P · T)where P is people count and T is average tags per person.
Discussion
The inverted index is the canonical structure for “find all X with property in set Y” queries. Scanning the union of people sets for each question tag is I/O-optimal: you only visit relevant people, once per their appearing tag.
The ranking variant (by shared tag count) requires counting overlaps, turning each people-per-tag list into a counter. Use a hash map to accumulate counts across question tags.
Follow-ups
(F1) Rank by shared tag count: Maintain a counter; increment for each tag in question matched by a person’s tags.
(F2) Top K matches per question: Use a heap of size K to keep only the highest-scoring people without full sort.
(F3) Inverse: top N questions per person: Swap direction; build question-to-tags index and accumulate matches.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Brute force | O(P · Q · T) | O(1) | Tiny data; simplest. |
| Inverted index | O((P+Q)·T + matches) | O(P·T) | Standard; scales well. |
| Ranked (counter) | O((P+Q)·T + Q·matches·log K) | O(P·T) | When ranking needed. |
88. Boba Delivery — Max Deliveries Within Distance
Problem Statement
1-D axis: given positions of people and boba shops (both as integers), and a max delivery distance R, maximize the number of deliveries where each person receives exactly one cup from one shop and each shop delivers exactly one cup. Each delivery distance must be ≤ R.
Input: Two sorted arrays of integers (people and shop positions) and integer R.
Output: Maximum number of deliveries and a matching list (person_pos, shop_pos) pairs.
Constraints: 1 ≤ n, m ≤ 10^5; R ≥ 0.
Example: People: [0, 4, 8], Shops: [3], R=5. Output: 2 matches. One possible matching: (0, 3) (distance 3), (4, 3) invalid (distance 1 but same shop). Correct: only 1 match (4, 3).
Solution
Sort both arrays. Use two pointers: for each person, match with the earliest available shop within distance R. This greedy choice is exchange-safe: keeping a shop for a later person cannot help count, since later people are farther right.
#include <bits/stdc++.h>
using namespace std;
int maxDeliveries(vector<int> people, vector<int> shops, int R) {
sort(people.begin(), people.end());
sort(shops.begin(), shops.end());
int i = 0, j = 0, count = 0;
while (i < (int)people.size() && j < (int)shops.size()) {
if (shops[j] < people[i] - R) {
j++;
} else if (shops[j] > people[i] + R) {
i++;
} else {
count++;
i++; j++;
}
}
return count;
}- Time:
O((n + m) log(n + m))for sorting. - Space:
O(1)extra (ignoring input).
Discussion
The two-pointer technique works because both arrays are sorted. At each step, we decide: skip the current shop (if it’s too far left), skip the current person (if the shop is too far right), or match them (in range). The exchange argument shows that matching the leftmost person with the leftmost feasible shop is optimal: no benefit in “saving” that shop for someone farther right, who may not reach it.
Follow-ups
(F1) Each shop can deliver K cups: Use a capacity counter; when shop is exhausted, move to the next.
(F2) Minimize total distance subject to max count: Use lex-min: first maximize count, then among all max-count matchings, minimize total distance.
(F3) 2-D coordinates: Sort by x-coordinate, then sweep; requires radial distance check, making it harder.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Brute force pairs | O(n·m) | O(n·m) | Tiny n, m. |
| Greedy two-pointer | O((n+m)·log(n+m)) | O(1) | Default; efficient. |
| Flow/Hungarian | O(n^3) | O(n^2) | Multi-capacity per entity. |
89. Trip Expense Settlement
Problem Statement
A group of friends shared expenses on a trip. Each expense record specifies a payer and a list of payees who split that expense equally. Compute the minimum number of payments required to settle all debts.
Input: List of expenses, each: {payer, amount, payees}.
Output: Minimum number of inter-person transfers.
Constraints: Number of people ≤ 12.
Example: Expenses: [{payer: 'alice', amount: 4000, payees: ['bob', 'jess', 'alice', 'sam']}, {payer: 'jess', amount: 2000, payees: ['jess', 'alice']}] Net balances: alice +2000, bob −1000, jess 0, sam −1000. Output: 2 transfers.
Solution
Phase 1: Compute net balance per person. Phase 2: Find the minimum number of transfers to balance everyone. This second phase is NP-hard in general (reducible from 3-partition), but feasible for small n via DFS with memoization.
#include <bits/stdc++.h>
using namespace std;
int minTransfers(vector<double> balances) {
vector<double> nonzero;
for (double b : balances) {
if (abs(b) > 1e-9) nonzero.push_back(b);
}
function<int(int)> dfs = [&](int i) -> int {
while (i < (int)nonzero.size() && abs(nonzero[i]) < 1e-9) i++;
if (i == (int)nonzero.size()) return 0;
int best = INT_MAX;
set<double> tried;
for (int j = i + 1; j < (int)nonzero.size(); j++) {
if (nonzero[j] * nonzero[i] < 0 && tried.find(nonzero[j]) == tried.end()) {
tried.insert(nonzero[j]);
nonzero[j] += nonzero[i];
best = min(best, 1 + dfs(i + 1));
nonzero[j] -= nonzero[i];
}
}
return best;
};
return dfs(0);
}- Time:
O(n!)worst-case, but fine for n ≤ 13. - Space:
O(n)recursion depth.
Discussion
The trick is splitting into two phases: netting is O(n), but minimizing transfers is the hard part. The DFS explores all possible ways to pair creditors and debtors, memoizing to avoid recomputation on identical balance sets.
Most interview interviewers accept either the exponential-time DFS or the greedy (two-heap) settlement heuristic. The two-heap greedy is O(n log n) but not guaranteed optimal; however, for practical trip sizes it produces near-optimal results.
Follow-ups
(F1) Output the actual payment list: Modify DFS to track pairings, or use the two-heap greedy to directly generate transfers.
(F2) Multi-way split (not equal share): Adjust the netting formula to weight each payee’s proportion differently.
(F3) Settle with a bank account: Introduce a virtual “bank” node; everyone transfers to/from it. Simplifies to 1 payment per person off-zero balance.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Two-heap greedy | O(n log n) | O(n) | Practical; heuristic. |
| DFS + memo | O(n!) | O(n) | Small n; optimal. |
| LP/flow formulation | Poly | Poly | If exact opt needed; overkill. |
90. RPC Timeout Detection with LinkedHashMap
Problem Statement
Stream of RPC events: (type, rpc_id, timestamp) where type ∈ {start, end}. A timeout occurs if an RPC is still active (started but not ended) after T seconds have elapsed since its start.
Input: List of events in chronological order; timeout threshold T.
Output: Return True if any RPC times out; False if all RPCs complete within T.
Constraints: Timestamps are non-decreasing; handle up to 10^6 events.
Example: Events: [(start, id=1, t=0), (start, id=2, t=1), (end, id=1, t=5), (end, id=2, t=8)], T=6. RPC 2 is active at t=8 but started at t=1, duration 7 > T=6. Output: True.
Solution
Use a linked hash map (insertion-ordered) to track active RPCs. At each event with timestamp now, check if the oldest active RPC (first in insertion order) has now - start_time > T. Remove RPCs on end events.
#include <bits/stdc++.h>
using namespace std;
bool detectTimeout(vector<tuple<string,int,int> > events, int T) {
map<int, int> active; // rpc_id -> start_time
vector<int> insertOrder;
for (auto &ev : events) {
string type = std::get<0>(ev);
int id = std::get<1>(ev);
int now = std::get<2>(ev);
if (!insertOrder.empty()) {
int oldId = insertOrder.front();
if (active.find(oldId) != active.end()) {
int oldStart = active[oldId];
if (now - oldStart > T) return true;
}
}
if (type == "start") {
active[id] = now;
insertOrder.push_back(id);
} else {
active.erase(id);
}
}
return false;
}- Time:
O(n)where n is the number of events. - Space:
O(k)where k is max concurrent RPCs.
Discussion
The LinkedHashMap (or in C++, tracking insertion order separately) is crucial because timestamps are non-decreasing. This means insertion order = start-time order. The oldest active RPC is always at the front, allowing O(1) peek.
A plain HashMap would require iterating all active RPCs each event to find the oldest, leading to O(n²) overall. A TreeMap would work but is overkill (O(log k) per op vs O(1) peek).
Follow-ups
(F1) Rate-limit: up to K concurrent RPCs: Check size(active) <= K before accepting a start event.
(F2) Oldest-K timeouts: Iterate from front of map until timestamps exceed threshold.
(F3) Streaming with time ticks: Maintain a separate “current time” and re-check the head periodically.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| LinkedHashMap | O(n) | O(k) | Default; insertion order = time order. |
| TreeMap | O(n log k) | O(k) | Overkill; unnecessary log factor. |
| Full scan each event | O(n²) | O(k) | Only if k is large and events few. |
91. Sliding Window Average After Removing Top-K
Problem Statement
Array of integers, sliding window size w, integer k. For each window position, remove the k largest elements and compute the average of the remaining w−k elements.
Input: Array nums, window size w, top-k count k.
Output: List of averages for each window.
Constraints: 1 ≤ k < w ≤ n ≤ 10^5.
Example: nums=[1,3,7,8,5,6,4], w=3, k=1 Window [1,3,7] → remove 7 → avg of [1,3] = 2.0. Window [3,7,8] → remove 8 → avg of [3,7] = 5.0.
Solution
Partition each window into two sorted containers: lower (smallest w−k elements) and upper (largest k elements). Maintain the sum of lower. On slide, add/remove elements and rebalance to keep sizes correct.
#include <bits/stdc++.h>
using namespace std;
vector<double> slidingAvgTopKRemoved(vector<int> nums, int w, int k) {
vector<double> result;
multiset<int> lower, upper;
long long sumLower = 0;
auto add = [&](int x) {
if ((int)upper.size() < k) {
upper.insert(x);
if (!lower.empty() && *upper.begin() < *lower.rbegin()) {
int a = *upper.begin(); upper.erase(upper.begin());
int b = *lower.rbegin(); lower.erase(prev(lower.end()));
upper.insert(b);
lower.insert(a);
sumLower += a - b;
}
} else {
if (!upper.empty() && x > *upper.begin()) {
int y = *upper.begin(); upper.erase(upper.begin());
upper.insert(x);
lower.insert(y);
sumLower += y;
} else {
lower.insert(x);
sumLower += x;
}
}
};
auto remove = [&](int x) {
if (!upper.empty() && upper.find(x) != upper.end() && *upper.find(x) >= *upper.begin()) {
upper.erase(upper.find(x));
if (!lower.empty()) {
int y = *lower.rbegin(); lower.erase(prev(lower.end()));
sumLower -= y;
upper.insert(y);
}
} else {
lower.erase(lower.find(x));
sumLower -= x;
}
};
for (int i = 0; i < (int)nums.size(); i++) {
add(nums[i]);
if (i >= w) remove(nums[i - w]);
if (i >= w - 1) {
int denom = w - k;
if (denom > 0) {
result.push_back((double)sumLower / denom);
}
}
}
return result;
}- Time:
O(n log w)where n is array length. - Space:
O(w)for the two multisets.
Discussion
The two-multiset partition avoids iterating all k elements per window. The invariant |upper| = k, |lower| = w−k is maintained by careful rebalancing on each add/remove. The sum-tracking for lower is critical: computing it fresh each window would be O(w) per window, making the total O(n·w).
Follow-ups
(F1) Remove bottom-k instead: Swap the roles of lower/upper; compute sum_upper instead.
(F2) Median: Generalize to two multisets of size w/2 each; compute median directly.
(F3) Remove top-k percentile: Sort per window and find the k-th largest; similar two-partition approach.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Sort each window | O(n·w·log w) | O(w) | Tiny windows; simplest. |
| Two multisets | O(n·log w) | O(w) | Default; optimal. |
| Lazy heap + counter | O(n·log w) | O(w) | Alternative; more bookkeeping. |
92. Packet Split — Minimize Max Size
Problem Statement
Given total data size N and max packet capacity C, split N into the minimum number of packets (each ≤ C), then among all such splits, distribute the data to minimize the maximum packet size.
Input: Long integers N (total data), C (max capacity).
Output: Minimum number of packets k, and the size of each packet.
Constraints: N, C > 0.
Example: N=11, C=4. Min packets: k = ceil(11/4) = 3. Distribute evenly: 11 = 4+4+3. Output: [4, 4, 3] with max=4.
Solution
Step 1: Compute k = ⌈N/C⌉. Step 2: Distribute N evenly among k packets: base = ⌊N/k⌋, remainder = N mod k. The first remainder packets get base+1; the rest get base.
#include <bits/stdc++.h>
using namespace std;
vector<long long> splitPackets(long long N, long long C) {
long long k = (N + C - 1) / C; // ceil(N / C)
long long base = N / k;
long long rem = N % k;
vector<long long> sizes;
for (long long i = 0; i < rem; i++) {
sizes.push_back(base + 1);
}
for (long long i = 0; i < k - rem; i++) {
sizes.push_back(base);
}
return sizes;
}- Time:
O(1)math;O(k)to materialize the list. - Space:
O(k)for output.
Discussion
The solution is pure arithmetic. The minimum number of packets is determined by C alone (pigeonhole). Given k, minimizing the maximum size is a load-balancing problem: the most-even distribution (differing by at most 1) is optimal.
Verify: if all packets have size base except rem packets with size base+1, then total is (k−rem)·base + rem·(base+1) = k·base + rem = ⌊N/k⌋·k + (N mod k) = N. ✓
Follow-ups
(F1) Minimum packet size too: If each packet must also be ≥ L, feasibility requires L·k ≤ N ≤ C·k. Distribution becomes a constrained problem (L to C per packet).
(F2) Fixed k (given externally): Skip Step 1; go directly to even distribution.
(F3) Cost proportional to max packet: This is the objective here; min-max is the dual of minimizing the largest packet.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Ceiling division + modular arithmetic | O(k) | O(k) | Default; closed-form. |
| Binary search on max size | O(log C) | O(k) | Over-engineered. |
| Greedy bin-packing | O(n·log k) | O(k) | If packets have heterogeneous constraints. |
93. The Skyline Problem
Problem Statement
List of buildings represented as [left, right, height]. Output the skyline as a list of key points [x, height] where height changes occur, sorted by x-coordinate, with no consecutive duplicate heights.
Input: List of buildings; each building is [l, r, h].
Output: Skyline points.
Constraints: 1 ≤ n ≤ 10^4.
Example: Buildings: [[2,9,10],[3,7,15],[5,12,12]] Skyline: [[2,10],[3,15],[7,12],[12,0]].
Solution
Use a sweep-line algorithm with a max-heap. Create events at each building’s start (with negative height for sorting) and end (with 0 height). At each event, maintain the set of active building heights in a max-heap. When the maximum height changes, emit a key point.
#include <bits/stdc++.h>
using namespace std;
vector<pair<int,int> > skyline(vector<vector<int> > buildings) {
vector<tuple<int,int,int> > events;
for (auto &b : buildings) {
events.push_back({b[0], -b[2], b[1]}); // start: negative height
events.push_back({b[1], 0, 0}); // end
}
sort(events.begin(), events.end());
vector<pair<int,int> > result;
priority_queue<pair<int,int> > heap;
heap.push({0, INT_MAX}); // ground
for (auto &ev : events) {
int x = std::get<0>(ev);
int negH = std::get<1>(ev);
int r = std::get<2>(ev);
if (negH != 0) {
heap.push({negH, r});
}
while (!heap.empty() && heap.top().second <= x) {
heap.pop();
}
int curHeight = -heap.top().first;
if (result.empty() || result.back().second != curHeight) {
result.push_back({x, curHeight});
}
}
return result;
}- Time:
O(n log n)for sorting and heap operations. - Space:
O(n).
Discussion
The sweep-line + lazy-heap approach avoids explicit removal at building ends. Instead, we lazily discard expired buildings when they bubble to the top. This elegantly handles overlaps: the max at each x-coordinate is what matters.
The negative height encoding ensures that at the same x, start events (taller buildings first) are processed before end events, avoiding spurious height drops.
Follow-ups
(F1) Output total area under skyline: Integrate: for each consecutive pair of x-coordinates, add (x2 − x1) × height_between.
(F2) Dynamic skyline: Add/remove buildings online; use a segment tree or balanced BST for O(log n) per update.
(F3) 3D skylines: Fundamentally harder; no standard efficient algorithm.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Sweep + lazy heap | O(n log n) | O(n) | Default; elegant. |
| Sweep + sorted multiset | O(n log n) | O(n) | Similar; explicit removal on end. |
| Divide & conquer | O(n log n) | O(n) | Alternative; harder to code. |
94. Unique Paths with Diagonal Moves
Problem Statement
m×n grid. Count the number of distinct paths from bottom-left corner (m−1, 0) to top-right corner (0, n−1) using three types of moves: up, right, or diagonal up-right.
Input: Integers m (rows), n (columns).
Output: Total count of distinct paths.
Constraints: 1 ≤ m, n ≤ 100.
Example: m=2, n=2: paths are (1 right, 1 up), (1 up, 1 right), (1 diagonal). Count = 3.
Solution
Let dp[i][j] = number of paths from (0,0) to (i,j). Recurrence: dp[i][j] = dp[i-1][j] + dp[i][j-1] + dp[i-1][j-1]. Base case: dp[0][0] = 1, all dp[0][j] = 1, all dp[i][0] = 1.
#include <bits/stdc++.h>
using namespace std;
long long uniquePaths(int m, int n) {
vector<vector<long long> > dp(m, vector<long long>(n, 0));
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (i == 0 || j == 0) {
dp[i][j] = 1;
} else {
dp[i][j] = dp[i-1][j] + dp[i][j-1] + dp[i-1][j-1];
}
}
}
return dp[m-1][n-1];
}- Time:
O(m·n). - Space:
O(m·n)orO(n)with rolling array.
Discussion
This is a direct generalization of the classic 2-move grid DP. The diagonal move is simply an extra term. The result is the Delannoy number D(m-1, n-1), which counts lattice paths from (0,0) to (m-1, n-1) with moves {±1 row, ±1 col} in any combination (including diagonals).
For small m, n, DP is practical. For very large m, n, the closed form D(a,b) = sum_{k=0}^{min(a,b)} C(a,k) · C(b,k) · 2^k can be faster (O(min(m,n)) arithmetic ops).
Follow-ups
(F1) Paths of exact length L: Introduce a third dimension: dp[i][j][len]. O(m·n·L) time.
(F2) Min-cost path: Track minimum cost instead of count; recurrence becomes min of three costs.
(F3) Obstacles: Set dp[i][j] = 0 for blocked cells; omit them from recurrence.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| DP table | O(m·n) | O(m·n) | Standard; simple. |
| Rolling array | O(m·n) | O(n) | Space-conscious. |
| Closed-form (Delannoy) | O(min(m,n)) | O(1) | Very large m, n. |
95. Pi Digits Match Index Positions
Problem Statement
Given the first n digits of Pi as a string (0-indexed internally, 1-indexed logically), find all indices i (1-indexed) such that the decimal representation of i appears as a substring in Pi, ending exactly at position i.
Input: String of Pi digits (e.g., “314159265…”), length n.
Output: List of indices (1-indexed) where the match occurs.
Constraints: 1 ≤ n ≤ 10^6.
Example: Pi = “31415926535…” i=5: str(5)=“5”, pi[5]=“5” (0-indexed pi[4]). Match at 1-indexed position 5. ✓ i=13: str(13)=“13”, pi[12..13]=“13” (0-indexed pi[11..12]). Match. ✓
Solution
For each index i from 1 to n, compute k = length of str(i). Check if pi[i−k..i−1] (0-indexed) equals str(i). Record matches.
#include <bits/stdc++.h>
using namespace std;
vector<int> piIndexMatches(string pi) {
vector<int> matches;
int n = (int)pi.length();
for (int i = 1; i <= n; i++) {
string s = to_string(i);
int k = (int)s.length();
if (i - k < 1) continue;
// pi[1..n] is pi[0..n-1] internally
// pi[i-k+1..i] (1-indexed) is pi[i-k..i-1] (0-indexed)
// But pi string is 0-indexed, so pi[i-k..i) in C++ substring
string sub = pi.substr(i - k, k);
if (sub == s) {
matches.push_back(i);
}
}
return matches;
}- Time:
O(n log n)where log n is the average substring length (log₁₀ i). - Space:
O(n)for output in worst case.
Discussion
The main pitfall is off-by-one indexing. The problem uses 1-indexed positions, but C++ strings are 0-indexed. Careful bookkeeping is essential. The substring comparison is O(k) = O(log i), so the total time is acceptable even for n=10^6.
Follow-ups
(F1) Streaming Pi: Maintain a sliding buffer of log₁₀(current_index) characters. For each new digit appended, check all indices whose string endpoints align with the new position.
(F2) Match anywhere, not just at position i: Use KMP or rolling hash to find all occurrences of each i’s string in Pi.
(F3) First k matches: Terminate early after finding k matches.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Linear scan with substr | O(n log n) | O(n) | Default; clear. |
| Precompute suffix array | O(n log n) | O(n) | If many queries needed. |
| Rolling hash | O(n) avg | O(n) | Faster on average. |
96. Daily Jobs Scheduling With Overflow
Problem Statement
A server runs a fixed set of jobs daily, each with a scheduled (start, end) time. Overlapping jobs are merged into a single interval. If the merged total duration exceeds the day length D, overflow spills into the next day. Question: does the server reach a steady state (repeating schedule)?
Input: List of jobs with (start, end) times; day length D.
Output: Boolean indicating whether steady state is reached.
Constraints: Assume jobs are consistent each day; analyze cycle detection.
Example: Jobs: [(0, 5), (4, 7)] merge to [(0, 7)]. If D=10, total 7 ≤ 10, so trivially steady (same jobs each day). If D=5, overflow 2 units → next day starts with 2 units of deferred work.
Solution
Use a state-based simulation: each day, merge overlapping jobs, compute total duration, and simulate overflow. Track the carry-forward state. Detect cycles using a hash map of seen states. If a repeat is found, steady state exists.
#include <bits/stdc++.h>
using namespace std;
typedef pair<int,int> pii;
vector<pii> mergeIntervals(vector<pii> ivs) {
if (ivs.empty()) return {};
sort(ivs.begin(), ivs.end());
vector<pii> out;
out.push_back(ivs[0]);
for (int i = 1; i < (int)ivs.size(); i++) {
if (ivs[i].first <= out.back().second) {
out.back().second = max(out.back().second, ivs[i].second);
} else {
out.push_back(ivs[i]);
}
}
return out;
}
bool reachSteadyState(vector<pii> jobs, int D, int maxDays = 10000) {
map<vector<pii>, int> seen;
vector<pii> current = mergeIntervals(jobs);
for (int day = 0; day < maxDays; day++) {
if (seen.find(current) != seen.end()) return true;
seen[current] = day;
int total = 0;
for (auto &p : current) total += p.second - p.first;
if (total <= D) {
current = mergeIntervals(jobs);
} else {
vector<pii> carry;
int done = 0;
for (auto &p : current) {
int dur = p.second - p.first;
if (done + dur <= D) {
done += dur;
} else {
int cut = p.first + (D - done);
carry.push_back({cut, p.second});
break;
}
}
current = mergeIntervals(jobs);
for (auto &c : carry) current.push_back(c);
current = mergeIntervals(current);
}
}
return false;
}- Time: O(state_count · merging_cost), bounded by cycle length.
- Space: O(state_count) for the hash map.
Discussion
The finite state-space principle guarantees eventual cycle detection (or termination). Since each carry-over state is a bounded list of intervals, the space of possible states is finite.
Interviewers often appreciate recognizing that this is a dynamical system problem: the state space is finite, so convergence is guaranteed. The question becomes whether a cycle is detected within reasonable iterations.
Follow-ups
(F1) Detect cycle length: Track the first and second occurrences of a state; cycle length = day2 − day1.
(F2) Simulate k days: Compute steady-state structure, then project forward.
(F3) Job scheduling with priority: Change merge strategy; may alter steady-state behavior.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Simulation with hashing | O(states · merge) | O(states) | Default; finite state space. |
| Floyd cycle detection | O(states · merge) | O(1) | Memory-constrained. |
| Mathematical analysis | Problem-specific | O(1) | If special structure exists. |
97. K Smallest Pair Sums
Problem Statement
Two sorted arrays arr1 (length n) and arr2 (length m), integer k. Return the k pairs (arr1[i], arr2[j]) with the smallest sums arr1[i] + arr2[j].
Input: Sorted arrays arr1, arr2; integer k.
Output: List of k pairs (or fewer if fewer pairs exist).
Constraints: 1 ≤ n, m ≤ 1000; 1 ≤ k ≤ n·m.
Example: arr1=[1,7,11], arr2=[2,4,6], k=3. Output: [(1,2), (1,4), (1,6)] with sums 3, 5, 7.
Solution
Use a min-heap with tuples (sum, i, j). Start with all pairs from row 0: (arr1[0] + arr2[j], 0, j) for j=0..min(m-1, k-1). For each popped pair (sum, i, j), push the next candidate (arr1[i] + arr2[j+1], i, j+1) if j+1 < m and not seen.
#include <bits/stdc++.h>
using namespace std;
vector<pair<int,int> > kSmallestPairs(vector<int> arr1, vector<int> arr2, int k) {
priority_queue<tuple<int,int,int>, vector<tuple<int,int,int> >, greater<tuple<int,int,int> > > heap;
for (int i = 0; i < (int)min((int)arr1.size(), k); i++) {
heap.push({arr1[i] + arr2[0], i, 0});
}
vector<pair<int,int> > result;
while (!heap.empty() && (int)result.size() < k) {
int sum = std::get<0>(heap.top());
int i = std::get<1>(heap.top());
int j = std::get<2>(heap.top());
heap.pop();
result.push_back({arr1[i], arr2[j]});
if (j + 1 < (int)arr2.size()) {
heap.push({arr1[i] + arr2[j+1], i, j+1});
}
}
return result;
}- Time:
O(k log k). - Space:
O(k).
Discussion
The row-based approach (each row i advances monotonically in j) avoids duplicate tracking. The key insight is that we don’t need to enumerate all n·m pairs; only the frontier matters. By popping the smallest pair and pushing its “successors,” we ensure we process pairs in sorted order of their sums.
Follow-ups
(F1) K largest pairs: Negate the sums, use a max-heap, or invert the arrays.
(F2) K smallest sums from M arrays: Extend to M-dimensional state (i1, i2, ..., iM); heap size becomes O(k·M).
(F3) K smallest sums where i ≤ j (same array): Restrict j ≥ i in initialization.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Row-based heap | O(k log k) | O(k) | Default; no visited set. |
| Full visited set | O(k log k) | O(k) | Alternative; handles all orderings. |
| Binary search on sum | O((n+m) log(max_sum) log k) | O(k) | If k is very large. |
98. Grid Unlock Patterns
Problem Statement
N×M grid of dots. Count unlock patterns of length ≥ L where: - Each dot visited at most once. - Moves are straight lines (any direction, any distance). - A move is invalid if an unvisited dot lies strictly between the start and end dots on the line. - Pattern length = number of dots visited.
Input: Integers N, M, L.
Output: Count of valid patterns with length ≥ L.
Constraints: 1 ≤ N, M ≤ 7 (practical for brute DFS); 1 ≤ L ≤ N·M.
Example: 3×3 grid (9 dots), L=4. Count patterns of length 4..9 that satisfy the intermediate-blocking rule.
Solution
Precompute, for each pair of cells (a, b), the bitmask of cells strictly between them on the line (using GCD-based lattice enumeration). Use backtracking with a visited bitmask; at each step, try all unvisited cells that don’t have unvisited blockers.
#include <bits/stdc++.h>
using namespace std;
int countPatterns(int N, int M, int L) {
int tot = N * M;
vector<pair<int,int> > cells;
for (int r = 0; r < N; r++) {
for (int c = 0; c < M; c++) {
cells.push_back({r, c});
}
}
vector<vector<long long> > between(tot, vector<long long>(tot, 0));
for (int a = 0; a < tot; a++) {
for (int b = 0; b < tot; b++) {
if (a == b) continue;
int r1 = cells[a].first, c1 = cells[a].second;
int r2 = cells[b].first, c2 = cells[b].second;
int dr = r2 - r1, dc = c2 - c1;
int g = __gcd(abs(dr), abs(dc));
if (g <= 1) continue;
int sr = dr / g, sc = dc / g;
for (int t = 1; t < g; t++) {
int rr = r1 + t * sr, cc = c1 + t * sc;
between[a][b] |= (1LL << (rr * M + cc));
}
}
}
long long count = 0;
function<void(int,long long,int)> dfs = [&](int pos, long long visited, int len) {
if (len >= L) count++;
if (len == tot) return;
for (int nxt = 0; nxt < tot; nxt++) {
if (visited & (1LL << nxt)) continue;
long long need = between[pos][nxt];
if ((visited & need) != need) continue;
dfs(nxt, visited | (1LL << nxt), len + 1);
}
};
for (int start = 0; start < tot; start++) {
dfs(start, 1LL << start, 1);
}
return (int)count;
}- Time:
O(tot!)worst case; memoization reduces to O(2^tot · tot²). - Space:
O(2^tot)for memoization table.
Discussion
The between-mask precomputation is crucial: we avoid recomputing lattice points on each DFS transition. The bitmask representation allows fast membership and intersection checks.
The intermediate-blocking rule is the crux: a move is valid only if all in-between cells are already visited. This enforces a strict ordering constraint that makes the problem tractable (and prevents certain shortcuts).
Follow-ups
(F1) Exactly length L: Change len >= L to len == L.
(F2) Count distinct shapes (ignoring starting position): Divide by symmetry group size or canonicalize patterns.
(F3) Enumerate, not count: Yield patterns instead of incrementing a counter.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| DFS without memo | O(tot!) | O(tot) | Tiny grids (3×3). |
| DFS with memo | O(2^tot · tot²) | O(2^tot) | Grids up to 4×4 or 5×5. |
| Heuristic pruning | Problem-specific | Problem-specific | Larger grids; approximate. |
99. Delivery Routes — Minimize Dangerous Stops
Problem Statement
Undirected graph with some nodes marked “dangerous.” Two tasks:
Task 1: Find shortest path from src to dst avoiding dangerous nodes (except src/dst).
Task 2: If no safe path exists, find shortest path with minimum dangerous nodes visited; ties broken by minimum total edges.
Input: Graph (n nodes, edges list), src, dst, dangerous node set.
Output: Path length and composition (edges, dangerous stops).
Constraints: 1 ≤ n ≤ 10^4; 1 ≤ edges ≤ 10^5.
Example: Graph: 0-1-2-3, nodes 1 and 2 dangerous. src=0, dst=3. Task 1: direct path 0-1-2-3 has 2 dangerous. No safe path. → -1. Task 2: min dangerous = 2 (any path must pass through 1 or 2). Return (2, 3).
Solution
Task 1: BFS on the subgraph excluding dangerous nodes. Task 2: Dijkstra on a 2D cost space (danger_count, edge_count), compared lexicographically.
#include <bits/stdc++.h>
using namespace std;
typedef pair<int,int> pii;
int shortestAvoid(int n, vector<pii> edges, int src, int dst, set<int> dangerous) {
vector<vector<int> > adj(n);
for (auto &e : edges) {
adj[e.first].push_back(e.second);
adj[e.second].push_back(e.first);
}
set<int> danger(dangerous.begin(), dangerous.end());
danger.erase(src); danger.erase(dst);
vector<int> dist(n, -1);
queue<int> q;
dist[src] = 0;
q.push(src);
while (!q.empty()) {
int u = q.front(); q.pop();
if (u == dst) return dist[u];
for (int v : adj[u]) {
if (danger.count(v) || dist[v] != -1) continue;
dist[v] = dist[u] + 1;
q.push(v);
}
}
return -1;
}
pair<int,int> shortestMinDanger(int n, vector<pii> edges, int src, int dst, set<int> danger_set) {
vector<vector<int> > adj(n);
for (auto &e : edges) {
adj[e.first].push_back(e.second);
adj[e.second].push_back(e.first);
}
vector<pair<int,int> > dist(n, {INT_MAX, INT_MAX});
priority_queue<pair<pair<int,int>, int>, vector<pair<pair<int,int>, int> >, greater<pair<pair<int,int>, int> > > pq;
int start_cost = (danger_set.count(src) ? 1 : 0);
dist[src] = {start_cost, 0};
pq.push({{start_cost, 0}, src});
while (!pq.empty()) {
auto p = pq.top(); pq.pop();
int dc = p.first.first, ec = p.first.second;
int u = p.second;
if (make_pair(dc, ec) > dist[u]) continue;
if (u == dst) return {dc, ec};
for (int v : adj[u]) {
int nd = dc + (danger_set.count(v) ? 1 : 0);
int ne = ec + 1;
if (make_pair(nd, ne) < dist[v]) {
dist[v] = {nd, ne};
pq.push({{nd, ne}, v});
}
}
}
return {-1, -1};
}- Time:
O(V + E)for Task 1 (BFS);O((V + E) log V)for Task 2 (Dijkstra). - Space:
O(V + E).
Discussion
Task 1 is straightforward BFS filtering. Task 2 introduces tuple comparison: lexicographic ordering on (danger, edges) makes Dijkstra work naturally on multi-objective optimization. The heap respects tuple ordering, so the first path to dst is optimal.
Follow-ups
(F1) Weighted edges: Extend the cost tuple to (danger, weight) or (danger, weight, edge_count) depending on priorities.
(F2) Limit K dangerous stops: State becomes (node, dangerous_used); Dijkstra on expanded state space.
(F3) Real-time dynamic graph: Recompute on each edge update; may use caching or incremental algorithms.
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| BFS (Task 1) | O(V+E) | O(V) | Default for unweighted. |
| Dijkstra tuples (Task 2) | O((V+E) log V) | O(V) | Multi-objective; standard. |
| 0/1 BFS + deque | O(V+E) | O(V) | If costs are 0 or 1. |
100. Max Subarray Sum Between Equal Values
Problem Statement
Given an array of integers, find the maximum sum of a subarray whose first and last elements are equal. The subarray must have at least two occurrences of that value (at the boundaries).
Input: Array of integers.
Output: Maximum sum, or None if no such subarray exists.
Constraints: 1 ≤ n ≤ 10^5.
Example: Array: [1, 2, 3, 2, 5, 3]. Subarray [3, 2, 5, 3] (indices 2..5) has equal endpoints (value 3) and sum 13. Subarray [2, 3, 2] (indices 1..3) has equal endpoints (value 2) and sum 7. Maximum: 13.
Solution
Compute prefix sums. For each value v, track the minimum prefix sum just before the first occurrence of v. When encountering v again at index j, candidate sum = prefix[j] − min_prefix[v]. Update min_prefix[v] for future occurrences.
#include <bits/stdc++.h>
using namespace std;
long long maxSumEqualEndpoints(vector<int> arr) {
int n = (int)arr.size();
long long best = LLONG_MIN;
map<int, long long> minPref;
long long prefix = 0;
for (int j = 0; j < n; j++) {
int v = arr[j];
prefix += v;
if (minPref.find(v) != minPref.end()) {
best = max(best, prefix - minPref[v]);
}
long long pj_minus_1 = prefix - arr[j];
if (minPref.find(v) == minPref.end() || pj_minus_1 < minPref[v]) {
minPref[v] = pj_minus_1;
}
}
return (best == LLONG_MIN) ? 0 : best;
}- Time:
O(n). - Space:
O(u)where u = distinct values.
Discussion
The core idea is similar to “max subarray with given sum” (LC 560). For a fixed value v, we want to pair two occurrences of v at indices i < j, maximizing prefix[j] − prefix[i−1] (the sum of arr[i..j]). To maximize this, given j, we minimize prefix[i−1] over all earlier occurrences of v.
Follow-ups
(F1) Min sum between equal endpoints: Track max_prefix[v] instead; candidate = max_prefix[v] − prefix[j].
(F2) At most k intermediates between endpoints: Use a sliding window with two pointers, tracking count of distinct values.
(F3) Each value’s best pair separately: Return a dictionary mapping value → (best_sum, indices).
Trade-offs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Prefix sum + map | O(n) | O(u) | Default; one-pass. |
| Brute force pairs | O(n²) | O(1) | Tiny arrays. |
| Segment tree / sparse table | O(n log n) | O(n) | Range query extensions. |