algoverse

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:

  1. Problem Statement — a clean, unambiguous specification.
  2. Solution — the intended approach with a complete C++11 implementation.
  3. Discussion — why the approach works, key observations, common pitfalls.
  4. Follow-ups — realistic extensions to push on.
  5. 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:

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;
}

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:

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:

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;
}

(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:

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:

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); }
};

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:

  1. For each edge, compute the time interval [t_add, t_remove) during which it exists.
  2. Place each such interval on the O(log q) nodes of a segment tree whose leaves are timestamps.
  3. 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:

  1. 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.
  2. 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.
  3. 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:

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:

  1. If we strip the .s from both strings, the resulting sequences of Ls and Rs must be identical. (Pieces never swap relative order.)
  2. The i-th non-dot in start must be able to reach the i-th non-dot in target given its movement direction:
    • An L only moves left, so its index in start is >= its index in target.
    • An R only moves right, so its index in start is <= its index in target.

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;
}

Part B — Circular. The same “pieces cannot pass each other” invariant holds, but now “order” means cyclic order. The reachability check becomes:

  1. Strip the .s from both strings. Let the resulting sequences be A and B, with original positions posA and posB.
  2. B must be a cyclic rotation of A. (If it isn’t even that, no sequence of moves can produce it.) You can detect this with the classic B is a substring of A + A test, which gives you the rotation offset off.
  3. Once you have off, piece i in A must move from posA[i] to posB[(i - off + m) % m], where m = |A|. Each piece must travel only in its own direction (L decreases index mod n, R increases index mod n), 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;
}

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:

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).

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:

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:

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;
    }
};

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;
}

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:

(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:

  1. Compute each edge’s lifetime [t_add, t_remove) by scanning the logs.
  2. Place each lifetime on the O(log m) nodes of a segment tree whose leaves are the distinct timestamps.
  3. 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.

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:

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:

Contradictory operations (e.g., peer(a, b) when a and b already have different managers) should be rejected.

Solution

Maintain two structures:

  1. A peer DSU grouping employees who must share the same direct manager.
  2. 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;
    }
};

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:

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.

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:

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):

  1. Insert p into its cell.
  2. Collect candidate points from the 3x3 block around p.
  3. For each pair (q, r) of candidates other than p, verify all three pairwise distances are below T. 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:

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;
}

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:

Your design must state two things explicitly:

  1. Whether the index pointer follows the element it was pointing at (if that element was inside the moved range) or the absolute numeric position.
  2. Whether to is 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;
}

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;
}

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;
}

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;
}

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;
}

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:

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());
}

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
}

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:

  1. Strings are returned in order of non-decreasing length, and
  2. No character appears more than T times 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:

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;
    }
};

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];
}

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

  1. Flood-fill the island containing (r, c) to mark all land cells.
  2. For each water cell adjacent to the island, perform a flood-fill and check if it touches the grid boundary.
  3. 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;
}

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: - +xtotal = total + x, prevMul = x - -xtotal = total - x, prevMul = -x - *xtotal = 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;
}

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;
}

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;
}

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;
}

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;
}

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);
    }
};

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;
}

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;
}

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;
}

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;
}

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);
    }
};

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];
    }
};

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.

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];
}

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;
}

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;
}

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;
    }
};

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()];
    }
};

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;
}

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;
}

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;
}

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;
}

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;
}

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;
}

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;
}

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;
}

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;
}

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;
}

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;
}

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;
}

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;
}

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;
}

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)];
    }
};

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;
}

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;
}

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;
}

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;
}

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;
}

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];
}

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;
}

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;
}

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;
}

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;
}

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;
}

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;
}

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;
}

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;
}

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;
}

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;
}

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;
}

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;
}

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;
}

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;
}

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;
}

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;
}

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;
}

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;
}

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;
}

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;
}

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;
}

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());
}

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;
}

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);
}

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;
}

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;
}

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;
}

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;
}

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];
}

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;
}

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;
}

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;
}

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;
}

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};
}

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;
}

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.