Divide and Conquer Algorithms Explained
Divide and Conquer Algorithms Explained
37
Consider the following pseudo-code for Merge Sort.
Algorithm 12 MergeSort(A[1..n])
Input: An array A[1..n] with n numbers.
Output: The array A sorted.
1: if n = 1 then
2: return A
3: else
4: m = ↑n/2↓
5: Aω =MergeSort(A[1..m])
6: Ar =MergeSort(A[(m + 1)..n])
7: Merge Aω and Ar into one single sorted array B.
8: return B
9: end if
Here is the trace of this algorithm on a small input. Consider the following input.
First, the algorithm recursively sorts A[1..4] = [5, 9, 1, 5]. Then, the algorithm recursively
sorts A[5..8] = [8, 4, 20, 0]. We get
Then, the algorithm “merges Aω and Ar into one single sorted array B.” How does this merge
step work? We look at the first element in Aω and the first element in Ar and we transfert
the smallest of the two into B. Then we repeat. Here is what we get with our example.
Aω Ar B
[1, 5, 5, 9] [0, 4, 8, 20] B =[]
[1, 5, 5, 9] [4, 8, 20] B = [0]
[5, 5, 9] [4, 8, 20] B = [0, 1]
[5, 5, 9] [8, 20] B = [0, 1, 4]
[5, 9] [8, 20] B = [0, 1, 4, 5]
[9] [8, 20] B = [0, 1, 4, 5, 5]
[9] [20] B = [0, 1, 4, 5, 5, 8]
[] [20] B = [0, 1, 4, 5, 5, 8, 9]
[] [] B = [0, 1, 4, 5, 5, 8, 9, 20]
In total, to merge Aω and Ar , MergeSort performs n ↔ 1 comparisons (there are n numbers
to transfer into B). And we must not forget that right at the beginning, the algorithm
performs one comparison to decide whether n = 1. As such, we have a total of n comparisons,
excluding the recursive calls to MergeSort.
38
Let us now figure out the number of comparisons performed by MergeSort. Let T (n) be the
number of comparisons performed by MergeSort on an input (an array) of size n. We have
T (n) = T (m) + T (n ↔ m) + n
= T (↑n/2↓) + T (↗n/2↘) + n.
The presence of the floor function ↑·↓ and the ceiling function ↗·↘ is a little annoying. If n
was a power of two, we could write
T (n) = 2 T (n/2) + n,
which is much easier to handle. We can, for instance use unfolding to solve this recurrence.
Indeed, if n = 2k for an integer k ≃ 1, then
T (n) = 2 T (n/2) + n
! ! " "
n/2 n
= 2 2T + +n
2 2
# $
= 22 T n/22 + 2n
! ! " "
2 n/22 n
= 2 2T + 2 + 2n
2 2
# $
= 23 T n/23 + 3n
! ! " "
3 n/23 n
= 2 2T + 3 + 3n
2 2
4
# 4
$
= 2 T n/2 + 4n
..
.
# $
= 2k T n/2k + kn (2.1)
= n T (n/n) + n log2 (n) since n = 2k ,
= n + n log2 (n) since f (n/n) = f (1) = 1,
# $
= n 1 + log2 (n)
= O(n log(n)). (2.2)
What do we do now if n is not a power of 2? Exercise 13 asks you to prove (by induction
on n) that
T (n) = T (↑n/2↓) + T (↗n/2↘) + n
solves to T (n) = O(n log(n)) in general.
When we analyze the running time of a recursive algorithm, most of the time, we will have
to deal with expressions like “T (↑n/2↓)” or “T (↗n/2↘)”. If it turns out that n is even, then
we can forget about the floor/ceiling functions. Otherwise, what do we do? We do not want
39
to have to go through a proof by induction every time1 . It turns out there is a theorem due
to Akra-Bazzi which says... very roughly speaking... that we do not need to worry about
floor/ceiling function in these recursive equations2 . We can simply ignore them.
In an array A[1..n] of n numbers, the entry A[i] is a peak if it is greater than or equal to all
of its neighbours in A. What does this mean? What do we mean by “all of its neighbours”?
• If n = 1, then A[1] is the only element. As such, it does not have any neighbour. By
default, we say that A[1] is a peak.
• If n ≃ 2, then A[1] has exactly one neighbour, namely A[2]. The entry A[n] also has
exactly one neighbour, namely A[n ↔ 1]. So A[1] is a peak if A[1] ≃ A[2] and A[n] is a
peak if A[n] ≃ A[n ↔ 1].
• If n ≃ 3, then for all 1 < i < n, A[i] has exactly two neighbours, namely A[i ↔ 1] and
A[i + 1]. So A[i] is a peak if A[i] ≃ A[i ↔ 1] and A[i] ≃ A[i + 1].
Be careful! A peak is an entry of the array, not the index of this entry.
1
Such inductive arguments involving floor/ceiling functions can be very tedious.
2
The exact statement of Akra-Bazzi theorem is actually much more involved, but this is beyond the
scope of this course. The algorithms and the recursive equations we will encounter this semester are “simple
enough” for us to ignore floor/ceiling functions and proceed with unfolding, or use the Master Theorem as
we will discuss later.
40
In the following array,
A = [1, 2, 2, 3, ↔5, 7, 8, 11]
the peaks are A[2] = 2, A[4] = 3 and A[8] = 11. In the following array,
A = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
Proof. If n = 1, then A[1] is, by default, a peak as we already discussed. For the rest of the
proof, assume n > 1.
Let 1 ⇐ k ⇐ n be such that A[k] is maximum in A. Since A[k] is maximum, then it is larger
than or equal to its neighbour(s). Therefore, A[k] is a peak.
So we can reformulate the problem as:
Is there a faster solution than the brute force approach? Let us try to use the divide-and-
conquer approach! Think about binary search... what can we learn from the element in the
middle of the array? Consider the following array.
41
The middle element is A[5] = ↔5, which is not a peak since A[4] = 3 > ↔5 = A[5].
Intuitively, since the left neighbour of A[5] is larger than A[5], it seems promising to recurse
on the left part of A, i.e., to recurse on A[1..5].
In A[1..5], the middle element is A[3] = 2, which is not a peak since A[4] = 3 > 2 = A[3].
We then recurse on the right part of A[1..5], i.e., on A[3..5].
Algorithm 13 1D-Peak
Input: An array A[1..n] of n numbers.
Output: A peak in A.
1: if n ⇐ 2 then
2: if n = 1 then
3: return A[1]
4: else if A[1] ≃ A[2] then
5: return A[1]
6: else
7: return A[2]
8: end if
9: else
10: m = ↑(n + 1)/2↓
11: if A[m] ≃ A[m ↔ 1] and A[m] ≃ A[m + 1] then
12: return A[m]
13: else if A[m] < A[m ↔ 1] then
14: Call 1D-Peak(A[1..m])
15: else
16: Call 1D-Peak(A[m..n])
17: end if
18: end if
We now have to analyse the running time of this algorithm and we have to explain why this
algorithm is correct. Let us start with the analysis of the running time. Let T (n) be the
running time of 1D-Peak on an input of size n. If n ⇐ 2, then T (n) ⇐ 7. Otherwise, we have
42
Let us unfold this equation. Therefore, assume n = 2k for an integer k. We find
T (n) ⇐ T (n/2) + 16
! ! " "
n/2
⇐ T + 16 + 16
2
= T (n/22 ) + 2 · 16
! ! " "
n/22
⇐ T + 16 + 2 · 16
2
= T (n/23 ) + 3 · 16
! ! " "
n/23
⇐ T + 16 + 3 · 16
2
= T (n/24 ) + 4 · 16
..
.
= T (n/2k→1 ) + (k ↔ 1) · 16
= T (2) + (k ↔ 1) · 16
⇐ 7 + 16(log2 (n) ↔ 1)
= ↔9 + 16 log2 (n)
= O(log(n)).
Proof. By the definition of 1D-Peak, if 1D-Peak makes a recursive call, then n ≃ 3. There-
fore, m = ↑(n + 1)/2↓ satisfies 2 ⇐ m ⇐ n ↔ 1, from which 1 ⇐ m ↔ 1 ⇐ n ↔ 2 and
3 ⇐ m + 1 ⇐ n. In other words, the neighbours of A[m] are well defined.
Assume 1D-Peak makes a recursive call on the left sub-array A↑ = A[1..m] of A[1..n]. Then we
have A[m] < A[m↔1] (and A[m↔1] is well-defined since m↔1 ≃ 1). Consider the maximum
element M of A↑ . Since M is maximum in A↑ , we have M ≃ A[m ↔ 1] > A[m]. Therefore,
M ⇒= A[m], from which M belongs to A↑ together with all of its neighbours. Moreover, since
M is maximum, M is greater than or equal to all of its neighbours. Therefore, M is a peak
both in A↑ and in A.
If 1D-Peak makes a recursive call on the right sub-array A[m..n] of A, we can make a
symmetric argument.
The 1D-Peak algorithm always makes a recursive call on a sub-array A↑ of A that is strictly
smaller than A. Therefore, either it returns a peak of A↑ (which is a peak of A by the previous
lemma) or it reaches the base case. By the definition of 1D-Peak, if 1D-Peak reaches the
base case, then it returns a peak (which is a peak of A by the previous lemma).
43
2.2.2 Finding a Peak in a Two-Dimensional Array
44
1 n
1
up
down
Boundary cells
Interior cells
If a number A[i, j] is on one of the four corners of the square3 , then A[i, j] has two neighbours.
If A[i, j] is on one of the four edges of the square but not on a corner4 , then A[i, j] has three
neighbours. We refer to the set of all edge cells as boundary cells. The cells that are not on
the boundary are referred to as interior cells.
And otherwise, A[i, j] has four neighbours. We also have to be careful about the border case
where n = 1. In this case, A[1, 1] has no neighbour and is considered to be a 2D-peak by
default.
In the following table, the peaks are the numbers in red.
1 2 3 4 5 6 7
3 0 0 0 0 0 6
5 0 0 0 0 0 9
7 0 0 8 0 0 10
9 0 0 0 0 1 12
9 0 0 0 0 0 11
10 9 8 7 8 9 10
45
INPUT: A 2D-array of numbers A[1..n, 1..n], where n ≃ 1.
OUTPUT: “NONE” if there is no peak in A, a peak if there is at least one peak in A.
Lemma 3. Let A[1..n, 1..n] be a 2D-array of numbers. Then A contains at least one peak.
Proof. If n = 1, then A[1, 1] is, by default, a peak as we already discussed. For the rest of
the proof, assume n > 1.
Can you think of a brute force solution for this problem? What is the running time you
get? Can we do better? How can we solve this problem e!ciently? Let us try to use the
divide-and-conquer approach... but how do we split the problem into sub-problems??
7 8 9 11 12 13 14 2 0
6 7 8 10 10 11 12 0 1
5 6 7 9 8 10 11 1 2
4 5 6 8 9 8 10 2 0
3 4 5 6 8 7 9 0 1
2 3 4 5 6 6 8 1 2
1 2 3 4 5 5 7 2 0
2 3 0 1 2 3 4 0 1
3 0 1 2 3 4 0 1 2
This enables us to decide whether or not the center element is a peak, but then what?
46
7 8 9 10 12 13 10 2 0
6 7 8 11 13 14 12 0 1
5 6 7 9 9 15 11 1 2
4 5 6 8 8 16 10 2 0
3 4 5 6 8 7 9 0 1
2 3 4 5 6 6 8 1 2
1 2 3 4 5 5 7 2 0
2 3 0 1 2 3 4 0 1
3 0 1 2 3 4 0 1 2
It seems reasonable to consider the maximum element m of this column and to see whether
it is a peak. If m is a peak, then we are done. Otherwise, at least one of the leftward or the
rightward neighbour of m has to be strictly bigger.
7 8 9 10 12 13 10 2 0 7 8 9 10 12 13 10 2 0
6 7 8 11 13 14 12 0 1 6 7 8 11 13 14 12 0 1
5 6 7 9 9 15 11 1 2 5 6 7 9 9 15 11 1 2
4 5 6 8 8 16 10 2 0 4 5 6 8 8 16 10 2 0
3 4 5 6 8 7 9 0 1 3 4 5 6 8 7 9 0 1
2 3 4 5 6 6 8 1 2 2 3 4 5 6 6 8 1 2
1 2 3 4 5 5 7 2 0 1 2 3 4 5 5 7 2 0
2 3 0 1 2 3 4 0 1 2 3 0 1 2 3 4 0 1
3 0 1 2 3 4 0 1 2 3 0 1 2 3 4 0 1 2
So we recurse on the side where we can find a strictly bigger neighbour of m. (If both
the leftward and the rightward neighbours are strictly bigger than m, then we can pick
arbitrarily.)
47
7 8 9 10 12 13 10 2 0 7 8 9 10 12 13 10 2 0
6 7 8 11 13 14 12 0 1 6 7 8 11 13 14 12 0 1
5 6 7 9 9 15 11 1 2 5 6 7 9 9 15 11 1 2
4 5 6 8 8 16 10 2 0 4 5 6 8 8 16 10 2 0
3 4 5 6 8 7 9 0 1 3 4 5 6 8 7 9 0 1
2 3 4 5 6 6 8 1 2 2 3 4 5 6 6 8 1 2
1 2 3 4 5 5 7 2 0 1 2 3 4 5 5 7 2 0
2 3 0 1 2 3 4 0 1 2 3 0 1 2 3 4 0 1
3 0 1 2 3 4 0 1 2 3 0 1 2 3 4 0 1 2
7 8 9 10 12 13 10 2 0 7 8 9 10 12 13 10 2 0
6 7 8 11 13 14 12 0 1 6 7 8 11 13 14 12 0 1
5 6 7 9 9 15 11 1 2 5 6 7 9 9 15 11 1 2
4 5 6 8 8 16 10 2 0 4 5 6 8 8 16 10 2 0
3 4 5 6 8 7 9 0 1 3 4 5 6 8 7 9 0 1
2 3 4 5 6 6 8 1 2 2 3 4 5 6 6 8 1 2
1 2 3 4 5 5 7 2 0 1 2 3 4 5 5 7 2 0
2 3 0 1 2 3 4 0 1 2 3 0 1 2 3 4 0 1
3 0 1 2 3 4 0 1 2 3 0 1 2 3 4 0 1 2
7 8 9 10 12 13 10 2 0 7 8 9 10 12 13 10 2 0
6 7 8 11 13 14 12 0 1 6 7 8 11 13 14 12 0 1
5 6 7 9 9 15 11 1 2 5 6 7 9 9 15 11 1 2
4 5 6 8 8 16 10 2 0 4 5 6 8 8 16 10 2 0
3 4 5 6 8 7 9 0 1 3 4 5 6 8 7 9 0 1
2 3 4 5 6 6 8 1 2 2 3 4 5 6 6 8 1 2
1 2 3 4 5 5 7 2 0 1 2 3 4 5 5 7 2 0
2 3 0 1 2 3 4 0 1 2 3 0 1 2 3 4 0 1
3 0 1 2 3 4 0 1 2 3 0 1 2 3 4 0 1 2
48
7 8 9 10 12 13 10 2 0
6 7 8 11 13 14 12 0 1
5 6 7 9 9 15 11 1 2
4 5 6 8 8 16 10 2 0
3 4 5 6 8 7 9 0 1
2 3 4 5 6 6 8 1 2
1 2 3 4 5 5 7 2 0
2 3 0 1 2 3 4 0 1
3 0 1 2 3 4 0 1 2
We can argue that this approach is indeed correct. And we can show that it solves the
problem in O(n log(n)) time (refer to Exercise 4).
Can we do better?
Let us try the following approach. In order to simplify the discussion, we now assume that
n = 2k + 1 for an integer k ≃ 1 (everything works if n is an arbitrary integer, this is only
to simplify the discussion). Let us consider a window frame that we superimpose onto the
array. The window frame of A corresponds to the union of the three rows 1, 2k→1 + 1, and
n, together with the three columns 1, 2k→1 + 1, and n.
1 2k 1
+1 n 1 5 9
1 1
2k 1
+1 5
n 9
n = 2k + 1 n = 23 + 1
window frame of a 2D-array
We start by scanning all the numbers in this window frame and we search for a maximum
element m. Let us say that the maximum element is A[i, j] = m. Since this maximum is
part of the window frame, then i ⇑ {1, 2k→1 + 1, n} or j ⇑ {1, 2k→1 + 1, n}.
49
1 2k 1
+1 n 1 5 9
1 1 7 6 12 2 4 7 0 5 6
1 5 7
1 11 0
2 4 8
2k 1
+1 m 5 0 0 1 6 5 4 11 12 7
3 0 9
7 0 1
3 9 2
n 9 5 5 5 0 10 11 0 10 6
n = 2k + 1 n = 23 + 1
⇥ ⇤
A 2k 1
+ 1, n 1 =m A[5, 8] = 12
1 2k 1
+1 n 1 5 9
1 1 7 6 12 2 4 7 0 5 6
1 5 7
1 11 0
< m 2 4 2 8
2k 1
+1 m 5 0 0 1 6 5 4 11 12 7
m 3 0 12 9
7 0 1
3 9 2
n 9 5 5 5 0 10 11 0 10 6
n = 2k + 1 n = 23 + 1
⇥ ⇤
A 2k 1
+ 1, n 1 =m A[5, 8] = 12
m is a peak 12 is a peak
50
1 2k 1
+1 n 1 5 9
1 1 7 6 12 2 4 7 0 5 6
1 5 7
1 11 0
< m 2 4 2 8
k 1
2 +1 m 5 0 0 1 6 5 4 11 12 7
> m 3 0 13 9
7 0 1
3 9 2
n 9 5 5 5 0 10 11 0 10 6
n = 2k + 1 n = 23 + 1
⇥ ⇤
A 2k 1
+ 1, n 1 =m A[5, 8] = 12
If A[i, j] = m is not a peak, then there must be a neighbour5 A[i↑ , j ↑ ] of A[i, j] that is
strictly bigger than A[i, j], i.e., A[i↑ , j ↑ ] > A[i, j]. The neighbour A[i↑ , j ↑ ] cannot be part of
the window frame of A, otherwise this would contradict the fact that A[i, j] is a maximum
element
# k→1in the$window
# k→1frame$ of A. As such, A[i , j ] is in the interior of one of the following
↑ ↑
four 2 +1 ⇓ 2 + 1 sub-arrays
% # $ # $&
At,ω = A 1.. 2k→1 + 1 , 1.. 2k→1 + 1 ,
% # $ # $ &
At,r = A 1.. 2k→1 + 1 , 2k→1 + 1 ..n ,
%# $ # $&
Ab,ω = A 2k→1 + 1 ..n, 1.. 2k→1 + 1 ,
%# $ # $ &
Ab,r = A 2k→1 + 1 ..n, 2k→1 + 1 ..n .
Let A↑ ⇑ {At,ω , At,r , Ab,ω , Ab,r } be the sub-array of A such that A[i↑ , j ↑ ] is in the interior of A↑ .
We then recurse on A↑ .
5
The possible neighbours of A[i, j] are A[i ↔ 1, j], A[i + 1, j], A[i, j ↔ 1] and A[i, j + 1], depending on
whether or not A[i, j] is on the boundary or on a corner of A.
51
1 2k 1
+1 n 1 5 9
1 1 7 6 12 2 4 7 0 5 6
1 5 7
1 11 0
< m 2 4 2 8
2k 1
+1 m 5 0 0 1 6 5 4 11 12 7
> m 3 0 13 9
7 0 1
3 9 2
n 9 5 5 5 0 10 11 0 10 6
n = 2k + 1 n = 23 + 1
⇥ ⇤
A 2k 1
+ 1, n 1 =m A[5, 8] = 12
⇥ ⇤
A 2k 1
+ 2, n 1 >m A[6, 8] = 12
We recurse on A0 = Ab,r . We recurse on A0 = Ab,r .
And when do we stop the recursion? Either when we find a peak or when n = 21 + 1 = 3. If
we reach the base case where n = 3, the window frame is equal to the entire array (do you
see why?) In such a case, we simply scan all the numbers in the array to find a peak. As we
scan the array, we compare each number to all of its neighbours to see whether or not it is
a peak.
We can summarize this algorithm in the following way. To simplify the presentation of the
pseudo-code, we still assume that n = 2k + 1 for an integer k ≃ 1.
52
We now have to analyse the running time of this algorithm and we have to explain why this
algorithm is correct. Let us start with the analysis of the running time. The pseudo-code
we presented for 2D-Peak is somehow high level. Except for Lines 1 and 10, some details
are missing. Let us discuss these missing details.
Line 2: In this case, A is 3 ⇓ 3. As such, A contains 9 numbers. Each of these 9 numbers
must be compared to at most four other numbers (their neighbours). For instance, for
A[2, 2], we need to evaluate the following Boolean expression
Depending on how we analyse it, this Boolean expression requires up to 16 steps. For
the other elements of A, the Boolean expression is made of 2 or 3 comparisons only.
As such, this line can be executed in less than 9 · 16 = 151 steps.
Line 4: This requires scanning 3 rows and 3 columns to find the maximum element. Thus,
this line takes at most 6n steps.
Line 5: This can be done using a simple Boolean expression which requires less than 16
steps.
Line 8: This can be done using a simple Boolean expression which requires less than 16
steps.
Line 9: This can also be done using a simple Boolean expression which requires less than
16 steps.
So in total, we can write T (3) ⇐ 151 and
Depending on how we analyze the pseudo-code of 2D-peak, we might get an additive constant
di"erent than “50”, but what really matters is that it is an additive constant. Before we
proceed with the unfolding, let us clarify something. We made the assumption that n = 2k +1
for an integer k ≃ 1. As we already explained, the only reason why we made this assumption
was to simplify the presentation of the algorithm and the presentation of the pseudo-code.
If no assumption had been made on n, we would get the same recursive equation (2.3) for
the running time of 2D-peak. Moreover, for n = 1, we would get T (1) = 1. Indeed, if a
2D-array is 1 ⇓ 1, then the only element in the array is a peak by default. So we would just
return this element.
Now, we have a function T (n) which satisfies (2.3) and we want to know how fast T (n)
grows. We can forget about the algorithm. This is a purely mathematical question. And
how do we answer it? Using unfolding! And for unfolding, as usual, we are going to assume
that n is a power of 2. Assume n = 2ω for an integer ω ≃ 1. We find
T (n) ⇐ T (n/2) + 6n + 50
53
! ! " "
n/2
⇐ T + 6(n/2) + 50 + 6n + 50
2
! "
2 1
= T (n/2 ) + 6n 1 + + 2 · 50
2
! ! " " ! "
n/22 2 1
⇐ T + 6(n/2 ) + 50 + 6n 1 + + 2 · 50
2 2
! "
3 1 1
= T (n/2 ) + 6n 1 + + + 3 · 50
2 4
! ! " " ! "
n/23 3 1 1
⇐ T + 6(n/2 ) + 50 + 6n 1 + + + 3 · 50
2 2 4
! "
4 1 1 1
= T (n/2 ) + 6n 1 + + + + 4 · 50
2 4 8
..
.
ω→1
'
ω 1
= T (n/2 ) + 6n + ω · 50
i=0
2i
! "
ω 2
= T (n/2 ) + 6n 2 ↔ ω + ω · 50
2
! "
2
= T (n/n) + 6n 2 ↔ + log2 (n) · 50
n
= 1 + 12n ↔ 12 + log2 (n) · 50
= O(n).
In conclusion, 2D-P eak takes O(n) time.
Now that the unfolding is done, let us get back to our assumption that n = 2k + 1 for an
integer k. Why is this algorithm# correct?
$ First
# notice
$ that since n = 2k + 1, then if we
recurse on A↑ , then A↑ has size 2k→1 + 1 ⇓ 2k→1 + 1 , which satisfies the precondition of
the algorithm.
Lemma 4. If 2D-Peak makes a recursive call on a sub-array A↑ of A[1..n, 1..n], then A↑
contains a peak of A[1..n, 1..n].
Proof. By the definition of 2D-Peak, if 2D-Peak makes a recursive call, then n ≃ 5 (remember
our assumption: n = 2k + 1 for an integer k ≃ 1).
First consider the case where 2D-Peak makes a recursive call on A↑ = At,ω . Let A[i, j] be
the maximum element in the window frame of A and let A[i↑ , j ↑ ] be the neighbour of A[i, j]
such that A[i↑ , j ↑ ] > A[i, j] and A[i↑ , j ↑ ] is in the interior of A↑ . Now let M be the maximum
element of A↑ .
Observe that M does not belong to the boundary of A↑ . Otherwise, it would belong to the
window frame of A. This would contradict the fact that A[i, j] is a maximum element in the
54
window frame of A. Therefore, all the neighbours of M in A are also in A↑ . Moreover, we
have M ≃ A[i↑ , j ↑ ] > A[i, j]. Therefore, M is a peak both in A↑ and in A.
If 2D-Peak makes a recursive call on At,r , Ab,ω or Ab,r , the argument is the same.
The 2D-Peak algorithm always makes a recursive call on a sub-array A↑ of A that is strictly
smaller than A. Therefore, either it returns a peak of A↑ (which is a peak of A by the previous
lemma) or it reaches the base case. By the definition of 2D-Peak, if 2D-Peak reaches the
base case, then it returns a peak (which is a peak of A by the previous lemma).
where n
'
cij = aik bkj
k=1
55
2: for i = 1 to n do
3: for j = 1 to n do
4: cij = 0
5: for k = 1 to n do
6: cij = cij + aik bkj
7: end for
8: end for
9: end for
10: return C
How can we divide and conquer? Assume n = 2k and observe the following recursive struc-
ture.
1 0 ↔1 2 0 1 2 3 9 3 ↔3 1
3 1 1 1 ↔2 1 ↔1 1 4 5 4 9
=
0 0 0 1 1 0 1 0 5 1 ↔2 ↔1
2 4 7 1 5 1 ↔2 ↔1 4 7 5 9
! " ! " ! " ! " ! " ! "
1 0 ↔1 2 0 1 2 3 9 3 ↔3 1
3
1" !1 1" !↔2 1" ! ↔1
1 " ! 4 5" ! 4 9 "
! =
0 0 0 1 1 0 1 0 5 1 ↔2 ↔1
2 4 7 1 5 1 ↔2 ↔1 4 7 5 9
In general, we have ! " ! "
A11 A12 B11 B12
A= B=
A21 A22 B21 B22
! " ! "! "
C11 C12 A11 A12 B11 B12
C= = ,
C21 C22 A21 A22 B21 B22
where
C11 = A11 B11 + A12 B21 ,
C12 = A11 B12 + A12 B22 ,
C21 = A21 B11 + A22 B21 ,
C22 = A21 B12 + A22 B22 .
We can easily turn this observation into a recursive algorithm. If T (n) is the running time
of this recursive algorithm, then we get
T (n) = 8 T (n/2) + 4 (n/2)2 = 8 T (n/2) + n2 .
56
So? How good is this? Let us unfold this recurrence. Assume n = 2k for an integer k, and
observe that T (1) = 1.
T (n) = 8 T (n/2) + n2
! ! " . /"
n/2 n 2
= 8 8T + + n2
2 2
# $
= 82 T n/22 + 2n2 + n2
! ! " . /"
2 n/22 n 2
= 8 8T + 2 + 2n2 + n2
2 2
# $
= 83 T n/23 + 4n2 + 2n2 + n2
! ! " . /"
3 n/23 n 2
= 8 8T + 3 + 4n2 + 2n2 + n2
2 2
# $
= 84 T n/24 + 8n2 + 4n2 + 2n2 + n2
..
.
# $
= 8k T n/2k + 2k→1 n2 + ... + 22 n2 + 21 n2 + 20 n2
k→1
'
# $3 # $
= 2k T n/2k + n2 2i
3 2
# k
$i=0
= n T (1) + n 2 ↔ 1
# $
= n3 T (1) + n2 n ↔ 1
= 2n3 ↔ n2
Unfortunately, this recursive approach did not work. From here, we have two options: the
divide-and-conquer approach does not work, or it does work, but we need to find a better
trick. And since we are in a chapter about divide-and-conquer, then, most likely, there is a
way to apply this technique here.
Volker Strassen in 1969 figured out how to do that. Here is how Strassen’s algorithm works.
Let us define the following ten matrices.
S1 = B12 ↔ B22
S2 = A11 + A12
S3 = A21 + A22
S4 = B21 ↔ B11
S5 = A11 + A22
S6 = B11 + B22
S7 = A12 ↔ A22
S8 = B21 + B22
S9 = A11 ↔ A21
57
S10 = B11 + B12
Why not?! And since we are having fun, let us define the following seven matrices (it’s free!)
C11 = P5 + P4 ↔ P2 + P6 ,
C12 = P1 + P2 ,
C21 = P3 + P 4 ,
C22 = P5 + P 1 ↔ P3 ↔ P7 .
Let us unfold this recurrence. Assume n = 2k for an integer k, and observe that T (1) = 1.
We find
9
T (n) = 7 T (n/2) + n2
! ! 2" "
n/2 9 . n /2 9
= 7 7T + + n2
2 2 2 2
58
# $ 7 9 9
= 72 T n/22 + · n2 + n2
! ! 4" 2 2 "
n/2 2
9 n /2
. 7 9 9
= 72 7 T + 2
+ · n2 + n2
2 2 2 4 2 2
! "2
# $ 7 9 2 7 9 2 9 2
= 73 T n/23 + n + · n + n
4 2 4 2 2
! ! 3
" . / " ! "2
n/2 9 n 2 7 9 2 7 9 2 9 2
= 73 7 T + 3
+ n + · n + n
2 2 2 4 2 4 2 2
! "3 ! "2
# $ 7 9 2 7 9 2 7 9 2 9 2
= 74 T n/24 + n + n + · n + n
4 2 4 2 4 2 2
...
! "k→1 ! "2 ! "1 ! "0
k
# k
$ 7 9 2 7 9 2 7 9 2 7 9 2
= 7 T n/2 + n + ... + n + n + n
4 2 4 2 4 2 4 2
k→1 ! "i
k
# k
$ 9 2' 7
= 7 T n/2 + n
2 i=0
4
# 7 $k
# $ 9 ↔1
= 7k T n/2k + n2 47
2 ↔1
! 4k "
k
# k
$ 2 7
= 7 T n/2 + 6n ↔1
4k
# $
# log (7) $k # $ log2 (7) k
k 2 2
= 2 2 T n/2 + 6n ↔1
(22 )k
# $log (7)
# k $log2 (7) # $ k 2
2
= 2 T n/2k + 6n2 ↔1
(2k )2
= 7nlog2 (7) ↔ 6n2
# $
= O nlog2 (7) ,
where log2 (7) ↖ 2.81 < 3! In this unfolding, we used the following trick: a = blogb (a) . When
you think about it... this is simply the definition of the logarithmic function...
There exist some faster algorithms to solve this problem. One that is well-known is due to
Coppersmith & Winograd (1990), which takes O (n2.38 ) time. However, the description of
this algorithm is so involved that it is not clear whether it can actually be implemented.
59
same logic. Why not do it once and for all? Let us say we have a general recurrence
T (n) = a · T (n/b) + nd
for some constants a ≃ 1, b > 1 and d ≃ 0. Let us try to unfold it (we assume n = bk and
T (1) = C ste for some integer k and some constant C ste ). We find
T (n) = a · T (n/b) + nd
! ! " . / "
n/b n d
=a a·T + + nd
b b
a
= a2 · T (n/b2 ) + d · nd + nd
! ! b"
n/b 2 . n /d " a
2
=a a·T + 2 + d · nd + nd
b b b
. a /2 a
= a3 · T (n/b3 ) + d nd + d · nd + nd
! ! b
" b
n/b 3 . n /d " . a /2 a
= a3 a · T + 3 + d nd + d · nd + nd
b b b b
. a /3 . a /2 a
= a4 · T (n/b4 ) + d nd + d nd + d · nd + nd
b b b
..
.
. a /k→1 . a /2 a
= ak · T (n/bk ) + d nd + ... + d nd + d · nd + nd
b b b
k→1 .
' /
a i
= ak · T (1) + nd
i=0
bd
and here we must be careful. The value of the sum depends on whether a < bd , a = bd or
a > bd . We skip the details, but if you do the math, you get the following theorem.
Theorem 2.1 (Master Theorem). Let T : N ↔→ R+ be a function. Suppose that for an
integer m ≃ 1,
O(1) if n = m,
T (n) =
a · T (n/b) + O(n ) if n > m,
d
In the definition of T (n), one can think of m as the size of the input for the base case of
the recursion. The expression n/b corresponds to the size of the sub-problems and a is the
60
number of recursive calls. Finally, O(nd ) is the amount of work that is done outside of the
recursive calls.
Merge sort: T (n) = 2T (n/2) + O(n)
1D-Peak: T (n) = T (n/2) + O(1)
2D-Peak: T (n) = T (n/2) + O(n)
Strassen: T (n) = 7 T (n/2) + O(n2 )
Binary Search: T (n) = T (n/2) + O(1)
Before we move any further, let us clarify what we mean by “k-th smallest element”. Given
a list S = ↙a1 , a2 , a3 , ..., an ∝, the k-th smallest element of S is the element we would find at
position k if the list was sorted. For instance, the 1-st smallest element of S is the minimum
element of S, and the n-th smallest element of S is the maximum element of S.
Consider the following list: ↙1, 4, 6, 0, 1, 1, 5∝. What is the 3-rd smallest element? Well, if the
list was sorted, then the list would be ↙0, 1, 1, 1, 4, 5, 6∝. And what is the element at position
3? It is 1. So the 3-rd smallest element of ↙1, 4, 6, 0, 1, 1, 5∝ is 1.
Let us consider the list ↙1, 4, 6, 0, 1, 1, 5∝ again. What is the 1-st smallest element? It is 0.
What is the 7-th smallest element? It is 6. And what is the 4-th smallest element? It is 1.
When k = ↗n/2↘, then the k-th smallest element is the median. This is why people sometimes
refer to this problem as Finding the Median.
The definition of the k-th smallest element seems to suggest that we sort the list and return
the element at position k. So we get the following algorithm for free!
Algorithm EasySelect(S, k)
Input: A list S of n numbers and an integer k with 1 ⇐ k ⇐ n.
Output: The k-th smallest element in S.
1: Sort S
2: return the element at position k
61
• The return step takes O(k) = O(n) time (don’t forget that we are working with a list).
And then, obviously, the next question is... can we do faster? In EasySelect, where is the
bottleneck? What is the most expensive step? Indeed, it is the sorting step. So can we solve
this problem without sorting? When you think about it, we do not need to know the rank of
all elements in the list, we only need to know the element at rank k. So why sort the whole
list?
And since we are in a chapter about divide-and-conquer, let us try to use this approach!
Algorithm Selection(S, k)
Input: A list S of n numbers and an integer k with 1 ⇐ k ⇐ n.
Output: The k-th smallest element of S.
1: if |S| = 1 then
2: return the only element in S
3: else
4: Choose an element p in S (call it the pivot)
5: Split S into S< , S= and S>
6: if k ⇐ |S< | then
7: Run Selection(S< , k)
8: else if k > |S< | + |S= | then
9: Run Selection(S> , k ↔ |S< | ↔ |S= |)
10: else
11: return p
12: end if
13: end if
On Line 4 of Selection, we choose an element p from S. The sets S< , S= and S> from Line 5
are defined as follows. The set S< contains all elements from S that are strictly smaller than
p, the set S= contains all occurrences of p in S, and the set S> contains all elements from S
that are strictly larger than p. We can then decide where to recurse depending on how big
k is compared to S< and S< ′ S= .
The running time of Selection(S, k) depends on the pivot p. In the worst case, here is what
can happen:
• S is sorted,
• k = 1,
62
• in each recursive call, p is chosen as the largest element6 .
Let us analyse the running time in such a situation. We can assume that choosing the pivot
(Line 4) takes O(1) time. If |S| = n, then splitting S into the three subsets (Line 5) takes
O(n) time. Observe that, since k = 1 and p is the largest element of the list, we have
S> = ↙ ∝, S= = ↙p∝ and S< = S \ S= . As such, we recurse on a list of size n ↔ 1. The total
running time then becomes
n + (n ↔ 1) + (n ↔ 2) + (n ↔ 3) + ... + 3 + 2 + 1 = O(n2 ).
A good case would be where, in each recursive call, p is chosen as the median. In this case,
the running time of Selection would satisfy
T (n) = T (n/2) + n.
Using the Master Theorem with a = 1, b = 2 and d = 1, we find T (n) = O(n), which would
be great! The problem is that we do not know how to find the median of S. This is precisely
the problem we are trying to solve (with k = ↗n/2↘)! So instead of finding the median itself,
let us try to figure out how to find a number that is “close” to the median.
One option would be to choose p randomly. Intuitively, on average, p will be close to the
median. In order to analyze the running time of this approach, you will need to study
randomized algorithms (and this semester, we only do deterministic).
How close to the median do we need p to be in order to get a linear-time algorithm? And
how can we find such a p e!ciently?
Here is what we mean by “close to the median” (From now on, to simplify the discussion, we
assume that all elements in S are di"erent).
Assume that there is a constant 0 < ε < 1 such that in Selection(S, p), it is always possible
to choose a pivot p satisfying |S< | ⇐ ε n and |S> | ⇐ ε n
If we can satisfy this assumption, then the running time of Selection becomes
T (n) = T (ε n) + n
= T (ε2 n) + ε n + n
= T (ε3 n) + ε2 n + ε n + n
..
.
n→1
'
= T (1) + n εi
i=0
6
We did not specify how to choose p. As such, depending on how the algorithm is implemented, we could
end up in a situation where p is chosen as the largest element at each recursive call.
63
1 ↔ εn
=1+n
1↔ε
1
⇐1+n
1↔ε
= O(n).
This is what Blum, Floyd, Pratt, Rivest and Tarjan discovered in 1973. Here is the trick.
Step 2 : For i = 1, 2, ..., n/5, compute the median of the i-th group, call this median mi .
We can now use the p from Step 3 in Selection(S, k). Let us see why p is a good pivot.
In other words, what value of ε do we get with this pivot? Let us start by estimating how
many numbers in S are larger than p.
Suppose we write the elements of S into columns of 5 elements. Each column corresponds
to one of the groups we created in Step 1. Suppose we sort each column from bottom to
top7 .
sorted
sorted
medians
n/5 n/5
Then where is the median of each group? In the middle row. Now suppose we sort the
columns with respect to their medians. The group of 5 with the smallest median then
corresponds to the leftmost column and the group of 5 with the largest median corresponds
to the rightmost column So where is p now? It appears in the middle row and the middle
column.
7
The algorithm is not sorting anything. This is part of our counting argument.
64
sorted
sorted
p medians
n/10
n/5
65
We will prove, by induction on n, that T (n) = O(n).
Lemma 5. Let T (n) be a function such that
Proof. We will prove by induction on n that there exists a constant c for which T (n) ⇐ c · n.
The crux of the proof is that we do not know yet what c is. We will proceed with an inductive
argument which will tell us what c should be for the proof to work. We sometimes refer to
such a proof technique as constructive induction.
For the base case, observe that (2.4) only applies when n ≃ 5. So for the base case, assume
1 ⇐ n ⇐ 5. We do not know yet what c is, but we can take any constant c such that
T (1) ⇐ c · 1,
T (2) ⇐ c · 2,
T (3) ⇐ c · 3,
T (4) ⇐ c · 4,
T (5) ⇐ c · 5.
67