Advanced

Divide and conquer

Divide and conquer splits a problem into two independent halves, solves each by recursion, and combines the answers. The split is what gives the log depth.

What you will see

4

About 3 min of reading

Quick check

Before this new lesson, one small question about the lesson you just finished. Recall it, do not look it up.

Try It

Run a divide and recursion.

1

Sum by splitting.

def total(a): if not a: return 0 if len(a) == 1: return a[0] mid = len(a) // 2 return total(a[:mid]) + total(a[mid:]) print(total([1, 2, 3, 4]))

Before you run it, say out loud what it will print. Then open the answer.

What it prints
10
2

Max by splitting.

def big(a): if len(a) == 1: return a[0] mid = len(a) // 2 left = big(a[:mid]) right = big(a[mid:]) return left if left > right else right print(big([3, 9, 1, 7]))

Before you run it, say out loud what it will print. Then open the answer.

What it prints
9
3

Depth of halving 8.

def depth(n): if n <= 1: return 0 return 1 + depth(n // 2) print(depth(8))

Before you run it, say out loud what it will print. Then open the answer.

What it prints
3
4

Max of one item.

def big(a): if len(a) == 1: return a[0] mid = len(a) // 2 left = big(a[:mid]) right = big(a[mid:]) return left if left > right else right print(big([4]))

Before you run it, say out loud what it will print. Then open the answer.

What it prints
4

Put it in order

Before you write anything, put these lines back in the right order. Click a line to move it up one step. This support step is easier than writing code from scratch, and it is here to make the last example stick.

Data Flow

Divide and conquer splits a problem into independent halves, solves each by recursion, and combines the answers, giving log depth.
1 split into halves
↓
2 solve each half
↓
3 combine answers
↓
4 log depth
Line by line

Read the first example one line at a time. A line that prints shows what appears on screen; the rest set things up quietly.

def total(a): sets nothing on screen
if not a: sets nothing on screen
return 0 sets nothing on screen
if len(a) == 1: sets nothing on screen
return a[0] sets nothing on screen
mid = len(a) // 2 sets nothing on screen
return total(a[:mid]) + total(a[mid:]) sets nothing on screen
print(total([1, 2, 3, 4])) 10
Why this works
Split shape sets cost

Halving gives log depth. Splitting off one item at a time gives linear depth. The split is the whole design choice.

Combine must be cheap

If combining costs more than linear, the divide and conquer loses its speed. The merge in merge sort is kept linear on purpose.

Examples

Split in two

def total(a):
    if not a:
        return 0
    if len(a) == 1:
        return a[0]
    mid = len(a) // 2
    return total(a[:mid]) + total(a[mid:])
print(total([1, 2, 3, 4]))

Two halves summed on their own, then added. The work splits instead of stepping one at a time.

Find the max

def big(a):
    if len(a) == 1:
        return a[0]
    mid = len(a) // 2
    left = big(a[:mid])
    right = big(a[mid:])
    return left if left > right else right
print(big([3, 9, 1, 7]))

The bigger of the two half-maxes is the whole max. Two recursions, one combine.

Depth is log

def depth(n):
    if n <= 1:
        return 0
    return 1 + depth(n // 2)
print(depth(8))

Halving each call reaches one in three steps. The depth is the log, not the count.

Count the calls

def calls(n):
    if n <= 1:
        return 1
    return 1 + calls(n // 2) * 0 + calls(n // 2)
print(calls(8))

One call per node of the split tree. Halving keeps the count near n.

Combine step matters

def total(a):
    if not a:
        return 0
    if len(a) == 1:
        return a[0]
    mid = len(a) // 2
    return total(a[:mid]) + total(a[mid:])
print(total([5, 5]))

The combine is the plus. Change it and you change what the whole computes.

Base is size one

def big(a):
    if len(a) == 1:
        return a[0]
    mid = len(a) // 2
    left = big(a[:mid])
    right = big(a[mid:])
    return left if left > right else right
print(big([4]))

A single item is both the max and the base. Splitting stops at one.

Empty guard first

def total(a):
    if not a:
        return 0
    if len(a) == 1:
        return a[0]
    mid = len(a) // 2
    return total(a[:mid]) + total(a[mid:])
print(total([]))

Guard the empty case before splitting. An empty split can loop.

Two halves are independent

def big(a):
    if len(a) == 1:
        return a[0]
    mid = len(a) // 2
    left = big(a[:mid])
    right = big(a[mid:])
    return left if left > right else right
print(big([2, 8]))

Neither half needs the other until the combine. That independence is what makes it parallel-friendly.

Words this lesson uses

Each one is defined here on this site. Open the entry for the full definition and the other words it points at.

← Remembering past answers
Previous Lesson
Remembering past answers
Next Lesson
Tree traversal
Tree traversal →