콘텐츠로 이동

Competitive Programming Internals — Under the Hood

Antti Laaksonen's Competitive Programmer's Handbook · Internal Mechanics

Not a tutorial. This document maps selected techniques from the cited handbook to memory structures, complexity transitions, DP table order, and bit operations.


Reading contract: Source scope is the July 3, 2018 handbook draft named at the end. Judge throughput, recursion limits, integer widths, memory limits, and library costs vary by platform, so the tables are estimation heuristics, not acceptance guarantees. Before selecting an algorithm, bind the claim to the exact constraints, input distribution, numeric range, data-structure variant, and worst-case or expected bound. Selection is complete only when correctness and resource bounds fit the judge limits with margin; a timeout, overflow, excessive memory use, or violated invariant returns the choice to an unverified state.

1. Time Complexity — The Internal Machine Model

1.1 Complexity Estimation Table — What Actually Runs

Complexity classes constrain growth, while actual runtime depends on the judge hardware, language, constants, cache behavior, and time limit. The often-used ~10⁸ simple operations/second figure is only a rough sizing heuristic and must be calibrated against the target judge.

flowchart TD
    subgraph INPUT_SIZE["Input Size n"]
        N1["n ≤ 10"]
        N2["n ≤ 20"]
        N3["n ≤ 500"]
        N4["n ≤ 5000"]
        N5["n ≤ 10⁶"]
        N6["n ≤ 10⁷–10⁸"]
    end

    subgraph COMPLEXITY["Target Complexity"]
        C1["O(n!) — permutation generation"]
        C2["O(2ⁿ) — subset enumeration"]
        C3["O(n³) — 3-nested loops, matrix multiply"]
        C4["O(n²) — 2-nested loops"]
        C5["O(n log n) — sort, tree ops"]
        C6["O(n) — linear scan"]
    end

    N1 --> C1
    N2 --> C2
    N3 --> C3
    N4 --> C4
    N5 --> C5
    N6 --> C6

1.2 Maximum Subarray Sum — Three Algorithm Generations

The classic O(n³)→O(n²)→O(n) reduction demonstrates how memory access patterns change complexity:

sequenceDiagram
    participant ARRAY as Array a[0..n-1]
    participant ALG as Algorithm
    participant BEST as Best Sum

    Note over ALG: O(n³) — Triple loop
    loop All O(n²) pairs (i,j)
        loop Sum from i to j
            ALG->>ARRAY: Read a[k] for k in [i..j]
            ALG->>BEST: Compare sum(i..j) with best
        end
    end

    Note over ALG: O(n²) — Prefix sum trick
    Note over ARRAY: Precompute prefix[k] = sum(0..k-1) in one pass
    loop All O(n²) pairs (i,j)
        ALG->>ARRAY: sum(i..j) = prefix[j+1] - prefix[i]   ← O(1)!
        ALG->>BEST: Update best
    end

    Note over ALG: O(n) — Kadane's algorithm
    Note over ALG: State: max_ending_here, max_so_far
    loop Each element a[k]
        ALG->>ALG: max_ending_here = max(a[k], max_ending_here + a[k])
        ALG->>BEST: max_so_far = max(max_so_far, max_ending_here)
    end

Kadane's state machine — the invariant that makes O(n) work:

stateDiagram-v2
    [*] --> POSITIVE: max_ending_here > 0
    POSITIVE --> POSITIVE: a[k] added keeps sum positive
    POSITIVE --> RESET: a[k] makes sum ≤ 0
    RESET --> POSITIVE: a[k] > 0 — start fresh subarray here
    RESET --> RESET: a[k] ≤ 0 — continue discarding

    note right of POSITIVE
        max_ending_here = max(a[k], max_ending_here + a[k])
        If max_ending_here resets to a[k]:
        this element starts a new candidate subarray
    end note

2. Sorting Internals — Memory Movement Patterns

2.1 Merge Sort — Cache Line Behavior

Merge sort's recursive halving creates a call stack that determines memory access locality:

sequenceDiagram
    participant STACK as Call Stack
    participant MEM as Memory (array)
    participant MERGE as Merge Buffer

    STACK->>MEM: mergesort(0, n-1)
    STACK->>STACK: push frame: mergesort(0, n/2)
    STACK->>STACK: push frame: mergesort(0, n/4)
    Note over STACK: Recurse down to base cases (1-element)
    Note over STACK: Stack depth: O(log n)

    STACK->>MERGE: merge(0, n/4, n/2)
    MERGE->>MEM: Read L[0..n/4] and R[n/4..n/2] sequentially
    MERGE->>MEM: Write merged result to temp buffer
    Note over MERGE: O(n) work, O(n) temp memory
    Note over MERGE: Each merge pass touches 2 cache lines simultaneously

    STACK->>MERGE: merge(0, n/2, n-1) — final merge
    MERGE-->>MEM: Write final sorted array

Sorting algorithm comparison — internal operation counts:

flowchart LR
    subgraph COUNTING["Counting Sort O(n+k)"]
        CS1["Pass 1: count[a[i]]++\nfor each element\n→ populates count[] array"]
        CS2["Pass 2: compute prefix sums\ncount[i] += count[i-1]\n→ output positions"]
        CS3["Pass 3: place elements\noutput[count[a[i]]-1] = a[i]\n→ stable placement"]
        CS1 --> CS2 --> CS3
    end

    subgraph COMPARISON["Comparison-based O(n log n) lower bound"]
        CB1["Decision tree height ≥ log₂(n!) ≈ n log n\nEach comparison → one branch"]
        CB2["Any algorithm needs ≥ n log n comparisons\nin the worst case"]
        CB1 --> CB2
    end

2.2 Binary Search — Invariant Maintenance

stateDiagram-v2
    [*] --> SETUP: lo=0, hi=n-1
    SETUP --> LOOP: Invariant: answer in [lo..hi]
    LOOP --> CHECK: mid = (lo+hi)/2
    CHECK --> LEFT: a[mid] > target → hi=mid-1
    CHECK --> RIGHT: a[mid] < target → lo=mid+1
    CHECK --> FOUND: a[mid] == target
    LEFT --> LOOP: Invariant maintained: answer in [lo..mid-1]
    RIGHT --> LOOP: Invariant maintained: answer in [mid+1..hi]
    FOUND --> [*]: Return mid
    LOOP --> NOTFOUND: lo > hi
    NOTFOUND --> [*]: Return -1

    note right of LOOP
        Each iteration: search space halves
        [lo..hi] shrinks from n to 1 in log₂(n) steps
    end note

3. Data Structures — Memory Layout and Operation Costs

3.1 Dynamic Arrays — Amortized Doubling

flowchart TD
    subgraph GROWTH["Vector Doubling Strategy"]
        G1["capacity=1, size=0\n[_]"]
        G2["push_back: capacity=2\n[a|_]"]
        G3["push_back: capacity=4\n[a|b|_|_]"]
        G4["push_back: capacity=8\n[a|b|c|d|_|_|_|_]"]
        G1 --> G2 --> G3 --> G4
    end

    subgraph AMORTIZED["Amortized Analysis"]
        A1["Copy cost when doubling:\n1+2+4+...+n = 2n total copies\nfor n pushbacks"]
        A2["Amortized O(1) per pushback\n= O(n) total / n operations"]
        A3["Worst case single pushback: O(n)\n(when doubling triggers)"]
        A1 --> A2 --> A3
    end

3.2 Segment Tree — Internal Node Memory Layout

Segment trees store a complete binary tree in a flat array with the 1-indexed parent-child relationship:

block-beta
    columns 8
    block:ROOT["Index 1\ntree[1]\nsum[0..7]"]:8
    end
    block:L2A["tree[2]\nsum[0..3]"]:4
    block:L2B["tree[3]\nsum[4..7]"]:4
    block:L3A["tree[4]\n[0..1]"]:2
    block:L3B["tree[5]\n[2..3]"]:2
    block:L3C["tree[6]\n[4..5]"]:2
    block:L3D["tree[7]\n[6..7]"]:2
    tree8["[8]\na[0]"]
    tree9["[9]\na[1]"]
    tree10["[10]\na[2]"]
    tree11["[11]\na[3]"]
    tree12["[12]\na[4]"]
    tree13["[13]\na[5]"]
    tree14["[14]\na[6]"]
    tree15["[15]\na[7]"]

Navigation rules: left child = 2k, right child = 2k+1, parent = k/2

sequenceDiagram
    participant QUERY as query(l, r)
    participant TREE as Segment Tree Nodes
    participant RESULT as Accumulated Sum

    Note over QUERY: query(3, 6) — sum of a[3..6]
    QUERY->>TREE: Check node 1 (covers 0..7): not within [3..6]
    TREE->>TREE: Recurse to node 2 (0..3) and node 3 (4..7)
    TREE->>TREE: Node 2 (0..3): partially overlaps [3..6]
    TREE->>TREE: Recurse to node 5 (2..3): partially overlaps
    TREE->>TREE: Node 11 (3..3): fully within [3..6] → ADD tree[11]
    TREE->>RESULT: += tree[11]
    TREE->>TREE: Node 3 (4..7): partially overlaps [3..6]
    TREE->>TREE: Node 6 (4..5): fully within [3..6] → ADD tree[6]
    TREE->>RESULT: += tree[6]
    TREE->>TREE: Node 7 (6..7): partially overlaps
    TREE->>TREE: Node 14 (6..6): fully within → ADD tree[14]
    TREE->>RESULT: += tree[14]
    RESULT-->>QUERY: Return sum = tree[11]+tree[6]+tree[14]
    Note over QUERY: O(log n) nodes visited

3.3 Binary Indexed Tree (Fenwick) — Bit Magic Navigation

The BIT uses LSB (least significant bit) to determine which range each index covers:

flowchart LR
    subgraph BIT_STRUCTURE["BIT Internal Structure"]
        B1["bit[1] = a[1]\nLSB(1)=1 → covers 1 element"]
        B2["bit[2] = a[1]+a[2]\nLSB(2)=2 → covers 2 elements"]
        B3["bit[3] = a[3]\nLSB(3)=1 → covers 1 element"]
        B4["bit[4] = a[1]+a[2]+a[3]+a[4]\nLSB(4)=4 → covers 4 elements"]
        B5["bit[5] = a[5]\nLSB(5)=1 → covers 1 element"]
        B6["bit[6] = a[5]+a[6]\nLSB(6)=2 → covers 2 elements"]
        B7["bit[7] = a[7]\nLSB(7)=1 → covers 1 element"]
        B8["bit[8] = a[1]+...+a[8]\nLSB(8)=8 → covers 8 elements"]
    end

    subgraph QUERY["sum(1..k) traversal"]
        Q1["k=7: add bit[7], k -= LSB(7)=1 → k=6"]
        Q2["k=6: add bit[6], k -= LSB(6)=2 → k=4"]
        Q3["k=4: add bit[4], k -= LSB(4)=4 → k=0"]
        Q4["Done in 3 steps = log₂(8) steps"]
        Q1 --> Q2 --> Q3 --> Q4
    end

    subgraph UPDATE["update(k, delta) traversal"]
        U1["k=3: update bit[3], k += LSB(3)=1 → k=4"]
        U2["k=4: update bit[4], k += LSB(4)=4 → k=8"]
        U3["k=8: update bit[8], k += LSB(8)=8 → k=16 > n"]
        U4["Done in 3 steps = log₂(8) steps"]
        U1 --> U2 --> U3 --> U4
    end

4. Dynamic Programming — Table Fill Mechanics

4.1 Coin Change — State Transition Graph

stateDiagram-v2
    [*] --> DP_INIT: dp[0] = 0, dp[x] = ∞ for x > 0
    DP_INIT --> FILL: For x = 1 to target
    FILL --> TRANSITION: For each coin c in coins
    TRANSITION --> CHECK: if x ≥ c
    CHECK --> UPDATE: dp[x] = min(dp[x], dp[x-c] + 1)
    UPDATE --> FILL: Continue to next x
    FILL --> DONE: dp[target] = min coins needed

    note right of TRANSITION
        dp[x] = min coins to make sum x
        Reachability: if dp[x-c] ≠ ∞
        then we can reach x via coin c
    end note

Memory access pattern — 1D DP fill for coin problem:

block-beta
    columns 7
    IDX["Index:"] IDX0["0"] IDX1["1"] IDX2["2"] IDX3["3"] IDX4["4"] IDX5["5"]
    INIT["Init:"] V0["0"] V1["∞"] V2["∞"] V3["∞"] V4["∞"] V5["∞"]
    COIN1["After c=1:"] W0["0"] W1["1"] W2["2"] W3["3"] W4["4"] W5["5"]
    COIN2["After c=3:"] X0["0"] X1["1"] X2["2"] X3["1"] X4["2"] X5["3"]
    COIN3["After c=4:"] Y0["0"] Y1["1"] Y2["2"] Y3["1"] Y4["1"] Y5["2"]

4.2 Longest Increasing Subsequence (LIS) — Two Approaches

flowchart LR
    subgraph O_N2["O(n²) DP approach"]
        DP1["dp[i] = length of LIS ending at i"]
        DP2["dp[i] = max{dp[j]+1 : j < i, a[j] < a[i]}"]
        DP3["For each i: scan all j < i — O(n) per element"]
        DP1 --> DP2 --> DP3
    end

    subgraph O_NlogN["O(n log n) patience sorting"]
        PS1["Maintain tails[] array:\ntails[k] = smallest tail of any\nIS of length k+1 seen so far"]
        PS2["For each a[i]:\nbinary search for first tail ≥ a[i]"]
        PS3["Replace that tail with a[i]\nOR append a[i] to extend LIS"]
        PS1 --> PS2 --> PS3
    end

    O_N2 --> MEMORY["O(n) memory for dp[]\nO(n²) total ops"]
    O_NlogN --> MEMORY2["O(n) memory for tails[]\nO(n log n) — binary search each step"]

LIS patience sort state evolution for [3, 1, 4, 1, 5, 9, 2, 6]:

sequenceDiagram
    participant A as Array element
    participant T as tails[]
    participant LEN as LIS length

    A->>T: 3 → tails=[3], LEN=1
    A->>T: 1 → replace tails[0]=3 with 1 → tails=[1], LEN=1
    A->>T: 4 → 4>all tails → tails=[1,4], LEN=2
    A->>T: 1 → replace tails[0]=1 with 1 → tails=[1,4], LEN=2
    A->>T: 5 → 5>all tails → tails=[1,4,5], LEN=3
    A->>T: 9 → 9>all tails → tails=[1,4,5,9], LEN=4
    A->>T: 2 → replace tails[1]=4 with 2 → tails=[1,2,5,9], LEN=4
    A->>T: 6 → replace tails[2]=5 with 6 → tails=[1,2,6,9], LEN=4
    Note over T,LEN: Final LIS length = 4 (e.g., [1,4,5,9] or [1,4,5,6])

4.3 Grid Path DP — 2D Memory Fill Order

flowchart TD
    subgraph GRID["Grid DP (count paths to bottom-right)"]
        INIT["dp[0][0] = 1 (start)"]
        FILL["Fill left-to-right, top-to-bottom"]
        RECUR["dp[i][j] = dp[i-1][j] + dp[i][j-1]\n(from above + from left)"]
        BOUND["dp[i][j] = 0 if wall/blocked"]
        INIT --> FILL --> RECUR --> BOUND
    end

    subgraph MEMORY_LAYOUT["Memory Access Pattern"]
        R0["Row 0: [1, 1, 1, 1, ...]"]
        R1["Row 1: [1, 2, 3, 4, ...]"]
        R2["Row 2: [1, 3, 6, 10, ...]"]
        R3["Row 3: [1, 4, 10, 20, ...]"]
        R0 --> R1 --> R2 --> R3
    end

    subgraph OPTIMIZATION["1D Memory Optimization"]
        OPT["Use single dp[n] array\nUpdate in-place left-to-right:\ndp[j] += dp[j-1] per row"]
        OPT2["From O(n²) space to O(n) space\ndp[j] from previous row + dp[j-1] from current row"]
        OPT --> OPT2
    end

4.4 Knapsack — 2D vs 1D Memory and Fill Direction

sequenceDiagram
    participant ITEMS as Items (weight, value)
    participant DP as dp[0..W] array
    participant OPT as Optimal Value

    Note over DP: 0/1 Knapsack: each item usable ONCE
    Note over DP: Fill RIGHT-TO-LEFT to avoid reuse

    loop For each item (w, v)
        loop j from W down to w
            DP->>DP: dp[j] = max(dp[j], dp[j-w] + v)
            Note over DP: Reading dp[j-w] from "previous item's row"
            Note over DP: because we fill right-to-left
        end
    end

    Note over DP: UNBOUNDED Knapsack: each item usable MANY times
    Note over DP: Fill LEFT-TO-RIGHT to allow reuse!
    loop For each item (w, v)
        loop j from w up to W
            DP->>DP: dp[j] = max(dp[j], dp[j-w] + v)
            Note over DP: Reading dp[j-w] from "current item's row"
            Note over DP: because we fill left-to-right
        end
    end

    DP-->>OPT: dp[W] = maximum value

Critical difference: direction of inner loop determines if items can be reused:

flowchart LR
    subgraph RIGHTLEFT["Right-to-left (0/1 knapsack)"]
        RL1["When computing dp[j]:\ndp[j-w] was NOT updated yet\nin this item's pass\n→ comes from previous item row\n→ item used at most once"]
    end

    subgraph LEFTRIGHT["Left-to-right (unbounded knapsack)"]
        LR1["When computing dp[j]:\ndp[j-w] was ALREADY updated\nin this item's pass\n→ item can be included again\n→ item usable multiple times"]
    end

5. Amortized Analysis — Two Pointers and Sliding Window

5.1 Two Pointers — Telescoping State

stateDiagram-v2
    [*] --> INIT: left=0, right=0, window_sum=0
    INIT --> EXPAND: Expand right pointer
    EXPAND --> CHECK: window_sum > target?
    CHECK --> SHRINK: Yes → shrink from left
    CHECK --> EXPAND: No → expand right
    SHRINK --> CHECK: window_sum -= a[left], left++
    EXPAND --> DONE: right == n
    DONE --> [*]: Found minimum window

    note right of EXPAND
        left and right each move
        at most n times total
        → O(n) total moves
        → O(n) amortized
    end note

5.2 Sliding Window Minimum — Monotone Deque

The monotone deque maintains the invariant that front always holds the current window minimum:

sequenceDiagram
    participant A as Array a[0..n-1]
    participant DEQUE as Deque (front=min, back=newest)
    participant OUT as Output minimums

    Note over DEQUE: Window size k=3, process a=[1,3,2,5,4]
    A->>DEQUE: i=0, a[0]=1 → deque: [0] (indices)
    A->>DEQUE: i=1, a[1]=3 → 3>a[0], append → deque: [0,1]
    A->>DEQUE: i=2, a[2]=2 → pop 1(a[1]=3>2), append → deque:[0,2]
    DEQUE->>OUT: Window [0..2]: min = a[deque.front=0] = 1

    A->>DEQUE: i=3, a[3]=5 → pop nothing, append → deque:[0,2,3]
    DEQUE->>DEQUE: Remove front 0 (outside window [1..3])
    DEQUE->>OUT: Window [1..3]: min = a[deque.front=2] = 2

    A->>DEQUE: i=4, a[4]=4 → pop 3(a[3]=5>4), append → deque:[2,4]
    DEQUE->>DEQUE: Remove front 2 (outside window [2..4])
    DEQUE->>OUT: Window [2..4]: min = a[deque.front=2] = 2

    Note over DEQUE: Each element enters/exits deque at most once → O(n) total

6. Bit Manipulation — Register-Level Operations

6.1 Set Representation in Integers

Every subset of {0..n-1} maps to one integer. Operations become single CPU instructions:

block-beta
    columns 9
    B8["bit 8"] B7["bit 7"] B6["bit 6"] B5["bit 5"] B4["bit 4"] B3["bit 3"] B2["bit 2"] B1["bit 1"] B0["bit 0"]
    S8["0"] S7["0"] S6["0"] S5["0"] S4["1"] S3["0"] S2["0"] S1["1"] S0["0"]
    L8[" "] L7[" "] L6[" "] L5[" "] L4["∈S"] L3[" "] L2[" "] L1["∈S"] L0[" "]

Set {1, 4} represented as integer 18 = 2¹ + 2⁴

flowchart LR
    subgraph OPERATIONS["Bit Set Operations (O(1) each)"]
        ADD["Add element x:\nS |= (1 << x)"]
        DEL["Delete element x:\nS &= ~(1 << x)"]
        MEMBER["Test x ∈ S:\n(S >> x) & 1"]
        UNION["Union A ∪ B:\nA | B"]
        INTER["Intersection A ∩ B:\nA & B"]
        DIFF["Difference A \\ B:\nA & ~B"]
        SIZE["Popcount |S|:\n__builtin_popcount(S)"]
    end

    subgraph SUBSET_ITER["Iterate subsets of S"]
        SI1["for b=(b-1)&S; b!=0; b=(b-1)&S"]
        SI2["Each iteration: b = next smaller subset of S"]
        SI3["(b-1) clears lowest set bit and fills below\n& S keeps only bits that were in S"]
        SI1 --> SI2 --> SI3
    end

6.2 Bitmask DP — Subset State Transitions

sequenceDiagram
    participant STATES as DP States (2^n integers)
    participant TRANS as Transition via bitmask
    participant RESULT as Optimal result

    Note over STATES: TSP-like: dp[mask][v] = shortest path\nvisiting exactly vertices in mask, ending at v

    Note over STATES: Base: dp[1<<v][v] = 0 for all v (start anywhere)

    loop For each mask (in increasing order of bits)
        loop For each endpoint v in mask
            loop For each neighbor u NOT in mask
                STATES->>TRANS: new_mask = mask | (1<<u)
                TRANS->>STATES: dp[new_mask][u] = min(dp[new_mask][u],\ndp[mask][v] + dist[v][u])
            end
        end
    end

    STATES-->>RESULT: min over v of dp[(1<<n)-1][v] + dist[v][start]
    Note over RESULT: Time: O(2^n · n²), Space: O(2^n · n)

7. Graph Algorithms — Internal Traversal State

7.1 DFS/BFS — Stack vs Queue Memory Dynamics

flowchart TD
    subgraph DFS_STACK["DFS via Explicit Stack"]
        DS1["Push start node"]
        DS2["Pop node u, mark visited"]
        DS3["Push all unvisited neighbors of u"]
        DS4["Stack holds 'frontier of paths not yet explored'"]
        DS1 --> DS2 --> DS3 --> DS4 --> DS2
    end

    subgraph BFS_QUEUE["BFS via FIFO Queue"]
        BQ1["Enqueue start node, mark distance[start]=0"]
        BQ2["Dequeue node u"]
        BQ3["For unvisited neighbor v:\nmark distance[v] = distance[u]+1, enqueue v"]
        BQ4["Queue holds 'wave frontier at current distance'"]
        BQ1 --> BQ2 --> BQ3 --> BQ4 --> BQ2
    end

    subgraph PROPERTY["Key Property Difference"]
        P1["DFS: explores one path fully before backtracking\nDepth-first = LIFO order"]
        P2["BFS: explores all nodes at distance d before d+1\nBreadth-first = FIFO order → shortest paths in unweighted graphs"]
    end

7.2 Dijkstra — Priority Queue State Evolution

sequenceDiagram
    participant PQ as Priority Queue (min-heap by dist)
    participant DIST as dist[] array
    participant VISITED as visited[] set

    Note over PQ: Initial: push (0, source), dist[source]=0, all others=∞

    loop While PQ not empty
        PQ->>DIST: Pop (d, u) — minimum distance node
        alt u already visited
            Note over PQ: Skip — stale entry in PQ
        else
            DIST->>VISITED: Mark u as visited
            loop For each edge (u, v, w)
                DIST->>DIST: if dist[u] + w < dist[v]
                DIST->>DIST: dist[v] = dist[u] + w
                DIST->>PQ: push (dist[v], v)
                Note over PQ: Lazy deletion: old (dist_old, v) still in PQ\nwill be ignored when popped (u already visited)
            end
        end
    end
    Note over DIST: All dist[] = shortest path distances from source

7.3 Bellman-Ford — Edge Relaxation Wave

flowchart TD
    subgraph BELLMAN_FORD["Bellman-Ford Rounds"]
        R0["Round 0: dist[source]=0, all others=∞"]
        R1["Round 1: Relax all m edges\n→ correct shortest paths using ≤1 edge"]
        R2["Round 2: Relax all m edges again\n→ correct shortest paths using ≤2 edges"]
        RK["Round k: correct for ≤k edge paths"]
        DONE["Round n-1: correct for all paths (if no negative cycle)\nif any improvement in Round n → NEGATIVE CYCLE exists"]
        R0 --> R1 --> R2 --> RK --> DONE
    end

    subgraph NEG_DETECT["Negative Cycle Detection"]
        ND1["Run n rounds instead of n-1"]
        ND2["If dist[v] still decreases in round n:\nnegative cycle reachable to v"]
        ND1 --> ND2
    end

8. String Algorithms — Pattern Matching Internals

8.1 Z-Algorithm — Z-Array Construction

The Z-array at index i stores the length of the longest substring starting at i that matches a prefix of the string:

sequenceDiagram
    participant S as String s
    participant Z as Z[] array
    participant WIN as [l, r] current Z-box

    Note over S,WIN: s = "aabxaa", compute Z[]
    Note over WIN: Z[0] = n by convention (whole string)

    loop i from 1 to n-1
        alt i > r (outside current Z-box)
            Z->>S: Naive compare: s[0..?] vs s[i..?]
            S-->>Z: Z[i] = matched length
            Z->>WIN: Update l=i, r=i+Z[i]-1 if Z[i]>0
        else i ≤ r (inside current Z-box)
            Z->>Z: k = i - l (position within current match)
            alt Z[k] < r - i + 1
                Z->>Z: Z[i] = Z[k] (bounded by box)
            else Z[k] >= r - i + 1
                Z->>S: Extend beyond r: compare s[r+1..] vs s[r-i+1..]
                S-->>Z: Z[i] = r-i+1 + extension
                Z->>WIN: Update l=i, r=i+Z[i]-1
            end
        end
    end
    Note over Z: Each character compared at most twice → O(n) total

8.2 Huffman Coding — Greedy Tree Construction

sequenceDiagram
    participant FREQ as Character Frequencies
    participant PQ as Min-Priority Queue (by freq)
    participant TREE as Huffman Tree

    FREQ->>PQ: Insert all characters as leaf nodes with their frequencies

    loop While PQ.size > 1
        PQ->>TREE: Pop two minimum-frequency nodes L and R
        TREE->>TREE: Create internal node with freq = L.freq + R.freq
        TREE->>TREE: Internal.left = L, Internal.right = R
        TREE->>PQ: Push internal node back to PQ
    end

    PQ->>TREE: Final node = root of Huffman tree
    TREE->>TREE: Traverse: left edge = '0', right edge = '1'
    TREE-->>FREQ: Assign codewords — frequently used chars get shorter codes

    Note over TREE: Optimal prefix-free code: total bit length minimized
    Note over TREE: Proof: exchange argument — swapping two sub-trees of\ndifferent depths never improves total cost

9. Range Queries — Sparse Table and Sqrt Decomposition

9.1 Sparse Table — Offline O(1) RMQ

flowchart TD
    subgraph PRECOMPUTE["Sparse Table Build O(n log n)"]
        SP1["sparse[i][j] = min of a[i..i+2^j-1]"]
        SP2["Base: sparse[i][0] = a[i] for all i"]
        SP3["Recurrence: sparse[i][j] = min(sparse[i][j-1],\nsparse[i+2^(j-1)][j-1])"]
        SP4["Fill j=1..log n, i=0..n-1"]
        SP2 --> SP3 --> SP4
    end

    subgraph QUERY["Range Min Query O(1)"]
        Q1["RMQ(l, r): compute k = floor(log₂(r-l+1))"]
        Q2["Answer = min(sparse[l][k], sparse[r-2^k+1][k])"]
        Q3["Two overlapping blocks of size 2^k covering [l..r]"]
        Q4["Overlap is OK for min — repeated elements don't affect result"]
        Q1 --> Q2 --> Q3 --> Q4
    end

    subgraph TRADEOFF["Space vs Update"]
        T1["O(n log n) space, O(1) query, NO updates allowed"]
        T2["vs Segment tree: O(n) space, O(log n) query, O(log n) update"]
    end

9.2 Mo's Algorithm — Query Reordering

Mo's algorithm answers offline range queries by reordering them to minimize total pointer movement:

sequenceDiagram
    participant QUERIES as Queries [l,r]
    participant SORT as Sort by (block(l), r)
    participant PTRS as [cur_l, cur_r] pointers
    participant ANSWER as Per-query answer

    QUERIES->>SORT: Sort queries: block size = √n
    Note over SORT: Primary: block(l) = l / √n
    Note over SORT: Secondary: r ascending in odd blocks, descending in even

    loop For each sorted query (l, r)
        loop Extend/shrink cur_r to r
            PTRS->>PTRS: add/remove a[cur_r] from current window
            Note over PTRS: cur_r pointer moves at most O(n) per block
        end
        loop Move cur_l to l
            PTRS->>PTRS: add/remove a[cur_l] from current window
            Note over PTRS: cur_l moves O(√n) per query
        end
        PTRS->>ANSWER: Record current window statistic
    end

    Note over PTRS: Total r moves: O(n √n) — r sweeps n per block, √n blocks
    Note over PTRS: Total l moves: O(q √n) — each query moves l ≤ √n
    Note over PTRS: Total: O((n+q)√n)

10. Number Theory — Modular Arithmetic Internals

10.1 Modular Exponentiation — Binary Method

sequenceDiagram
    participant EXP as exponent b (binary)
    participant RESULT as result = 1
    participant BASE as base = a

    Note over EXP: Compute a^b mod m
    Note over EXP: b in binary: b = b_{k}...b_1 b_0

    loop While b > 0
        alt b is odd (b & 1 == 1)
            RESULT->>RESULT: result = result * base % m
        end
        BASE->>BASE: base = base * base % m
        EXP->>EXP: b >>= 1 (right shift)
    end

    RESULT-->>EXP: Return result = a^b mod m
    Note over EXP: O(log b) multiplications instead of O(b)
    Note over EXP: Key: a^b = (a^(b/2))² if b even\na^b = a * a^(b-1) if b odd

10.2 Sieve of Eratosthenes — Cache Access Pattern

flowchart TD
    subgraph SIEVE["Sieve Internal State"]
        S1["is_prime[0..n] = true initially"]
        S2["Mark 0,1 as false"]
        S3["For p=2 to √n:\nif is_prime[p]:\nmark p², p²+p, p²+2p, ... as false"]
        S4["After sieve: is_prime[k] = true ⟺ k is prime"]
        S1 --> S2 --> S3 --> S4
    end

    subgraph CACHE["Cache Behavior"]
        C1["Inner loop: stride = p\nFor small p: stride small, cache-friendly"]
        C2["For large p (near √n): few multiples to mark\ntotal work = O(n log log n)"]
        C3["Harmonic series: n/2 + n/3 + n/5 + ... ≈ n·Σ(1/p) ≈ n ln ln n"]
        C1 --> C3
        C2 --> C3
    end

    subgraph SEGMENTED["Segmented Sieve for Large n"]
        SS1["Sieve primes up to √n first"]
        SS2["Process array in blocks of √n (fits in L1 cache)"]
        SS3["For each block: mark multiples of precomputed primes"]
        SS4["Cache-optimal: O(n log log n) but with better constant"]
        SS1 --> SS2 --> SS3 --> SS4
    end

11. Game Theory — Nim and Sprague-Grundy

11.1 Nim — XOR Invariant

flowchart TD
    subgraph NIM_STATE["Nim State Analysis"]
        N1["Piles: (a₁, a₂, ..., aₙ)"]
        N2["Nim-value = a₁ XOR a₂ XOR ... XOR aₙ"]
        N3["P-position (previous player wins) = Nim-value 0"]
        N4["N-position (next player wins) = Nim-value ≠ 0"]
        N1 --> N2 --> N3
        N1 --> N2 --> N4
    end

    subgraph PROOF["Why XOR works"]
        P1["Terminal: all piles 0 → XOR=0 → P-position ✓"]
        P2["From XOR≠0: always exists a move to XOR=0\n(take from pile where highest set bit of XOR is set)"]
        P3["From XOR=0: any move creates XOR≠0\n(must change at least one pile)"]
        P1 --> P2 --> P3
    end

    subgraph GRUNDY["Sprague-Grundy Generalization"]
        G1["Grundy(pos) = MEX of {Grundy(next_pos) : all moves}"]
        G2["MEX = Minimum Excludant = smallest non-negative integer not in set"]
        G3["Composite game: XOR of individual Grundy values"]
        G4["Grundy=0 → P-position (losing for mover)"]
        G1 --> G2 --> G3 --> G4
    end

12. Summary — Algorithm Selection Decision Tree

flowchart TD
    PROBLEM["Problem Input"] --> Q1{"Query type?"}

    Q1 -->|"Range sum/min/max\nonline updates"| SEG["Segment Tree\nO(log n) query+update"]
    Q1 -->|"Range sum\nonly additions"| BIT["Binary Indexed Tree (Fenwick)\nO(log n) simpler code"]
    Q1 -->|"Range min/max\nno updates"| SPARSE["Sparse Table\nO(1) query, O(n log n) build"]
    Q1 -->|"Many offline queries\nexpensive range stat"| MO["Mo's Algorithm\nO((n+q)√n)"]

    Q1 -->|"Shortest path\nnon-negative weights"| DIJ["Dijkstra\nO((V+E) log V)"]
    Q1 -->|"Shortest path\nnegative weights"| BELL["Bellman-Ford\nO(VE)"]
    Q1 -->|"All-pairs shortest"| FW["Floyd-Warshall\nO(V³)"]

    Q1 -->|"DP with subset state\nn ≤ 20"| BMASK["Bitmask DP\nO(2^n · n²)"]
    Q1 -->|"Counting/opt\noverlapping subproblems"| DP["Standard DP\nidentify state, recurrence, order"]

    Q1 -->|"Connectivity\nMST"| UF["Union-Find / Kruskal\nO(m log m)"]
    Q1 -->|"Matching"| HOPKARP["Hopcroft-Karp (bipartite)\nO(m√n)"]

    Q1 -->|"Max flow"| FF["Ford-Fulkerson / Dinic\nO(V²E) or O(E√V)"]
    Q1 -->|"String matching"| Z["Z-algorithm\nO(n+m)"]

Source: Antti Laaksonen, "Competitive Programmer's Handbook" (Draft July 3, 2018). All diagrams synthesize internal algorithmic mechanics — data flows, memory layouts, state transitions — not reproduced text.