G03: Algorithm design, certificates, and honest complexity
On this page
- Prerequisites and observable goals
- The central question: how can a result carry evidence?
- A derived dynamic program: nonadjacent tasks
- Flow, residual capacity, and reversible decisions
- Why the terminal residual cut proves optimality
- Complexity belongs to a representation
- Amortized, expected, and approximate are different
- Independent practice
- Smallest useful hints
- Complete solutions: G03
- Readiness and continuation
Prerequisites and observable goals#
foundation C05–C08 and C16: induction, graphs, dynamic programming, and complexity classes.
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 , and adjacent tasks cannot both be selected. Let be the greatest value achievable using only tasks through . A solution either excludes task , leaving value at most , or includes it, excluding and leaving at most . These alternatives are exhaustive, so
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 , the table is . Reconstruction starts at : , so take 4 and continue at 2. Take 2 for a total . Greedily taking the largest remaining individual value happens to succeed here but fails on : picking the middle 10 loses to both endpoints totaling 12.
The table uses arithmetic comparisons and additions. Value-only computation can retain two prior entries, using 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 , sink , nonnegative capacities , 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 with . Residual capacity is
Increasing net flow by on decreases and increases . 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 with capacities , , , , and . Send two units along , two along , then one along . The total is five. The cut separating alone has outgoing capacity , 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 containing but not , the total flow value equals net flow leaving : contributions between vertices inside cancel in pairs, and internal vertices conserve flow. Net outgoing flow cannot exceed the sum of capacities from to its complement. Thus every cut is an upper bound.
When no residual path from to remains, let be all vertices reachable from by positive residual arcs. Every arc from 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 : residual shortest distances do not decrease, and a particular directed edge can become critical only times as its relevant distance increases. With adjacency lists, a BFS is , giving the familiar bound after restricting to the relevant graph. This is not a generic bound for arbitrary path selection.
Our educational implementation scans all entries of a residual-matrix row for each visited vertex. One BFS costs even on sparse graphs. Combining the augmentation bound with these scans yields as a conservative operation bound, plus the final BFS; for the initial work still exists. State the bound as . Storage is . 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 and size after at least the first insertion, use potential for the usual doubling regime. An ordinary insertion costs one and increases potential by two, giving amortized cost three. A full insertion from costs , changes capacity to , and changes potential from to 2; amortized cost is . 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.
For values with no adjacent selections, compute the full table, reconstruct an optimum, and justify the recurrence.
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.
In a unit-capacity bipartite matching network, a first augmenting path matches left vertex to right vertex . Edges are –, –, and –. Show the next residual path that reaches a matching of size two.
A developer reports for the provided residual-matrix implementation. Identify the mismatch and derive the stated conservative matrix bound.
Explain with formulas why dynamic-array doubling gives constant amortized insertion cost, yet one insertion can cost . State the initial-state qualification of the potential.
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 . At 5, exclusion attains 15 while inclusion gives , so skip 5. At 4, inclusion gives , so take 4, then take 1 in the first two entries. The set 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.01G03.02Show solution
Lower and upper bounds meet
Use flows , with all other conventional forward flows zero. At , inflow 3 equals outflow ; at , inflow 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.02G03.03Show solution
Undoing a local choice enables a better one
After carries one unit, use . The arc is the reverse residual arc of the earlier match. Augmenting one cancels –, adds – and –, 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.03G03.04Show solution
Count the matrix scans
BFS visits at most vertices and scans potential neighbors for each, costing . With successful augmentations and one final search, total is 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.04G03.05Show solution
A sequence bound permits a costly event
After the first allocation, the doubling regime maintains enough occupancy that . A non-resizing append has amortized cost . A resizing append from full capacity has actual cost and potential change , 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 elements, so its individual latency is linear. A real-time latency requirement therefore cannot be satisfied merely by citing amortized throughput.
Back to G03.05G03.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.
"""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))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]