Part 4 · Algorithms · Module 35

Problem-Solving Patterns

Knowing fifty algorithms is not the same as solving a problem you have never seen. This module turns Part 4 into a repeatable method: a sequence of steps that works on unfamiliar problems, a catalogue that maps the signals in a problem statement to the technique that fits, and a practice system that makes the patterns stick.

⏱ ~2.5 hoursPrerequisites: M30, M31, M32, M33

A repeatable method

Strong problem solvers are not faster thinkers; they waste less time. They follow roughly the same steps every time, so they never start coding before they understand the problem, and never optimize before they have a correct baseline.

  1. Understand. Restate the problem in your own words. Pin down inputs (types, sizes, ranges, sorted? duplicates? negatives?), the exact output, and what to do with invalid or empty input. Ask questions.
  2. Examples. Work two or three by hand, including a tiny one and a tricky one. Hand-solving is where patterns appear: "I kept looking back for the last smaller value…" is a monotonic stack waiting to happen.
  3. Brute force. State the simplest correct algorithm and its complexity, even if it is hopeless. It proves you understand the problem and becomes your test oracle.
  4. Optimize. Find the bottleneck in the brute force and ask which pattern removes it (the catalogue below). Use the input size to target a complexity: n ≤ 20 allows O(2n); n ≤ 103 allows O(n²); n ≤ 105–106 needs O(n log n) or O(n).
  5. Code. Write clean code with good names. Small helpers beat clever one-liners.
  6. Test. Run your examples, then edge cases, then (when possible) random inputs against the brute force.
  7. Analyze. State time and space complexity and justify them. Mention trade-offs.

The size-to-complexity table is worth memorizing, because Python executes roughly 107 simple operations per second:

n up toFeasible complexityTypical techniques
10–12O(n!)permutations, backtracking
20–25O(2n), O(2n·n)subsets, bitmask DP
500O(n³)interval DP, Floyd-Warshall
5,000O(n²)2-D DP, all pairs
106O(n log n)sorting, heaps, binary search
107+O(n)hashing, two pointers, sliding window, prefix sums

The pattern catalogue

Most problems are variations on a dozen patterns. The skill is reading the signals in the statement.

Signal in the problemTechniqueTypical costModule
"have I seen this before?", pairs summing to a target, counting frequenciesHashing (dict/set/Counter)O(n)M23
sorted array, pairs/triples, palindromes, partitioning in placeTwo pointersO(n)M30
contiguous subarray/substring with a constraint ("longest", "at most k")Sliding windowO(n)M30
range sums asked many timesPrefix sumsO(n) + O(1)/queryM30
"minimum X such that feasible", monotone yes/no answerBinary search on the answerO(n log range)M30
grids, mazes, "fewest steps", connected regionsBFS / DFSO(V + E)M31
"k largest/smallest", merging sorted streams, repeatedly take the bestHeapO(n log k)M25
count ways, min/max cost, choices that constrain later choicesDynamic programmingstates × transitionM32
intervals, scheduling, "always take the best available" with a proofGreedyO(n log n)M33
"all combinations/arrangements", puzzles, constraintsBacktrackingexponential, prunedM33
dynamic connectivity, "are these in the same group?", merging setsUnion-find≈ O(1) amortizedM27
"next greater/smaller element", spans, histogram areasMonotonic stackO(n)M22

A decision flowchart

flowchart TD
  A[Read the problem: input size, output, constraints] --> B{Enumerate all
arrangements or subsets?} B -- yes --> BT[Backtracking with pruning
bitmasks if n ≤ 20] B -- no --> C{Graph, grid, or
relationships?} C -- "shortest steps" --> BFS[BFS] C -- "weighted shortest" --> DJ[Dijkstra / A*] C -- "groups / connectivity" --> UF[Union-find or DFS] C -- no --> D{Contiguous subarray
or substring?} D -- yes --> SW[Sliding window or prefix sums] D -- no --> E{Sorted input or
sortable without losing info?} E -- "pairs / partition" --> TP[Two pointers] E -- "find a value" --> BS[Binary search] E -- no --> F{Monotone feasibility:
if x works, x+1 works?} F -- yes --> BA[Binary search on the answer] F -- no --> G{Count ways or optimize
over sequential choices?} G -- yes --> H{Greedy choice
provable?} H -- yes --> GR[Greedy] H -- no --> DP[Dynamic programming] G -- no --> I{Top k, or repeatedly
extract the best?} I -- yes --> HP[Heap] I -- no --> J{Next greater or smaller
for each element?} J -- yes --> MS[Monotonic stack] J -- no --> HS[Hashing for O(1) lookups
then revisit the brute force]

Walkthrough 1: two sum (hashing)

Problem. Given a list of integers and a target, return the indexes of two different elements that sum to the target (assume exactly one answer).

Brute force: check every pair, O(n²). Bottleneck: for each x we scan the whole list looking for target - x. Pattern: "have I seen the complement?" is a set or dict lookup. One pass, storing each value's index after checking, so an element never pairs with itself.

Analysis: O(n) expected time, O(n) space. If the input were sorted, two pointers would give O(n) time and O(1) space; trading space for time is a choice worth saying out loud.

Walkthrough 2: longest substring without repeats (sliding window)

Problem. Return the length of the longest substring with no repeated characters.

Brute force: check all O(n²) substrings, each in O(n): O(n³). Bottleneck: neighboring substrings share almost everything. Pattern: "contiguous" + "longest with a constraint" = sliding window. Grow the right edge; when the constraint breaks, move the left edge past the previous occurrence of the repeated character. Each index enters and leaves the window at most once: O(n).

Walkthrough 3: ship within D days (binary search on the answer)

Problem. Packages with weights must ship in order over at most D days. Find the minimum ship capacity that makes it possible.

Brute force: try every capacity from max(weights) up to sum(weights), simulating each: O(n · sum). Signal: feasibility is monotone: if capacity c works, every larger capacity works too. A monotone yes/no predicate over a range is exactly what binary search needs (M30), even though we are searching over answers, not an array.

Analysis: O(n log(sum)) time, O(1) space. The template (find the smallest x where feasible(x) is true) solves a whole family: minimum eating speed, smallest divisor, splitting arrays, placing routers.

Walkthrough 4: rotting oranges (multi-source BFS)

Problem. In a grid, 2 = rotten orange, 1 = fresh, 0 = empty. Each minute, rotten oranges rot their four neighbors. Return the minutes until no fresh orange remains, or −1 if some never rot.

Brute force: simulate minute by minute, scanning the whole grid each time: O((rows·cols)²). Signal: grid + "minimum time/steps" = BFS. Start the queue with all rotten oranges at once (multi-source BFS); BFS layers are minutes. Each cell enters the queue once: O(rows·cols).

Walkthrough 5: k most frequent words (heap)

Problem. Return the k most frequent words, ties broken alphabetically.

Brute force: count, then sort all distinct words: O(u log u) for u distinct words. That is often fine! Signal: "top k" with k much smaller than u suggests a heap of size k: O(u log k). heapq.nsmallest does this for you with a key.

Walkthrough 6: daily temperatures (monotonic stack)

Problem. For each day, how many days until a warmer temperature? (0 if never.)

Brute force: for each day scan forward: O(n²) on a decreasing list. Signal: "next greater element" for every position. Keep a stack of indexes whose answer is still unknown; their temperatures are decreasing from bottom to top (that is the monotonic invariant). A new warmer day resolves every cooler day on top. Each index is pushed and popped once: O(n).

Testing edge cases

Most wrong answers come from a handful of input shapes. Run through this list for every problem:

  • empty input; a single element; two elements
  • all elements equal; all distinct
  • already sorted; reverse sorted
  • negatives, zero, very large values
  • the answer at the very start or very end
  • no valid answer; many valid answers
  • maximum input size (performance)
  • off-by-one boundaries: < vs <=, inclusive vs exclusive ends

The most powerful technique is differential testing: compare the optimized solution with the brute force on thousands of small random inputs. When they disagree, you have a minimal failing case for free.

Communicating complexity

Whether in an interview, a design document, or a code review, a complexity claim should be precise and justified.

How to practice

Patterns become automatic through deliberate, spaced practice, not marathon sessions.

Keep a problem journal

For each problem record: the signal you noticed (or missed), the pattern, the key insight in one sentence, your complexity, and the bug you made. The journal is your personal catalogue.

Spaced repetition

Re-solve each problem from scratch after 1 day, 3 days, 1 week, and 1 month. If you get stuck, reset the schedule. Re-solving beats re-reading: retrieval is what builds memory.

Timed sessions

Practice in 45-minute blocks: 5 to understand, 10 to design, 20 to code, 10 to test. If you are stuck after 20 minutes, read a hint, not the full solution, then journal what you missed.

Mix the patterns

Practicing ten sliding-window problems in a row teaches you to apply a pattern you were told to use. Interleave topics so you practice choosing the pattern, which is the real skill.

Talk out loud

Explain your reasoning as you go, as you would in an interview or pair-programming session. It exposes gaps and builds the habit of communicating trade-offs.

Review others' solutions

After solving, read two other solutions. Note one idiom or trick you will reuse. Then close them and rewrite yours.

Practice

Six problems, six different patterns, deliberately unlabeled in their titles. Before coding, write down which signal you notice and which technique it suggests.

Write count_pairs_with_diff(nums, k) returning the number of distinct value pairs (a, b) with b - a == k, where both values occur in nums (k ≥ 0; for k == 0 a value must occur at least twice). Aim for O(n).

Hint

Signal: "does the complement exist?" Use a Counter; for k > 0 count values v with v + k present.

Show solution
from collections import Counter

def count_pairs_with_diff(nums: list[int], k: int) -> int:
    counts = Counter(nums)
    if k == 0:
        return sum(1 for c in counts.values() if c > 1)
    return sum(1 for v in counts if v + k in counts)

Given positive integers and a target, write shortest_subarray(nums, target) returning the length of the shortest contiguous subarray whose sum is at least target, or 0 if none exists. Aim for O(n).

Hint

Signal: contiguous + positive numbers + "shortest with sum ≥". Grow the right edge, and while the window sum is at least the target, record its length and shrink from the left.

Show solution
def shortest_subarray(nums: list[int], target: int) -> int:
    best = len(nums) + 1
    left = window = 0
    for right, x in enumerate(nums):
        window += x
        while window >= target:
            best = min(best, right - left + 1)
            window -= nums[left]
            left += 1
    return 0 if best == len(nums) + 1 else best

Koko has piles of bananas and h hours (h ≥ len(piles)). Each hour she picks one pile and eats up to speed bananas from it. Write min_speed(piles, h) returning the smallest integer speed that finishes everything within h hours.

Hint

Signal: "smallest value such that it is feasible", and higher speeds are always feasible if a lower one is. Binary search speeds in [1, max(piles)]; hours for a pile are -(-pile // speed) (ceiling division).

Show solution
def min_speed(piles: list[int], h: int) -> int:
    lo, hi = 1, max(piles)
    while lo < hi:
        mid = (lo + hi) // 2
        hours = sum(-(-p // mid) for p in piles)
        if hours <= h:
            hi = mid
        else:
            lo = mid + 1
    return lo

Write count_islands(grid) where grid is a list of strings of "#" (land) and "." (water). An island is a group of land cells connected horizontally or vertically. Return the number of islands. Do not use recursion (large islands would exceed the recursion limit).

Hint

Signal: grid + connected regions. Scan every cell; on unvisited land, increment the count and flood-fill with an explicit stack or deque, marking cells as visited.

Show solution
def count_islands(grid: list[str]) -> int:
    seen: set[tuple[int, int]] = set()
    islands = 0
    for r, row in enumerate(grid):
        for c, cell in enumerate(row):
            if cell != "#" or (r, c) in seen:
                continue
            islands += 1
            stack = [(r, c)]
            seen.add((r, c))
            while stack:
                cr, cc = stack.pop()
                for nr, nc in ((cr + 1, cc), (cr - 1, cc), (cr, cc + 1), (cr, cc - 1)):
                    if 0 <= nr < len(grid) and 0 <= nc < len(grid[nr]) and grid[nr][nc] == "#" and (nr, nc) not in seen:
                        seen.add((nr, nc))
                        stack.append((nr, nc))
    return islands

Write next_greater(nums) returning a list where position i holds the first value to the right of nums[i] that is strictly greater, or -1 if there is none. Aim for O(n).

Hint

Signal: "next greater" for every element. Keep a stack of indexes still waiting for an answer; when a bigger value arrives, pop and fill them in.

Show solution
def next_greater(nums: list[int]) -> list[int]:
    answer = [-1] * len(nums)
    stack: list[int] = []
    for i, x in enumerate(nums):
        while stack and nums[stack[-1]] < x:
            answer[stack.pop()] = x
        stack.append(i)
    return answer

There are n people labeled 0..n-1. logs is a list of (time, a, b) meaning a and b became friends at that time (not necessarily sorted). Friendship is transitive for being "in the same group". Write earliest_all_connected(n, logs) returning the earliest time at which everyone is in one group, 0 when n == 1, or -1 if that never happens.

Hint

Signal: groups merging over time. Sort the logs by time and use union-find with path compression; track the number of groups and return the time when it reaches 1.

Show solution
def earliest_all_connected(n: int, logs: list[tuple[int, int, int]]) -> int:
    parent = list(range(n))
    size = [1] * n
    groups = n

    def find(x: int) -> int:
        while parent[x] != x:
            parent[x] = parent[parent[x]]      # path halving
            x = parent[x]
        return x

    if groups == 1:
        return 0
    for time, a, b in sorted(logs):
        ra, rb = find(a), find(b)
        if ra == rb:
            continue
        if size[ra] < size[rb]:
            ra, rb = rb, ra
        parent[rb] = ra                         # union by size
        size[ra] += size[rb]
        groups -= 1
        if groups == 1:
            return time
    return -1

Mini-project

Mini-projectYour problem journal and a ten-problem set

Create a local practice repository that you will keep using after this course. It holds your journal, your solutions, and tests that let you re-solve any problem from scratch and check yourself instantly.

pixi init practice && cd practice
pixi add python pytest
mkdir -p problems tests
pixi run pytest -q                  # run everything
pixi run pytest -q -k sliding       # run one pattern
practice/
  pixi.toml
  JOURNAL.md                  # one entry per attempt
  problems/
    p01_two_sum.py            # each file: docstring statement, brute(), solve()
    ...
  tests/
    test_p01_two_sum.py       # examples + edge cases + random brute-vs-solve check
  1. Pick ten problems: at least one each of hashing, two pointers, sliding window, binary search on the answer, BFS/DFS, heap, DP, greedy, backtracking, and union-find or monotonic stack. Reuse ones from this Part, or find new ones.
  2. For each, write the statement as a docstring, then brute() and solve().
  3. Write tests: the examples, the edge-case checklist from this module, and a randomized comparison of solve with brute on small inputs.
  4. Add a journal entry per problem: date, signal, pattern, key insight, complexity, mistakes, and the next review date (1 day, 3 days, 1 week, 1 month).
  • Ten problems covering at least eight different patterns
  • Each problem has a brute force and an optimized solution
  • Each test file includes edge cases and a randomized oracle test
  • pixi run pytest passes for all ten
  • JOURNAL.md has an entry with review dates for every problem
  • You re-solved at least two problems from scratch a day later

Stretch goals: add a review Pixi task that prints which problems are due today by parsing the journal; time each solve() on the largest allowed input with timeit.

Quiz

Recap

Going further