Data Structures & Algorithms / Contracts, sequences, and search / Lesson 1
Contracts, invariants, and cost models
Prove what a loop computes, count the work it performs, and state the assumptions behind asymptotic claims.
On this page
Lesson goals
- State the problem before choosing a container
- An invariant connects partial work to the goal
- Count work from the actual loop bounds
State the problem before choosing a container
An algorithm needs an input contract, a result contract, and a resource model. Finding any matching pair is different from counting all matching pairs, and returning positions is different from returning values. Decide whether duplicate values represent distinct records and whether input may be mutated. These choices change both correctness and useful data structures. For the course, examples use small synthetic Python3 sequences, deterministic output, and no external services. Complexity explanations usually count primitive comparisons or accesses under a unit-cost model; large-integer arithmetic and expensive user-defined comparisons require additional accounting.
An invariant connects partial work to the goal
A loop invariant is a property true before the first iteration and preserved by every iteration. At termination it should imply the promised result. The first example maintains that total is the sum of exactly the elements already visited and visits is their count. Initialization handles the empty prefix, each addition extends the prefix by one value, and termination covers the full sequence. An invariant is more useful than a comment saying add numbers because it explains why no element is omitted or counted twice and makes the empty-input result explicit.
def measured_sum(values):
total = 0
visits = 0
for value in values:
total += value
visits += 1
return total, visits
print(measured_sum([1, 2, 3, 4]))
print(measured_sum([]))
(10, 4)
(0, 0)
One iteration processes one value. The counted visits equal input length; this is an operation-count illustration, not a timing benchmark.
Count work from the actual loop bounds
The second example visits every unordered pair of distinct positions once. For a sequence of length n, the inner counts are n-1, n-2, through zero, totaling n(n-1)/2. That is quadratic growth even though there is no explicit multiplication in the program. The j > i boundary prevents pairing an element with itself and prevents counting the same positional pair in both orders. Duplicate values can still participate in distinct positional pairs. The returned match count therefore follows the stated positional contract, which a set-based deduplication would change.
def pair_counts(values, target):
checks = matches = 0
for i in range(len(values)):
for j in range(i + 1, len(values)):
checks += 1
if values[i] + values[j] == target:
matches += 1
return checks, matches
print(pair_counts([2, 4, 7, 9], 11))
print(pair_counts([5], 10))
(6, 2)
(0, 0)
The two matching pairs are positions (0,3) and (1,2). A single 5 cannot pair with itself.
Separate upper bounds, tight bounds, and cases
Big O gives an asymptotic upper bound, while Theta expresses a matching upper and lower growth order under the chosen model. A linear algorithm is also O(n squared), so an unnecessarily loose upper bound can be true but unhelpful. Specify worst case, best case, expected case, or amortized cost rather than using them interchangeably. Early exit can make the best case constant while the worst case remains linear. Expected hashing behavior depends on assumptions and differs from amortized dynamic-array resizing. Always name the input quantity: vertices and edges are not the same variable.
Account for memory and hidden language work
A result list containing n values needs space proportional to n even if the loop uses only two local variables. A slice usually creates a new Python list, and repeated slicing inside recursion can add copying work not visible in a recurrence that counts comparisons only. Calling a helper once per iteration does not make that helper constant time. Include input representation, output size, and auxiliary storage separately. Python integers can grow in bit length, so summing enormous values has arithmetic costs beyond a simplistic fixed-word model. State the simplified model before using it.
Use tests as counterexample search, not a proof substitute
Test empty input, one element, duplicates, boundary positions, and a normal example chosen to expose the contract. Compare a faster algorithm against a simple reference implementation on small exhaustive inputs when practical. This is useful differential evidence, but a finite test set does not prove every possible input. Combine invariants and termination arguments with tests that would distinguish likely mistakes. Count operations to study growth before measuring tiny wall-clock intervals, which may be dominated by interpreter and scheduling overhead. The course validates safe local programs without load testing or external resources.
Common errors and fixes
Nested loops are called linear because each line is simple
- Why it happens
- The total iteration count was not summed.
- How to fix it
- Derive the sum from the actual bounds.
- Prevent it next time
- Count operations on small n and compare the formula.
A pair uses the same position twice
- Why it happens
- The contract for distinct positions was not encoded in bounds.
- How to fix it
- Require j greater than i or another proven distinct-index rule.
- Prevent it next time
- Test a single matching half-value.
A helper hides extra linear copying
- Why it happens
- Only the visible loop body was counted.
- How to fix it
- Inspect helper, slicing, and allocation costs.
- Prevent it next time
- State the cost model with the algorithm.
Check your understanding
1. What invariant proves measured_sum for an empty list?
Show worked solution
Before processing anything, total is the sum of the empty prefix, zero, and visits is zero. The loop performs no iterations, so that invariant already covers the complete input. The result (0,0) follows without an exceptional special case.
2. How many pair checks occur for five elements under pair_counts?
Show worked solution
There are 4+3+2+1+0=10 checks, independently of target values because the function does not exit early. The formula 5 times 4 divided by 2 agrees. Match count is separate from the work count.
Key takeaways
- State the problem before choosing a container
- Use tests as counterexample search, not a proof substitute