Divide and Conquer
Introduction
Divide and conquer is an general algorithmic technique that solves a problem by breaking it into smaller instances of the same problem, solving those instances recursively, and combining their solutions into a solution for the original. A common example of this is mergesort: we split an array in half, sort each half recursively, and merge the two sorted halves back together in linear time. The result is an sorting algorithm that beats other sorting approaches.
The reason this technique is so powerful is that the "combine" step is often much cheaper than solving the problem from scratch. When we can stitch subproblem solutions together efficiently, the savings compound across every level of the recursion, and an algorithm that looks quadratic collapses into something nearly linear. The art of designing a divide and conquer algorithm usually lies in figuring out what information each subproblem should return so that the combine step has everything it needs.
General Framework
Every divide and conquer algorithm follows the same three-step recipe:
- Divide. Break the problem into one or more subproblems, each a smaller instance of the original. Typically we split the input into pieces of size for some constant ; for example, mergesort splits the array into two halves, so .
- Conquer. Solve each subproblem recursively. Once the subproblems are small enough (usually constant size), solve them directly as a base case.
- Combine. Merge the solutions of the subproblems into a solution for the original problem.
To turn this recipe into a concrete algorithm, we need to specify three things: how we split the input, what the base case is, and how we combine subresults. To reason about correctness, we typically use induction on the size of the input. We show that the base case is handled correctly, then assume the recursive calls return correct solutions on their smaller inputs (the inductive hypothesis) and argue that the combine step produces a correct solution for the full input.
Runtime Analysis
Analyzing the runtime is where divide and conquer takes a bit more work than other algorithms. Most recurrence relations will take on the form
where a problem of size will split into subproblems each of size , and the divide and combine steps together take time (e.g. depending on the algorithm).
Unraveling Recurrences
We start analyzing the recurrence by recursing down one layer of our formula.
The recursion bottoms out when the subproblem hits the base case, , i.e. at . Substituting that in:
Taking for some , substitute this into each term of the sum:
This factor is constant across the sum, so we pull it out. We are left with a geometric series with ratio :
Then, we consider the following 3 cases when evaluating this sum:
Case 1: . In this case, , so the terms grow and the sum is dominated by its last term. Applying the finite geometric sum formula and dropping some of the constant terms:
We can use some math stuff to flip and :
Then, we can get a final result for our summation as:
Case 2: . Here, , so we can upper-bound this sum by the convergence of a geometric series:
Case 3: . In this case, . Since for all , we have:
Master Theorem
Fast Fourier Transform
Practice
Problem 1. Consider a list of numbers . We say that two numbers and from this list form an inversion if but . For example, a list of numbers sorted in ascending order would have inversions, while a list of numbers sorted in descending order would have inversions (each pair of numbers form an inversion). Design a divide and conquer algorithm that runs in time that counts the number of inversions of a list of numbers. You may assume is a power of 2.
Hint 1
Let the goal of our divide and conquer be to find the number of inversions in a list of numbers . What are our subproblems here (hint: split the list in half)?
Hint 2
If we split our list into subproblems and , note that any inversion pair either has both indices in the left half, both indices in the right half or in the left half and in the right half. The first two cases are handled recursively, but how do you efficiently count the "split" inversions where ? (hint: sorting may help)
Solution
Coming soon!
Problem 2. Assume a group of friends each independently decide to take a piece of candy from a bowl. Each friend has their own probability of taking a piece of candy; let be the probablity that the th friend takes candy from the bowl (thus with probability , they don't take the candy). We want to know, for , the probability that exactly friends take candy. Note that the probability of all friends taking candy is , while the probability that no one takes candy is . Give a divide and conquer algorithm to find the probabilities that exactly friends take candy from the bowl. You may assume that there is always enough candy in the bowl for everyone to take a piece if they choose to. Your algorithm should run in polynomial time, and you can assume is a power of 2.
Hint 1
Use Fast Fourier Transform. How can you represent these probabilities as a convolution or polynomial multiplication?
Hint 2
Consider a small example where and . After finding the probability that we take candies, create polynomials and and multiply them together. What do the coefficients of our new polynomial represent in this case?
Solution
Coming soon!