Foundations to FrontierPart IV: Algorithms and systemsBezhig

C07: Algorithm design, correctness, and complexity

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

On this page
  1. A. Prerequisites and observable goals
  2. B. The central question: why does this method solve this problem?
  3. C. Divide and conquer: merge sort
  4. D. Greedy methods need an exchange argument, not confidence
  5. E. Dynamic programming makes dependencies explicit
  6. F. Backtracking explores choices with justified pruning
  7. G. Independent practice
  8. H. Complete solutions: C07
  9. I. Retention and readiness

A. Prerequisites and observable goals#

Before

C02 search; C04–C06 proofs, recurrences, and data structures.

After

Choose among divide-and-conquer, greedy, dynamic programming, and backtracking; prove a recurrence-based algorithm correct; and analyze the operations of a specific implementation.

Level: Degree core.

B. The central question: why does this method solve this problem?#

An algorithm is a finite, precisely described procedure under a model of computation. An implementation selects a language, representation, and operational details. Correctness asks whether the procedure terminates with the specified result on every valid input. Complexity asks how resources grow with input size under a stated cost model. Benchmarking asks what a particular implementation does on a particular environment and workload. None of these questions substitutes for the other two.

For eventually nonnegative functions, f(n)=O(g(n))f(n)=O(g(n)) means there exist constants c,n0c,n_0 such that f(n)≤cg(n)f(n)\le c g(n) for all n≥n0n\ge n_0. Ω\Omega gives a corresponding eventual lower bound; Θ\Theta gives both. The notation does not itself mean worst case. One can describe worst-case, average-case, expected, or amortized costs using it. Average case needs an input distribution. Expected cost may instead average over an algorithm’s random choices for a fixed input. Amortized cost bounds a sequence of operations without requiring probability.

A loop that doubles an index until it reaches nn has logarithmically many iterations, because 2k≥n2^k\ge n after about log⁡2n\log_2n doublings. Two nested loops are not automatically quadratic: count their actual ranges. A triangular loop with inner length ii has total ∑i=1ni=n(n+1)/2\sum_{i=1}^n i=n(n+1)/2. A nested loop that each time halves a remaining range can have a different sum entirely.

C. Divide and conquer: merge sort#

Split a sequence into two smaller sequences, recursively sort each, and merge them. During merging, maintain the invariant that the output contains the smallest already selected elements in sorted order, while the unconsumed parts of both inputs remain sorted. Compare the two next candidates and append the smaller. If they tie, choosing the left candidate preserves the relative order of equal-key records from the original sequence; this is stability.

Correctness follows by induction on length. Length zero or one is already sorted. Assuming the recursive calls return sorted versions of the smaller halves, the merge invariant gives a sorted sequence containing exactly the union of their elements with multiplicity. The halves shrink, so recursion terminates. With linear-time merging, the recurrence is T(n)=2T(n/2)+Θ(n)T(n)=2T(n/2)+\Theta(n) in the balanced model, yielding Θ(nlog⁡n)\Theta(n\log n) comparisons/work. The lab allocates output lists and slices, so its space behavior must be analyzed as implemented rather than calling it “in-place.”

Insertion sort is a useful contrast: it maintains a sorted prefix and inserts the next item in the correct position. It is simple and can be efficient on small or nearly sorted inputs, but has quadratic worst-case shifting/comparison cost. Heap sort gives O(nlog⁡n)O(n\log n) worst-case time with a different memory/stability tradeoff. Quicksort can have excellent practical behavior, expected O(nlog⁡n)O(n\log n) time with appropriate randomization, and quadratic worst case under unsuitable pivots. A standard library’s documented sort should normally be preferred over a classroom reimplementation unless the exercise is to understand the algorithm. [ALGO]

The comparison-sorting lower bound comes from a decision tree: sorting nn distinct keys must distinguish n!n! possible orders. A binary comparison tree needs height at least log⁡2(n!)=Ω(nlog⁡n)\log_2(n!)=\Omega(n\log n). Counting sort can avoid that comparison lower bound by using extra assumptions about a bounded integer key universe. Different assumptions mean a different problem model, not a contradiction.

The merge, frozen in mid-step. The output holds the two smallest elements in order, and what remains of each run is still sorted, so the next output must be one of the two front elements. Here they tie, and taking the left one keeps equal keys in their original order.

D. Greedy methods need an exchange argument, not confidence#

For selecting the maximum number of nonoverlapping intervals, choosing the earliest-finishing available interval works under the standard unweighted scheduling model. Given an optimal selection, replace its first interval with an earliest-finishing one. This replacement finishes no later and leaves at least as much room for the remaining intervals, so there is an optimal solution beginning with the greedy choice. Repeat on the remaining compatible intervals. The exchange argument is the reason the local rule is globally valid.

A superficially similar greedy rule can fail elsewhere. To make value 6 with denominations 1,3,41,3,4, repeatedly taking the largest possible coin yields 4+1+14+1+1, three coins. The optimum is 3+33+3, two coins. “Always take the largest” has no valid general exchange argument for arbitrary coin systems. A few successful examples or familiar real-world denominations do not establish the rule under changed assumptions.

E. Dynamic programming makes dependencies explicit#

Let D(a)D(a) be the minimum number of coins needed to make amount aa, with D(0)=0D(0)=0 and unreachable amounts assigned infinity. For positive denominations,

D(a)=1+min⁡d≤aD(a−d)D(a)=1+\min_{d\le a}D(a-d)

over reachable predecessor amounts. Every optimal nonempty solution has some last coin dd; removing it leaves an optimal solution for a−da-d, or replacing that remainder with a better one would improve the original. This is optimal substructure. Computing amounts in increasing order is valid because each dependency is smaller.

For denominations 1,3,41,3,4, the table from amount 0 through 6 is 0,1,2,1,1,2,20,1,2,1,1,2,2. Store a chosen last coin for each reachable amount to reconstruct an actual solution, not merely its count. With kk denominations and target AA, this implementation uses O(Ak)O(Ak) time and O(A)O(A) storage. The bound is in the numeric amount AA, not its bit length log⁡A\log A; this distinction becomes important in complexity theory. Dynamic programming is not automatically a polynomial-time algorithm in the encoded input length.

F. Backtracking explores choices with justified pruning#

Backtracking builds a partial candidate, rejects it as soon as a necessary condition fails, and explores alternatives. For queens on a chessboard, place one queen per row and reject a partial placement sharing a column or diagonal. A four-row solution uses zero-based columns (1,3,0,2)(1,3,0,2). Pruning is sound because adding more queens cannot repair an existing attack between two placed queens. The worst-case search can remain exponential; pruning improves the explored space without changing the need for a correct rejection condition.

Choose a method by structure. Independent smaller subproblems suggest divide and conquer. Repeated overlapping subproblems and an acyclic dependency relation suggest dynamic programming. A provable exchange property may justify greedy choice. Constraint satisfaction without a simpler decomposition may call for backtracking. When the first attempt fails, identify whether it lacks a correct local-choice argument, an adequate state definition, a termination measure, or enough computational resources. Do not replace those questions with algorithm-name guessing.

G. 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.

C07.01CSolution

Count the executions of a body in for i in range(n): for j in range(i): body. Give an exact count and a tight asymptotic bound. State what is being treated as a unit-cost operation.

C07.02CSolution

Merge the sorted sequences [1,4,4] and [2,4,7], distinguishing equal-key records by their original side. State the merge invariant and how tie handling affects stability.

C07.03CSolution

Build the minimum-coin table through amount 6 for denominations 1,3,41,3,4. Explain the optimal-substructure argument and reconstruct an optimum. Identify the failure in the largest-coin-first alternative.

C07.04TSolution

A planner proposes earliest-finishing-interval greedy selection for intervals with different monetary rewards, claiming the unweighted proof still applies. Give a counterexample and explain which part of the exchange argument no longer preserves the objective.

C07.05LabSolution

Implement stable merge_sort and reconstructing min_coins in algorithms_lab.py. Do not mutate the sorting input. Return None for an unreachable coin amount, reject invalid denominations and negative amounts, and verify results against simple references on small cases. Provide separate correctness and complexity arguments.

Optional small hints

C07.01: sum inner-loop lengths from zero through n−1n-1. C07.04: one long high-reward interval can outweigh several short low-reward intervals. C07.05: store the decision used to reach each amount.

H. Complete solutions: C07#

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

C07.01Show solution

Count the actual ranges

The body runs 0+1+⋯+(n−1)=n(n−1)/20+1+\cdots+(n-1)=n(n-1)/2 times. For large nn this is Θ(n2)\Theta(n^2). The analysis treats one execution of body as constant cost; if it scans another length-nn structure, the total changes. Loop-control overhead is also quadratic in this simple model but does not alter the tight class. The exact formula checks small cases: for n=3n=3, counts are 0+1+2=30+1+2=3.

Back to C07.01
C07.02Show solution

Stable merging preserves earlier equal keys

Label the left records 1L,4L1,4L21_L,4_{L1},4_{L2} and the right records 2R,4R,7R2_R,4_R,7_R. The stable merged order is 1L,2R,4L1,4L2,4R,7R1_L,2_R,4_{L1},4_{L2},4_R,7_R. At each step the emitted prefix is sorted and contains the smallest selected records from the two inputs. Choosing the left record on equal keys preserves the original order between halves. Choosing the right tie first can still produce numerically sorted keys but violate stability; sortedness and stability are distinct requirements.

Back to C07.02
C07.03Show solution

A table of justified subproblems

D(0)=0D(0)=0, D(1)=1D(1)=1, D(2)=2D(2)=2, D(3)=1D(3)=1, D(4)=1D(4)=1, D(5)=2D(5)=2, D(6)=2D(6)=2. For amount 6, choosing last coin 3 uses D(3)+1=2D(3)+1=2, reconstructing 3+33+3. Any solution has a last coin from the allowed set, and its remainder cannot use fewer coins than the table’s optimum. The recurrence therefore considers every possible last-coin class and selects the best. Largest-first gives 4+1+14+1+1, so its local choice excludes the global optimum in this instance.

Back to C07.03
C07.04Show solution

The objective changed

Use half-open intervals [0,1)[0,1) with reward 1, [1,2)[1,2) with reward 1, and [0,2)[0,2) with reward 100. Earliest-finish selection can choose the two short intervals for total reward 2, while the long interval yields 100. Replacing the first interval in an optimal schedule with an earlier-finishing one no longer preserves or improves reward. It preserves compatibility space but not the objective value. Weighted interval scheduling requires a different argument and is a classic dynamic-programming continuation.

Back to C07.04
C07.05Show solution

Complete reference algorithms

code/solutions/algorithms_lab.pyPython
"""C07.05: sorting and exact minimum-coin dynamic programming."""
def merge_sort(values: list[int]) -> list[int]:
    if len(values) < 2:
        return values.copy()
    mid = len(values) // 2
    left, right = merge_sort(values[:mid]), merge_sort(values[mid:])
    out: list[int] = []
    i = j = 0
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:
            out.append(left[i]); i += 1
        else:
            out.append(right[j]); j += 1
    out.extend(left[i:]); out.extend(right[j:])
    return out

def min_coins(coins: list[int], amount: int) -> list[int] | None:
    if type(amount) is not int or amount < 0:
        raise ValueError("nonnegative integer amount required")
    if not coins or any(type(c) is not int or c <= 0 for c in coins):
        raise ValueError("positive integer denominations required")
    best = [amount + 1] * (amount + 1)
    last = [0] * (amount + 1)
    best[0] = 0
    for a in range(1, amount + 1):
        for c in sorted(set(coins)):
            if c <= a and best[a - c] + 1 < best[a]:
                best[a], last[a] = best[a - c] + 1, c
    if best[amount] > amount:
        return None
    out = []
    while amount:
        out.append(last[amount])
        amount -= last[amount]
    return out

if __name__ == "__main__":
    print(merge_sort([4, 1, 3, 1]))
    print(min_coins([1, 3, 4], 6))

Merge sort’s induction and merge invariant prove sortedness and preservation of all input values. The implementation returns fresh lists, and choosing the left element under <= preserves stability for an appropriate total order. Slicing and merging incur linear work per level; peak live list storage is O(n)O(n) plus a logarithmic call stack in this sequential implementation, while total allocation traffic across the run can be O(nlog⁡n)O(n\log n). The coin loop computes each amount only after all smaller dependencies, and reconstruction decreases the amount by a positive coin each time. Thus it terminates with the recorded optimum. Its O(Ak)O(Ak) numeric-amount complexity is separate from the observed runtime and from the correctness proof. Generated tests catch implementation mistakes; the inductive arguments explain the general cases under the stated assumptions.

Back to C07.05

I. Retention and readiness#

Retrieve induction, invariants, and amortized versus expected analysis. Advance when you can reject a tempting greedy rule with a counterexample, define a dynamic-programming state precisely, and defend a complexity count without relying on memorized labels.