Skip to content

Technique Layering: When One Technique Isn't Enough

Quick Revisit

  • USE WHEN: single algorithm solves part of problem but not all — need two+ techniques composed together
  • INVARIANT: decompose into outer structure (search/traversal) and inner structure (query/decision)
  • TIME: product of component complexities | SPACE: sum of component spaces
  • CLASSIC BUG: applying techniques in wrong order — outer technique must "call" the inner one, not vice versa
  • PRACTICE: 08-ladder-mixed

Most Codeforces 1800+ problems aren't about a single algorithm. They're about recognizing that the problem decomposes into two or three known techniques stacked together. The skill isn't knowing segment trees or binary search -- it's seeing that you need a segment tree inside a binary search.

Difficulty: [Advanced]CF Rating Range: 1800-2400 Prerequisites: Problem Pattern Recognition, familiarity with techniques from Chapters 1-9

Contents


Why Problems Layer Techniques

A 1400-rated problem says: "Find the shortest path." You run BFS. Done.

An 1800-rated problem says: "Find the shortest path, but you have k keys to collect and doors only open with the right key." Now BFS alone isn't enough -- you need BFS on a state graph where each state is (position, key bitmask). That's BFS + DP (bitmask).

The jump from 1400 to 1800 is rarely about knowing a harder algorithm. It's about combining two algorithms you already know. Here's a systematic catalog.

The combination catalog

For each combination, name four things: the outer layer (the loop or search structure), the inner layer (the per-step engine), the state passed between them, and the total complexity they yield together.

#CombinationOuterInnerState passedTotalAha moment
1BFS / Dijkstra + bitmask DPBFS over an expanded state graphedge relaxation(node, mask)O((V2k)+(E2k))"I need to revisit nodes" → more dimensions in the state
2Binary search + greedy/DP feasibilitybinary search on the numeric answergreedy or DP feasibility checkcandidate answer MO(logAcheck)"Minimize the max / maximize the min" → "is M achievable?"
3DSU + sorted-edge greedysweep edges in sorted orderDSU unite / findcurrent component partitionO(ElogE+Eα(n))"merge components in cost order" → Kruskal pattern (and its cousins)
4Monotonic stack + DP / direct formulaone pass with the stackthe formula evaluated at each popleft/right boundary of each elementO(n)"I need boundaries defined by ordering" → monotonic stack
5Monotonic stack + sparse tablestack gives a range [L,R] for each elementO(1) RMQ inside that rangethe range itselfO(nlogn) preprocess, O(n) queries"stack gives ranges, but I need aggregate info inside"
6Trie + XORone query per numberwalk binary trie MSB→LSBpartial XOR prefixO(nlogV)"XOR is bit-independent" → process bits in a trie
7Euler tour + BIT / segment treeDFS to flatten tree1D point-update / range-sum on flattened arrayhalf-open range [tin[v], tout[v])O((n+q)logn)"subtree operation" → flatten, then use a 1D structure
8Heavy-light decomposition + segment treepath decompositionsegment tree per chainchain index + chain offsetO((n+q)log2n)"path query/update on a tree" → cut into O(logn) chains
9Mo's algorithm + frequency countersorted query orderadd/remove pointers around windowleft, right, current answerO((n+q)n)"offline range queries with no clean structure"

Every entry follows the same shape: the outer layer iterates / searches; the inner layer answers a fast question; state flows from outer to inner.

When recognizing the layering lets you collapse it

The most interesting cases are the ones where, after you spot the layering, you realize the two layers can be fused into one.

  • Binary search + segment-tree query → walk the segment tree. "Find the smallest prefix index where sumX" looks like O(log2n) per query: binary search the index, query the segment tree at each step. But the segment tree already encodes the search structure. Walk it directly — at each node, descend into the left child if its sum already reaches X, otherwise subtract and go right. One logn instead of two. Same idea: "k-th 1 in a 0/1 array", "first index with value X".

  • Two pointers + prefix sums → a single pass. A naive layering tracks a window with two pointers and queries a prefix-sum array inside the loop. When the window invariant is monotone, the prefix sum reduces to a running total maintained alongside the pointers — no array needed.

  • Sort + binary-search-the-answer → greedy on sorted input. A common 1800–2100 trap: binary searching the answer when sorting plus a single greedy pass already settles it. Always pause and ask: if I sort, does the answer become a one-pass scan?

The lesson: layering is a hypothesis. Implement the obvious two-layer solution mentally, then look for a fusion before you actually code.

How to Spot a Layered Problem

Three signals that a problem needs more than one technique:

  1. Multiple constraints. "Shortest path" (BFS) + "with at most k stops" (state DP). Two constraints often mean two techniques.

  2. Preprocessing step implied. The problem's inner loop is expensive, but a different structure (sorted order, tree flattening, sparse table) makes each iteration cheap. If you think "if only I could answer X in O(1)..." -- that's the second layer.

  3. "For each X, compute Y." The phrase "for each node / for each query / for each element" suggests the outer structure, while "compute Y" suggests the inner technique. Example: "for each node, find the sum of values in its subtree" -> Euler tour (outer) + BIT (inner).

Worked Example: BFS on a Product Graph

Problem: Given an n×m grid with k6 keys (labeled a-f) and corresponding locked doors (labeled A-F), find the shortest path from start to goal.

cpp
#include <bits/stdc++.h>
using namespace std;

int bfs_keys(vector<string>& grid, int n, int m) {
    // State: (row, col, keys_bitmask)
    int full = 0;
    int sr, sc;
    for (int i = 0; i < n; i++)
        for (int j = 0; j < m; j++) {
            if (grid[i][j] == '@') sr = i, sc = j;
            if (grid[i][j] >= 'a' && grid[i][j] <= 'f')
                full |= 1 << (grid[i][j] - 'a');
        }

    // dist[r][c][mask] = shortest distance
    vector dist(n, vector(m, vector<int>(1 << 6, INT_MAX)));
    queue<tuple<int,int,int>> q;
    dist[sr][sc][0] = 0;
    q.push({sr, sc, 0});
    int dx[] = {0,0,1,-1}, dy[] = {1,-1,0,0};

    while (!q.empty()) {
        auto [r, c, mask] = q.front(); q.pop();
        if (mask == full) return dist[r][c][mask];
        for (int d = 0; d < 4; d++) {
            int nr = r+dx[d], nc = c+dy[d];
            if (nr<0||nr>=n||nc<0||nc>=m||grid[nr][nc]=='#') continue;
            char ch = grid[nr][nc];
            if (ch>='A' && ch<='F' && !(mask & (1<<(ch-'A')))) continue;
            int nmask = mask;
            if (ch>='a' && ch<='f') nmask |= 1<<(ch-'a');
            if (dist[nr][nc][nmask] > dist[r][c][mask]+1) {
                dist[nr][nc][nmask] = dist[r][c][mask]+1;
                q.push({nr, nc, nmask});
            }
        }
    }
    return -1;
}

Two techniques: BFS (shortest path on unweighted graph) + bitmask DP (tracking which keys are held). Neither alone solves the problem.

Worked Example: Euler Tour + BIT

Problem: Rooted tree, support "add x to node v" and "query sum of subtree of v."

cpp
#include <bits/stdc++.h>
using namespace std;

int timer = 0;
vector<int> tin, tout;
vector<vector<int>> adj;
int bit[200005];
int n;

void dfs(int v, int p) {
    tin[v] = ++timer;
    for (int u : adj[v]) if (u != p) dfs(u, v);
    tout[v] = timer;
}

void update(int i, int delta) {
    for (; i <= n; i += i & (-i)) bit[i] += delta;
}

int query(int i) {
    int s = 0;
    for (; i > 0; i -= i & (-i)) s += bit[i];
    return s;
}

// Add x to node v:       update(tin[v], x)
// Query subtree sum of v: query(tout[v]) - query(tin[v] - 1)

Euler tour maps subtrees to contiguous ranges; BIT answers range-sum queries. The combination turns a tree problem into an array problem.

Practice Problems

ProblemTechniquesCF Rating
CF 877D - Olya and Energy DrinksBFS + Deque (0-1 BFS variant)2100
CF 1354D - MultisetSegment Tree + Binary Search (walk on tree)1900
CF 1095F - Make It ConnectedDSU + Greedy (Kruskal-style)1900
CF 1760G - SlavicG's Favorite ProblemBFS + XOR state1900
CF 1700E - Serega and Segment TreeSegment Tree + analysis2300
CF 706D - Vasiliy's MultisetTrie + XOR1800
CF 838B - Diverging DirectionsEuler Tour + Segment Tree2200
CF 1555E - Boring SegmentsBinary Search + Segment Tree2200

Built for the climb from Codeforces 1100 to 2100.