Logo

2. Divide and Conquer

Divide and Conquer...

  • Breaks a problem into subproblems which are the same problem but smaller
  • Recursing through the subproblems and subsequently combining their answers

2.1 Multiplication

The product of two complex numbers can be reduced to 3 multiplications instead of the usual 4. (Gauss)

(a+bi)(c+di)=acbd+(bc+ad)i=(a+b)(c+d)acbd(a+bi)(c+di) = ac-bd+(bc+ad)i = (a+b)(c+d)-ac-bd

This small constant factor improvement seems negligible; however, when applied at every step in a recursion, it has a noticeable impact on time complexity.

We can apply the same concept to integer multiplication. Let x,yx,y be two nn-bit integers, and, for convenience, let n=2k, kNn=2^{k},\ k\in \mathbb{N}. We now split x,yx,y into two chunks of equal bit length:

x=2n/2xL+xRy=2n/2yL+yRx = 2^{n/2}x_{L}+x_{R} \qquad y=2^{n/2}y_{L}+y_{R}

And, following Gauss's idea, expand and simplify the multiplication:

xy=2nxLyL+2n/2(xLyR+xRyL)+xRyR(1)=2nxLyL+2n/2[(xL+xR)(yL+yR)xLyLxRyR]+xRyR(2)\begin{align} xy &= 2^{n}x_{L}y_{L}+2^{n/2}(x_{L}y_{R}+x_{R}y_{L})+x_{R}y_{R} & (1) \\ &= 2^{n}x_{L}y_{L}+2^{n/2}[(x_{L}+x_{R})(y_{L}+y_{R})-x_{L}y_{L}-x_{R}y_{R}]+x_{R}y_{R} & (2) \end{align}

Note that we only have to perform 3 multiplications:

xLyLxRyR(xL+xR)(yL+yR)x_{L}y_{L} \qquad x_{R}y_{R} \qquad (x_{L}+x_{R})(y_{L}+y_{R})

Let T(n)T(n) represent the overall running time of the multiplication algorithm, provided an nn-bit input. Expression (1)(1) then has a recurrence relation of

T(n)=4T(n2)+O(n)T(n)=4T\left( \frac{n}{2} \right)+O(n)

since each multiplication (xLyL,xRyL,xLyR,xRyRx_{L}y_{L},x_{R}y_{L},x_{L}y_{R},x_{R}y_{R}) should take T(n2)T\left( \frac{n}{2} \right), since its an n2\frac{n}{2}-bit multiplication, and both summing nn-bit numbers and left-shifting (the 2k2^{k} coefficients) are linear operations in nn.

Deriving the explicit form of this recurrence relation results in T(n)=O(n2)T(n)= O(n^{2}), which provides no improvement over the standard multiplication method taught in school.

In contrast, expression (2)(2) has the recurrence relation of

T(n)=3T(n2)+O(n)T(n)=3T\left( \frac{n}{2} \right)+O(n)

since it has 3 multiplications, and the rest of the operations are still linear in nn, as previously stated.

Deriving the explicit form of this recurrence relation results in T(n)=O(n1.59)T(n)= O(n^{1.59}), a significant improvement! But why?

Consider the tree formed by the recursion. At each successive level, the number of subproblems triple (33 multiplications), but the size of each subproblem is halved (n2\frac{n}{2} bit length). That is,

At depth kk in the tree, there are 3k3^{k} subproblems of size n/2kn/2^{k}. For each subproblem, a linear amount of work is necessary to identify the next subproblems and combine the answers. In other words, the time spent at depth kk (and only depth kk) is

3k×O(n2k)=(32)k×O(n)3^{k}\times O\left( \frac{n}{2^{k}} \right)=\left( \frac{3}{2} \right)^{k}\times O(n)

In other words, the overall time complexity can be represented by a finite geometric series.

T(n)=O(n)+(32)O(n)++(32)log2nO(n)(3)\begin{align} T(n)= O(n)+\left( \frac{3}{2} \right)O(n)+\dots+\left( \frac{3}{2} \right)^{\log_{2}n}O(n) && (3) \end{align}

Since the common factor of the geometric series is greater than 11, the runtime is dominated by the last term of the series, or the last level of subproblems. This is because this series increases extremely quickly from term to term. Thus,

T(n)=(32)log2nO(n)=O(3log2n)×O(n2log2n)=O(3log2n)T(n)= \left( \frac{3}{2} \right)^{\log_{2}n}O(n)=O(3^{\log_{2}n})\times O\left( \frac{n}{2^{\log_{2}n}} \right)=O(3^{\log_{2}n})

This can actually be rewritten as O(nlog23)O(n^{\log_{2}3}) by applying chain rule (for logarithms, not derivatives) in reverse:

O(3log2n)=O(3(log23)(log3n))=O(nlog23)O(n1.59)O(3^{\log_{2}n})=O(3^{(\log_{2}3)(\log_{3}n)})=O(n^{\log_{2}3})\approx\boxed{O(n^{1.59})}

*Note that, without Gauss's trick, the derivation comes out to 4log2n=n24^{\log_{2}n}=n^{2}, i.e. no improvement

2.2 Recurrence Relations

Divide and conquer algorithms all follow the same pattern. They split a problem of size nn into aa subproblems of size nb\frac{n}{b}, and then combine these answers in O(nd)O(n^{d}) time. This allows us to write their runtimes recursively with the equation

T(n)=aT(n/b)+O(nd)T(n)=aT(\lceil n/b \rceil )+O(n^{d})

We can summarize this in a general "Master Theorem" that explicitly describes the time complexity of a divide and conquer algorithm parameterized by n,a,b,dn,a,b,d.

Master Theorem
If T(n)=aT(n/b)+O(nd)T(n)=aT(\lceil n/b \rceil)+O(n^{d}) for constants a>0,b>1,d0a>0,b>1,d\geq0, then

T(n)={O(nd),d>logbaO(ndlogn),d=logbaO(nlogba),d<logbaT(n)= \left\{ \begin{matrix} O(n^{d}), & d > \log_{b}a \\ O(n^{d}\log n), & d=\log_{b}a \\ O(n^{\log_{b}a}), & d < \log_{b}a \end{matrix} \right.

Proof. We follow the previous derivation for expression (3)(3) to find the finite geometric series

T(n)=O(nd)+(abd)O(nd)++(abd)logbnO(nd)T(n)= O(n^{d})+\left( \frac{a}{b^{d}} \right)O(n^{d})+\dots+\left( \frac{a}{b^{d}} \right)^{\log_{b}n}O(n^{d})

*Ensure this makes sense before proceeding. The finer details of derivation are purposely left out to force readers to think here :)

Case 1: abd<1\frac{a}{b^{d}}<1
The series is decreasing. Since it is geometric, the first term dominates the sum, i.e. the time complexity is O(nd)O(n^{d}).

Case 2: abd=1\frac{a}{b^{d}}=1
The terms of the series are all just O(nd)O(n^{d}). There are O(logn)O(\log n) terms in the series, so the time complexity is O(ndlogn)O(n^{d}\log n).

Case 3: abd>1\frac{a}{b^{d}}>1
The series is increasing. This is the same as the multiplication algorithm case. Thus, the last term dominates the sum, i.e. the time complexity is O(nlogba)O(n^{\log_{b}a}), as derived previously.

2.3 Mergesort

Mergesort is one of the most widely used sorting algorithms. The algorithm is a simple application of divide-and-conquer. It starts by splitting the array into halves, and recursing mergesort on each half. This propagates all the way down to singletons, i.e. arrays of size one. Then, we traverse back up the recursion step, merging the two arrays returned from our recursive calls into one sorted array. A diagram is displayed below.

mergesort.png

Merging can be done in O(n)O(n), the subtree splits in 22 at every level, and the subproblem size halves between levels. Thus, the recurrence relation is

T(n)=2T(n2)+O(n)T(n)=2T\left( \frac{n}{2} \right)+O(n)

Which comes out to be O(nlogn)O(n\log n).

Mergesort can actually be made iterative by queuing all singleton arrays at the beginning, and at each step removing popping two arrays, merging them, and pushing them into the queue.

Once can actually note that the time complexity of general sorting (not including specialized sorting algorithms like radix sort) is Ω(nlogn)\Omega(n\log n), i.e. lower bound. This is derived by considering the number of comparisons required for sorting an array by creating a binary tree with all possible permutations as the leaves and comparisons as the intermediary (non-root and non-leaf) nodes. Thus, the depth of the tree (number of comparisons required) is at least log(n!)\log(n!). By Stirling's Formula,

n!π(2n+13)nnenn!\approx \sqrt{ \pi\left( 2n+\frac{1}{3} \right) }\cdot n^{n}\cdot e^{-n}

Thus, log(n!)nlogn\log(n!)\geq n\log n, and sorting's time complexity is at least nlognn\log n. Thus, mergesort is optimal!

2.4 Medians

The naive solution to find a median is sorting. However, this has O(nlogn)O(n\log n) time complexity. We can do better!

Randomized

Description

We can use a randomized divide-and-conquer algorithm. It is known as quickselect, and can actually be used for finding the kkth smallest number, not just the median. It is very similar to quicksort.

QuickSelect(S:array, k:int)\text{QuickSelect}(S: \text{array},\ k:\text{int})

  1. Randomly pick a pivot point vv from the array SS
  2. Split SS into three arrays, S,Sv,SrS_{\ell},S_{v},S_{r} defined as follows:
S={aSa<v}Sv={aSa=v}Sr={aSa>v}\begin{align*} S_{\ell} &= \{ a\in S\mid a<v \} \\ S_{v} &= \{ a\in S\mid a=v \} \\ S_{r} &= \{ a\in S\mid a>v \} \end{align*}
  1. Return one of the following, which may require recursion:
kS    QuickSelect(S,k)S<kS+Sv    Return vS+Sv<k    QuickSelect(Sr,kS)Sv\begin{align*} k\leq \lvert S_{\ell} \rvert &\implies\text{QuickSelect}(S_{\ell},k) \\ \lvert S_{\ell} \rvert <k\leq \lvert S_{\ell} \rvert +\lvert S_{v} \rvert &\implies \text{Return }v \\ \lvert S_{\ell} \rvert +\lvert S_{v} \rvert <k &\implies \text{QuickSelect}(S_{r},k-\lvert S_{\ell} \rvert )-\lvert S_{v} \rvert \end{align*}

The three arrays S,Sv,SrS_{\ell},S_{v},S_{r} can be computed from SS in O(n)O(n) time complexity. And, at each step, the subproblem shrinks to a size of max(S,Sr)\mathrm{\mathrm{max}(\lvert S_{\ell} \rvert,\lvert S_{r} \rvert)}.

Asymptotic Analysis

Since this is a randomized algorithm, its time complexity is dependent on the choice of pivot.

Worst Case

The worst case scenario occurs when our chosen pivot is always the largest or smallest element of the array. This will cause the subproblem to shrink by only one element each time, which results in a time complexity of

n+(n1)++n2=Θ(n2)n+(n-1)+\dots +\frac{n}{2}=\Theta(n^{2})

However, this is probabilistically impossible.

Best Case

The best case scenario occurs when our chosen pivot is always the median. This results in the recurrence relation

T(n)=T(n2)+O(n)T(n)=T\left( \frac{n}{2} \right)+O(n)

By the Master Theorem, this is just O(n)O(n). Again, though, this is extremely unlikely to occur.

Average

Let vv be the chosen pivot. Let vv be good if it is within the interquartile range (25th to 75th) percentile of the array. Good choices of vv guarantee that S,Sr34S\lvert S_{\ell} \rvert,\lvert S_{r} \rvert\leq \frac{3}{4}\lvert S \rvert. And we expect to pick, on average, 22 choices of vv before getting a good vv.

Proof
Let EE be the expected number of choices before a good vv. If the current choice is good, we're done. If the current choice is bad, we need to repeat. Thus, E=1+12EE=1+\frac{1}{2}E. That is, E=2E=2.

Thus, on average, after two split operations, the subproblem will be at most 34\frac{3}{4} of the original problem size. Therefore, we can write the average recurrence relation

T(n)T(3n4)+O(n)T(n)\leq T\left( \frac{3n}{4} \right)+O(n)

Note that it would be 2O(n)2O(n) since its two split operations, but we ignore constant factors as usual.

Using the Master Theorem, we can conclude that T(n)=O(n)T(n)=O(n). In other words, the algorithm should return the correct answer in linear time complexity, on average.

Deterministic

Description

There also exists a deterministic divide-and-conquer version of QuickSelect\text{QuickSelect} that uses as pivot algorithm known as median of medians. It is parameterized by xx, the size of each subarray constructed. xx must be at least 44.

MedianOfMedians(S:array, x:int)\text{MedianOfMedians}(S:\text{array},\ x:\text{int})

  1. Split the array into contiguous subarrays of size kk.
  2. Find the median of each array by calling the algorithm on each array. (Or, alternatively, using a naive subroutine since xx is generally small).
  3. Find the median of the medians mm by recursively calling the algorithm on the array of medians.
  4. The median of medians is used to choose to recurse on at most 70% of the array, with the exact proportion dependent on the value of kk in QuickSelect\text{QuickSelect} (assuming x=5x=5). See [[#Deterministic#Asymptotic Analysis|below]] for an explanation of how the split works.

Asymptotic Analysis

We will choose x=5x=5 for convenience of analysis, and analyze the time complexity of QuickSelect\text{QuickSelect} when using MedianOfMedians\text{MedianOfMedians} as a pivot selection algorithm.

medianofmedians.png First, we will consider how we are able to eliminate 30% of the array. Consider the image above. Here, an array of 100 elements has been divided into 20 subarrays of size 5. The medians are all in row 3, and the median of medians is the element highlighted in red. Everything grayed out is less than the median of medians.

The properties of the median of medians guarantees that everything in the top left corner bounded by the row and column of the median of medians (row 1 to 3, columns 1 to 10). This makes sense because the median of medians is greater than all the median of all subarrays of size 5 before it, and the median of each subarray is greater than the elements above it in the table.

In other words, finding the median of medians allows us to identify with certainty the lower 30% of array elements. A similar thought process follows to identify the upper 30% of array elements.

Therefore, instead of fully partitioning the array based on a pivot, we can use the median of medians as a pivot and subsequently recurse on either the lower or upper of 70% of the array based on kk. We cannot recurse on the lower/upper 30% of the array because there exist some values not within those regions that are less/greater than the median of medians, respectively.

Thus, the recurrence relation is

T(n)T(n/5)+T(710n)+O(n)T(n)\leq T(n/5)+T\left( \frac{7}{10}n \right)+O(n)

The T(n/5)T(n/5) is for finding the median of the medians, the T(710n)T\left( \frac{7}{10}n \right) is for the next subproblem, and the O(n)O(n) is for the partitioning.

To derive the explicit time complexity it helps to draw out the tree for a recurrence relation of the form T(n)=T(an)+T(bn)T(n)=T(an)+T(bn). I'm not dedicated enough to draw a tree, but you'll notice that the total sum of the subproblem sizes at the kkth level is (a+b)kn(a+b)^{k}n. Since the combination step (partitioning) is O(n)O(n) complexity, the time complexity of the kkth level is O((a+b)kn)O((a+b)^{k}n). Note that this means the time complexity is simply a geometric series.

T(n)=O(n)+O((a+b)n)++O((a+b)kn)T(n)=O(n)+O((a+b)n)+\dots +O((a+b)^{k}n)

Where kk here represents the last level. Recall that, in previous proofs, either the first or last term dominates in a finite geometric series. Here, the common ratio depends on a+ba+b. For our recurrence relation, a+b=15+710=910<1a+b=\frac{1}{5}+\frac{7}{10}=\frac{9}{10}<1. Therefore, the first term dominates. That is,

T(n)=Θ(n)T(n)=\Theta(n)

2.5 Matrix Multiplication

We will assume we are multiplying 2 n×nn\times n matrices. The naive algorithm for matrix multiplication is O(n3)O(n^{3}), as there are O(n2)O(n^{2}) entries to be computed, and each requires O(n)O(n) time.

We can form a naive divide-and-conquer algorithm too, by splitting each matrix into n/2×n/2n/2\times n/2 blocks, and computing their products before combining. Unfortunately, the recurrence relation is

T(n)=8T(n/2)+O(n2)T(n)=8T(n/2)+O(n^{2})

Which comes out to O(n3)O(n^{3}). However, Volker Strassen came up with a very clever improvement on this divide-and-conquer algorithm. In particular, he figured out how to compute the product of two n×nn\times n matrices XX and YY from just seven n/2×n/2n/2\times n/2 subproblems.

XY=[P5+P4P2+P6P1+P2P3+P4P1+P5P3P7]XY=\begin{bmatrix} P_{5}+P_{4}-P_{2}+P_{6} & P_{1}+P_{2} \\ P_{3}+P_{4} & P_{1}+P_{5}-P_{3}-P_{7} \end{bmatrix}

Where

P1=A(FH)P5=(A+D)(E+H)P2=(A+B)HP6=(BD)(G+H)P3=(C+D)EP7=(AC)(E+F)P4=D(GE)\begin{align*} P_{1}&=A(F-H) & P_{5} &= (A+D)(E+H) \\ P_{2} &= (A+B)H & P_{6} &= (B-D)(G+H) \\ P_{3} &= (C+D)E & P_{7} &= (A-C)(E+F) \\ P_{4} &= D(G-E) \end{align*}

The new recurrence relation is

T(n)=7T(n/2)+O(n2)T(n)=7T(n/2)+O(n^{2})

Applying the Master Theorem gives us O(nlog27)O(n2.81)O(n^{\log_{2}7})\approx O(n^{2.81}).

Aside: Binary Exponentiation

Naive

The naive exponentiation algorithm to calculate ana^{n} is to just multiply aa with itself times. This has a time complexity of Θ(n)\Theta(n), if you consider multiplication as a constant time operation. However, multiplication is not constant time for binary exponentiation! Consider that the time complexity of multiplication grows with respect to the number of digits of the number (the maximum between the two being multiplied). Let aa have mm digits. Then a2a^{2} has about 2m2m digits, a3a^{3} has about 3m3m digits, ..., and ana^{n} has about nmnm digits. Thus, the number of digits grows with respect to nn. Therefore, this has time complexity O(n2.59)O(n^{2.59}) .

Binary

Instead of doing that, we can use the following algorithm.

def exp(a, n):
	if n % 2 == 1: return a * exp(a*a, n>>2)
	else: return exp(a*a, n>>2)

This has O(logn)O(\log n) time complexity without considering the time complexity of multiplication. Considering the complexity of multiplication, it has time complexity O(lognn1.59)O(\log n\cdot n^{1.59}).

Aside: Fibonacci Sequence

Naive

The naive algorithm for the Fibonacci sequence is simply to do a simple recursion.

def fib(n):
	if n == 0: return n
	if n == 1: return n
	return fib(n - 1) + fib(n - 2)

However, this has exponential time complexity. We proceed via induction.

The recurrence relation is T(n)=T(n1)+T(n2)+O(1)T(n)=T(n-1)+T(n-2)+O(1). We seek to prove that the time complexity is O(2n)O(2^{n}).

Base Case: n=1n=1
Trivial.

Inductive Hypothesis: 1nk1\leq n\leq k
Assume the time complexity is O(2n)O(2^{n}) for 1nk1\leq n\leq k.

Inductive Step: n=k+1n=k+1

T(k+1)=T(k)+T(k1)+O(1)=2k+2k1+O(1)=O(2k+1)T(k+1)=T(k)+T(k-1)+O(1)=2^{k}+2^{k-1}+O(1)=O(2^{k+1})

Thus, the time complexity is O(2n)O(2^{n}). In fact, we can product an even tighter bound of O(ϕn)O(\phi^{n}), where ϕ\phi is the golden ratio. This can be intuitively explained as follows.

Assume T(n)=anT(n)=a^{n}, where aR+a\in \mathbb{R}^{+}. Then,

an=an1+an2a2=a+10=a2a1a=1±52=ϕ\begin{align*} a^{n} &= a^{n-1}+a^{n-2} \\ a^{2} &= a + 1 \\ 0 &= a^{2}-a-1 \\ a &= \frac{1\pm \sqrt{ 5 }}{2}=\phi \end{align*}

Dynamic Programming

We can definitely compute it faster though! Notice how we are recomputing values we already computed? (In other words, calling Fib with the same argument multiple times). We can instead memoize this, remembering what the output of Fib was for that input.

def fib(n):
	dp = [0 for _ in range(n + 1)]
	dp[1] = 1
	for i in range(2, n + 1):
		dp[i] = dp[i - 1] + dp[i - 2]
	return dp[n]

This allows for linear time complexity... right?

Sorta. If you consider addition to be an O(1)O(1) operation, then, yes, this is O(n)O(n). Technically, addition is an O(n)O(n) operation for the Fibonacci sequence! Let's set some bounds for the Fibonacci sequence.

Fn=Fn1+Fn2=2Fn2+Fn3>2Fn2F_{n}=F_{n-1}+F_{n-2}=2F_{n-2}+F_{n-3}>2F_{n-2}

But also, since Fn2<Fn1F_{n-2}<F_{n-1},

Fn<2Fn1F_{n}<2F_{n-1}

Therefore, 2n/2<Fn<2n2^{n/2}<F_{n}<2^{n}. With this, nn is a good approximation of the number of digits of FnF_{n}. (Technically, n/3n/3, but constant factors don't matter). Therefore, the addition operation actually takes O(n)O(n) time complexity for calculating FnF_{n}.

We thus must consider the number of steps for the addition operation at every step of the for loop. The total sum is

1+2+3++n=n(n+1)2=O(n2)1+2+3+\dots +n=\frac{n(n+1)}{2}=O(n^{2})

Thus, the dynamic programming method actually has a time complexity of O(n2)O(n^{2}).

Binary Matrix Exponentiation

We can note the following formula for calculating the Fibonacci sequence:

[Fn1Fn]=[0111][Fn2Fn1]\begin{bmatrix} F_{n-1} \\ F_{n} \end{bmatrix} =\begin{bmatrix} 0 & 1 \\ 1 & 1 \end{bmatrix} \begin{bmatrix} F_{n-2} \\ F_{n-1} \end{bmatrix}

Extrapolating, we get

[Fn1Fn][0111]n[Fn2Fn1]\begin{bmatrix} F_{n-1} \\ F_{n} \end{bmatrix} \begin{bmatrix} 0 & 1 \\ 1 & 1 \end{bmatrix} ^{n} \begin{bmatrix} F_{n-2} \\ F_{n-1} \end{bmatrix}

Through matrix diagonalization, we can make the time complexity of binary matrix exponentiation essentially equivalent in time complexity to [[#Binary|binary integer exponentiation]]. Thus, the fastest algorithm is actually O(lognn1.59)O(\log n\cdot n^{1.59}).

*Note that taking the limit of this matrix exponentiation produces the golden ratio ϕ\phi, as confirmed in the previous algebraic derivation of ϕ\phi.

Aside: Closest Pair

Given a set PP of nn points in a plane, calculate the Euclidean distance between the closest pair of points.

Naive

There is clearly an O(n2)O(n^{2}) algorithm that checks the distance between every pair of points, and finds the minimum.

Divide and Conquer

Algorithm

Choose a pivot point (x,y)(x,y) in the middle of the points in terms of xx-coordinate. Divide the graph into two sets LL and RR, corresponding to points left and right of the pivot, respectively. Then, calculate the closest pair distance in LL and the closest pair distance in RR by recursing the algorithm on these subproblems. Let dL,dRd_{L},d_{R} denote these distances. Let d=min(dL,dR)d=\mathrm{min}(d_{L},d_{R}). We can prove some properties for this problem.

Property 1: If pLp \in L, qRq\in R, and Δ(p, q)\Delta(p,\ q) (distance between pp and qq) is less than dd, then p.xpivotx<d\lvert p.x-\text{pivot}_{x} \rvert<d and q.xpivotx<d\lvert q.x-\text{pivot}_{x} \rvert < d.

Proof: We proceed via proof by contraposition. Let pp lie outside the region of the plane where pivotxd<x<pivotx+d\text{pivot}_{x}-d<x<\text{pivot}_{x}+d. Since qRq\in R, Δ(p.x, q.x)>Δ(p, (pivotx, p.y))\Delta(p.x,\ q.x)>\Delta(p,\ (\text{pivot}_{x},\ p.y)) Then, it is guaranteed that p.xpivotxd    Δ(p, q)d\lvert p.x-\text{pivot}_{x} \rvert\geq d\implies \Delta(p,\ q)\geq d.

\square

Property 2: Any tile TT that is d2×d2\frac{d}{2}\times \frac{d}{2} in size that is entirely contained in LL or RR can have at most one point.

Proof: We proceed via contradiction. Assume p,qS\exists p,q\in S and p,qp,q are contained in TT. Then, Δ(p,q)(d2)2+(d2)2=d2<d\Delta(p,q)\leq \sqrt{ \left( \frac{d}{2} \right)^{2}+\left( \frac{d}{2} \right)^{2} }=\frac{d}{\sqrt{ 2 }}<d. That is, this pair of points would have formed the closest pair in LL or RR, which is a contradiction.

\square

Property 3: Let pp lie inside the region of the plane where pivotxd<x<pivotx+d\text{pivot}_{x}-d<x<\text{pivot}_{x}+d. Then, there are at most 77 points in PP within the same region such that p.yq.yp.y\leq q.y and Δ(p, q)\Delta(p,\ q) could be less than dd.

Proof: Consider the 2d×d2d\times d region within the aforementioned pivotxd<x<pivotx+d\text{pivot}_{x}-d<x<\text{pivot}_{x}+d region where pp lies in the vertical bottom of the 2d×d2d\times d region. Subdivide the 2d×d2d\times d region into 88 d2×d2\frac{d}{2}\times \frac{d}{2} squares, and arbitrarily assign pp to one of the squares it borders. Consider that any point qq above this 2d×d2d\times d region certainly has Δ(p, q)Δ(p.y, q.y)d\Delta(p,\ q)\geq\Delta(p.y,\ q.y)\geq d. Therefore, qq must exist within one of the other 77 squares     \implies there are at most 77 points in PP within the same region such that the statement is true.

\square

We can now describe the algorithm.

  1. First, we preprocess the set of points PP, producing a list PxP_{x}, the set of points sorted by xx, and PyP_{y}, the analogue for yy.
  2. We define a function Closest(Px,Py)\text{Closest}(P_{x},P_{y})
    1. Let n=Pxn=\lvert P_{x} \rvert. If n3n\leq3, just brute force.
    2. Choose the middle pivot point, "middle" in terms of xx. Partition PxP_{x} into LxL_{x} and RxR_{x} in O(1)O(1) time. Partition PyP_{y} into LyL_{y} and RyR_{y} in O(n)O(n) time. These lists should remain sorted according to their subscript.
    3. Recurse on the subproblems. That is, dL=Closest(Lx,Ly)d_{L}=\text{Closest}(L_{x},L_{y}) and dR=Closest(Rx,Ry)d_{R}=\text{Closest}(R_{x},R_{y}). Then, d=min(dL,dR)d=\mathrm{min}(d_{L},d_{R}),
    4. Define the middle 2d×d2d\times d region (strip) and conquer. We write the pseudocode here:
      # construct strip, sorted by y
      strip = []
      for p in Py:
          if abs(p.x - pivot.x) < d:
       	   strip.append(p)
      
       # process closest pair candidates
       k = len(strip)
       for i in range(k):
       	for j in range(i + 1, k): # this will run at most 7 times
       		if strip[j][1] - strip[i][1] >= d:
       			break
       		d = min(d, distance(strip[i], strip[j]))
      

Time Complexity Analysis

Preprocessing takes O(nlogn)O(n\log n) time. Partitioning takes O(n)O(n) time. Recursing on the subproblems takes 2T(n/2)2T(n/2) time. Processing the strip takes O(n)O(n) time, with a small constant factor. The recurrence relation is thus

T(n)=2T(n/2)+O(n)T(n)=2T(n/2)+O(n)

By the Master Theorem, T(n)=O(nlogn)T(n)=O(n\log n). Since preprocessing also has O(nlogn)O(n\log n) time complexity, the overall algorithm has O(nlogn)O(n\log n) time complexity!

\square