Foundations to FrontierPart IX: The graduate coreBezhig

G03: Algorithm design, certificates, and honest complexity

6 exercises, each with a complete solution. 1 figure. 1 code listing.

On this page
  1. Prerequisites and observable goals
  2. The central question: how can a result carry evidence?
  3. A derived dynamic program: nonadjacent tasks
  4. Flow, residual capacity, and reversible decisions
  5. Why the terminal residual cut proves optimality
  6. Complexity belongs to a representation
  7. Amortized, expected, and approximate are different
  8. Independent practice
  9. Smallest useful hints
  10. Complete solutions: G03
  11. Readiness and continuation

Prerequisites and observable goals#

Before

foundation C05–C08 and C16: induction, graphs, dynamic programming, and complexity classes.

After

Derive a dynamic program, justify a residual-flow algorithm with a cut certificate, and separate mathematical operation counts from implementation costs.

The central question: how can a result carry evidence?#

Some problems have compact certificates. A path proves reachability, but not necessarily shortest distance. A feasible flow and a cut of the same value jointly prove optimality. A dynamic program can provide a reconstruction of its choices. Design the certificate while designing the algorithm: it creates an independent way to challenge a plausible-looking result.

Start with the problem’s quantifiers and constraints. “Choose a cheapest subset” is not enough. What does the subset need to cover? Are costs nonnegative? Can one item be used multiple times? Are decisions divisible? A small change may turn greedy selection from correct into wrong, or require a different state in a dynamic program.

A derived dynamic program: nonadjacent tasks#

Suppose tasks lie in a chain, each has value vi≥0v_i\ge0, and adjacent tasks cannot both be selected. Let D[i]D[i] be the greatest value achievable using only tasks 11 through ii. A solution either excludes task ii, leaving value at most D[i−1]D[i-1], or includes it, excluding i−1i-1 and leaving at most vi+D[i−2]v_i+D[i-2]. These alternatives are exhaustive, so

D[0]=0,D[1]=v1,D[i]=max⁡(D[i−1],vi+D[i−2]).D[0]=0,\quad D[1]=v_1,\qquad D[i]=\max(D[i-1],v_i+D[i-2]).

Induction proves optimality: assume the smaller prefixes are solved optimally; every feasible prefix solution belongs to one of the two cases and cannot beat the larger expression; combining the appropriate optimal smaller solution achieves that expression. The recurrence is not a slogan about “optimal substructure”; it is a proof by a last-decision partition.

For values (4,7,5,6)(4,7,5,6), the table is D[0..4]=(0,4,7,9,13)D[0..4]=(0,4,7,9,13). Reconstruction starts at i=4i=4: 13=6+D[2]13=6+D[2], so take 4 and continue at 2. Take 2 for a total 7+6=137+6=13. Greedily taking the largest remaining individual value happens to succeed here but fails on (6,10,6)(6,10,6): picking the middle 10 loses to both endpoints totaling 12.

The table uses O(n)O(n) arithmetic comparisons and additions. Value-only computation can retain two prior entries, using O(1)O(1) auxiliary slots. Reconstructing choices then needs stored decisions or recomputation. Large integer bit costs also matter when values are not bounded machine words.

Flow, residual capacity, and reversible decisions#

A directed network has source ss, sink tt, nonnegative capacities c(u,v)c(u,v), and conservation at internal vertices. A conventional feasible flow has nonnegative edge quantities bounded by capacities. For implementation with possible edges in both directions, it is convenient to track a net antisymmetric flow f(u,v)=−f(v,u)f(u,v)=-f(v,u) with f(u,v)≤c(u,v)f(u,v)\le c(u,v). Residual capacity is

r(u,v)=c(u,v)−f(u,v)≥0.r(u,v)=c(u,v)-f(u,v)\ge0.

Increasing net flow by δ\delta on (u,v)(u,v) decreases r(u,v)r(u,v) and increases r(v,u)r(v,u). The reverse residual edge is permission to undo a prior decision, not a new physical network link. A residual algorithm that omits reverse updates can get stuck below the optimum.

Consider four vertices (s,a,b,t)(s,a,b,t) with capacities s→a=3s\to a=3, s→b=2s\to b=2, a→b=1a\to b=1, a→t=2a\to t=2, and b→t=3b\to t=3. Send two units along s,a,ts,a,t, two along s,b,ts,b,t, then one along s,a,b,ts,a,b,t. The total is five. The cut separating ss alone has outgoing capacity 3+2=53+2=5, so no flow can exceed five. The constructed flow and cut certify one another.

Labels are capacities, not measured bandwidth. The dashed cut crosses exactly the source’s two outgoing arcs. Flow conservation makes internal movement cancel when computing net flow through a cut.

Why the terminal residual cut proves optimality#

For any cut SS containing ss but not tt, the total flow value equals net flow leaving SS: contributions between vertices inside SS cancel in pairs, and internal vertices conserve flow. Net outgoing flow cannot exceed the sum of capacities from SS to its complement. Thus every cut is an upper bound.

When no residual path from ss to tt remains, let SS be all vertices reachable from ss by positive residual arcs. Every arc from SS outward has zero residual, otherwise its endpoint would be reachable. Therefore net flow on each such arc equals its capacity. Summing gives a flow equal to that cut’s capacity, meeting the upper bound. Hence the flow is maximum and the cut minimum. This argument is a proof of the certificate condition, not merely empirical agreement. The lab’s integer capacities also guarantee each successful augmentation makes positive integral progress. [G-FLOW]

Complexity belongs to a representation#

Edmonds–Karp chooses a shortest-in-edge-count residual path by BFS. The standard augmentation argument bounds successful augmentations by O(VE)O(VE): residual shortest distances do not decrease, and a particular directed edge can become critical only O(V)O(V) times as its relevant distance increases. With adjacency lists, a BFS is O(V+E)O(V+E), giving the familiar O(VE2)O(VE^2) bound after restricting to the relevant graph. This is not a generic bound for arbitrary path selection.

Our educational implementation scans all VV entries of a residual-matrix row for each visited vertex. One BFS costs O(V2)O(V^2) even on sparse graphs. Combining the augmentation bound with these scans yields O(V3E)O(V^3E) as a conservative operation bound, plus the final BFS; for E=0E=0 the initial O(V2)O(V^2) work still exists. State the bound as O(V2+V3E)O(V^2+V^3E). Storage is O(V2)O(V^2). Copying a sparse matrix and calling it an adjacency list would not change the executed work.

Amortized, expected, and approximate are different#

For a dynamic array doubling capacity on a full insertion, most insertions are cheap and a few copy many elements. At capacity CC and size nn after at least the first insertion, use potential Φ=2n−C≥0\Phi=2n-C\ge0 for the usual doubling regime. An ordinary insertion costs one and increases potential by two, giving amortized cost three. A full insertion from n=Cn=C costs C+1C+1, changes capacity to 2C2C, and changes potential from CC to 2; amortized cost is (C+1)+(2−C)=3(C+1)+(2-C)=3. Initial allocation is handled separately. This pays for an expensive deterministic event by accounting across a sequence; it is not a probability claim.

Expected hash-table lookup depends on a randomness/input model. A 2-approximation bound compares a feasible result with an unknown optimum for every permitted instance; it does not mean “wrong half the time.” NP-completeness concerns polynomial reductions and verification, not a blanket prohibition on useful exact solutions for bounded or structured instances. When an exact method is too costly, make the tradeoff explicit: approximate objective, restricted input class, probabilistic failure, or a resource cutoff with an incomplete answer.

Independent practice#

F foundation C standard course work T transfer S optional stretch L runnable lab or project. Attempt the complete set before opening its solutions.

G03.01CSolution

For values (5,1,2,10,6)(5,1,2,10,6) with no adjacent selections, compute the full DD table, reconstruct an optimum, and justify the recurrence.

G03.02CSolution

For the four-vertex network in the chapter, give a feasible value-five flow and a capacity-five cut. Explain why either alone is insufficient to prove both feasibility and optimality.

G03.03TSolution

In a unit-capacity bipartite matching network, a first augmenting path matches left vertex AA to right vertex XX. Edges are AA–XX, AA–YY, and BB–XX. Show the next residual path that reaches a matching of size two.

G03.04CSolution

A developer reports O(VE2)O(VE^2) for the provided residual-matrix implementation. Identify the mismatch and derive the stated conservative matrix bound.

G03.05TSolution

Explain with formulas why dynamic-array doubling gives constant amortized insertion cost, yet one insertion can cost Θ(n)\Theta(n). State the initial-state qualification of the potential.

G03.06LSolution

Implement max_flow and cut_capacity in starters/flow.py. Accept square 2–64 vertex matrices of nonnegative strict integers with zero diagonal and distinct valid endpoints. Return flow value, reachable cut side, and residual matrix. Test against enumeration of every source-containing/sink-excluding cut for small random graphs, including antiparallel edges, zero flow, and invalid capacities.

Smallest useful hints#

For .01, partition on whether the last task is chosen. For .03, the path may use a reverse residual edge. For .06, copying the matrix avoids mutating a caller’s capacity data; do not mistake residual capacity for original capacity when evaluating the certificate.

Complete solutions: G03#

Compare the first step where your work differs. A corrected answer is not yet an independent success.

G03.01Show solution

A prefix table with reconstructible decisions

The entries are D[0]=0,D[1]=5,D[2]=5,D[3]=7,D[4]=15,D[5]=15D[0]=0,D[1]=5,D[2]=5,D[3]=7,D[4]=15,D[5]=15. At 5, exclusion attains 15 while inclusion gives 6+7=136+7=13, so skip 5. At 4, inclusion gives 10+D[2]=1510+D[2]=15, so take 4, then take 1 in the first two entries. The set {1,4}\{1,4\} totals 15 and has no adjacent pair. Every feasible optimum either contains the last task or not, so the recurrence covers all cases; the induction proof in the chapter applies at each table position. A correct table without reconstruction would not identify the actual chosen tasks.

Back to G03.01
G03.02Show solution

Lower and upper bounds meet

Use flows f(s,a)=3,f(s,b)=2,f(a,t)=2,f(a,b)=1,f(b,t)=3f(s,a)=3,f(s,b)=2,f(a,t)=2,f(a,b)=1,f(b,t)=3, with all other conventional forward flows zero. At aa, inflow 3 equals outflow 2+12+1; at bb, inflow 2+12+1 equals outflow 3; capacities hold. The source cut has capacity 5, giving an upper bound. A feasible flow alone supplies a lower bound; a cut alone supplies an upper bound. Their equality proves optimality of the feasible flow.

Back to G03.02
G03.03Show solution

Undoing a local choice enables a better one

After s→A→X→ts\to A\to X\to t carries one unit, use s→B→X→A→Y→ts\to B\to X\to A\to Y\to t. The arc X→AX\to A is the reverse residual arc of the earlier match. Augmenting one cancels AA–XX, adds BB–XX and AA–YY, and yields two disjoint matches. Rejecting a path simply because one arc was absent in the original graph would lose this repair mechanism.

Back to G03.03
G03.04Show solution

Count the matrix scans

BFS visits at most VV vertices and scans VV potential neighbors for each, costing O(V2)O(V^2). With O(VE)O(VE) successful augmentations and one final search, total is O(V2+V3E)O(V^2+V^3E) under constant-cost arithmetic, plus matrix initialization already absorbed. The adjacency-list implementation has a different per-search cost. This distinction becomes relevant when comparing sparse instances; the theorem about path choice does not silently convert a matrix into a list.

Back to G03.04
G03.05Show solution

A sequence bound permits a costly event

After the first allocation, the doubling regime maintains enough occupancy that Φ=2n−C≥0\Phi=2n-C\ge0. A non-resizing append has amortized cost 1+2=31+2=3. A resizing append from full capacity CC has actual cost C+1C+1 and potential change 2−C2-C, also totaling 3. Summing amortized costs telescopes the potential terms; nonnegative final potential bounds total actual cost by a constant times the number of appends plus the initial condition. The resizing event itself still copies CC elements, so its individual latency is linear. A real-time latency requirement therefore cannot be satisfied merely by citing amortized throughput.

Back to G03.05
G03.06Show solution

Complete residual implementation

The returned cut side comes from the final failed BFS. The tests independently enumerate cuts on 125 small seeded graphs, compare the optimum, check nonnegative residuals and net conservation, and verify that input capacities are unchanged. Enumeration is an exponential-size oracle for small tests, not a replacement for the scalable algorithm. The certificate proof remains separate from those tests.

graduate_code/solutions/flow.pyPython
"""G03: Edmonds-Karp with a residual matrix and a min-cut certificate."""
from collections import deque


def max_flow(capacity, source, sink):
    n = len(capacity)
    if not 2 <= n <= 64 or any(len(row) != n for row in capacity):
        raise ValueError("square capacity matrix with 2..64 vertices required")
    if any(type(c) is not int or c < 0 for row in capacity for c in row):
        raise ValueError("nonnegative integer capacities required")
    if (type(source) is not int or type(sink) is not int
            or not 0 <= source < n or not 0 <= sink < n or source == sink):
        raise ValueError("distinct valid source and sink required")
    if any(capacity[i][i] != 0 for i in range(n)):
        raise ValueError("self-loop capacities must be zero")
    residual = [list(row) for row in capacity]
    value = 0
    while True:
        parent = [-1] * n
        parent[source] = source
        todo = deque([source])
        while todo and parent[sink] == -1:
            u = todo.popleft()
            for v in range(n):
                if residual[u][v] > 0 and parent[v] == -1:
                    parent[v] = u
                    todo.append(v)
        if parent[sink] == -1:
            reachable = {i for i, p in enumerate(parent) if p != -1}
            return value, reachable, residual
        delta = None
        v = sink
        while v != source:
            u = parent[v]
            delta = residual[u][v] if delta is None else min(delta, residual[u][v])
            v = u
        v = sink
        while v != source:
            u = parent[v]
            residual[u][v] -= delta
            residual[v][u] += delta
            v = u
        value += delta


def cut_capacity(capacity, reachable):
    return sum(c for u, row in enumerate(capacity) for v, c in enumerate(row)
               if u in reachable and v not in reachable)


if __name__ == "__main__":
    c = [[0, 3, 2, 0], [0, 0, 1, 2], [0, 0, 0, 3], [0, 0, 0, 0]]
    value, side, _ = max_flow(c, 0, 3)
    print(value, sorted(side), cut_capacity(c, side))
Back to G03.06

Readiness and continuation#

Explain the reverse edge without using the phrase “because the algorithm says so.” Show why no residual path gives a cut certificate. Then change the representation and recalculate cost. Further work can add min-cost flow, approximation proofs, randomized concentration bounds, and fixed-parameter methods; none is claimed implemented by this lab. Use the original maximum-flow chapter for a fuller proof of the shortest-augmenting-path complexity bound. [G-FLOW]