3.7.1 · D3Algorithm Paradigms

Worked examples — Brute force — exhaustive search, when acceptable

2,382 words11 min readBack to topic

The scenario matrix

Every brute-force problem lives in exactly one search-space shape, and each shape has a size formula (all derived in the parent note). The matrix below lists every case class this topic can throw, and which worked example nails it.

Cell Search-space shape Size formula Danger case to cover Example
A Unordered pairs double-counting vs Ex 1
B All subsets → the empty set case Ex 2
C All permutations is small enough? Ex 3
D -tuples over alphabet short code = weak Ex 4
E Feasibility limit (too slow?) plug into reject brute force Ex 5
F Degenerate / empty input size or no valid answer exists Ex 6
G Word problem (real world) model then count translate words → count Ex 7
H Exam trap (growth vs bound) compare two formulas "exponential = bad" myth Ex 8

The danger case column is the whole point: a case is not covered until you've shown what happens at its edge.

Figure — Brute force — exhaustive search, when acceptable

Ex 1 — Cell A: unordered pairs (and the double-count edge)

  1. Count the candidates. Why this step? Brute force always starts by sizing the space. One candidate = one unordered pair of positions. With that is Not — because and are the same pair, and a number can't pair with itself.

  2. Enumerate without double-counting. Why this step? If we let the inner loop run from we'd visit each pair twice and waste half the work (and possibly report a "pair" of an element with itself, distance , which is wrong). Fix: inner loop starts at i+1.

def closest_pair(a):
    n = len(a); best = None
    for i in range(n):
        for j in range(i+1, n):     # j > i  -> distinct, no double count
            d = abs(a[i]-a[j])
            if best is None or d < best[0]:
                best = (d, i, j)
    return best
  1. Run it by hand on the closest region. Sorted values are ; the tightest gap is and (distance ), at positions (value ) and (value ). All other adjacent gaps ( too!) — check: as well. So there are two pairs at distance ; brute force returns the first one it meets in loop order.

Verify: number of pairs . Minimum distance over all pairs (from and ). ✔


Ex 2 — Cell B: all subsets, including the empty set

  1. Count. Why? Each of the items is independently in or out subsets. Crucially this includes the empty set (mask , sum ).

  2. Enumerate with bitmasks. Why bitmasks? A number mask from to has one bit per item; bit set ⇔ item chosen. This is the cleanest way to hit all subsets exactly once (see Bitmasking).

def has_subset_sum(vals, target):
    n = len(vals)
    for mask in range(1 << n):          # 0 .. 2^n - 1  (includes mask=0)
        s = sum(vals[i] for i in range(n) if mask & (1 << i))
        if s == target:
            return True
    return False
  1. Walk the winning mask. Why? We want to see the answer, not just trust it. Mask selects items and → values (no). Mask selects items ✔. So the answer is True.

  2. The edge. Why cover it? Empty input breaks naive code. Here : the loop runs once with mask=0, sum . So an empty item list can only match target = 0.

Verify: number of subsets . Subset sums to → answer True. Empty-list on target → True; on target → False. ✔


Ex 3 — Cell C: all permutations (is small enough?)

  1. Count. Why? The tour is a permutation of the non-start cities . Number of orderings . (If we fixed the start, permuting all would over-count rotations — fixing city removes that.)

  2. Feasibility check first. Why? Permutation brute force is only safe for tiny . Rule of thumb: must stay under . Here — trivially fine. For cities it's (still fine); for cities too slow, switch to Dynamic Programming (Held–Karp).

from itertools import permutations
def best_tour(cost):
    n = len(cost); best = None
    for perm in permutations(range(1, n)):   # (n-1)! orders
        route = [0] + list(perm) + [0]
        d = sum(cost[route[k]][route[k+1]] for k in range(len(route)-1))
        best = d if best is None else min(best, d)
    return best
  1. Hand-check one small case. Let costs be . Route . Route . Best over all is 6.

Verify: number of tours . Minimum tour length for the given matrix . ✔


Ex 4 — Cell D: -tuples over an alphabet (short code = weak)

  1. Count the tuples. Why? Each of the positions independently picks one of symbols → candidates. Under , so it cracks instantly.

  2. Compare with the PIN. Why? To see that alphabet size and length trade off. A -digit PIN is : . So the -digit PIN actually has more candidates than the -letter password — length matters more than you'd guess.

  3. The lesson. Why? This is exactly why short codes are weak (parent note, Ex 3): only becomes attacker-proof when is large. lowercase letters give — now brute force fails, as it should.

Verify: ; ; so the PIN space is larger by a factor . ✔


Ex 5 — Cell E: the feasibility limit (reject brute force)

  1. Estimate the work. Why? The parent's feasibility test: . Here

  2. Compare to the line in the sand. Why? ops/sec is roughly one second of compute. , so brute force needs ~50 seconds — it will time out. Reject it; use a hash set () or sort + two pointers (see Time Complexity & Big-O).

  3. Where's the boundary? Why? Solve . So brute force is fine up to about and hopeless well before — matching the parent's table row ": maybe".

Verify: (reject). Break-even . ✔


Ex 6 — Cell F: degenerate / empty input (no valid answer)

  1. Count candidates for each. Why? A pair needs two distinct positions.

    • (a) : pairs → loop body never runs → returns None.
    • (b) : pairs → also None. A single element cannot pair with itself.
    • (c) : pair → checks ✔ → returns .
  2. Why this matters. Why? The dangerous case is : naive code that assumes "there's always a pair" crashes or returns garbage. The correct brute force simply enumerates zero candidates and reports "no answer" — which is the right behaviour.

  3. The general rule. Why? Empty enumeration ⇒ empty answer. Brute force is automatically correct on degenerate inputs as long as you don't special-case wrongly.

Verify: pairs for are ; only case (c) succeeds, returning indices with . ✔


Ex 7 — Cell G: a real-world word problem

  1. Model it as tuples. Why? "At most coins, each of types" → we try every multiset of size . A clean brute force enumerates counts with .
def fewest_coins(target, coins=(1,3,4), cap=3):
    best = None
    for c1 in range(cap+1):
      for c3 in range(cap+1):
        for c4 in range(cap+1):
          if c1+c3+c4 <= cap:
            if 1*c1 + 3*c3 + 4*c4 == target:
              used = c1+c3+c4
              best = used if best is None else min(best, used)
    return best
  1. Search for total . Why enumerate? No greedy shortcut is guaranteed for arbitrary coin sets (greedy can fail — see Greedy Algorithms). Brute force is the safe baseline. Candidates hitting : (two coins), (three coins), ... The minimum is coins ().

  2. Real-world caveat. Why the cap? Without the " coins" cap the space is infinite, so brute force needs a bound. For unbounded change, this becomes a Dynamic Programming problem.

Verify: using coins; no single coin equals , so minimum is . ✔


Ex 8 — Cell H: the exam trap (growth rate vs input bound)

  1. Plug in the bounds. Why? Big-O is meaningless without the constraint. Compute actual operation counts:

  2. Compare. Why? is ~38000× smaller than . So the "exponential" P finishes almost instantly while the "polynomial" Q times out. The exam's claim is false.

  3. The takeaway. Why? Growth rate ranks algorithms as on the same input, not across different bounds. Always substitute the given constraint before discarding brute force. This is precisely the parent note's steel-man mistake.

Verify: , so P does fewer operations. ✔


Recall Which cell? (self-test)

"Find all pairs summing to " — which shape? ::: Cell A, unordered pairs, . "Try every ordering of jobs" — which shape? ::: Cell C, permutations, . "Every subset that fits a knapsack" — which shape? ::: Cell B, subsets, . "Guess a PIN of length " — which shape? ::: Cell D, -tuples, . Break-even for under ops? ::: about ().


See also: Backtracking (brute force that prunes dead branches early), NP-Hard Problems (where brute force + pruning is often all we have), and the Hinglish version.