Skip to content

Intermediate: core algorithms · Recursion and backtracking · topic 22 of 61 · 35 min

Divide and conquer

Explanation

Divide and conquer splits a problem into smaller parts of the same kind (usually halves), solves each part recursively, and combines the answers. Because the size halves at each level, the recursion is only about log₂ n deep. Fast power, recursive binary search and merge sort all follow this pattern.

Three steps: divide, conquer, combine

Divide and conquer is a way to design recursive algorithms. Divide: split the input into smaller parts, usually two halves. Conquer: solve each part with a recursive call (tiny parts are base cases). Combine: build the full answer from the answers of the parts. Recursion from Topic 1 shrinks the problem by one each time; divide and conquer shrinks it by half, which is why it is so much faster and so much shallower.

Think of it like this: A teacher has 400 answer sheets to check. She splits them into two piles and gives each pile to an assistant, who splits it again for two helpers, and so on until each person has a few sheets. Each person checks their sheets and passes the totals back up. That is divide (split piles), conquer (check a small pile) and combine (add the totals).

Fast power: x^n in O(log n)

The simple power from Topic 1 makes n multiplications. But x^n = (x^(n/2))² when n is even, and x × x^(n−1) when n is odd. Compute the half power ONCE, store it, and square it. Each step halves n, so there are about log₂ n steps, each with one or two multiplications. This idea is called exponentiation by squaring.

python
def fast_pow(x, n):
    if n == 0:
        return 1
    half = fast_pow(x, n // 2)   # one recursive call, stored
    if n % 2 == 0:
        return half * half
    return half * half * x

print(fast_pow(2, 10))   # Output: 1024
print(fast_pow(3, 5))    # Output: 243
n = 10 → 5 → 2 → 1 → 0: five calls instead of eleven. For n = 10⁶ it is about 21 calls.
Careful: Writing fast_pow(x, n // 2) * fast_pow(x, n // 2) makes TWO calls per level, and the total goes back to O(n). Store the half result in a variable and use it twice.

In contests and in cryptography, powers are huge, so you take the remainder modulo m at every multiplication: (a × b) mod m = ((a mod m) × (b mod m)) mod m. The numbers then never grow beyond m², and x^n mod m costs O(log n) multiplications. Python's built-in pow(x, n, m) uses the same idea.

Binary search, recursively

In a sorted list, compare the target with the middle item. If they are equal, you are done. If the target is smaller, it can only be in the left half; otherwise the right half. That is divide and conquer with only ONE half to solve and nothing to combine. The recurrence is T(n) = T(n/2) + O(1), which is O(log n) time and O(log n) stack space. Module 6 writes it as a loop with O(1) space and adds the tricky variants.

python
def bsearch(nums, target, lo=0, hi=None):
    if hi is None:
        hi = len(nums) - 1
    if lo > hi:                      # empty range: not found
        return -1
    mid = (lo + hi) // 2
    if nums[mid] == target:
        return mid
    if nums[mid] < target:
        return bsearch(nums, target, mid + 1, hi)
    return bsearch(nums, target, lo, mid - 1)

print(bsearch([2, 5, 8, 12, 16], 12))   # Output: 3
print(bsearch([2, 5, 8, 12, 16], 7))    # Output: -1
Passing lo and hi avoids copying halves with slices, which would add O(n) work.

Merging two sorted halves

Merge sort is divide and conquer with a real combine step. Split the list in half, sort each half recursively, then merge: two fingers walk the two sorted halves, and the smaller of the two current items goes to the output each time. Merging is O(n), the recursion is log₂ n levels deep, and every level merges n items in total, so merge sort is O(n log n), the recurrence T(n) = 2T(n/2) + n you met in Module 1. Module 6 builds the full sort.

python
def merge(a, b):
    i = j = 0
    out = []
    while i < len(a) and j < len(b):
        if a[i] <= b[j]:
            out.append(a[i]); i += 1
        else:
            out.append(b[j]); j += 1
    return out + a[i:] + b[j:]

print(merge([1, 4, 9], [2, 3, 10]))   # Output: [1, 2, 3, 4, 9, 10]

Counting inversions: an idea to remember

An inversion is a pair of positions i < j where nums[i] > nums[j]; it measures how unsorted a list is. Checking every pair is O(n²). During a merge, whenever an item from the RIGHT half is placed before the remaining items of the left half, it forms an inversion with every one of them, so you add len(left) − i in one step. Counting during merge sort gives all inversions in O(n log n). You will code it in Module 6.

Common mistakes

  • Slicing halves (nums[:mid], nums[mid:]) inside binary search: each slice copies, so the search becomes O(n). Fix: pass lo and hi indexes.
  • Calling the same half twice in fast power. Fix: store half = fast_pow(x, n // 2) once.
  • Forgetting the base case for an empty or one-item range, so the recursion never ends. Fix: handle lo > hi (empty) and lo == hi (one item) first.
  • Computing mid so the range does not shrink, for example recursing on (lo, mid) when mid can equal hi. Fix: use mid − 1 and mid + 1 after checking mid, or test a two-item range by hand.
  • Not taking the remainder at every step in modular power, so numbers grow huge and slow. Fix: apply % m after every multiplication.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Divide and conquer · 1/8

Divide · Conquer · Combine

This lesson is about divide and conquer: split the problem, solve the parts, and combine the answers.

Prefer a video by a teacher? Find free videos on “divide and conquer algorithm fast exponentiation binary search recursion explained” on YouTube ↗

In real life: Secure payments and modular power

When a phone connects to a bank or a UPI app over HTTPS, the key exchange and digital signatures used in public-key cryptography compute numbers like x^n mod m, where n and m have hundreds of digits. Multiplying x by itself n times would never finish; exponentiation by squaring needs only about as many steps as n has bits. Real libraries add many protections, but this halving idea is at the core.

python
def pow_mod(x, n, m):
    if n == 0:
        return 1 % m
    half = pow_mod(x, n // 2, m)
    result = half * half % m
    if n % 2 == 1:
        result = result * x % m
    return result

n = 2 ** 200 + 7
print(pow_mod(5, n, 1_000_000_007) == pow(5, n, 1_000_000_007))
Shows True: about 200 halving steps for an exponent with 61 digits.

Practice (0/4 done)

Task 1 · Code · Python

Write fast_pow(x, n) that returns x to the power n for an integer n ≥ 0 using exponentiation by squaring: O(log n) multiplications and O(log n) recursion depth. Do not use ** or pow. The tests use n = 1,000,000, which the simple n-deep recursion cannot handle. Example: fast_pow(2, 10) returns 1024.

Task 2 · Code · Python

Write pow_mod(x, n, m) that returns (x to the power n) % m for integers n ≥ 0 and m ≥ 1, using fast power and taking % m after every multiplication, so numbers stay small. Do not use ** or the built-in pow; the tests compare your answers with pow(x, n, m). x may be negative; the answer must be in the range 0 to m − 1, like Python's %. Example: pow_mod(2, 10, 1000) returns 24.

Task 3 · Code · Python

Write binary_search(nums, target, lo=0, hi=None) RECURSIVELY. nums is sorted in ascending order with DISTINCT values. Return the index of target, or -1 if it is missing. Pass lo and hi to the recursive call instead of slicing, so it runs in O(log n) time. Example: binary_search([2, 5, 8, 12, 16], 12) returns 3.

Task 4 · Code · Python

Write max_dc(nums) that returns the largest value of a NON-EMPTY list by divide and conquer: the maximum of a range is the larger of the maximum of its left half and the maximum of its right half; a one-item range is its own maximum. Use a helper with lo and hi indexes (no slicing) and do not call max(). The recursion depth is only about log₂ n, so a list of 100,000 items works, while a "first item vs the rest" recursion would hit the recursion limit.

Quick quiz

Question 1

1.What are the three steps of divide and conquer?

Question 2

2.How many multiplications does fast power need for n = 1,000,000, roughly?

Question 3

3.A fast power writes return fast_pow(x, n // 2) * fast_pow(x, n // 2) for even n. What is its time?

Question 4

4.Why pass lo and hi instead of slicing nums[mid + 1:] in recursive binary search?

Question 5

5.In [3, 1, 2], how many inversions are there?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

What are the three steps of divide and conquer?

Answer: Divide into parts, solve each part recursively, combine the answers

Divide splits the input, conquer solves smaller copies, combine builds the full answer.

How many multiplications does fast power need for n = 1,000,000, roughly?

Answer: Between 20 and 40

n halves about 20 times (2²⁰ ≈ 10⁶), with one or two multiplications per step.

A fast power writes return fast_pow(x, n // 2) * fast_pow(x, n // 2) for even n. What is its time?

Answer: O(n)

Two calls per level double the work at each level: 1 + 2 + 4 + … + n calls, which is O(n).

Why pass lo and hi instead of slicing nums[mid + 1:] in recursive binary search?

Answer: Slicing copies the half, adding O(n) work and memory

A slice copies about n/2 items, so the search would cost O(n) instead of O(log n).

In [3, 1, 2], how many inversions are there?

Answer: 2

The pairs (3, 1) and (3, 2) are out of order; (1, 2) is fine.

What is the Big-O of merge sort?Related

Answer: O(n log n)

About log n levels of splitting, with n items merged at each level.

From Recursion and sorting algorithms

What is the extra space of a typical merge sort on an array?Related

Answer: O(n)

Merging needs a separate output list of up to n items.

From Merge sort and quick sort

Linear search for a target that is the first item takes one step. Which case is that?Related

Answer: Best case

The input that makes the code do the least work is the best case.

From Best, average and worst case; analysing loops and recursion

What is the extra space of a recursive function that goes n levels deep and creates no lists?Related

Answer: O(n)

n frames wait on the call stack at the deepest point, so the space is O(n).

From Recursion: functions that call themselves

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 3 more topics in “Recursion and backtracking” before its exam: Backtracking: subsets and permutations, Backtracking with pruning, Memoization: remembering answers.
  2. Module 5 exam: Recursion and backtracking · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Sorting and searching. 61 topics left in the course before the final exam.

← Previous: Recursion: functions that call themselves