Intermediate: core algorithms · Recursion and backtracking · topic 22 of 61 · 35 min
Divide and conquer
Explanation
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.
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.
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: 243In 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.
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: -1Merging 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.
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
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.
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))Practice (0/4 done)
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.
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.
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.
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
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Backtracking: subsets and permutationsRecursion and backtracking · same module
- Backtracking with pruningRecursion and backtracking · same module
- Recursion and sorting algorithmsPython Programming · Algorithms, data structures and testing
- Merge sort and quick sortSorting and searching
- RecursionC Programming (from zero to systems) · Functions and arrays
- Binary searchSorting and searching
Questions people ask about this
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.
What is the extra space of a typical merge sort on an array?Related
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).
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
- 3 more topics in “Recursion and backtracking” before its exam: Backtracking: subsets and permutations, Backtracking with pruning, Memoization: remembering answers.
- Module 5 exam: Recursion and backtracking · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, Sorting and searching. 61 topics left in the course before the final exam.