---
title: "Divide and Conquer Introduction"
format:
  html:
    toc: true
    number-sections: true
    code-line-numbers: true
---
[In-class notes](hand_written_notes/Divide_and_Conquer.pdf)

## Learning Goals 

- Describe Divide and Conquer Structure


## Divide and Conquer

### Structure

A divide and conquer algorithm is a recursive algorithm that divides the 
original problem into equal sized parts, recursively solves each part, and then combines the solutions to the parts to get a solution to the whole.

In fact, most of you are familiar with a divide and conquer algorithm, MergeSort:

```python
Mergesort(A)
# Input: Integer array A of size n
# Output: Sorted array

# Base case:
if A==1:
	return A

# Preprocessing
# None for MergeSort

# Divide and Conquer
A1=MergeSort(A[1:n/2])
A2=MergeSort(A[n/2+1:n])

#Combine
p1,p2=1
for i=1 to n:
	if A1[p1]<A2[p2]:
		A1[i]=A1[p1]
		p1++
	else:
		A[i]=A2[p2]
		p2++
```

It is not important at this point that you remember the details of MergeSort. Instead, let's look at the general structure of the algorithm. 

- **Base case:** (lines 5-7)  there is a non-recursive base case that kicks in 
when the input is small enough.
- **Divide and Conquer:** (lines 12-14) The heart of the divide and conquer algorithm. The input is divided into equal sized parts. (In this case, the array is divided into two halves, but in some cases, the input is divided into thirds or fourths, etc.) Then the algorithm is recursively run on each part.
- **Combine:** (lines 16-24) The outputs of the divide and conquer step need to be combined to create the solution for the entire input. This step is usually the most difficult part of the algorithm to code and prove correctness of. 
- **Preprocessing:** (lines 9-10) In MergeSort, no additional steps need to be taken before the input is divided, but in some algorithms, a preprocessing step is needed.

Note that not all recursive algorithms are divide and conquer algorithms, for example, the recursive search algorithm below is not divide and conquer because the size of the subarray in the recursive call in line 15 is just 1 less than the original input, not a half, or a third (etc) of the size.

```python
Search(A,t)
# Input: Array A, target t
# Output: Position of t in A, or 0 if not present

# Base case:
if A==1 and A[1]=t:
	return 1
else:
	return 0

# Recursion
if A[end]=t:
	return end
else
	return Search(A[1:end-1],t)
```

### Proving Correctness

A divide and conquer algorithm can be proven correct using strong induction. We need strong induction because the size of the input in the recursive call is potentially much less than the size of the input to the original problem. For the Search algorithm above, because the input to the recursive call is an array with size just one less than the size of the original array, regular induction could be used.

If you need a refresher on how to use strong induction to prove the correctness of a recursive algorithm, check out this [review video](https://midd.hosted.panopto.com/Panopto/Pages/Viewer.aspx?id=3aeac144-0b31-40fe-86da-ab5e010f6172).

### Analyzing the Runtime

To analyze the runtime, you should create a recurrence relation of the runtime, and then solve using the tree formula (aka master method) or the expand and hope method. (For a refresher on the expand and hope method, see this [review video](https://midd.hosted.panopto.com/Panopto/Pages/Viewer.aspx?id=fcc08e32-14ce-431d-bf5f-ae37015c2f1f).)

For MergeSort, if $T(n)$ is the runtime for an array of size $n$, the recurrence relation is something like:
$$ T(n)=
\begin{cases}&  ............. &\textrm{ if }n\leq 1 \\
 &...T(n/2)... &\textrm{ else}
 \end{cases}
$$

where the base case runtime applies if the input array size is at most 1, and the runtime in the general case involves the runtime on an input of size $n/2$, since we do recursive calls on inputs of size $n/2$.




