Skip to content

Algorithms and Data Structures

import Citations from ‘@components/Citations.astro’

Definition. f(n)=O(g(n))f(n) = O(g(n)) if there exist constants c>0c > 0 and n0n_0 such that f(n)≤c⋅g(n)f(n) \leq c \cdot g(n) for all n≥n0n \geq n_0.

Definition. f(n)=Ω(g(n))f(n) = \Omega(g(n)) if there exist constants c>0c > 0 and n0n_0 such that f(n)≥c⋅g(n)f(n) \geq c \cdot g(n) for all n≥n0n \geq n_0.

Definition. f(n)=Θ(g(n))f(n) = \Theta(g(n)) if f(n)=O(g(n))f(n) = O(g(n)) and f(n)=Ω(g(n))f(n) = \Omega(g(n)).

Definition. f(n)=o(g(n))f(n) = o(g(n)) if lim⁡n→∞f(n)/g(n)=0\lim_{n \to \infty} f(n)/g(n) = 0.

Definition. f(n)=ω(g(n))f(n) = \omega(g(n)) if lim⁡n→∞g(n)/f(n)=0\lim_{n \to \infty} g(n)/f(n) = 0.

Theorem 1.1. f(n)=O(g(n))f(n) = O(g(n)) if and only if g(n)=Ω(f(n))g(n) = \Omega(f(n)).

Proof. Suppose f(n)=O(g(n))f(n) = O(g(n)). Then there exist c,n0c, n_0 such that f(n)≤c⋅g(n)f(n) \leq c \cdot g(n) for all n≥n0n \geq n_0 Hence g(n)≥(1/c)⋅f(n)g(n) \geq (1/c) \cdot f(n) for all n≥n0n \geq n_0 So g(n)=Ω(f(n))g(n) = \Omega(f(n)). The converse follows by symmetry. ■\blacksquare

Theorem 1.2. f(n)=Θ(g(n))f(n) = \Theta(g(n)) if and only if there exist constants c1,c2>0c_1, c_2 > 0 and n0n_0 such that c1⋅g(n)≤f(n)≤c2⋅g(n)c_1 \cdot g(n) \leq f(n) \leq c_2 \cdot g(n) for all n≥n0n \geq n_0.

Proof. By definition, f(n)=Θ(g(n))f(n) = \Theta(g(n)) means f(n)=O(g(n))f(n) = O(g(n)) and f(n)=Ω(g(n))f(n) = \Omega(g(n)). The former gives f(n)≤c2⋅g(n)f(n) \leq c_2 \cdot g(n) for some c2>0c_2 > 0 And the latter gives f(n)≥c1⋅g(n)f(n) \geq c_1 \cdot g(n) for some c1>0c_1 > 0. Combining yields the stated inequality. ■\blacksquare

Theorem 1.3 (Limit Rule). If lim⁡n→∞f(n)/g(n)=c\lim_{n \to \infty} f(n)/g(n) = c where 0<c<∞0 < c < \infty Then f(n)=Θ(g(n))f(n) = \Theta(g(n)). If c=0c = 0 Then f(n)=O(g(n))f(n) = O(g(n)). If c=∞c = \infty Then g(n)=O(f(n))g(n) = O(f(n)).

Proof. If c=0c = 0 Then for any ε>0\varepsilon > 0 There exists n0n_0 such that f(n)/g(n)<εf(n)/g(n) < \varepsilon for all n≥n0n \geq n_0 So f(n)≤ε⋅g(n)f(n) \leq \varepsilon \cdot g(n) Establishing f(n)=O(g(n))f(n) = O(g(n)). If 0<c<∞0 < c < \infty Take ε=c/2\varepsilon = c/2; then (c/2)⋅g(n)≤f(n)≤(3c/2)⋅g(n)(c/2) \cdot g(n) \leq f(n) \leq (3c/2) \cdot g(n) for sufficiently large nn Giving Θ\Theta. The c=∞c = \infty case is symmetric. ■\blacksquare

Proposition 1.4. Asymptotic notation is transitive: if f=O(g)f = O(g) and g=O(h)g = O(h) Then f=O(h)f = O(h).

Proof. There exist c1,n1c_1, n_1 with f(n)≤c1g(n)f(n) \leq c_1 g(n) for n≥n1n \geq n_1 And c2,n2c_2, n_2 with g(n)≤c2h(n)g(n) \leq c_2 h(n) for n≥n2n \geq n_2. Then f(n)≤c1c2h(n)f(n) \leq c_1 c_2 h(n) for n≥max⁡(n1,n2)n \geq \max(n_1, n_2). ■\blacksquare

Proposition 1.5. OO-notation is reflexive: f=O(f)f = O(f) for all ff.

Proof. Take c=1c = 1 and n0=1n_0 = 1. Then f(n)≤1⋅f(n)f(n) \leq 1 \cdot f(n) for all n≥1n \geq 1. ■\blacksquare

Worked Example: Proving $n^2 + 3n + 1 = O(n^2)$

We must find c>0c > 0 and n0n_0 such that n2+3n+1≤c⋅n2n^2 + 3n + 1 \leq c \cdot n^2 for all n≥n0n \geq n_0.

Note that n2+3n+1≤n2+3n2+n2=5n2n^2 + 3n + 1 \leq n^2 + 3n^2 + n^2 = 5n^2 for all n≥1n \geq 1 (since n≥1n \geq 1 implies 3n≤3n23n \leq 3n^2 and 1≤n21 \leq n^2).

So take c=5c = 5 and n0=1n_0 = 1.

To show tightness, note n2+3n+1≥n2n^2 + 3n + 1 \geq n^2 for all n≥0n \geq 0 So n2+3n+1=Θ(n2)n^2 + 3n + 1 = \Theta(n^2).

Worked Example: Proving $2^n \neq O(n^k)$ for any constant $k$

By the limit rule: lim⁡n→∞2n/nk=∞\lim_{n \to \infty} 2^n / n^k = \infty for any fixed kk (this follows from repeated application of L”Hôpital’s rule, or from the fact that log⁡(2n)=nlog⁡2\log(2^n) = n \log 2 grows faster than log⁡(nk)=klog⁡n\log(n^k) = k \log n). Therefore 2n=ω(nk)2^n = \omega(n^k) for all kk And in particular 2n≠O(nk)2^n \neq O(n^k).

Worked Example: Sum of Harmonic Series and Its Use in Analysis

The kk-th harmonic number is Hk=∑i=1k1/iH_k = \sum_{i=1}^{k} 1/i. We show Hk=Θ(log⁡k)H_k = \Theta(\log k).

Upper bound: Hk=∑i=1k1/i≤1+∫1k1x dx=1+ln⁡k=O(log⁡k)H_k = \sum_{i=1}^{k} 1/i \leq 1 + \int_1^k \frac{1}{x}\, dx = 1 + \ln k = O(\log k).

Lower bound: Hk≥∫1k+11x dx=ln⁡(k+1)=Ω(log⁡k)H_k \geq \int_1^{k+1} \frac{1}{x}\, dx = \ln(k+1) = \Omega(\log k).

Therefore Hk=Θ(log⁡k)H_k = \Theta(\log k). This is used extensively in analysing algorithms like binary search on unbounded domains and the expected depth of elements in a random BST.

Worked Example: Analysing a Nested Loop Algorithm

Consider the algorithm:

for i = 1 to n:
for j = i to n:
constant-time work

The total number of iterations is ∑i=1n(n−i+1)=∑k=1nk=n(n+1)/2=Θ(n2)\sum_{i=1}^{n}(n - i + 1) = \sum_{k=1}^{n} k = n(n+1)/2 = \Theta(n^2).

Now consider:

for i = 1 to n:
j = 1
while j < n:
j = j * 2
constant-time work

The inner loop runs ⌊log⁡2n⌋+1=O(log⁡n)\lfloor \log_2 n \rfloor + 1 = O(\log n) times, and the outer loop runs nn times. Total: O(nlog⁡n)O(n \log n).

Now consider the triple loop:

for i = 1 to n:
for j = 1 to i:
for k = 1 to j:
constant-time work

Total iterations: ∑i=1n∑j=1ij=∑i=1ni(i+1)2=Θ(n3)\sum_{i=1}^{n} \sum_{j=1}^{i} j = \sum_{i=1}^{n} \frac{i(i+1)}{2} = \Theta(n^3).

ClassNameExample
O(1)O(1)ConstantArray access by index
O(log⁡n)O(\log n)LogarithmicBinary search
O(n)O(n)LinearLinear search
O(nlog⁡n)O(n \log n)Log-linearMerge sort, heap sort
O(n2)O(n^2)QuadraticBubble sort, insertion sort
O(n3)O(n^3)CubicNaive matrix multiplication
O(2n)O(2^n)ExponentialNaive Fibonacci, subset problems
O(n!)O(n!)FactorialGenerating all permutations

Proposition 1.6. These classes form a strict hierarchy:

O(1)⊊O(log⁡n)⊊O(n)⊊O(nlog⁡n)⊊O(n2)⊊O(n3)⊊O(2n)⊊O(n!)O(1) \subsetneq O(\log n) \subsetneq O(n) \subsetneq O(n \log n) \subsetneq O(n^2) \subsetneq O(n^3) \subsetneq O(2^n) \subsetneq O(n!)

Many divide-and-conquer algorithms yield recurrences of the form:

T(n)=aT(n/b)+f(n)T(n) = aT(n/b) + f(n)

Where a≥1a \geq 1 is the number of subproblems, b>1b > 1 is the factor by which the input is divided, and f(n)f(n) is the cost of dividing and combining.

Theorem 1.7 (Master Theorem). Let a≥1a \geq 1 and b>1b > 1 be constants, let f(n)f(n) be a function, and let T(n)T(n) be defined on the nonnegative integers by the recurrence T(n)=aT(n/b)+f(n)T(n) = aT(n/b) + f(n) Where we interpret n/bn/b to mean either ⌊n/b⌋\lfloor n/b \rfloor or ⌈n/b⌉\lceil n/b \rceil. Let c=log⁡bac = \log_b a. Then:

  1. If f(n)=O(nc−ε)f(n) = O(n^{c - \varepsilon}) for some ε>0\varepsilon > 0 Then T(n)=Θ(nc)T(n) = \Theta(n^c).
  2. If f(n)=Θ(nclog⁡kn)f(n) = \Theta(n^c \log^k n) for some k≥0k \geq 0 Then T(n)=Θ(nclog⁡k+1n)T(n) = \Theta(n^c \log^{k+1} n).
  3. If f(n)=Ω(nc+ε)f(n) = \Omega(n^{c + \varepsilon}) for some ε>0\varepsilon > 0 And if af(n/b)≤qf(n)a f(n/b) \leq q f(n) for some constant q<1q < 1 and all sufficiently large nn (the regularity condition), then T(n)=Θ(f(n))T(n) = \Theta(f(n)).

Proof (Case 1 sketch). The recursion tree has depth log⁡bn\log_b n. At level ii There are aia^i nodes, each costing f(n/bi)f(n/b^i). Since f(n)=O(nc−ε)f(n) = O(n^{c - \varepsilon}) The cost at level ii is ai⋅(n/bi)c−ε=nc−ε⋅(a/bc−ε)ia^i \cdot (n/b^i)^{c - \varepsilon} = n^{c - \varepsilon} \cdot (a / b^{c - \varepsilon})^i. The total cost is dominated by the leaves (level log⁡bn\log_b n), which contribute alog⁡bn=nca^{\log_b n} = n^c. The internal levels contribute a geometric series with ratio a/bc−ε=bε>1a / b^{c - \varepsilon} = b^\varepsilon > 1 So the leaf level dominates. ■\blacksquare

Worked Example: Merge Sort Recurrence

Merge sort divides into 2 subproblems of size n/2n/2 and combines in O(n)O(n) time.

T(n)=2T(n/2)+Θ(n)T(n) = 2T(n/2) + \Theta(n)

Here a=2a = 2, b=2b = 2 So c=log⁡22=1c = \log_2 2 = 1. We have f(n)=Θ(n)=Θ(nclog⁡0n)f(n) = \Theta(n) = \Theta(n^c \log^0 n) Which is Case 2 with k=0k = 0.

Therefore T(n)=Θ(n1log⁡1n)=Θ(nlog⁡n)T(n) = \Theta(n^1 \log^1 n) = \Theta(n \log n).

Worked Example: Binary Search RecurrenceT(n)=T(n/2)+O(1)T(n) = T(n/2) + O(1)

Here a=1a = 1, b=2b = 2 So c=log⁡21=0c = \log_2 1 = 0. We have f(n)=O(1)=O(n0)f(n) = O(1) = O(n^0). This matches Case 2 with k=0k = 0.

Therefore T(n)=Θ(log⁡n)T(n) = \Theta(\log n).

Worked Example: Strassen's Matrix Multiplication

Strassen’s algorithm divides into 7 subproblems of size n/2n/2 and combines in O(n2)O(n^2) time.

T(n)=7T(n/2)+O(n2)T(n) = 7T(n/2) + O(n^2)

Here a=7a = 7, b=2b = 2 So c=log⁡27≈2.807c = \log_2 7 \approx 2.807. We have f(n)=O(n2)=O(nc−ε)f(n) = O(n^2) = O(n^{c - \varepsilon}) with ε=c−2≈0.807\varepsilon = c - 2 \approx 0.807 Which is Case 1.

Therefore T(n)=Θ(nlog⁡27)=Θ(n2.807)T(n) = \Theta(n^{\log_2 7}) = \Theta(n^{2.807}).

Amortised analysis gives the average cost per operation over a sequence of operations, even when Individual operations may be expensive.

Methods:

  1. Aggregate analysis: Compute total cost of nn operations, divide by nn.
  2. Accounting method: Assign an amortised cost to each operation; the sum of amortised costs must be an upper bound on the total actual cost.
  3. Potential method: Define a potential function Φ\Phi; the amortised cost of the ii-th operation is c^i=ci+Φ(Di)−Φ(Di−1)\hat{c}_i = c_i + \Phi(D_i) - \Phi(D_{i-1}).

Theorem 1.8 (Potential Method). If Φ(Di)≥Φ(D0)\Phi(D_i) \geq \Phi(D_0) for all i≥1i \geq 1 Then the total amortised cost ∑i=1nc^i\sum_{i=1}^{n} \hat{c}_i is an upper bound on the total actual cost ∑i=1nci\sum_{i=1}^{n} c_i.

Proof. ∑i=1nc^i=∑i=1n(ci+Φ(Di)−Φ(Di−1))=∑i=1nci+Φ(Dn)−Φ(D0)≥∑i=1nci\sum_{i=1}^{n} \hat{c}_i = \sum_{i=1}^{n} (c_i + \Phi(D_i) - \Phi(D_{i-1})) = \sum_{i=1}^{n} c_i + \Phi(D_n) - \Phi(D_0) \geq \sum_{i=1}^{n} c_i. ■\blacksquare

Example (Dynamic Array). A dynamic array doubles in size when full. Insertion is O(1)O(1) Amortised: most insertions cost O(1)O(1); occasional resizing costs O(n)O(n) But is paid for by the Surplus from previous O(1)O(1) insertions.

Worked Example: Dynamic Array Amortised Analysis (Accounting Method)

Assign an amortised cost of 3 per insertion. The actual cost of insertion without resizing is 1. We store the surplus of 2 as “credit” on each element in the array.

When the array of capacity kk is full and we must resize to capacity 2k2k:

  • The resizing cost is kk (copy all kk elements).
  • We have 2k2k credits stored (2 per element).
  • We spend kk credits on the copy, leaving kk credits.
  • The new array has kk elements, so we need 2k2k more credits, but we already have kk.
  • Subsequent insertions build up credits again.

Since the total credits are always non-negative, the amortised cost of 3 per insertion covers all actual costs.

Worked Example: Dynamic Array Amortised Analysis (Potential Method)

Define the potential function Φ(D)=2n−m\Phi(D) = 2n - m where nn is the number of elements and mm is the capacity. We require m≥nm \geq n So Φ≥0\Phi \geq 0 (since 2n−n=n≥02n - n = n \geq 0).

Case 1: No resize. c^=1+(2(n+1)−m)−(2n−m)=1+2=3\hat{c} = 1 + (2(n+1) - m) - (2n - m) = 1 + 2 = 3.

Case 2: Resize from m=nm = n to m=2nm = 2n. c^=(1+n)+(2(n+1)−2n)−(2n−n)=(n+1)+2−n=3\hat{c} = (1 + n) + (2(n+1) - 2n) - (2n - n) = (n + 1) + 2 - n = 3.

In both cases, the amortised cost is O(1)O(1).

Array. Contiguous memory, O(1)O(1) access by index, O(n)O(n) insertion/deletion (shift required).

OperationArraySingly Linked ListDoubly Linked List
Access by indexO(1)O(1)O(n)O(n)O(n)O(n)
Search (by value)O(n)O(n)O(n)O(n)O(n)O(n)
Insert at headO(n)O(n)O(1)O(1)O(1)O(1)
Insert at tailO(1)O(1)O(n)O(n)O(1)O(1)*
Delete at headO(n)O(n)O(1)O(1)O(1)O(1)
Delete at tailO(n)O(n)O(n)O(n)O(1)O(1)*
Insert at positionO(n)O(n)O(n)O(n)O(1)O(1)*
Space overhead00O(n)O(n)O(n)O(n)

*Given a pointer to the node or its predecessor.

Linked List. Each node stores data and a pointer to the next node. O(1)O(1) insertion/deletion at Head (given pointer), O(n)O(n) access by position.

Doubly Linked List. Each node has pointers to both next and previous nodes. O(1)O(1) insertion And deletion at any position (given pointers to the node and its neighbours).

Worked Example: Reversing a Singly Linked List

To reverse a singly linked list in O(n)O(n) time and O(1)O(1) space:

prev = null
curr = head
while curr != null:
next = curr.next
curr.next = prev
prev = curr
curr = next
head = prev

At each iteration, we move one node to the front of the reversed list. After nn iterations, all nn nodes are reversed.

Stack. Last-in, first-out (LIFO). Operations: push, pop, peek, all O(1)O(1).

Queue. First-in, first-out (FIFO). Operations: enqueue, dequeue, peek, all O(1)O(1).

Implementation. Both can be implemented with arrays (with circular buffer) or linked lists.

Worked Example: Implementing a Queue with Two Stacks

A queue can be implemented using two stacks in_stack and out_stack.

  • enqueue(x): push xx onto in_stack. O(1)O(1).
  • dequeue(): if out_stack is empty, pop all elements from in_stack and push onto out_stack. Then pop from out_stack.

The dequeue operation is O(1)O(1) amortised: each element is moved from in_stack to out_stack at most once across all operations. Over nn operations, the total number of element moves is at most nn Giving O(1)O(1) amortised per dequeue.

A hash table maps keys to values using a hash function h:K→0,1,…,m−1h : K \to \\{0, 1, \ldots, m - 1\\}.

Collision resolution:

  • Chaining: Each bucket is a linked list. Average case: O(1+α)O(1 + \alpha) where α=n/m\alpha = n/m is the load factor.
  • Open addressing (linear probing): Insert into the next available slot. Average case: O(1/(1−α))O(1/(1-\alpha)).

Theorem 2.1 (Uniform Hashing). Under the assumption of simple uniform hashing, the expected Length of a chain in a hash table with chaining is α=n/m\alpha = n/m.

Proof. Under simple uniform hashing, each of the nn keys is equally likely to hash to any of the mm slots. For any given key kk The expected number of other keys that hash to the same slot as kk is (n−1)/m<α(n - 1)/m < \alpha. Including kk itself, the expected chain length is 1+(n−1)/m≤1+α1 + (n-1)/m \leq 1 + \alpha. ■\blacksquare

Theorem 2.2 (Successful Search with Chaining). Under simple uniform hashing, the expected time for a successful search in a hash table with chaining is O(1+α/2)O(1 + \alpha/2).

Proof. The expected length of the list containing the searched element is 1+(n−1)/m1 + (n - 1)/m Since n−1n - 1 other elements are distributed uniformly. On average, the searched element is halfway through its list, giving (1+(n−1)/m)/2+1/2(1 + (n-1)/m)/2 + 1/2 comparisons (we examine elements before the target plus the target itself). This equals 1+α/2−1/(2m)=O(1+α)1 + \alpha/2 - 1/(2m) = O(1 + \alpha). ■\blacksquare

Double Hashing. Uses two hash functions: h(k,i)=(h1(k)+i⋅h2(k)) mod mh(k, i) = (h_1(k) + i \cdot h_2(k)) \bmod m Where h2(k)h_2(k) is relatively prime to mm. This avoids the clustering problems of linear probing.

Worked Example: Choosing a Good Hash Function

For integer keys, a common choice is the multiplication method: h(k)=⌊m⋅(k⋅A mod 1)⌋h(k) = \lfloor m \cdot (k \cdot A \bmod 1) \rfloor where A=(5−1)/2≈0.618A = (\sqrt{5} - 1)/2 \approx 0.618 (the inverse golden ratio).

For string keys, the polynomial rolling hash: h(s)=(∑i=0k−1s[i]⋅pk−1−i) mod mh(s) = \left(\sum_{i=0}^{k-1} s[i] \cdot p^{k-1-i}\right) \bmod m where pp is a prime (e.g., 31 or 127).

Both avoid clustering better than simple modulo hashing when the input distribution is adversarial.

Binary Search Tree (BST). For each node: left subtree values <\lt node value, right subtree values >\gt node value.

  • Search, insert, delete: O(h)O(h) where hh is the height.
  • For a balanced BST, h=O(log⁡n)h = O(\log n).

Theorem 2.3. The expected height of a randomly built BST with nn distinct keys is O(log⁡n)O(\log n).

Proof. Let XnX_n be the height of a BST built from nn random keys. Let Yn=2XnY_n = 2^{X_n}. Then E[Yn]≤14∑i=0n−1(ni)E[Yi]E[Yn−1−i]/n\mathrm{E}[Y_n] \leq \frac{1}{4} \sum_{i=0}^{n-1} \binom{n}{i} \mathrm{E}[Y_i] \mathrm{E}[Y_{n-1-i}] / n. Using the indicator random variable technique, E[Yn]≤cn+1n3/2∑i=0n−1i3/2(n−1−i)3/2cicn−1−i≤c⋅n3/2\mathrm{E}[Y_n] \leq \frac{c^{n+1}}{n^{3/2}} \sum_{i=0}^{n-1} \frac{i^{3/2}(n-1-i)^{3/2}}{c^i c^{n-1-i}} \leq c \cdot n^{3/2} for some constant cc. Taking logs gives E[Xn]=E[log⁡Yn]≤log⁡E[Yn]=O(log⁡n)\mathrm{E}[X_n] = \mathrm{E}[\log Y_n] \leq \log \mathrm{E}[Y_n] = O(\log n) by Jensen’s inequality. ■\blacksquare

Definition. An AVL tree is a BST where for every node, the heights of its left and right subtrees differ by at most 1. The balance factor of a node is the height of its left subtree minus the height of its right subtree; valid balance factors are −1,0,1\\{-1, 0, 1\\}.

Rotations. After insertion or deletion, the balance factor may become ±2\pm 2. This is fixed by rotations:

  • Right rotation (LL case): Balance factor +2+2 at node AA Balance factor +1+1 at left child BB.

    A B
    / \ / \
    B C → D A
    / \ / \
    D E E C
  • Left rotation (RR case): Mirror of right rotation.

  • Left-Right rotation (LR case): Balance factor +2+2 at AA Balance factor −1-1 at left child BB. First left-rotate BB Then right-rotate AA.

  • Right-Left rotation (RL case): Mirror of LR case.

Theorem 2.4. An AVL tree with nn nodes has height h≤1.4404⋅log⁡2(n+2)−1.3277h \leq 1.4404 \cdot \log_2(n + 2) - 1.3277.

Proof. Let N(h)N(h) be the minimum number of nodes in an AVL tree of height hh. We have N(0)=1N(0) = 1, N(1)=2N(1) = 2 And N(h)=1+N(h−1)+N(h−2)N(h) = 1 + N(h-1) + N(h-2) for h≥2h \geq 2. This is the Fibonacci recurrence, giving N(h)=Fh+3−1N(h) = F_{h+3} - 1. Using Fh=ϕh−ϕ^h5F_h = \frac{\phi^h - \hat{\phi}^h}{\sqrt{5}} where ϕ=1+52\phi = \frac{1+\sqrt{5}}{2} We get N(h)>ϕh/5−1N(h) > \phi^h / \sqrt{5} - 1 So h<log⁡ϕ(5(n+1))≈1.4404log⁡2(n+1)h \lt \log_\phi(\sqrt{5}(n + 1)) \approx 1.4404 \log_2(n + 1). ■\blacksquare

Corollary 2.5. All AVL tree operations (search, insert, delete) run in O(log⁡n)O(\log n) time.

Proof. Each operation visits O(h)=O(log⁡n)O(h) = O(\log n) nodes along a root-to-leaf path, and each rotation is O(1)O(1). Since at most O(log⁡n)O(\log n) rotations are needed (at most 2 per level), the total cost is O(log⁡n)O(\log n). ■\blacksquare

Definition. A red-black tree is a BST satisfying:

  1. Every node is either red or black.
  2. The root is black.
  3. Every leaf (NIL sentinel) is black.
  4. If a node is red, both its children are black.
  5. For every node, all simple paths from the node to descendant leaves contain the same number of black nodes (the black-height).

Theorem 2.6. A red-black tree with nn internal nodes has height h≤2log⁡2(n+1)h \leq 2 \log_2(n + 1).

Proof. Let rr be the root. By property 5, at least half the nodes on any root-to-leaf path are black (by property 4, no two reds are adjacent), so the black-height bh(r)≥h/2bh(r) \geq h/2. Let S(h,bh)S(h, bh) be the minimum number of internal nodes in a subtree of height hh and black-height bhbh. Then S(h,bh)≥2bh−1S(h, bh) \geq 2^{bh} - 1 (proved by induction on bhbh Using the fact that each child has black-height at least bh−1bh - 1 and the root is black). Since bh(r)≥h/2bh(r) \geq h/2 and n≥2bh(r)−1≥2h/2−1n \geq 2^{bh(r)} - 1 \geq 2^{h/2} - 1 We get h≤2log⁡2(n+1)h \leq 2 \log_2(n + 1). ■\blacksquare

Insertion. Insert as in a standard BST (colour the new node red), then fix violations by recolouring and rotating up to O(log⁡n)O(\log n) times.

Deletion. More complex than insertion. When a black node is removed, we may need to fix the black-height property by recolouring and rotating. The fix-up procedure takes O(log⁡n)O(\log n) time.

Definition. A B-tree of minimum degree t≥2t \geq 2 is a rooted tree satisfying:

  1. Every node has at most 2t−12t - 1 keys.
  2. Every non-root node has at least t−1t - 1 keys (hence at least tt children).
  3. The root has at least 1 key.
  4. All leaves appear at the same depth.

Theorem 2.7. A B-tree with nn keys and minimum degree tt has height h≤log⁡tn+12h \leq \log_t \frac{n+1}{2}.

Proof. The root has at least 1 key and 2 children. At depth 1, each node has at least t−1t - 1 keys and tt children. At depth d≥1d \geq 1 The number of nodes is at least 2td−12t^{d-1} Each with at least t−1t - 1 keys. So the total number of keys is at least 1+2(t−1)∑i=0h−1ti=1+2(t−1)⋅th−1t−1=2th−11 + 2(t-1) \sum_{i=0}^{h-1} t^i = 1 + 2(t-1) \cdot \frac{t^h - 1}{t - 1} = 2t^h - 1. Setting n≥2th−1n \geq 2t^h - 1 gives h≤log⁡t((n+1)/2)h \leq \log_t((n+1)/2). ■\blacksquare

Worked Example: Red-Black Tree Insertion Trace

Insert keys 7, 3, 18, 10, 22, 8, 11, 26 into an initially empty red-black tree.

Insert 7: Root is coloured black. T=[7(B)]T = [7(B)]

Insert 3: Left child of 7, coloured red. T=[7(B)←3(R)]T = [7(B) \leftarrow 3(R)]. Valid (root is black, no red-red violation).

Insert 18: Right child of 7, coloured red. T=[3(R)←7(B)→18(R)]T = [3(R) \leftarrow 7(B) \rightarrow 18(R)]. Valid.

Insert 10: Insert as left child of 18, coloured red. Now 18 has red child 10. Uncle of 10 is 3(R), Case 1 (uncle is red): recolour uncle 3 black, parent 18 black, grandparent 7 red. Grandparent 7 is root, so recolour 7 black.

Result: T=[3(B)←7(B)→18(B)]T = [3(B) \leftarrow 7(B) \rightarrow 18(B)] with 10(R)10(R) under 1818.

Insert 22: Right child of 18, coloured red. Uncle of 22 is 10(R), Case 1 again: recolour 10 black, 22 black, 18 red. Parent 18 is not root, grandparent is 7(B). Now 7 has right child 18(R). Check 7’s left child: 3(B). No violation.

Insert 8: Left child of 10, coloured red. Uncle of 8 is 22(R), Case 1: recolour 8 black, 22 black, 10 red. Now 10(R) is left child of 18(B). Check grandparent 7: right child 18(B) with left child 10(R). No red-red violation.

Insert 11: Right child of 10, coloured red. Now 10(R) has both children red, wait, 10 is red and 11 is red: violation. Uncle of 11 is 8(R), Case 1: recolour 8 black, 11 black, 10 red. Grandparent 18(B): 10(R) is left child, 22(B) is right child. No violation.

Insert 26: Right child of 22, coloured red. Uncle of 26 is 10(R), Case 1: recolour 10 black, 26 black, 22 red. Now 22(R) is right child of 18(B). Check left child of 18: 10(B). Valid.

A skip list is a probabilistic data structure that provides O(log⁡n)O(\log n) expected-time search, insertion, and deletion. It consists of multiple levels of linked lists.

Structure:

  • Level 0 is a standard linked list containing all elements in sorted order.
  • Each element is promoted to level 1 with probability pp ( p=1/2p = 1/2).
  • Each level-ii element is promoted to level i+1i+1 independently with probability pp.
  • A sentinel head node links to the first element at every level.

Theorem 2.8. The expected number of levels in a skip list with nn elements is O(log⁡n)O(\log n).

Proof. An element reaches level ii with probability pip^i. The expected number of elements at level ii is npinp^i. The highest non-empty level is approximately log⁡1/pn\log_{1/p} n. ■\blacksquare

Theorem 2.9. Search, insert, and delete in a skip list take O(log⁡n)O(\log n) expected time.

Proof. The search path at each level ii moves right to find an element whose successor at level ii is greater than the target, then drops to level i−1i - 1. At each level, the expected number of rightward steps is at most 1/p1/p (a geometric distribution argument). With O(log⁡1/pn)O(\log_{1/p} n) levels, the total expected work is O(log⁡n/p)=O(log⁡n)O(\log n / p) = O(\log n). ■\blacksquare

Worked Example: Skip List Search

Search for key 25 in a skip list containing keys [3,7,12,19,25,31,42][3, 7, 12, 19, 25, 31, 42] with the following level structure (probability p=1/2p = 1/2):

Level 3: [HEAD] -------------------------> [31] ---------->
Level 2: [HEAD] ---------> [12] -------------------------->
Level 1: [HEAD] -> [7] -> [12] -> [19] -> [25] -> [31] -> [42]
Level 0: [HEAD] -> [7] -> [12] -> [19] -> [25] -> [31] -> [42]

Search starts at HEAD, level 3:

  • Move right to 31. 31>2531 > 25 Drop to level 2.
  • At level 2, move right to 12. 12≤2512 \leq 25 Move right, next is NIL. Drop to level 1.
  • At level 1, move right to 19. 19≤2519 \leq 25 Move right to 25. 25=2525 = 25. Found!

The search examined keys 31, 12, 19, 25 to 4 comparisons across 3 levels.

Worked Example: Skip List Insertion

Insert key 15 into the skip list above.

  1. Search for insertion position: traverse levels 3, 2, 1 to find that 15 goes between 12 and 19.
  2. Insert 15 at level 0 (always). Randomly promote: suppose coin flip says promote to level 1. Insert at level 1. Flip again: promote to level 2. Insert at level 2. Flip again: don’t promote. Stop.
Level 3: [HEAD] -------------------------> [31] ---------->
Level 2: [HEAD] ---------> [12] -> [15] ------------------>
Level 1: [HEAD] -> [7] -> [12] -> [15] -> [19] -> [25] -> [31] -> [42]
Level 0: [HEAD] -> [7] -> [12] -> [15] -> [19] -> [25] -> [31] -> [42]

Insertion takes O(log⁡n)O(\log n) expected time (search path + insertions at promoted levels).

A Bloom filter is a space-efficient probabilistic data structure for set membership testing. It may produce false positives but never false negatives.

Structure: A bit array of mm bits (initially all 0) and kk independent hash functions h1,…,hkh_1, \ldots, h_k.

Operations:

  • Insert xx: set bits h1(x),h2(x),…,hk(x)h_1(x), h_2(x), \ldots, h_k(x) to 1.
  • Query xx: return true if all h1(x),…,hk(x)h_1(x), \ldots, h_k(x) are set to 1.

Theorem 2.10. After inserting nn elements into a Bloom filter of size mm with kk hash functions, the probability of a false positive is approximately (1−e−kn/m)k\left(1 - e^{-kn/m}\right)^k.

Proof. After inserting nn elements, each of the mm bits is set to 0 with probability (1−1/m)kn≈e−kn/m(1 - 1/m)^{kn} \approx e^{-kn/m}. The probability that all kk bits for a query element are set is (1−e−kn/m)k(1 - e^{-kn/m})^k. ■\blacksquare

Optimal number of hash functions. The false positive rate is minimised when k=(m/n)ln⁡2k = (m/n) \ln 2 Giving a minimum rate of approximately (1/2)k=0.6185m/n(1/2)^k = 0.6185^{m/n}.

Worked Example: Bloom Filter Configuration

We want to store n=10000n = 10000 URLs with a false positive rate of at most 1%.

From the formula: (1−e−kn/m)k≤0.01(1 - e^{-kn/m})^k \leq 0.01.

Optimal k=(m/n)ln⁡2k = (m/n) \ln 2. At the optimal point, the false positive rate is (1/2)k≈0.6185m/n(1/2)^k \approx 0.6185^{m/n}.

We need 0.6185m/n≤0.010.6185^{m/n} \leq 0.01 So m/n≥ln⁡(0.01)/ln⁡(0.6185)≈9.585m/n \geq \ln(0.01) / \ln(0.6185) \approx 9.585.

Take m=95850m = 95850 bits ≈11.5\approx 11.5 KB. Then k=(95850/10000)ln⁡2≈6.64k = (95850 / 10000) \ln 2 \approx 6.64 So use k=7k = 7 hash functions.

This requires only 11.5 KB of memory versus approximately 600 KB for storing the URLs as 60-byte strings.

Worked Example: Bloom Filter Operations Trace

A Bloom filter has m=10m = 10 bits, k=2k = 2 hash functions: h1(x)=x mod 10h_1(x) = x \bmod 10, h2(x)=(x⋅3) mod 10h_2(x) = (x \cdot 3) \bmod 10.

Insert 15: h1(15)=5h_1(15) = 5, h2(15)=5⋅3 mod 10=5h_2(15) = 5 \cdot 3 \bmod 10 = 5. Set bit 5. Bit array: 0000010000

Insert 22: h1(22)=2h_1(22) = 2, h2(22)=22⋅3 mod 10=6h_2(22) = 22 \cdot 3 \bmod 10 = 6. Set bits 2, 6. Bit array: 0010010100

Insert 37: h1(37)=7h_1(37) = 7, h2(37)=37⋅3 mod 10=1h_2(37) = 37 \cdot 3 \bmod 10 = 1. Set bits 1, 7. Bit array: 0110010110

Query 22: h1(22)=2h_1(22) = 2 (set), h2(22)=6h_2(22) = 6 (set). Result: possibly in set (true positive).

Query 42: h1(42)=2h_1(42) = 2 (set), h2(42)=42⋅3 mod 10=6h_2(42) = 42 \cdot 3 \bmod 10 = 6 (set). Result: possibly in set (false positive!).

Query 50: h1(50)=0h_1(50) = 0 (not set). Result: definitely not in set (true negative).

A binary heap is a complete binary tree satisfying the heap property:

  • Max-heap: parent ≥\geq children.
  • Min-heap: parent ≤\leq children.

Implemented as an array: parent of node ii is at ⌊(i−1)/2⌋\lfloor(i-1)/2\rfloor; children at 2i+12i+1 and 2i+22i+2.

Operations: insert O(log⁡n)O(\log n) Extract-max/min O(log⁡n)O(\log n) Peek O(1)O(1).

Theorem 2.11 (Build-Heap). buildHeap on an array of nn elements runs in O(n)O(n) time.

Proof. buildHeap calls heapify on each of the ⌊n/2⌋\lfloor n/2 \rfloor non-leaf nodes. The cost of heapify at a node of height hh is O(h)O(h). The total cost is ∑h=0⌊log⁡n⌋⌈n/2h+1⌉⋅O(h)=O(n∑h=0∞h/2h)=O(n)\sum_{h=0}^{\lfloor \log n \rfloor} \lceil n / 2^{h+1} \rceil \cdot O(h) = O\left(n \sum_{h=0}^{\infty} h / 2^h\right) = O(n)Since ∑h=0∞h/2h=2\sum_{h=0}^{\infty} h / 2^h = 2. ■\blacksquare

Worked Example: Build-Heap Step by Step

Build a max-heap from the array [4,10,3,5,1,8,2][4, 10, 3, 5, 1, 8, 2].

The array represents the tree:

4
/ \
10 3
/ \ / \
5 1 8 2

Non-leaf nodes (indices ⌊7/2⌋−1=2\lfloor 7/2 \rfloor - 1 = 2 down to 0): nodes 3, 10, 4.

Heapify index 2 (value 3): Children are 8 and 2. Swap 3 with 8. Array: [4,10,8,5,1,3,2][4, 10, 8, 5, 1, 3, 2].

4
/ \
10 8
/ \ / \
5 1 3 2

Heapify index 1 (value 10): Children are 5 and 1. 10 is largest. No swap.

Heapify index 0 (value 4): Children are 10 and 8. Swap with 10. Array: [10,4,8,5,1,3,2][10, 4, 8, 5, 1, 3, 2].

10
/ \
4 8
/ \ / \
5 1 3 2

Now heapify index 1 (value 4): Children are 5 and 1. Swap with 5. Array: [10,5,8,4,1,3,2][10, 5, 8, 4, 1, 3, 2].

10
/ \
5 8
/ \ / \
4 1 3 2

This is a valid max-heap. Total operations: 3 swaps = O(n)O(n).

The Union-Find data structure maintains a partition of a set into disjoint subsets, supporting:

  • MakeSet(xx): Create a singleton set x\\{x\\}.
  • Find(xx): Return the representative of the set containing xx.
  • Union(x,yx, y): Merge the sets containing xx and yy.

Implementation with path compression and union by rank:

  • Each set is represented as a rooted tree.
  • Find follows parent pointers and compresses the path (makes every node on the path point directly to the root).
  • Union attaches the shorter tree under the root of the taller tree (by rank/size).

Theorem 2.12. With both path compression and union by rank, the amortised time per operation is O(α(n))O(\alpha(n)) Where α(n)\alpha(n) is the inverse Ackermann function.

Proof (outline). The inverse Ackermann function α(n)\alpha(n) grows so slowly that α(n)≤4\alpha(n) \leq 4 for all practical values of nn (n≤22265536n \leq 2^{2^{2^{65536}}}). The proof uses a potential function argument on the levels of the tree. Each node is assigned a level based on its rank. The key insight is that path compression prevents the number of nodes at high levels from growing. The total number of Find operations that can charge to any level-ℓ\ell node is bounded by O(n⋅F−1(ℓ))O(n \cdot F^{-1}(\ell)) where FF is a fast-growing function. Summing over all levels gives O(nα(n))O(n \alpha(n)) total, hence O(α(n))O(\alpha(n)) amortised per operation. ■\blacksquare

Worked Example: Union-Find for Connected Components

Given an undirected graph with nn vertices and mm edges, determine the connected components.

for each vertex v:
MakeSet(v)
for each edge (u, v):
if Find(u) != Find(v):
Union(u, v)

After processing all edges, vertices in the same connected component have the same representative. Total time: O(n+mα(n))≈O(n+m)O(n + m \alpha(n)) \approx O(n + m).

A graph G=(V,E)G = (V, E) can be represented by:

  • Adjacency matrix: Aij=1A_{ij} = 1 if (i,j)∈E(i,j) \in E. Space: O(V2)O(V^2). Edge lookup: O(1)O(1).
  • Adjacency list: For each vertex, store a list of neighbours. Space: O(V+E)O(V + E). Iterating over neighbours: O(deg(v))O(\mathrm{deg}(v)).
OperationAdjacency MatrixAdjacency List
SpaceO(V2)O(V^2)O(V+E)O(V + E)
Check edge (u,v)(u,v)O(1)O(1)O(deg(u))O(\mathrm{deg}(u))
Iterate neighboursO(V)O(V)O(deg(v))O(\mathrm{deg}(v))
Add edgeO(1)O(1)O(1)O(1)
Remove edgeO(1)O(1)O(deg(u))O(\mathrm{deg}(u))

Algorithm. Divide the array in half, recursively sort each half, then merge.

MergeSort(A, l, r):
if l < r:
m = (l + r) / 2
MergeSort(A, l, m)
MergeSort(A, m+1, r)
Merge(A, l, m, r)

Theorem 3.1. Merge sort runs in O(nlog⁡n)O(n \log n) time in all cases (best, average, worst).

Proof. The recurrence is T(n)=2T(n/2)+O(n)T(n) = 2T(n/2) + O(n). By the Master theorem (case 2): a=2a = 2, b=2b = 2, f(n)=O(n)=O(nlog⁡ba)f(n) = O(n) = O(n^{\log_b a}) So T(n)=O(nlog⁡n)T(n) = O(n \log n). ■\blacksquare

Theorem 3.2. Merge sort is stable and requires O(n)O(n) auxiliary space.

Proof of stability. In Merge. When elements from the left and right halves are equal, we always take from the left half first. This preserves the relative order of equal elements. ■\blacksquare

Theorem 3.3 (Correctness of Merge). The Merge procedure produces a sorted array from two sorted subarrays.

Proof. Let A[l..m]A[l..m] and A[m+1..r]A[m+1..r] be sorted subarrays. The merge procedure maintains two indices ii and jj pointing to the next unmerged element in each subarray. At each step, the minimum of A[i]A[i] and A[j]A[j] is appended to the output. By induction on the number of elements placed: if kk elements have been placed and they are the kk smallest among the remaining elements, then the (k+1)(k+1)-th element placed is the minimum of the remaining elements. Since each subarray is sorted, the smallest remaining element is min⁡(A[i],A[j])\min(A[i], A[j]). ■\blacksquare

Theorem 3.4 (Optimality of Merge Sort). Merge sort achieves the optimal O(nlog⁡n)O(n \log n) comparison-based sorting bound.

Proof. By Theorem 3.7, no comparison sort can do better than Ω(nlog⁡n)\Omega(n \log n). Merge sort achieves O(nlog⁡n)O(n \log n) in all cases, so it is asymptotically optimal among comparison sorts. ■\blacksquare

Worked Example: Merge Sort Trace

Sort the array [38,27,43,3,9,82,10][38, 27, 43, 3, 9, 82, 10]:

Split: [38,27,43,3][38, 27, 43, 3] and [9,82,10][9, 82, 10]

Split: [38,27][38, 27] and [43,3][43, 3] Split: [38][38] and [27][27] → Merge: [27,38][27, 38] Split: [43][43] and [3][3] → Merge: [3,43][3, 43] Merge: [3,27,38,43][3, 27, 38, 43]

Split: [9,82][9, 82] and [10][10] Split: [9][9] and [82][82] → Merge: [9,82][9, 82] Merge: [9,10,82][9, 10, 82]

Final merge: [3,9,10,27,38,43,82][3, 9, 10, 27, 38, 43, 82]

Total comparisons: 13.

Algorithm. Choose a pivot, partition the array into elements ≤\leq pivot and >> pivot, recursively Sort each partition.

QuickSort(A, lo, hi):
if lo < hi:
p = Partition(A, lo, hi)
QuickSort(A, lo, p - 1)
QuickSort(A, p + 1, hi)

Partition (Lomuto): Select the last element as pivot. Iterate through the array, maintaining that elements A[lo..i]A[\mathrm{lo}..i] are ≤\leq pivot and A[i+1..j−1]A[i+1..j-1] are >> pivot.

Theorem 3.5. Quicksort runs in O(nlog⁡n)O(n \log n) expected time and O(n2)O(n^2) worst-case time.

Proof (expected case). If the pivot is chosen uniformly at random, the expected number of Comparisons is O(nlog⁡n)O(n \log n) by an indicator random variable argument.

Let XijX_{ij} be the indicator random variable that ziz_i and zjz_j are compared, where z1,…,znz_1, \ldots, z_n are the sorted elements. Since elements are compared only if one is an ancestor of the other in the recursion tree, and the pivot is chosen uniformly at random:

E[Xij]=Pr⁡(zi and zj are compared)=2j−i+1\mathrm{E}[X_{ij}] = \Pr(z_i \mathrm{~and~} z_j \mathrm{~are~compared}) = \frac{2}{j - i + 1}

The total number of comparisons is X=∑i<jXijX = \sum_{i < j} X_{ij} So:

E[X]=∑i=1n−1∑j=i+1n2j−i+1≤∑k=1nn⋅2k+1=O(nlog⁡n)\mathrm{E}[X] = \sum_{i=1}^{n-1} \sum_{j=i+1}^{n} \frac{2}{j - i + 1} \leq \sum_{k=1}^{n} n \cdot \frac{2}{k+1} = O(n \log n)

Worst case occurs when the pivot is always the smallest or largest element (e.g., already sorted Array with first-element pivot): T(n)=T(n−1)+O(n)=O(n2)T(n) = T(n-1) + O(n) = O(n^2). ■\blacksquare

Theorem 3.6. Quicksort is not stable but uses O(log⁡n)O(\log n) expected stack space.

Worked Example: Quicksort with Median-of-Three Pivot

Sort [3,7,8,5,2,1,9,5,4][3, 7, 8, 5, 2, 1, 9, 5, 4] using median-of-three (first, middle, last element).

Pivot selection: elements at positions 0, 4, 8 are 3,2,43, 2, 4. Median is 33.

Partition around 33: [2,1,3,8,5,7,9,5,4][2, 1, 3, 8, 5, 7, 9, 5, 4]

Recurse on [2,1][2, 1] (pivot 22): [1,2][1, 2] Recurse on [8,5,7,9,5,4][8, 5, 7, 9, 5, 4] (median of 8,7,48, 7, 4 is 77): [4,5,5,7,9,8][4, 5, 5, 7, 9, 8]

Recurse on [4,5,5][4, 5, 5] (pivot 55): [4,5,5][4, 5, 5] Recurse on [9,8][9, 8] (pivot 99): [8,9][8, 9]

Result: [1,2,3,4,5,5,7,8,9][1, 2, 3, 4, 5, 5, 7, 8, 9]

Algorithm. Build a max-heap in O(n)O(n) time, then repeatedly extract the maximum.

Theorem 3.7. Heapsort runs in O(nlog⁡n)O(n \log n) time in all cases, uses O(1)O(1) auxiliary space, and Is not stable.

Proof. Building the heap takes O(n)O(n) by Theorem 2.11. Each of the nn extract-max operations takes O(log⁡n)O(\log n) For a total of O(nlog⁡n)O(n \log n). Space is O(1)O(1) since the heap is stored in-place. ■\blacksquare

Theorem 3.8. Any comparison-based sorting algorithm requires Ω(nlog⁡n)\Omega(n \log n) comparisons in the Worst case.

Proof. A comparison-based sort can be modelled as a decision tree. The tree must have at least n!n! Leaves (one for each permutation). A binary tree with n!n! leaves has height at least log⁡2(n!)≥log⁡2((n/2)n/2)=(n/2)log⁡2(n/2)=Ω(nlog⁡n)\log_2(n!) \geq \log_2((n/2)^{n/2}) = (n/2)\log_2(n/2) = \Omega(n \log n).

More precisely, using Stirling’s approximation: log⁡2(n!)=nlog⁡2n−nlog⁡2e+O(log⁡n)=Ω(nlog⁡n)\log_2(n!) = n \log_2 n - n \log_2 e + O(\log n) = \Omega(n \log n). ■\blacksquare

Counting sort sorts integers in O(n+k)O(n + k) time where kk is the range of values.

Algorithm:

  1. Count occurrences: C[i]=C[i] = number of elements equal to ii.
  2. Compute prefix sums: C[i]=C[i] = number of elements ≤i\leq i.
  3. Place each element in its correct position (iterating backwards for stability).

Theorem 3.9. Counting sort runs in O(n+k)O(n + k) time and O(n+k)O(n + k) space, and is stable.

Proof. Step 1 takes O(n)O(n) time. Step 2 takes O(k)O(k) time. Step 3 takes O(n)O(n) time. The output array uses O(n)O(n) space and the count array uses O(k)O(k) space. Stability follows from iterating backwards in step 3: the last occurrence of each value is placed first, preserving the order of equal elements. ■\blacksquare

Radix sort processes digits from least significant to most significant (LSD radix sort) using a stable sort (e.g., counting sort) as a subroutine.

Theorem 3.10. LSD radix sort sorts nn integers with dd digits in base bb in O(d(n+b))O(d(n + b)) time.

Proof. We perform dd passes of counting sort, each taking O(n+b)O(n + b) time. After the ii-th pass, the array is sorted by the ii least significant digits. By induction on ii After dd passes the array is fully sorted. ■\blacksquare

Corollary 3.11. For dd-digit integers where d=O(1)d = O(1) (e.g., 32-bit integers), radix sort runs in O(n)O(n) time.

Worked Example: Counting Sort Trace

Sort the array [4,2,2,8,3,3,1][4, 2, 2, 8, 3, 3, 1] using counting sort.

Range of values: [1,8][1, 8] So k=8k = 8.

Step 1, Count: C=[0,1,2,2,1,0,0,0]C = [0, 1, 2, 2, 1, 0, 0, 0] (indices 1 through 8).

Step 2, Prefix sums: C=[0,1,3,5,6,6,6,6]C = [0, 1, 3, 5, 6, 6, 6, 6].

Step 3, Place (iterate backwards):

  • A[6]=1A[6] = 1: C[1]=1C[1] = 1 Place at position 0. C[1]=0C[1] = 0.
  • A[5]=3A[5] = 3: C[3]=5C[3] = 5 Place at position 4. C[3]=4C[3] = 4.
  • A[4]=3A[4] = 3: C[3]=4C[3] = 4 Place at position 3. C[3]=3C[3] = 3.
  • A[3]=8A[3] = 8: C[8]=6C[8] = 6 Place at position 5. C[8]=5C[8] = 5.
  • A[2]=2A[2] = 2: C[2]=3C[2] = 3 Place at position 2. C[2]=2C[2] = 2.
  • A[1]=2A[1] = 2: C[2]=2C[2] = 2 Place at position 1. C[2]=1C[2] = 1.
  • A[0]=4A[0] = 4: C[4]=6C[4] = 6 Place at position 5. C[4]=5C[4] = 5.

Result: [1,2,2,3,3,4,8][1, 2, 2, 3, 3, 4, 8].

Worked Example: Radix Sort Trace

Sort the array [170,45,75,90,802,24,2,66][170, 45, 75, 90, 802, 24, 2, 66] using LSD radix sort with base 10.

Pass 1 (ones digit): Sort by 0,5,5,0,2,4,2,60, 5, 5, 0, 2, 4, 2, 6. After stable counting sort: [170,90,802,2,24,45,75,66][170, 90, 802, 2, 24, 45, 75, 66]

Pass 2 (tens digit): Sort by 7,9,0,0,2,4,7,67, 9, 0, 0, 2, 4, 7, 6. After stable counting sort: [802,2,24,45,66,170,75,90][802, 2, 24, 45, 66, 170, 75, 90]

Pass 3 (hundreds digit): Sort by 8,0,0,0,0,1,0,08, 0, 0, 0, 0, 1, 0, 0. After stable counting sort: [2,24,45,66,75,90,170,802][2, 24, 45, 66, 75, 90, 170, 802]

Total: 3 passes, each O(n+10)=O(n)O(n + 10) = O(n). Total: O(3n)=O(n)O(3n) = O(n).

Problem. Sort data that is too large to fit in main memory.

Algorithm (External Merge Sort):

  1. Read chunks that fit in memory, sort each in memory, write sorted runs to disk.
  2. Repeatedly merge runs using a kk-way merge, reading/writing from disk.

Theorem 3.12. External merge sort with MM bytes of memory and NN bytes of data performs O(NMlog⁡k(NM))O\left(\frac{N}{M} \log_{k}\left(\frac{N}{M}\right)\right) passes, where kk is the merge fan-in.

Proof. The initial pass creates N/MN/M sorted runs of size MM. Each merge pass combines kk runs into 1, reducing the number of runs by a factor of kk. After log⁡k(N/M)\log_k(N/M) passes, a single sorted run remains. Each pass reads and writes all NN bytes, so total I/O is O(Nlog⁡k(N/M))O(N \log_k(N/M)). ■\blacksquare

AlgorithmBestAverageWorstSpaceStable
Merge SortO(nlog⁡n)O(n \log n)O(nlog⁡n)O(n \log n)O(nlog⁡n)O(n \log n)O(n)O(n)Yes
QuicksortO(nlog⁡n)O(n \log n)O(nlog⁡n)O(n \log n)O(n2)O(n^2)O(log⁡n)O(\log n)No
HeapsortO(nlog⁡n)O(n \log n)O(nlog⁡n)O(n \log n)O(nlog⁡n)O(n \log n)O(1)O(1)No
Counting SortO(n+k)O(n + k)O(n+k)O(n + k)O(n+k)O(n + k)O(n+k)O(n + k)Yes
Radix SortO(d(n+b))O(d(n+b))O(d(n+b))O(d(n+b))O(d(n+b))O(d(n+b))O(n+b)O(n + b)Yes

BFS explores the graph level by level from a source vertex.

BFS(G, s):
for each v in V:
d[v] = INF
pi[v] = NIL
d[s] = 0
Q = {s}
while Q is not empty:
u = Q.dequeue()
for each v in G.adj[u]:
if d[v] == INF:
d[v] = d[u] + 1
pi[v] = u
Q.enqueue(v)

Theorem 4.1. BFS runs in O(V+E)O(V + E) time and discovers shortest paths (in terms of number of Edges) from the source to all reachable vertices.

Proof. Time: Each vertex is enqueued at most once and dequeued at most once: O(V)O(V). Each edge is examined at most twice (once from each endpoint): O(E)O(E). Total: O(V+E)O(V + E).

Correctness: We prove by induction on the length of shortest paths. Let vv be a vertex discovered via edge (u,v)(u, v). At the time vv is discovered, d[u]d[u] equals the shortest-path distance from ss to uu (induction hypothesis). Since BFS processes vertices in non-decreasing order of distance, d[v]=d[u]+1d[v] = d[u] + 1 is the shortest distance to vv. Any path to vv must go through some vertex at distance at most d[u]d[u] (the predecessor on the path), and all such vertices have already been processed. ■\blacksquare

Worked Example: BFS on a Weighted Path Problem

Find the shortest path (fewest edges) from AA to all other vertices in the graph with edges: (A,B),(A,C),(B,D),(C,D),(C,E),(D,F),(E,F)(A,B), (A,C), (B,D), (C,D), (C,E), (D,F), (E,F).

Starting from AA:

  • Level 0: AA (distance 0)
  • Level 1: B,CB, C (distance 1)
  • Level 2: D,ED, E (distance 2)
  • Level 3: FF (distance 3)

Shortest path from AA to FF: A→C→E→FA \to C \to E \to F (or A→B→D→FA \to B \to D \to F or A→C→D→FA \to C \to D \to F), all of length 3.

DFS explores as far as possible along each branch before backtracking.

DFS(G):
for each v in V:
colour[v] = WHITE
pi[v] = NIL
time = 0
for each v in V:
if colour[v] == WHITE:
DFS-Visit(G, v)
DFS-Visit(G, u):
colour[u] = GREY
time += 1
d[u] = time
for each v in G.adj[u]:
if colour[v] == WHITE:
pi[v] = u
DFS-Visit(G, v)
colour[u] = BLACK
time += 1
f[u] = time

Theorem 4.2. DFS runs in O(V+E)O(V + E) time and can be used to detect cycles, find connected Components, compute topological orderings, and identify strongly connected components.

Proof (time). Each vertex is coloured exactly once (from WHITE to GREY) and finished exactly once (from GREY to BLACK): O(V)O(V). Each edge is examined at most twice (once in each direction for undirected, once for directed): O(E)O(E). Total: O(V+E)O(V + E). ■\blacksquare

Theorem 4.3 (Parenthesis Theorem). In any DFS, for any two vertices uu and vv Exactly one of the following holds: (1) d[u]<d[v]<f[v]<f[u]d[u] \lt d[v] \lt f[v] \lt f[u] (interval nesting), (2) d[v]<d[u]<f[u]<f[v]d[v] \lt d[u] \lt f[u] \lt f[v] (interval nesting), or (3) the intervals [d[u],f[u]][d[u], f[u]] and [d[v],f[v]][d[v], f[v]] are disjoint.

Proof. The DFS call stack forms a nesting of intervals. When we start visiting vv from uu We must finish vv before finishing uu Giving nesting. If uu and vv are in different DFS trees, their intervals are disjoint. ■\blacksquare

Theorem 4.4 (White-Path Theorem). vv is a descendant of uu in the DFS forest if and only if, at the time d[u]d[u] is discovered, there is a path from uu to vv consisting entirely of white vertices.

Proof. (⇒\Rightarrow) By induction on the depth of vv in the DFS tree. If vv is a child of uu Then vv was white when discovered from uu. If vv is a deeper descendant, the path goes through intermediate white vertices.

(⇐\Leftarrow) Suppose there is a white path from uu to vv at time d[u]d[u]. Let ww be the first vertex on this path discovered after uu. All vertices before ww on the path are still white (they can only be discovered after ww), so ww will be discovered from the path. By induction, vv is a descendant of ww Hence of uu. ■\blacksquare

A topological ordering of a DAG is a linear ordering of vertices such that for every directed edge (u,v)(u, v), uu appears before vv.

Algorithm: Run DFS on the DAG. Output vertices in reverse order of finishing times.

Theorem 4.5. The reverse post-order of a DFS on a DAG produces a valid topological ordering.

Proof. Suppose there is an edge (u,v)(u, v) but uu appears after vv in the ordering (i.e., f[u]<f[v]f[u] \lt f[v]). Since (u,v)(u, v) is an edge, when uu is being explored (coloured GREY), if vv is WHITE, then vv is discovered as a descendant of uu So f[v]<f[u]f[v] \lt f[u] Contradiction. If vv is GREY, we have a back edge, implying a cycle, contradicting that the graph is acyclic. If vv is BLACK, then f[v]<d[u]<f[u]f[v] \lt d[u] \lt f[u] Contradicting f[u]<f[v]f[u] \lt f[v]. ■\blacksquare

Two vertices uu and vv are strongly connected if there is a path from uu to vv and from vv to uu. A strongly connected component (SCC) is a maximal set of strongly connected vertices.

Kosaraju’s Algorithm:

  1. Run DFS on GG Recording finishing times.
  2. Compute GTG^T (transpose of GG: reverse all edges).
  3. Run DFS on GTG^T in decreasing order of finishing times. Each DFS tree is an SCC.

Theorem 4.6. Kosaraju’s algorithm correctly identifies all SCCs in O(V+E)O(V + E) time.

Proof. Let CC be the SCC containing the vertex ss with the highest finishing time in the first DFS. We claim that in GTG^T No vertex in CC can reach a vertex outside CC (otherwise ss would have a path to and from that vertex, placing it in CC). In the second DFS, starting from ss in GTG^T We discover exactly the vertices in CC. Removing CC and repeating the argument gives the remaining SCCs. ■\blacksquare

Tarjan’s Algorithm finds SCCs in a single DFS pass using a stack and low-link values, also in O(V+E)O(V + E) time.

Problem. Find shortest paths from a single source in a weighted graph with non-negative edge Weights.

Algorithm. Maintain a priority queue of vertices keyed by their current shortest-path estimate. Repeatedly extract the vertex with minimum distance and relax its edges.

Dijkstra(G, w, s):
for each v in V:
d[v] = INF
pi[v] = NIL
d[s] = 0
S = {} // processed vertices
Q = V // priority queue keyed by d[]
while Q is not empty:
u = ExtractMin(Q)
S = S ∪ {u}
for each v in G.adj[u]:
if d[v] > d[u] + w(u, v):
d[v] = d[u] + w(u, v)
pi[v] = u
DecreaseKey(Q, v, d[v])

Theorem 4.7. Dijkstra’s algorithm correctly computes shortest paths from the source.

Proof. We prove by induction on ∣S∣|S| that when a vertex uu is added to SS, d[u]=δ(s,u)d[u] = \delta(s, u) (the true shortest-path distance).

Base case: ss is the first vertex added, and d[s]=0=δ(s,s)d[s] = 0 = \delta(s, s).

Inductive step: Suppose all vertices in SS have correct distances. Let uu be the next vertex extracted from QQ. Suppose for contradiction that d[u]>δ(s,u)d[u] > \delta(s, u). Consider a shortest path PP from ss to uu And let (x,y)(x, y) be the first edge on PP where x∈Sx \in S and y∉Sy \notin S. Then δ(s,y)=δ(s,x)+w(x,y)=d[x]+w(x,y)\delta(s, y) = \delta(s, x) + w(x, y) = d[x] + w(x, y) (by induction). When xx was added to SS The edge (x,y)(x, y) was relaxed, so d[y]≤d[x]+w(x,y)=δ(s,y)d[y] \leq d[x] + w(x, y) = \delta(s, y). Since edge weights are non-negative, δ(s,y)≤δ(s,u)\delta(s, y) \leq \delta(s, u). But d[y]≤δ(s,y)≤δ(s,u)<d[u]d[y] \leq \delta(s, y) \leq \delta(s, u) \lt d[u] And yy is in QQ Contradicting that uu has the minimum dd-value in QQ. ■\blacksquare

Theorem 4.8. Dijkstra’s algorithm with a binary heap runs in O((V+E)log⁡V)O((V + E)\log V) time. With a Fibonacci heap: O(Vlog⁡V+E)O(V \log V + E).

Proof. Each vertex is extracted from the priority queue at most once: O(Vlog⁡V)O(V \log V). Each edge is Relaxed at most once, and each relaxation may cause a DecreaseKey: O(Elog⁡V)O(E \log V). Total: O((V+E)log⁡V)O((V + E)\log V). With a Fibonacci heap, DecreaseKey is O(1)O(1) amortised, giving O(Vlog⁡V+E)O(V \log V + E). ■\blacksquare

Worked Example: Dijkstra's Algorithm

Find shortest paths from AA in the graph:

  • A→4BA \xrightarrow{4} B, A→2CA \xrightarrow{2} C
  • B→3DB \xrightarrow{3} D, B→1EB \xrightarrow{1} E
  • C→1BC \xrightarrow{1} B, C→5DC \xrightarrow{5} D
  • D→2ED \xrightarrow{2} E

Initial: d[A]=0d[A]=0, d[B]=d[C]=d[D]=d[E]=∞d[B]=d[C]=d[D]=d[E]=\infty

Extract AA: relax B→4B \to 4, C→2C \to 2. Q=C(2),B(4),D(∞),E(∞)Q = \\{C(2), B(4), D(\infty), E(\infty)\\}

Extract CC: relax B→min⁡(4,2+1)=3B \to \min(4, 2+1)=3, D→2+5=7D \to 2+5=7. Q=B(3),D(7),E(∞)Q = \\{B(3), D(7), E(\infty)\\}

Extract BB: relax D→min⁡(7,3+3)=6D \to \min(7, 3+3)=6, E→3+1=4E \to 3+1=4. Q=E(4),D(6)Q = \\{E(4), D(6)\\}

Extract EE: relax D→min⁡(6,4+2)=6D \to \min(6, 4+2)=6. Q=D(6)Q = \\{D(6)\\}

Extract DD: no improvements.

Result: d[A]=0d[A]=0, d[B]=3d[B]=3, d[C]=2d[C]=2, d[D]=6d[D]=6, d[E]=4d[E]=4

Problem. Find shortest paths from a single source, allowing negative edge weights (but no negative Cycles).

BellmanFord(G, w, s):
for each v in V:
d[v] = INF
pi[v] = NIL
d[s] = 0
for i = 1 to |V| - 1:
for each edge (u, v) in E:
if d[v] > d[u] + w(u, v):
d[v] = d[u] + w(u, v)
pi[v] = u
for each edge (u, v) in E:
if d[v] > d[u] + w(u, v):
return "negative cycle detected"

Theorem 4.9. Bellman-Ford runs in O(VE)O(VE) time. It correctly detects negative-weight cycles.

Proof. After ii iterations of the relaxation loop, d[v]d[v] is at most the weight of the shortest path from ss to vv using at most ii edges. This is proved by induction on ii.

Base case (i=0i = 0): Only d[s]=0d[s] = 0 is finite, which is the weight of the empty path.

Inductive step: A shortest path from ss to vv using at most ii edges either uses at most i−1i-1 edges (handled by induction) or uses exactly ii edges, ending with edge (u,v)(u, v) where the shortest path to uu uses at most i−1i-1 edges. The relaxation of (u,v)(u, v) in iteration ii handles this case.

After V−1V - 1 iterations, all shortest paths (which have at most V−1V - 1 edges) are correct. A VV-th iteration that updates any distance indicates a path of length VV with strictly decreasing weight, which contains a cycle of negative weight. ■\blacksquare

Worked Example: Bellman-Ford with Negative Edge Weights

Find shortest paths from AA in the graph:

  • A→6BA \xrightarrow{6} B, A→7CA \xrightarrow{7} C
  • B→5CB \xrightarrow{5} C, B→−4DB \xrightarrow{-4} D, B→8EB \xrightarrow{8} E
  • C→−3DC \xrightarrow{-3} D, C→9EC \xrightarrow{9} E
  • D→2ED \xrightarrow{2} E, D→7FD \xrightarrow{7} F
  • E→−5FE \xrightarrow{-5} F

Initial: d=[∞,∞,∞,∞,∞,∞]d = [\infty, \infty, \infty, \infty, \infty, \infty] for [A,B,C,D,E,F][A, B, C, D, E, F]. d[A]=0d[A] = 0.

Iteration 1: Relax all edges.

  • (A,B)(A,B): d[B]=6d[B] = 6. (A,C)(A,C): d[C]=7d[C] = 7.
  • (B,C)(B,C): min⁡(7,6+5)=7\min(7, 6+5)=7. (B,D)(B,D): d[D]=6+(−4)=2d[D] = 6+(-4) = 2. (B,E)(B,E): d[E]=6+8=14d[E] = 6+8 = 14.
  • (C,D)(C,D): min⁡(2,7+(−3))=2\min(2, 7+(-3)) = 2. (C,E)(C,E): min⁡(14,7+9)=14\min(14, 7+9) = 14.
  • (D,E)(D,E): min⁡(14,2+2)=4\min(14, 2+2) = 4. (D,F)(D,F): d[F]=2+7=9d[F] = 2+7 = 9.
  • (E,F)(E,F): min⁡(9,4+(−5))=−1\min(9, 4+(-5)) = -1. After iter 1: d=[0,6,7,2,4,−1]d = [0, 6, 7, 2, 4, -1].

Iteration 2: Relax all edges.

  • (B,D)(B,D): min⁡(2,6+(−4))=2\min(2, 6+(-4)) = 2. (B,E)(B,E): min⁡(4,6+8)=4\min(4, 6+8) = 4.
  • (D,E)(D,E): min⁡(4,2+2)=4\min(4, 2+2) = 4. (D,F)(D,F): min⁡(−1,2+7)=−1\min(-1, 2+7) = -1.
  • (E,F)(E,F): min⁡(−1,4+(−5))=−1\min(-1, 4+(-5)) = -1. No changes: d=[0,6,7,2,4,−1]d = [0, 6, 7, 2, 4, -1].

Iteration 3—5: No changes. Algorithm terminates.

Negative cycle check: No edge can be relaxed. No negative cycle.

Result: d=[0,6,7,2,4,−1]d = [0, 6, 7, 2, 4, -1]. Shortest path to FF: A→B→D→E→FA \to B \to D \to E \to F Cost −1-1.

Worked Example: Bellman-Ford Negative Cycle Detection

Graph: A→1BA \xrightarrow{1} B, B→−3CB \xrightarrow{-3} C, C→2AC \xrightarrow{2} A. This has a cycle A→B→C→AA \to B \to C \to A of weight 1+(−3)+2=01 + (-3) + 2 = 0. Not negative.

Now add C→−1AC \xrightarrow{-1} A. Cycle weight: 1+(−3)+(−1)=−31 + (-3) + (-1) = -3. Negative cycle.

Initial: d[A]=0d[A] = 0 Rest ∞\infty.

Iteration 1: d[B]=1d[B] = 1, d[C]=−2d[C] = -2, d[A]=min⁡(0,−2+(−1))=−3d[A] = \min(0, -2 + (-1)) = -3.

Iteration 2: d[B]=−2d[B] = -2, d[C]=−5d[C] = -5, d[A]=−6d[A] = -6.

Iteration 3: d[B]=−5d[B] = -5, d[C]=−8d[C] = -8, d[A]=−9d[A] = -9.

Iteration 4: d[B]=−8d[B] = -8, d[C]=−11d[C] = -11, d[A]=−12d[A] = -12.

Iteration 5: d[B]=−11d[B] = -11, d[C]=−14d[C] = -14, d[A]=−15d[A] = -15.

Check (iteration 6): (A,B)(A,B): −15+1=−14<−11-15 + 1 = -14 \lt -11 Can still relax. Negative cycle detected!

Problem. Find all-pairs shortest paths.

Algorithm. For k=1,…,Vk = 1, \ldots, V: for each pair (i,j)(i, j) Check if going through vertex kk Improves the path.

dij(k)=min⁡(dij(k−1),dik(k−1)+dkj(k−1))d_{ij}^{(k)} = \min(d_{ij}^{(k-1)}, d_{ik}^{(k-1)} + d_{kj}^{(k-1)})

Derivation. Define dij(k)d_{ij}^{(k)} as the shortest-path distance from ii to jj using only intermediate vertices from 1,2,…,k\\{1, 2, \ldots, k\\}. Then:

  • dij(0)=w(i,j)d_{ij}^{(0)} = w(i,j) (the weight of edge (i,j)(i,j) Or ∞\infty if no edge).
  • For k≥1k \geq 1: The shortest path from ii to jj through vertices 1,…,k\\{1, \ldots, k\\} either does not use vertex kk (giving dij(k−1)d_{ij}^{(k-1)}) or uses vertex kk (giving dik(k−1)+dkj(k−1)d_{ik}^{(k-1)} + d_{kj}^{(k-1)}).

Theorem 4.10. Floyd-Warshall runs in O(V3)O(V^3) time and O(V2)O(V^2) space.

Proof. The triple nested loop (kk, ii, jj) executes V3V^3 iterations, each doing O(1)O(1) work. The distance matrix requires V2V^2 space. Note that dij(k)d_{ij}^{(k)} can overwrite dij(k−1)d_{ij}^{(k-1)} in place because dik(k)=dik(k−1)d_{ik}^{(k)} = d_{ik}^{(k-1)} and dkj(k)=dkj(k−1)d_{kj}^{(k)} = d_{kj}^{(k-1)} (paths from ii to kk and kk to jj using vertices up to kk cannot be improved by going through kk again without a negative cycle). ■\blacksquare

Worked Example: Floyd-Warshall on 4 Vertices

Find all-pairs shortest paths for the graph with vertices 1,2,3,4\\{1, 2, 3, 4\\} and edges: w(1,2)=3w(1,2) = 3, w(1,3)=8w(1,3) = 8, w(1,4)=−4w(1,4) = -4 w(2,1)=5w(2,1) = 5, w(2,3)=7w(2,3) = 7, w(2,4)=2w(2,4) = 2 w(3,1)=2w(3,1) = 2, w(3,4)=−1w(3,4) = -1 w(4,1)=6w(4,1) = 6, w(4,3)=9w(4,3) = 9.

Initial distance matrix D(0)D^{(0)}:

D(0)=(038−450722∞0−16∞90)D^{(0)} = \begin{pmatrix} 0 & 3 & 8 & -4 \\ 5 & 0 & 7 & 2 \\ 2 & \infty & 0 & -1 \\ 6 & \infty & 9 & 0 \end{pmatrix}

k=1k = 1 (through vertex 1): D(1)[2][3]=min⁡(7,5+8)=7D^{(1)}[2][3] = \min(7, 5 + 8) = 7. D(1)[2][4]=min⁡(2,5+(−4))=1D^{(1)}[2][4] = \min(2, 5 + (-4)) = 1. D(1)[3][2]=min⁡(∞,2+3)=5D^{(1)}[3][2] = \min(\infty, 2 + 3) = 5. D(1)[3][4]=min⁡(−1,2+(−4))=−2D^{(1)}[3][4] = \min(-1, 2 + (-4)) = -2. D(1)[4][2]=min⁡(∞,6+3)=9D^{(1)}[4][2] = \min(\infty, 6 + 3) = 9. D(1)[4][3]=min⁡(9,6+8)=9D^{(1)}[4][3] = \min(9, 6 + 8) = 9.

D(1)=(038−45071250−26990)D^{(1)} = \begin{pmatrix} 0 & 3 & 8 & -4 \\ 5 & 0 & 7 & 1 \\ 2 & 5 & 0 & -2 \\ 6 & 9 & 9 & 0 \end{pmatrix}

k=2k = 2 (through vertex 2): D(2)[1][3]=min⁡(8,3+7)=8D^{(2)}[1][3] = \min(8, 3 + 7) = 8. D(2)[1][4]=min⁡(−4,3+1)=−4D^{(2)}[1][4] = \min(-4, 3 + 1) = -4. D(2)[3][1]=min⁡(2,5+5)=2D^{(2)}[3][1] = \min(2, 5 + 5) = 2. D(2)[3][4]=min⁡(−2,5+1)=−2D^{(2)}[3][4] = \min(-2, 5 + 1) = -2. D(2)[4][1]=min⁡(6,9+5)=6D^{(2)}[4][1] = \min(6, 9 + 5) = 6. D(2)[4][3]=min⁡(9,9+7)=9D^{(2)}[4][3] = \min(9, 9 + 7) = 9.

D(2)=(038−45071250−26990)D^{(2)} = \begin{pmatrix} 0 & 3 & 8 & -4 \\ 5 & 0 & 7 & 1 \\ 2 & 5 & 0 & -2 \\ 6 & 9 & 9 & 0 \end{pmatrix}

k=3k = 3 (through vertex 3): D(3)[1][2]=min⁡(3,8+5)=3D^{(3)}[1][2] = \min(3, 8 + 5) = 3. D(3)[1][4]=min⁡(−4,8+(−2))=−4D^{(3)}[1][4] = \min(-4, 8 + (-2)) = -4. D(3)[2][1]=min⁡(5,7+2)=5D^{(3)}[2][1] = \min(5, 7 + 2) = 5. D(3)[2][4]=min⁡(1,7+(−2))=1D^{(3)}[2][4] = \min(1, 7 + (-2)) = 1. D(3)[4][1]=min⁡(6,9+2)=6D^{(3)}[4][1] = \min(6, 9 + 2) = 6. D(3)[4][2]=min⁡(9,9+5)=9D^{(3)}[4][2] = \min(9, 9 + 5) = 9.

D(3)=(038−45071250−26990)D^{(3)} = \begin{pmatrix} 0 & 3 & 8 & -4 \\ 5 & 0 & 7 & 1 \\ 2 & 5 & 0 & -2 \\ 6 & 9 & 9 & 0 \end{pmatrix}

k=4k = 4 (through vertex 4): D(4)[1][2]=min⁡(3,−4+9)=3D^{(4)}[1][2] = \min(3, -4 + 9) = 3. D(4)[1][3]=min⁡(8,−4+9)=5D^{(4)}[1][3] = \min(8, -4 + 9) = 5. D(4)[2][1]=min⁡(5,1+6)=5D^{(4)}[2][1] = \min(5, 1 + 6) = 5. D(4)[2][3]=min⁡(7,1+9)=7D^{(4)}[2][3] = \min(7, 1 + 9) = 7. D(4)[3][1]=min⁡(2,−2+6)=2D^{(4)}[3][1] = \min(2, -2 + 6) = 2. D(4)[3][2]=min⁡(5,−2+9)=5D^{(4)}[3][2] = \min(5, -2 + 9) = 5.

D(4)=(035−45071250−26990)D^{(4)} = \begin{pmatrix} 0 & 3 & 5 & -4 \\ 5 & 0 & 7 & 1 \\ 2 & 5 & 0 & -2 \\ 6 & 9 & 9 & 0 \end{pmatrix}

Kruskal’s Algorithm. Sort edges by weight, add edges to the MST as long as they don’t create a Cycle (using Union-Find). O(Elog⁡E)O(E \log E).

Prim’s Algorithm. Start from any vertex, repeatedly add the minimum-weight edge connecting the Tree to a non-tree vertex (using a priority queue). O((V+E)log⁡V)O((V + E)\log V).

Theorem 4.11 (Cut Property). For any cut of the graph, the minimum-weight edge crossing the cut Belongs to some MST.

Proof. Let (S,V∖S)(S, V \setminus S) be a cut and e=(u,v)e = (u, v) be the minimum-weight crossing edge with u∈Su \in S, v∉Sv \notin S. Let TT be an MST. If e∈Te \in T We are done. Otherwise, adding ee to TT creates a cycle. This cycle must cross the cut at least once more (it goes from uu to vv via some other path). Let e′e' be another crossing edge on this cycle. Since ee is the minimum-weight crossing edge, w(e)≤w(e′)w(e) \leq w(e'). Replacing e′e' with ee in TT gives a spanning tree of weight no greater than TT Hence an MST containing ee. ■\blacksquare

Theorem 4.12 (Cycle Property). For any cycle, the maximum-weight edge on the cycle does not belong To any MST.

Proof. Let CC be a cycle and ee be the maximum-weight edge on CC. Let TT be an MST. If e∉Te \notin T We are done. Otherwise, removing ee from TT disconnects it into two components. The rest of cycle CC must contain an edge e′≠ee' \neq e crossing this cut. Since w(e′)<w(e)w(e') \lt w(e) (if w(e′)=w(e)w(e') = w(e) We can replace either), replacing ee with e′e' gives a spanning tree of strictly smaller weight, contradicting the optimality of TT. ■\blacksquare

Theorem 4.13. Kruskal’s algorithm produces a minimum spanning tree.

Proof. We prove by induction on the number of edges added. At each step, the algorithm adds the minimum-weight edge that does not create a cycle. Consider the cut formed by the two connected components that the new edge connects. By the cut property, this minimum-weight crossing edge belongs to some MST. Since all previously added edges are in every MST (by induction), the edge set maintained by the algorithm is always a subset of some MST. ■\blacksquare

Theorem 4.14. Prim’s algorithm produces a minimum spanning tree.

Proof. By induction on the size of the tree. At each step, Prim’s adds the minimum-weight edge connecting the current tree TT to a vertex outside TT. Consider the cut (T,V∖T)(T, V \setminus T). By the cut property, the minimum-weight crossing edge belongs to some MST. ■\blacksquare

Worked Example: Kruskal's Algorithm

Find the MST of the graph with edges (sorted by weight): (D,E,2)(D, E, 2), (C,E,3)(C, E, 3), (A,B,4)(A, B, 4), (B,C,5)(B, C, 5), (B,E,6)(B, E, 6), (A,E,7)(A, E, 7), (A,D,8)(A, D, 8), (C,D,9)(C, D, 9).

Vertices: A,B,C,D,E\\{A, B, C, D, E\\}.

Sorted edges: (D,E,2)(D,E,2), (C,E,3)(C,E,3), (A,B,4)(A,B,4), (B,C,5)(B,C,5), (B,E,6)(B,E,6), (A,E,7)(A,E,7), (A,D,8)(A,D,8), (C,D,9)(C,D,9).

Process each edge:

  1. (D,E,2)(D, E, 2): Add. Forest: D−E\\{D-E\\}. Cost: 2.
  2. (C,E,3)(C, E, 3): Add. Forest: C−D−E\\{C-D-E\\}. Cost: 5.
  3. (A,B,4)(A, B, 4): Add. Forest: A−B\\{A-B\\}, C−D−E\\{C-D-E\\}. Cost: 9.
  4. (B,C,5)(B, C, 5): Add (connects two components). Forest: A−B−C−D−E\\{A-B-C-D-E\\}. Cost: 14.
  5. (B,E,6)(B, E, 6): Skip (creates cycle B−C−D−E−BB-C-D-E-B).
  6. (A,E,7)(A, E, 7): Skip (creates cycle).
  7. (A,D,8)(A, D, 8): Skip (creates cycle).
  8. (C,D,9)(C, D, 9): Skip (creates cycle).

MST: (D,E,2)(D,E,2), (C,E,3)(C,E,3), (A,B,4)(A,B,4), (B,C,5)(B,C,5). Total weight: 14.

Worked Example: Prim's Algorithm

Find the MST of the same graph starting from vertex AA.

Edges from AA: (A,B,4)(A,B,4), (A,E,7)(A,E,7), (A,D,8)(A,D,8). Minimum: (A,B,4)(A,B,4). Tree: A,B\\{A, B\\}. Cost: 4.

Edges crossing cut: (B,C,5)(B,C,5), (B,E,6)(B,E,6), (A,E,7)(A,E,7), (A,D,8)(A,D,8). Minimum: (B,C,5)(B,C,5). Tree: A,B,C\\{A, B, C\\}. Cost: 9.

Edges crossing cut: (C,E,3)(C,E,3), (C,D,9)(C,D,9), (B,E,6)(B,E,6), (A,E,7)(A,E,7), (A,D,8)(A,D,8). Minimum: (C,E,3)(C,E,3). Tree: A,B,C,E\\{A, B, C, E\\}. Cost: 12.

Edges crossing cut: (D,E,2)(D,E,2), (C,D,9)(C,D,9), (A,D,8)(A,D,8). Minimum: (D,E,2)(D,E,2). Tree: A,B,C,D,E\\{A, B, C, D, E\\}. Cost: 14.

MST: (A,B,4)(A,B,4), (B,C,5)(B,C,5), (C,E,3)(C,E,3), (D,E,2)(D,E,2). Total weight: 14 (same as Kruskal’s).

Dynamic programming (DP) solves problems by:

  1. Overlapping subproblems: The same subproblems are solved repeatedly.
  2. Optimal substructure: The optimal solution contains optimal solutions to subproblems.

Approaches:

  • Top-down (memoisation): Recursive with caching.
  • Bottom-up (tabulation): Fill a table iteratively from small subproblems to large.
AspectMemoisation (Top-Down)Tabulation (Bottom-Up)
ApproachRecursive with cacheIterative table fill
OrderNatural recursion orderDependency order
SpaceO(n)O(n) stack + O(n)O(n) tableO(n)O(n) table only
OverheadFunction call overheadMinimal
SubproblemsComputes only neededComputes all
Best forWhen not all subproblems neededWhen all needed

When to use which:

  • Use memoisation when the subproblem space is sparse (not all subproblems are needed).
  • Use tabulation when most subproblems are needed (avoids recursion overhead and stack overflow).
  • Both achieve the same asymptotic time complexity.

To prove that a problem has optimal substructure:

  1. Show that an optimal solution to the problem includes an optimal solution to a subproblem.
  2. Proved by contradiction: if the optimal solution contained a suboptimal sub-solution, replacing it with an optimal one would improve the overall solution.

Example (Shortest Path). If pp is a shortest path from uu to vv and ww is an intermediate vertex on pp Then the subpath of pp from uu to ww is a shortest path from uu to ww.

Proof. If not, there exists a shorter path p′p' from uu to ww. Then p′p' concatenated with the subpath of pp from ww to vv would be shorter than pp Contradicting that pp is a shortest path. ■\blacksquare

1D DP. dp[i]dp[i] depends on dp[j]dp[j] for j<ij < i. Example: Fibonacci, longest increasing subsequence.

2D DP. dp[i][j]dp[i][j] depends on dp[i′][j′]dp[i'][j'] for (i′,j′)(i', j') in some set. Example: edit distance, Matrix chain multiplication, longest common subsequence.

Interval DP. dp[i][j]dp[i][j] depends on dp[i′][j′]dp[i'][j'] where i≤i′≤j′≤ji \leq i' \leq j' \leq j. Example: Optimal BST, matrix chain multiplication.

Problem. Given nn items with weights w1,…,wnw_1, \ldots, w_n and values v1,…,vnv_1, \ldots, v_n And a knapsack of capacity WW Maximise the total value without exceeding the capacity.

Recurrence:

dp[i][c]={0ifi=0orc=0dp[i−1][c]ifwi>cmax⁡(dp[i−1][c],dp[i−1][c−wi]+vi)ifwi≤cdp[i][c] = \begin{cases} 0 & \mathrm{if} i = 0 \mathrm{ or} c = 0 \\ dp[i-1][c] & \mathrm{if} w_i > c \\ \max(dp[i-1][c], dp[i-1][c - w_i] + v_i) & \mathrm{if} w_i \leq c \end{cases}

Time: O(nW)O(nW). Space: O(nW)O(nW) (can be reduced to O(W)O(W) with 1D array).

Proof of correctness. For each item ii Either we don’t include it (value dp[i−1][c]dp[i-1][c]) or we include it (value vi+dp[i−1][c−wi]v_i + dp[i-1][c - w_i]). The optimal choice is the maximum. The base cases are correct. ■\blacksquare

Worked Example: 0/1 Knapsack

Items: (w=1,v=1),(w=3,v=4),(w=4,v=5),(w=5,v=7)\\{(w=1, v=1), (w=3, v=4), (w=4, v=5), (w=5, v=7)\\} Capacity W=7W = 7.

Building the DP table (items as rows, capacities 0-7 as columns):

c: 0 1 2 3 4 5 6 7
i=0: 0 0 0 0 0 0 0 0
i=1: 0 1 1 1 1 1 1 1
i=2: 0 1 1 4 5 5 5 5
i=3: 0 1 1 4 5 6 6 9
i=4: 0 1 1 4 5 7 8 9

Maximum value: dp[4][7]=9dp[4][7] = 9 (items 2 and 4: w=3+5=7w = 3 + 5 = 7, v=4+7=11v = 4 + 7 = 11, let me recalculate).

Correct: items 2 and 3 (w=3+4=7w=3+4=7, v=4+5=9v=4+5=9), or items 1, 2, 4 (w=1+3+5=9>7w=1+3+5=9 > 7 Not valid). Items 1, 3 (w=1+4=5w=1+4=5, v=1+5=6v=1+5=6), items 2, 4 (w=3+5=8>7w=3+5=8 > 7). Optimal: items 2 and 3 (w=3+4=7w=3+4=7, v=4+5=9v=4+5=9).

5.6 Worked Example: Edit Distance (Levenshtein Distance)

Section titled “5.6 Worked Example: Edit Distance (Levenshtein Distance)”

Problem. Given strings ss of length mm and tt of length nn Find the minimum number of insertions, deletions, and substitutions to transform ss into tt.

Recurrence:

dp[i][j]={jifi=0iifj=0dp[i−1][j−1]ifs[i]=t[j]1+min⁡(dp[i−1][j],dp[i][j−1],dp[i−1][j−1])ifs[i]≠t[j]dp[i][j] = \begin{cases} j & \mathrm{if} i = 0 \\ i & \mathrm{if} j = 0 \\ dp[i-1][j-1] & \mathrm{if} s[i] = t[j] \\ 1 + \min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) & \mathrm{if} s[i] \neq t[j] \end{cases}

Where the three cases in the minimum are: delete from ss Insert into ss Substitute in ss.

Time: O(mn)O(mn). Space: O(mn)O(mn) (can be reduced to O(min⁡(m,n))O(\min(m,n))).

Worked Example: Edit Distance

Compute the edit distance between “kitten” and “sitting”.

Building the DP table:

"" s i t t i n g
"" 0 1 2 3 4 5 6 7
k 1 1 2 3 4 5 6 7
i 2 2 1 2 3 4 5 6
t 3 3 2 1 2 3 4 5
t 4 4 3 2 1 2 3 4
e 5 5 4 3 2 2 3 4
n 6 6 5 4 3 3 2 3

Edit distance: dp[6][7]=3dp[6][7] = 3.

Transform: kitten → sitten (substitute k→s) → sittin (substitute e→i) → sitting (insert g).

5.7 Worked Example: Matrix Chain Multiplication

Section titled “5.7 Worked Example: Matrix Chain Multiplication”

Problem. Given matrices A1,A2,…,AnA_1, A_2, \ldots, A_n where AiA_i has dimensions pi−1×pip_{i-1} \times p_i Find the parenthesisation that minimises the total number of scalar multiplications.

Recurrence:

dp[i][j]={0ifi=jmin⁡i≤k<j(dp[i][k]+dp[k+1][j]+pi−1pkpj)ifi<jdp[i][j] = \begin{cases} 0 & \mathrm{if} i = j \\ \min_{i \leq k < j} (dp[i][k] + dp[k+1][j] + p_{i-1} p_k p_j) & \mathrm{if} i < j \end{cases}

Time: O(n3)O(n^3). Space: O(n2)O(n^2).

Proof of correctness. The optimal parenthesisation of Ai⋯AjA_i \cdots A_j splits at some position kk: (Ai⋯Ak)(Ak+1⋯Aj)(A_i \cdots A_k)(A_{k+1} \cdots A_j). The cost is the cost of the left subproduct plus the cost of the right subproduct plus the cost of multiplying the resulting matrices (pi−1⋅pk⋅pjp_{i-1} \cdot p_k \cdot p_j scalar multiplications). The optimal kk minimises this total. ■\blacksquare

Worked Example: Matrix Chain Multiplication

Matrices: A1A_1 (10×3010 \times 30), A2A_2 (30×530 \times 5), A3A_3 (5×605 \times 60). Dimensions: p=[10,30,5,60]p = [10, 30, 5, 60].

dp[1][1]=dp[2][2]=dp[3][3]=0dp[1][1] = dp[2][2] = dp[3][3] = 0.

dp[1][2]=p0p1p2=10×30×5=1500dp[1][2] = p_0 p_1 p_2 = 10 \times 30 \times 5 = 1500. Split at k=1k=1: (A1A2)(A_1 A_2).

dp[2][3]=p1p2p3=30×5×60=9000dp[2][3] = p_1 p_2 p_3 = 30 \times 5 \times 60 = 9000. Split at k=2k=2: (A2A3)(A_2 A_3).

dp[1][3]dp[1][3]: Try k=1k=1: dp[1][1]+dp[2][3]+10×30×60=0+9000+18000=27000dp[1][1] + dp[2][3] + 10 \times 30 \times 60 = 0 + 9000 + 18000 = 27000. Try k=2k=2: dp[1][2]+dp[3][3]+10×5×60=1500+0+3000=4500dp[1][2] + dp[3][3] + 10 \times 5 \times 60 = 1500 + 0 + 3000 = 4500.

Minimum: dp[1][3]=4500dp[1][3] = 4500 Split at k=2k=2: (A1(A2A3))(A_1(A_2 A_3)).

5.8 Worked Example: Longest Common Subsequence

Section titled “5.8 Worked Example: Longest Common Subsequence”

Problem. Given sequences X=(x1,…,xm)X = (x_1, \ldots, x_m) and Y=(y1,…,yn)Y = (y_1, \ldots, y_n) Find the LCS.

Recurrence:

dp[i][j]={0ifi=0orj=0dp[i−1][j−1]+1ifxi=yjmax⁡(dp[i−1][j],dp[i][j−1])ifxi≠yjdp[i][j] = \begin{cases} 0 & \mathrm{if} i = 0 \mathrm{ or} j = 0 \\ dp[i-1][j-1] + 1 & \mathrm{if} x_i = y_j \\ \max(dp[i-1][j], dp[i][j-1]) & \mathrm{if} x_i \neq y_j \end{cases}

Time: O(mn)O(mn). Space: O(mn)O(mn) (can be reduced to O(min⁡(m,n))O(\min(m,n)) for the length only).

Proof of correctness. If xi=yjx_i = y_j Any LCS of X[1..i]X[1..i] and Y[1..j]Y[1..j] must include xix_i So LCS=1+LCS(X[1..i−1],Y[1..j−1])\mathrm{LCS} = 1 + \mathrm{LCS}(X[1..i-1], Y[1..j-1]). If xi≠yjx_i \neq y_j The LCS either Excludes xix_i or excludes yjy_j Giving the max of the two subproblems. ■\blacksquare

Problem. Given coin denominations d1,…,dnd_1, \ldots, d_n and a target amount MM Find the minimum number of coins needed.

Recurrence:

dp[c]={0ifc=0min⁡i:di≤c(dp[c−di]+1)ifc>0dp[c] = \begin{cases} 0 & \mathrm{if} c = 0 \\ \min_{i: d_i \leq c}(dp[c - d_i] + 1) & \mathrm{if} c > 0 \end{cases}

Time: O(nM)O(nM). Space: O(M)O(M).

Proof of correctness. To make change for amount c>0c > 0 The last coin used must be some di≤cd_i \leq c. The remaining amount is c−dic - d_i And the optimal solution for cc uses 1+dp[c−di]1 + dp[c - d_i] coins. Taking the minimum over all valid did_i gives the optimal solution. ■\blacksquare

Worked Example: Coin Change

Denominations: 1,5,10,25\\{1, 5, 10, 25\\}. Target: M=63M = 63.

Bottom-up computation:

  • dp[0]=0dp[0] = 0
  • dp[1..4]=dp[c−1]+1=cdp[1..4] = dp[c - 1] + 1 = c (use pennies)
  • dp[5]=min⁡(dp[4]+1,dp[0]+1)=1dp[5] = \min(dp[4]+1, dp[0]+1) = 1 (use a nickel)
  • dp[10]=min⁡(dp[9]+1,dp[5]+1,dp[0]+1)=1dp[10] = \min(dp[9]+1, dp[5]+1, dp[0]+1) = 1 (use a dime)
  • …
  • dp[25]=1dp[25] = 1 (use a quarter)
  • dp[50]=2dp[50] = 2 (use two quarters)
  • dp[63]=min⁡(dp[62]+1,dp[58]+1,dp[53]+1,dp[38]+1)dp[63] = \min(dp[62]+1, dp[58]+1, dp[53]+1, dp[38]+1)

Working backwards: dp[63]=dp[38]+1=dp[13]+2=dp[3]+3=6dp[63] = dp[38] + 1 = dp[13] + 2 = dp[3] + 3 = 6.

Solution: 2 quarters + 1 dime + 3 pennies = 25+25+10+1+1+1=6325 + 25 + 10 + 1 + 1 + 1 = 63. 6 coins.

5.10 Worked Example: Longest Increasing Subsequence

Section titled “5.10 Worked Example: Longest Increasing Subsequence”

Problem. Given a sequence a1,…,ana_1, \ldots, a_n Find the length of the longest strictly increasing subsequence (not necessarily contiguous).

Recurrence: dp[i]=1+max⁡dp[j]:j<i and aj<aidp[i] = 1 + \max\\{dp[j] : j \lt i \mathrm{~and~} a_j \lt a_i\\} With dp[i]=1dp[i] = 1 if no such jj exists.

Time: O(n2)O(n^2). Space: O(n)O(n).

Worked Example: Longest Increasing Subsequence

Find the LIS of [10,22,9,33,21,50,41,60,80][10, 22, 9, 33, 21, 50, 41, 60, 80].

dp[0]=1dp[0] = 1 (just 10) dp[1]=dp[0]+1=2dp[1] = dp[0] + 1 = 2 (10, 22) dp[2]=1dp[2] = 1 (just 9) dp[3]=dp[1]+1=3dp[3] = dp[1] + 1 = 3 (10, 22, 33) dp[4]=dp[0]+1=2dp[4] = dp[0] + 1 = 2 (10, 21) dp[5]=dp[3]+1=4dp[5] = dp[3] + 1 = 4 (10, 22, 33, 50) dp[6]=dp[4]+1=3dp[6] = dp[4] + 1 = 3 (10, 21, 41) dp[7]=dp[5]+1=5dp[7] = dp[5] + 1 = 5 (10, 22, 33, 50, 60) dp[8]=dp[7]+1=6dp[8] = dp[7] + 1 = 6 (10, 22, 33, 50, 60, 80)

LIS length: 6.

Patience sorting approach (O(nlog⁡n)O(n \log n)): Maintain piles. For each card, place on the leftmost pile whose top card is ≥\geq the current card, or start a new pile on the right if no such pile exists. The number of piles equals the LIS length.

P: The class of decision problems solvable in polynomial time by a deterministic Turing machine.

NP: The class of decision problems solvable in polynomial time by a non-deterministic Turing Machine. Equivalently, problems whose “yes” instances have polynomial-time verifiable certificates.

NP-hard: A problem AA is NP-hard if every problem in NP can be reduced to AA in polynomial Time.

NP-complete: A problem is NP-complete if it is in NP and NP-hard.

Theorem 6.1. If any NP-complete problem is in P, then P = NP.

A polynomial-time reduction from problem AA to problem BB is a polynomial-time algorithm that Transforms instances of AA into instances of BB Preserving the answer.

Lemma 6.1. If A≤pBA \leq_p B and B∈PB \in P Then A∈PA \in P.

Lemma 6.2. If A≤pBA \leq_p B and AA is NP-hard, then BB is NP-hard.

Theorem 6.2 (Cook-Levin, 1971). SAT is NP-complete.

Proof sketch. We show that every problem in NP reduces to SAT. Let L∈NPL \in \mathrm{NP}. There exists a polynomial-time non-deterministic Turing machine MM that decides LL Running in time p(n)p(n) on inputs of length nn. For an input xx We construct a Boolean formula ϕx\phi_x that is satisfiable if and only if MM accepts xx.

The formula encodes:

  1. Tableau variables: T[i,j,σ]=1T[i, j, \sigma] = 1 iff cell (i,j)(i, j) of the computation tableau contains symbol σ\sigma. The tableau has p(n)+1p(n) + 1 rows and p(n)p(n) columns.
  2. Initial configuration: Row 0 encodes the initial state of MM on input xx.
  3. Transition constraints: Each 2×32 \times 3 window of the tableau corresponds to a valid transition of MM.
  4. Accepting configuration: Some cell in the last row contains the accept state.

Each of these constraints can be expressed as a polynomial-size CNF formula. The total formula ϕx\phi_x has size polynomial in nn and is satisfiable iff MM accepts xx. ■\blacksquare

SAT. Given a Boolean formula in CNF, is there a satisfying assignment?

3-SAT. SAT restricted to clauses with exactly 3 literals.

Vertex Cover. Given a graph G=(V,E)G = (V, E) and integer kk Is there a vertex cover of size ≤k\leq k?

Travelling Salesman Problem (decision version). Given a weighted graph and bound BB Is there a Tour of total weight ≤B\leq B?

Subset Sum. Given a set of integers and a target TT Is there a subset summing to TT?

Clique. Given a graph GG and integer kk Does GG contain a clique of size kk?

To prove a problem BB is NP-complete:

  1. Show B∈NPB \in \mathrm{NP} (polynomial-time verifiable certificate).
  2. Show a known NP-complete problem AA reduces to BB: A≤pBA \leq_p B.

Example. 3-SAT ≤p\leq_p Vertex Cover: construct a graph from the 3-SAT formula where each Variable and each clause become vertices, and edges enforce the constraint that a satisfying Assignment corresponds to a vertex cover.

Worked Example: 3-SAT $\leq_p$ Vertex Cover Reduction

Reduce 3-SAT formula ϕ=(x1∨xˉ2∨x3)∧(xˉ1∨x2∨x3)∧(x1∨x2∨xˉ3)\phi = (x_1 \vee \bar{x}_2 \vee x_3) \wedge (\bar{x}_1 \vee x_2 \vee x_3) \wedge (x_1 \vee x_2 \vee \bar{x}_3) to a vertex cover instance.

For each variable xix_i Create two vertices xix_i and xˉi\bar{x}_i connected by an edge (the “literal edge”).

For each clause CjC_j Create a triangle of 3 vertices cj1,cj2,cj3c_{j1}, c_{j2}, c_{j3}.

For each clause vertex cjkc_{jk} Connect it to the literal vertex corresponding to the kk-th literal of clause jj.

Claim: ϕ\phi is satisfiable iff the graph has a vertex cover of size k+2mk + 2m where kk is the number of variables and mm is the number of clauses.

(⇒\Rightarrow) If ϕ\phi is satisfiable, include in the cover: for each variable, the literal vertex matching the truth assignment (e.g., x1x_1 if x1=truex_1 = \mathrm{true}, xˉ1\bar{x}_1 if x1=falsex_1 = \mathrm{false}). This covers all literal edges (kk vertices). For each clause triangle, at least one literal in the clause is true, so the corresponding literal vertex covers one of the three edges from the triangle. Include the other two vertices of the triangle (2m2m vertices total).

(⇐\Leftarrow) A vertex cover of size k+2mk + 2m must include exactly one endpoint of each literal edge (otherwise the triangle requires 3 vertices). Set each variable according to which literal vertex is in the cover. Each clause triangle has exactly one uncovered vertex, whose edge to a literal vertex is covered by that literal vertex, meaning the clause is satisfied.

The reduction takes polynomial time (number of vertices and edges is polynomial in the formula size).