Space Complexity & Beyond
Space complexity reveals that asking "how much memory does a computation need?" leads to a rich hierarchy rivaling time complexity — and sometimes collapses it in surprising ways.
Overview
Space Complexity & Beyond
Time complexity asks: how long does a computation take? Space complexity asks: how much scratch paper does it need? At first glance this seems like a secondary concern — after all, we can always buy more RAM. But space complexity turns out to be a window into the deepest structure of computation, with its own hierarchy of classes, its own landmark theorems, and connections that loop back to time in ways no one fully understands.
The central insight is that space can be reused. A machine that makes one billion steps can reuse the same memory cell over and over, meaning space and time are fundamentally different resources. This reusability is what makes space complexity so interesting: a machine using only O(log n) space can still solve surprisingly hard problems, because it keeps resetting and reusing its tiny workspace.
This module builds the theory systematically. We start with the definition of space complexity, then explore the two most important space classes — L (logarithmic space, the "tiniest reasonable workspace") and PSPACE (polynomial space, "as much room as a polynomial allows"). Along the way we encounter Savitch's Theorem — a startling result that nondeterministic space can be simulated deterministically with only a quadratic blowup — and the Immerman–Szelepcsényi Theorem, which shows that NL = co-NL, a result with no known analogue for time. We close with a glimpse of randomized (BPP, RP) and quantum (BQP) complexity classes, and a map of the full known complexity hierarchy.
By the end, you will have a precise mental map of the frontier of theoretical computer science and the standing open questions (P vs NP, L vs P, PSPACE vs NP) that define the discipline.
Space complexity is not just an academic exercise — it shapes what is computable in practice. Embedded systems, streaming algorithms, and database query engines all operate under tight memory budgets. Understanding L and NL tells us precisely which graph problems (reachability, 2-SAT, bipartiteness) can be solved with negligible memory. Understanding PSPACE tells us which games (chess endgames, generalized Go, STRIPS planning) are not just "hard" but representatively complete for a whole class of computation. Savitch's Theorem, meanwhile, is a concrete tool: it lets us turn nondeterministic space algorithms into deterministic ones with a well-understood overhead, and its proof technique (recursive divide-and-conquer on configuration graphs) recurs throughout the field.
More broadly, the complexity hierarchy — with P, NP, co-NP, BPP, PH, PSPACE, EXP and beyond — is the landscape within which every algorithmic question lives. Knowing where a problem sits in this hierarchy tells you what kind of hardness result to expect, what algorithmic techniques are plausible, and how much hope there is for a polynomial-time solution. The randomized and quantum classes hint at what becomes possible when you add physical resources (coins, qubits) to computation, and BQP's relationship to PSPACE is one of the live frontiers of quantum computing theory.
The Complexity Hierarchy
The known complexity hierarchy. Green = log-space classes; blue = polynomial time; yellow = nondeterministic polynomial time; pink = polynomial space; purple = quantum; red = exponential. Solid arrows denote proven containment. The critical open questions are whether any of P, NP, PSPACE, or EXP are equal to their neighbors.
The concepts
01Space Complexity: The Formal Definition
The space complexity of a Turing machine M on input x is the number of distinct tape cells M visits during its computation. For a multi-tape TM, we distinguish the work tape (where computation happens) from the input tape (read-only) and output tape (write-only, if applicable). The reason for this separation is crucial: if we counted input cells, even reading the input would cost Ω(n) space, making sub-linear space classes impossible.
Formally, SPACE(f(n)) is the class of all languages L for which there exists a deterministic TM M such that, on every input of length n, M halts and uses at most O(f(n)) work tape cells. Similarly, NSPACE(f(n)) is the nondeterministic analogue.
A key sanity check: if M uses s(n) space, then M can be in at most |Q| · n · s(n) · |Γ|^{s(n)} distinct configurations (state, input head position, work tape content, work head position). This means if M uses s(n) ≥ log n space, the number of configurations is at most exponential in s(n), which is finite — so any halting TM that doesn't loop can be detected. This 'configuration counting' argument is the engine behind most space complexity proofs.
An analogy: think of a mathematician doing long division. The 'input' (the dividend and divisor) is written on a fixed whiteboard. The work tape is the scratch paper. Space complexity measures how much scratch paper is needed, regardless of how long the computation takes.
# Illustration: space-efficient substring search
# Uses O(1) extra space (pointer arithmetic only)
def contains(text: str, pattern: str) -> bool:
n, m = len(text), len(pattern)
# Work tape stores: i (O(log n) bits), j (O(log m) bits)
# No auxiliary arrays allocated
for i in range(n - m + 1):
match = True
for j in range(m):
if text[i + j] != pattern[j]:
match = False
break
if match:
return True
return False
# Space: O(log n) for loop counters; O(n) for input (not counted)
02PSPACE: The Class of Polynomial Space
PSPACE = ∪_k SPACE(n^k) is the class of languages decidable by a deterministic TM using at most polynomial space. It is the space analogue of P, but vastly more powerful.
Why is PSPACE so large? Because space can be reused. A computation using s(n) space can make up to exp(s(n)) steps before it must repeat a configuration and loop forever. So SPACE(n^k) ⊆ TIME(2^{n^k}), meaning polynomial space allows exponential time. This gives us: P ⊆ NP ⊆ PH ⊆ PSPACE ⊆ EXP. All inclusions are believed strict, but the only proven strict inclusion in this chain is PSPACE ⊊ EXP (by the space hierarchy theorem).
PSPACE-complete problems are the hardest problems in PSPACE (under polynomial-time reductions). The canonical one is TQBF (True Quantified Boolean Formula): given a fully quantified Boolean formula ∀x₁ ∃x₂ ∀x₃ ... φ(x₁,...,xₖ), is it true? TQBF captures the difficulty of adversarial game trees — the ∃ quantifiers represent our moves, the ∀ quantifiers represent the opponent's best responses.
Real-world PSPACE-complete problems:
- Generalized geography (the game where you name places starting with the last letter of the previous one).
- Generalized Hex (determining if the first player wins on an n×n board).
- STRIPS planning (classical AI planning with state-space search).
- Regular expression intersection non-emptiness (does the intersection of k regular languages contain a string?).
The deep mystery: NP ⊆ PSPACE, so every NP-hard problem is also in PSPACE. But is NP = PSPACE? Almost certainly not — a PSPACE-complete problem like TQBF likely requires exponential time even with nondeterminism — but this is open.
# TQBF solver: evaluate a quantified Boolean formula
# Uses O(n) space (recursion depth = number of variables)
def tqbf(formula, assignment={}):
"""formula is a list like ['forall','x1',['exists','x2', phi]]"""
if isinstance(formula, str): # literal
return assignment.get(formula, False)
if isinstance(formula, bool):
return formula
quantifier, var, subformula = formula
if quantifier == 'exists':
return (tqbf(subformula, {**assignment, var: False}) or
tqbf(subformula, {**assignment, var: True}))
else: # 'forall'
return (tqbf(subformula, {**assignment, var: False}) and
tqbf(subformula, {**assignment, var: True}))
# Space: O(depth) = O(number of quantifiers)
03Savitch's Theorem: Squaring the Space
Savitch's Theorem (1970): For any space-constructible function s(n) ≥ log n,
NSPACE(s(n)) ⊆ SPACE(s(n)²)
In particular, NPSPACE = PSPACE: nondeterministic polynomial space equals deterministic polynomial space. This is a stunning collapse — there is no known analogue for time (NP vs P is open).
Proof idea (REACH subroutine): The key insight is to simulate the nondeterministic machine's configuration graph. Define:
REACH(C_start, C_goal, t) = True iff there is a path from C_start to C_goal of length ≤ 2^t in the configuration graph.
Base case (t=0): check if C_start = C_goal or C_start → C_goal in one step.
Recursive case: REACH(C_start, C_goal, t) = ∃ C_mid : REACH(C_start, C_mid, t-1) ∧ REACH(C_mid, C_goal, t-1).
We iterate over all possible C_mid (there are at most 2^{O(s(n))} configurations). The recursion depth is at most t = O(s(n)), and each stack frame stores three configurations (each of size O(s(n))). Total space: O(s(n)²). The total time is exponential, but we only care about space.
Why this is beautiful: The algorithm performs recursive bisection on the configuration graph — it never builds the graph explicitly (that would cost exponential space), but instead re-explores it on demand. This is an early and profound example of the space-time tradeoff: we spend exponential time to save space. The same technique underlies many space-efficient algorithms, like the Hopcroft-Karp matching algorithm's phase decomposition.
# Savitch's REACH: does NFA/graph have path C_start→C_goal in ≤ 2^t steps?
# Space: O(t * |config|) = O(s(n)^2) for t = O(s(n))
def reach(graph, start, goal, t):
"""Assumes graph[u] = set of neighbors of u."""
if t == 0:
return start == goal or goal in graph.get(start, set())
# Try every possible midpoint (space: O(config_size) per frame)
for mid in all_configurations(graph):
if reach(graph, start, mid, t - 1) and \
reach(graph, mid, goal, t - 1):
return True
return False
# To decide if NSPACE(s(n)) machine M accepts x:
# Let t = O(s(n)), C_start = initial config, C_goal = accept config
# return reach(M_config_graph, C_start, C_goal, t)
04L and NL: Logarithmic Space
L = SPACE(log n) is the class of problems solvable with only O(log n) work tape space. Since log₂(10⁹) ≈ 30, this means a machine solving problems on billion-element inputs uses only about 30 bits of scratch space. That sounds impossibly small — yet L contains many practical problems:
- Evaluating Boolean circuits of logarithmic depth.
- Checking if a graph has an Eulerian path (parity of degrees).
- Sorting a linked list (merge sort on a pointer machine uses O(log n) space for the recursion stack).
- Regular language recognition (DFAs are exactly L-devices for regular languages).
NL = NSPACE(log n) is the nondeterministic version. By Savitch, NL ⊆ SPACE(log² n) ⊆ P. The canonical NL-complete problem is ST-REACHABILITY (s-t connectivity in a directed graph): given a directed graph G and vertices s, t, is there a path from s to t?
Proof that ST-REACH is in NL: Nondeterministically guess the next vertex on the path, one step at a time. Keep only the current vertex (O(log n) bits) and a step counter (O(log n) bits). Accept if you reach t within n steps.
Proof that ST-REACH is NL-hard: Any NL machine can be reduced to ST-REACH by mapping its computation to a configuration graph: nodes are configurations, edges are valid transitions, start = initial configuration, goal = accepting configuration.
The chain: L ⊆ NL ⊆ P ⊆ NP ⊆ PSPACE. We know L ⊊ PSPACE and NL ⊊ PSPACE, but L vs P and NL vs P are open. A striking fact: L ⊆ NC (Nick's class, parallel poly-log time), so L-problems are efficiently parallelizable.
# ST-REACHABILITY in NL: O(log n) space deterministic simulation
# (Explores BFS but only stores current frontier count + current node)
# This is the deterministic version using Reingold's algorithm idea
def st_reach_logspace(adj, n, s, t):
"""adj: adjacency list. Uses O(log n) space (vertex + counter)."""
# In practice: iterative deepening or BFS with implicit frontier
# Here we show the O(log^2 n) version (Savitch applied to NL)
from math import ceil, log2
T = ceil(log2(n + 1)) + 1 # depth bound O(log n)
def reach(u, v, depth):
if depth == 0:
return u == v or v in adj.get(u, [])
for mid in range(n):
if reach(u, mid, depth - 1) and reach(mid, v, depth - 1):
return True
return False
return reach(s, t, T)
# Space: O(T * log n) = O(log^2 n) — this is Savitch's bound for NL
05The Immerman-Szelepcsényi Theorem: NL = co-NL
One of the most surprising theorems in complexity theory: NL = co-NL (1987, independently by Neil Immerman and Róbert Szelepcsényi).
Remember that co-NL is the class of problems whose complements are in NL. So NL = co-NL means: if you can nondeterministically verify a yes path in log space, you can also nondeterministically verify a no answer.
For ST-REACHABILITY, the co-NL problem is ST-NON-REACHABILITY: given G, s, t, is there no path from s to t? Intuitively, this seems harder — how can a nondeterministic machine verify the absence of a path without exploring all exponentially many paths?
The key idea: inductive counting. The trick is to count reachable vertices. Let R_i = the number of vertices reachable from s in at most i steps. We can compute R_i inductively: R_0 = 1 (just s). For R_{i+1}: for each vertex v, nondeterministically guess whether v is reachable in i+1 steps, verify by guessing a path, and count. Once we have R_n (all reachable vertices), we can verify non-reachability of t: just check that t was not counted.
This is a remarkable instance of nondeterministic counting: the machine nondeterministically verifies a count by running multiple nondeterministic paths in a coordinated way. The space is still O(log n) because we only store the current count (O(log n) bits) and the current vertex.
Contrast with time: We do NOT know NP = co-NP. The theorem works for space but not time because the configuration graph has polynomial (not exponential) nodes when space is logarithmic, enabling the counting argument. This is a deep structural difference between space and time complexity.
# Sketch: counting reachable vertices in O(log^2 n) space
# (illustrates the inductive counting idea)
def count_reachable(adj, n, s):
"""Return |{v : v reachable from s}| using O(log^2 n) space."""
reachable_count = 1 # s itself
for i in range(1, n):
new_count = 0
for v in range(n):
# Is v reachable from s in ≤ i steps?
# Use reach() from Savitch (O(log^2 n) space)
if reach_logspace(adj, n, s, v, i):
new_count += 1
reachable_count = new_count
return reachable_count
# Then: st_non_reach(adj,n,s,t) iff t not counted in final reachable_count
# Full Immerman-Szelepcsenyi uses nondeterminism to do this in O(log n)
06The Space Hierarchy Theorem
Just as the time hierarchy theorem proves P ⊊ EXP by diagonalization, the Space Hierarchy Theorem proves that more space strictly buys more computational power:
Theorem: If f(n) and g(n) are space-constructible and f(n) = o(g(n)), then SPACE(f(n)) ⊊ SPACE(g(n)).
A function s(n) is space-constructible if a TM can compute 1^{s(n)} from 1^n using O(s(n)) space — intuitively, the machine can measure out exactly s(n) cells of space. All standard functions (log n, √n, n, n², 2^n) are space-constructible.
Proof sketch (diagonalization): Construct a machine D that, on input ⟨M, 1^n⟩:
- Simulate M on input ⟨M, 1^n⟩ using at most f(n) space.
- If M accepts within that space bound, reject; otherwise accept.
D uses O(g(n)) space (the simulation overhead). D cannot be computed in SPACE(f(n)) because it disagrees with every f(n)-space machine on at least one input. Hence D ∈ SPACE(g(n)) \ SPACE(f(n)).
Consequences:
- L ⊊ PSPACE (log n = o(n^k) for any k≥1).
- PSPACE ⊊ EXP.
- There are infinitely many distinct space classes between L and PSPACE (one for each n^{1/k}).
Why the time hierarchy uses a gap: The time hierarchy theorem needs f(n) log f(n) = o(g(n)) because simulation has a log overhead. Space simulation has no such overhead (you can measure space exactly), so the space hierarchy theorem holds for f(n) = o(g(n)) with no log factor. This is another place where space is 'cleaner' than time.
# Space hierarchy diagonalization (conceptual)
# D is in SPACE(g(n)) but NOT in SPACE(f(n))
def D(encoded_machine_M, n):
"""
Simulates M on input <M, 1^n> using at most f(n) space.
Disagrees with M's output.
"""
space_limit = f(n) # measured in cells
result = simulate_with_space_limit(encoded_machine_M,
input=encoded_machine_M + '1'*n,
limit=space_limit)
# Diagonal: flip the output
if result == 'accept':
return 'reject'
else:
return 'accept'
# D uses O(g(n)) space for simulation overhead
# D disagrees with every f(n)-space machine M_i on input <M_i, 1^{n_i}>
07The Complexity Hierarchy: A Full Map
Here is the known complexity landscape, from most restrictive to least:
AC⁰ ⊊ NC¹ ⊆ L ⊆ NL = co-NL ⊆ NC ⊆ P ⊆ NP ∩ co-NP ⊆ NP ∪ co-NP ⊆ PH ⊆ PSPACE = NPSPACE ⊆ EXP ⊆ NEXP ⊆ ...
Key facts and open questions:
| Pair | Known? | Open question |
|---|---|---|
| L vs P | Open | Does log space = poly time? |
| NL vs P | Open | Is nondeterministic log space ⊆ P? |
| P vs NP | THE open question | — |
| NP vs co-NP | Open (NP≠co-NP would imply P≠NP) | — |
| NP vs PSPACE | Open (PSPACE-complete outside NP?) | — |
| PSPACE vs EXP | Proven strict (hierarchy theorem) | — |
The polynomial hierarchy (PH): PH = ∪_k Σ_k^P where Σ_0^P = P, Σ_1^P = NP, Σ_2^P = NP^{NP} (NP with NP oracle), etc. PH is contained in PSPACE. If PH collapses (some level equals the previous), it would be a major result. Most believe PH is infinite.
Oracles and relativization: Baker-Gill-Solovay (1975) showed there exist oracles A, B such that P^A = NP^A and P^B ≠ NP^B. This means any proof of P≠NP must be non-relativizing — it must use structure specific to the computation model, not just the combinatorics of oracle queries. This partially explains why P vs NP is so hard.
NC (Nick's Class): NC = ∪_k DTIME(log^k n) on a PRAM with polynomially many processors. NC captures problems efficiently parallelizable. L ⊆ NC ⊆ P. Whether NC = P is open; P-complete problems (like circuit value problem) are believed not in NC.
08Randomized Complexity: BPP and RP
Real computers can flip coins. Randomized complexity classes capture what becomes efficiently solvable when the machine has access to random bits.
RP (Randomized Polynomial time): A language L ∈ RP if there exists a poly-time randomized TM M such that:
- x ∈ L → Pr[M accepts x] ≥ 1/2 (may miss, but not too often).
- x ∉ L → Pr[M accepts x] = 0 (never wrong on no-instances).
This is one-sided error. Example: Miller-Rabin primality testing (pre-AKS). RP ⊆ NP (guess the random bits nondeterministically).
co-RP: Same but flipped — no false positives, possible false negatives. co-RP ⊆ co-NP.
BPP (Bounded-error Probabilistic Polynomial time): L ∈ BPP if there exists a poly-time randomized TM M with:
- x ∈ L → Pr[M accepts x] ≥ 2/3.
- x ∉ L → Pr[M accepts x] ≤ 1/3.
This is two-sided error, but bounded away from 1/2. The error can be reduced to 2^{-k} by running M k times and taking majority vote (error decreases exponentially). BPP is widely believed to equal P (the derandomization conjecture), supported by pseudorandom generator constructions.
Key relationships:
- P ⊆ RP ⊆ BPP.
- BPP ⊆ PSPACE (exhaustive simulation over random choices).
- BPP ⊆ Σ_2^P ∩ Π_2^P (Sipser-Gács-Lautemann theorem).
- If one-way functions exist, then BPP = P (derandomization via PRGs).
ZPP (Zero-error Probabilistic Polynomial time): Always correct, expected poly time. ZPP = RP ∩ co-RP.
import random
def miller_rabin(n: int, k: int = 40) -> bool:
"""RP algorithm: if returns False, n is definitely composite.
If returns True, n is probably prime (error ≤ 4^{-k})."""
if n < 2: return False
if n == 2 or n == 3: return True
if n % 2 == 0: return False
# Write n-1 = 2^r * d
r, d = 0, n - 1
while d % 2 == 0:
r += 1
d //= 2
for _ in range(k):
a = random.randrange(2, n - 1)
x = pow(a, d, n)
if x == 1 or x == n - 1:
continue
for _ in range(r - 1):
x = pow(x, 2, n)
if x == n - 1:
break
else:
return False # Definitely composite
return True # Probably prime
# This is in RP: composites always rejected, primes accepted w.h.p.
09Quantum Complexity: BQP and Its Place in the Hierarchy
A quantum computer operates on qubits — superpositions of 0 and 1 — and uses interference and entanglement to explore many computation paths simultaneously. BQP (Bounded-error Quantum Polynomial time) is the quantum analogue of BPP: the class of problems solvable in polynomial time on a quantum computer with error ≤ 1/3.
Key problems in BQP:
- Shor's algorithm: Integer factoring and discrete logarithm are in BQP. This would break RSA/ECC if large quantum computers are built.
- Grover's algorithm: Unstructured search in O(√N) queries (versus O(N) classically). This is a quadratic speedup, not exponential.
- Quantum simulation: Simulating quantum physical systems (Feynman's original motivation).
BQP in the hierarchy:
- P ⊆ BPP ⊆ BQP: quantum is at least as powerful as classical randomized.
- BQP ⊆ PSPACE: a quantum computation can be simulated in polynomial space (by tracking the full 2^n amplitude vector, which has polynomial description in terms of circuit depth, though exponential size). More carefully: BQP ⊆ PP ⊆ PSPACE.
- NP vs BQP: We do NOT know NP ⊆ BQP. In particular, we don't know if quantum computers can solve NP-complete problems efficiently. Grover gives only a quadratic speedup — not enough to push NP into BQP.
- The quantum oracle separations (BBBV theorem) show that no quantum algorithm can solve unstructured search in subpolynomial queries, explaining why quantum computers do not obviously collapse NP into BQP.
Why this matters: The P vs NP question is about classical deterministic computation. BQP introduces a physically realizable model with provably different power (factoring in BQP but believed not in P). The relationships BQP ⊆ PSPACE and NP ⊄ BQP (presumably) mean that quantum computers do not 'solve everything' but do give real exponential advantages for specific structured problems.
# Grover's algorithm (pseudocode at the circuit level)
# Finds x* such that f(x*) = 1 in O(sqrt(2^n)) oracle calls
# Space: O(n) qubits = O(n) quantum bits
# Algorithm:
# 1. Prepare uniform superposition |ψ⟩ = (1/√N) Σ_x |x⟩ using H^⊗n
# 2. Repeat O(√N) times:
# a. Oracle: flip phase of |x*⟩: |x⟩ → (-1)^{f(x)} |x⟩
# b. Diffusion: reflect about |ψ⟩: 2|ψ⟩⟨ψ| - I
# 3. Measure: obtain x* with probability ≥ 2/3
def grover_iteration_count(N: int) -> int:
"""Optimal number of Grover iterations for N-element search."""
import math
return round(math.pi / 4 * math.sqrt(N))
# Classical: O(N) queries expected
# Grover: O(sqrt(N)) queries — provably optimal for quantum
# N = 2^256 (AES key space): sqrt(N) = 2^128 — still infeasible
# So AES-256 is quantum-safe against Grover
10Complete Problems and Reductions in Space Classes
A problem X is hard for a class C under a reduction type R if every problem in C reduces to X via R. X is complete for C if it is both C-hard and in C itself — it is a representative of the entire class.
Logspace reductions (≤_L): For L and NL, polynomial-time reductions are too powerful — they can solve the problem themselves! We use logspace many-one reductions: a function f computable in O(log n) space such that x ∈ A ⟺ f(x) ∈ B. These are the 'right' reductions for these tiny classes.
NL-complete problems (under ≤_L):
- ST-REACHABILITY (directed s-t connectivity). All NL problems reduce to this.
- 2-SAT (satisfiability of 2-CNF formulas). A remarkable result: 2-SAT is NL-complete.
- Bipartite matching has an NL algorithm (Hopcroft-Karp in logspace).
L-complete problems (under ≤_{AC0}): The circuit value problem for monotone circuits; evaluating a DFA.
PSPACE-complete problems (under ≤_P):
- TQBF (True Quantified Boolean Formulas) — the canonical one.
- Generalized Geography, Generalized Hex, Shannon switching game.
- STRIPS planning (in AI).
Why 2-SAT is in NL but 3-SAT is NP-complete: This is one of the most beautiful boundary phenomena in complexity. A 2-CNF clause (a ∨ b) is equivalent to (¬a → b) ∧ (¬b → a). So 2-SAT is really a graph reachability problem on an implication graph: unsatisfiable iff some variable x has a path x → ¬x AND ¬x → x in the implication graph. Reachability is in NL. 3-SAT doesn't have this structure — no such elegant reduction to reachability exists.
# 2-SAT solver using Kosaraju's SCC algorithm (logspace in theory)
# In O(n + m) time, O(n) space
def two_sat(n_vars, clauses):
"""clauses: list of (a, b) meaning (a OR b).
Literals: var i -> 2i, NOT var i -> 2i+1."""
from collections import defaultdict
INF = float('inf')
graph = defaultdict(list)
rgraph = defaultdict(list)
def pos(v): return 2 * v
def neg(v): return 2 * v + 1
def flip(l): return l ^ 1
for (a, b) in clauses:
# (a OR b) = (NOT a -> b) AND (NOT b -> a)
graph[flip(a)].append(b)
graph[flip(b)].append(a)
rgraph[b].append(flip(a))
rgraph[a].append(flip(b))
# Kosaraju's SCC to find strongly connected components
order, comp = [], {}
visited = set()
def dfs1(u):
visited.add(u)
for v in graph[u]:
if v not in visited: dfs1(v)
order.append(u)
def dfs2(u, c):
comp[u] = c
for v in rgraph[u]:
if v not in comp: dfs2(v, c)
for u in range(2 * n_vars):
if u not in visited: dfs1(u)
c = 0
for u in reversed(order):
if u not in comp:
dfs2(u, c); c += 1
# Satisfiable iff no variable and its negation in same SCC
return all(comp[pos(i)] != comp[neg(i)] for i in range(n_vars))
Key insights
- ✦Space can be reused, making SPACE(s(n)) potentially contain computations that take EXP(s(n)) time — space and time are fundamentally different resources.
- ✦Savitch's Theorem (NPSPACE = PSPACE) shows nondeterminism buys almost nothing for polynomial space, in sharp contrast to the time setting where NP vs P is wide open.
- ✦The Immerman-Szelepcsényi Theorem (NL = co-NL) is proved by a beautiful inductive counting argument — nondeterministic machines can certify non-reachability by counting reachable vertices.
- ✦2-SAT is in NL (and NL-complete) because 2-CNF reduces to graph reachability via implication graphs; 3-SAT is NP-complete because this reduction breaks down.
- ✦The space hierarchy theorem holds with a tighter gap (f = o(g)) than the time hierarchy (f log f = o(g)) because simulating space has no logarithmic overhead.
- ✦BQP ⊆ PSPACE: quantum computers do not escape polynomial space, and NP ⊄ BQP is believed to hold — quantum computers give exponential speedups only for specific structured problems, not for NP in general.
- ✦TQBF (True Quantified Boolean Formulas) is PSPACE-complete and captures the difficulty of adversarial game trees: ∃ moves are ours, ∀ moves are the opponent's.
- ✦The complexity hierarchy P ⊆ NP ⊆ PSPACE ⊆ EXP is fully believed to be strict at every step, but only PSPACE ⊊ EXP is proven — the rest remain open.
Common pitfalls
- ⚠Confusing space with time: a computation using s(n) space can take up to exp(s(n)) time, so PSPACE ⊆ EXP but they are not equal by the hierarchy theorem.
- ⚠Forgetting that space complexity counts the WORK TAPE only, not the input tape — otherwise no sublinear space class would be meaningful.
- ⚠Applying polynomial-time reductions when reducing within L or NL: polynomial-time reductions can themselves solve these problems, so logspace reductions (≤_L) are required.
- ⚠Believing NP = co-NP follows from NL = co-NL — the Immerman-Szelepcsényi argument fundamentally relies on the polynomial size of log-space configuration graphs, which does not apply to poly-time configuration graphs.
- ⚠Thinking quantum computers solve NP-complete problems efficiently — BQP and NP are incomparable under current knowledge, and Grover's algorithm gives only a quadratic speedup.
- ⚠Assuming Savitch's Theorem gives an efficient (practical) simulation — it does not; the simulation runs in exponential time. The theorem is about space, not time efficiency.
Practice
- 01easy
Prove that SPACE(log n) ⊆ P. That is, show any language decidable in O(log n) space is also decidable in polynomial time.
- 02medium
Implement Savitch's REACH algorithm for directed graph reachability. Measure its actual space usage (recursion depth times frame size) and verify it is O(log² n) for a graph with n vertices.
- 03medium
Show that 2-SAT is NL-complete under logspace reductions. First show 2-SAT ∈ NL by reducing it to ST-REACHABILITY on the implication graph. Then show NL-hardness by reducing ST-REACHABILITY to 2-SAT.
- 04hard
Prove that the language TQBF = {φ : φ is a true quantified Boolean formula} is in PSPACE. Then argue (informally) why TQBF is PSPACE-hard, i.e., every problem in PSPACE reduces to TQBF.
- 05easy
Implement the Miller-Rabin primality test and empirically verify that it is a one-sided error (RP) algorithm: test 100 known primes and 100 known composites with k=10 rounds each. What is the observed false negative rate on primes? On composites?
- 06hard
The polynomial hierarchy has levels Σ_k^P and Π_k^P. Define Σ_2^P and give a natural complete problem for it. Then prove BPP ⊆ Σ_2^P (the Sipser-Gács-Lautemann theorem). Sketch the proof.
Resources
Chapters 8-9 cover space complexity, Savitch's theorem, PSPACE-completeness, and L/NL with exceptional clarity. The standard undergraduate text.
The graduate-level bible of complexity theory. Chapters 4-5 cover space complexity and Savitch; Chapter 21 covers quantum. A draft is freely available from the authors.
Full lecture notes, problem sets, and exams from Sipser's own course at MIT. Covers space complexity in detail.
Stanford's undergraduate theory course, covering NL, PSPACE, and the hierarchy. Lecture notes available.
The original Immerman paper proving NL = co-NL via the inductive counting technique. Short and elegant.
Excellent overview of the complexity hierarchy, all major classes, and the status of open questions. Good for orientation.
The standard reference for quantum computing. Chapter 6 covers quantum complexity including BQP, Shor's algorithm, and Grover's algorithm.
Comprehensive list of PSPACE-complete problems with proof sketches. Good companion to the TQBF section.
Checkpoints — you can now…
- ✓You can formally define SPACE(f(n)) and NSPACE(f(n)) and explain why the input tape is excluded from the space count.
- ✓You can state and prove Savitch's Theorem (NPSPACE = PSPACE) by describing the REACH subroutine and explaining why the recursion uses O(s(n)²) space.
- ✓You can explain why ST-REACHABILITY is NL-complete: membership via nondeterministic path-guessing, and hardness via configuration graph reduction.
- ✓You can state the Immerman-Szelepcsényi Theorem (NL = co-NL) and sketch the inductive counting argument that makes it work.
- ✓You can place BPP, BQP, NP, co-NP, PH, and PSPACE correctly in the complexity hierarchy and identify which inclusions are proven versus conjectured.
- ✓You can explain why 2-SAT is in NL (via implication graph reachability) and why this argument does not extend to 3-SAT.
- ✓You can describe a PSPACE-complete problem (TQBF) and explain the connection to adversarial game trees with alternating quantifiers.