dis02-sol
dis02-sol
dis02-sol
Garg
Note: Your TA probably will not cover all the problems. This is totally fine, the discussion worksheets
are deliberately made long so they can serve as a resource you can use to practice, reinforce, and build
upon concepts discussed in lecture, readings, and the homework.
j
X
max A[k]
i≤j
k=i
For example, the maximum subarray sum of [−2, 1, −3, 4, −1, 2, 1, −5, 4] is 6, the sum of the contiguous
subarray [4, −1, 2, 1].
(a) Design an O(n log n)-time divide-and-conquer algorithm that finds the maximum subarray sum.
Hint: Split the array into two equally-sized pieces. What are the types of possibilities for the
max subarray, and how does this apply if we want to use divide and conquer?
Solution:
(a) Main idea: At first glance it seems like we have to find two indices i an j such that the
subarray A[i : j] has sum as large as possible. There are n2 possibilities. However, there is a
divide and conquer solution: split A into two equal pieces , L and R, defined as A[1 : n/2] and
A[n/2 + 1 : n]. There are two options for the subarray with the largest sum:
(i) It is contained entirely in L or R: Solve by recursion. Let s1 be the result of recursively
calling the algorithm on L, and s2 be the result of recursively calling the algorithm on R.
(ii) It “crosses the boundary”, i.e. starts in L and ends in R: if the maximum subarray
runs from A[i : j], then A[i, n/2] must be the maximum subarray ending at A[n/2], and
A[n/2 + 1, j] must be the maximum subarray starting at A[n/2 + 1]. It might seem like
we must now solve two largest subarray sum problems: find i such that the subarray sum
A[i, n/2] is as large as possible. And same for A[n/2+1, j]. Note that each of these require
finding just one index i or j for which there are only n/2 possibilities each. So each can be
computed in O(n) steps. e.g. to find i, compute each successive subarray sum A[k, n/2]
for k = 1 to n/2 and take the maximum. Finally, let s3 be the sum of the elements in
A[i, n/2] plus the sum of the elements in A[n/2 + 1, j].
If n = 1, return max{A[1], 0}. Else return max{s1 , s2 , s3 }.
(b) Proof of correctness: We prove by induction. When the array is length 1, our algorithm is
clearly correct.
When the array is length at least 2, there are two options for the subarray with the largest sum:
(i) It is contained entirely in L or R.
(ii) It “crosses the boundary”, starts before element n/2 and ends after element n/2.
In the first case, either s1 or s2 is outputted by the algorithm, which we inductively assume is
correct.
To handle the second case, the maximum subarray sum crossing the boundary must consist of
the subarray with the largest sum ending in element n/2, and the subarray with the largest
sum beginning with element n/2 + 1. So s3 will be outputted by the algorithm in this case,
which is correct.
(c) Runtime analysis: Our method for computing s3 takes O(n) time, since each of the sums of
A[i : n/2] and A[n/2 + 1 : i] is computed in O(1) time given a previous sum. So we get the
recurrence:
Using the Master theorem, this results in a runtime of O(n log n).
2 Pareto Optimality
Given a set of points P = {(x1 , y1 ), (x2 , y2 ) . . . (xn , yn )}, a point (xi , yi ) ∈ P is Pareto-optimal if there
does not exist any j ̸= i such that such that xj > xi and yj > yi . In other words, there is no point in
P above and to the right of (xi , yi ).
(a) Design a O(n log n)-time divide-and-conquer algorithm that given P , outputs all Pareto-optimal
points in P .
Hint: Split the array by x-coordinate. Show that all points returned by one of the two recursive
calls is Pareto-optimal, and that you can get rid of all non-Pareto-optimal points in the other
recursive call in linear time.
Solution:
Algorithm: Let L be the left half of the points when sorted by x-coordinate, and R be the right
half. Recurse on L and R, let L′ , R′ be the sets of Pareto-optimal points returned. We can compute
ymax , the largest y-coordinate in R, in a linear scan, and then remove all points in L′ with a smaller
y-coordinate. We then return the union of L′ , R′ .
Proof: We now prove the correctness of the algorithm on a sorted array of points. Note that in the
simplest case, there is only one point, which is by default Pareto-optimal. Now, say that there are
more than one points. Then, we can assume that L′ contains the points that are Pareto-optimal in
the set L and R′ contains the points that are Pareto-optimal in the set R. Every point in R′ is Pareto-
optimal in L ∪ R, since all points in L have smaller x-coordinates and can’t violate Pareto-optimality
of points in R′ . For each point in L′ , it’s Pareto-optimal in L ∪ R iff its y-coordinate is larger than
ymax , the largest y-coordinate in R. Hence, our algorithm returns a set that is Pareto-optimal in the
set L ∪ R.
Runtime: Using the master theorem, we can see this runs in T (n) = 2T (n/2) + O(n) = O(n log n)
time.
3 Counting Inversions
This problem arises in the analysis of rankings. Consider comparing two rankings. One way is to
label the elements (books, movies, etc.) from 1 to k according to one of the rankings, then order these
labels according to the other ranking, and see how many pairs are “out of order”.
We are given a sequence of k distinct numbers n1 , · · · , nk . We say that two indices i < j form an
inversion if ni > nj , that is, if the two elements ni and nj are “out of order.”
(a) Provide a divide and conquer algorithm to determine the number of inversions in the sequence
n1 , · · · , nk in time O(k log k).
Hint: Modify merge sort to count during merging. For reference, we provide pseudocode for
merge sort below:
def merge_sort(A):
if len(A) == 1: return A
# perform merge
D = []
while not B.empty() and not C.empty():
if B.empty():
D.extend(C)
else if C.empty():
D.extend(B)
else if B[0] < C[0]:
D.append(B[0])
B.popleft()
else:
D.append(C[0])
C.popleft()
return D
Solution:
4 Monotone matrices
A m-by-n matrix A is monotone if n ≥ m, each row of A has no duplicate entries, and it has the
following property: if the minimum of row i is located at column ji , then j1 < j2 < j3 . . . jm . For
example, the following matrix is monotone (the minimum of each row is bolded):
1 3 4 6 5 2
7 3 2 5 6 4
7 9 6 3 10 0
(a) Give an efficient (i.e., better than O(mn)-time) algorithm that finds the minimum in each row
of an m-by-n monotone matrix A.
Hint: monotonicity suggests that a binary-search-esque approach may be effective.
(c) Analyze the runtime of your algorithm. You do not need to write a formal recurrence relation;
an informal summary is fine.
Challenge: rigorously analyze the runtime via solving a recurrence relation using
the subproblem T (m, n).
Solution:
(i) Main idea: If A has one row, we just scan that row and output its minimum.
Otherwise, we find the smallest entry of the m/2-th row of A by just scanning the row. If this
entry is located at column j, then since A is a monotone matrix, the minimum for all rows above
the m/2-th row must be located to the left of the j-th column. i.e. in the submatrix formed by
rows 1 to m/2 − 1 and columns 1 to j − 1 of A, which we will denote by A[1 : m/2 − 1, 1 : j − 1].
Similarly, the minimum for all rows below the m/2-th row must be located to the right of the
j-th column. So we can recursively call the algorithm on the submatrices A[1 : m/2−1, 1 : j −1]
and A[m/2 + 1 : m, j + 1 : n] to find and output the minima for rows 1 to m/2 − 1 and rows
m/2 + 1 to m.
(ii) Proof of correctness: We will prove correctness by (total) induction on m.
As a base case, m = 1, and the algorithm explicitly finds and outputs the minimum of the single
row.
If A has more than one row, we of course find and output the correct row minimum for row
m/2. As argued above, the minima of rows 1 to m/2 − 1 of A are the same as the minima of the
submatrix A[1 : m/2 − 1, 1 : j − 1], and the minima of rows m/2 + 1 to m are the same as the
minima of the submatrix A[m/2 + 1 : m, j + 1 : n]. By the induction hypothesis, the algorithm
correctly outputs the minima of these matrices, and together with the m/2 row above, they
find and output the minima of all the rows of A.
(iii) Running time analysis: There are two ways to analyze the run time. One involves doing an
explicit accounting of the total number of steps at each level of the recursion, as we did before
we relied on the master theorem, or as we did in the proof of the master theorem. Since m is
halved at each step of recursion, there are log m levels of recursion. At each level of recursion,
the number of columns of the matrix get split into two – those associated with the left matrix
and those associated with the right matrix. Moreover, the number of steps required to perform
the split is just n, since it involves scanning all the entries of a single row. This means that at
any level of the recursion, all the submatrices
P have disjoint columns, meaning if the different
submatrices have nk columns, then k nk ≤ n. The total number P of steps required to split
these matrices to go to the next level of recursion is then just k nk ≤ n. So there are log m
levels of recursion, each taking total time n, for a grand total of n log m.
Actually to be more accurate, when m=1, log m = 0, so the expression should be n(log m + 1)
to get the base case right.
Another way to analyze the running time is by writing and solving a recurrence relation (though
again, this isn’t necessary for full credit). The recurrence is easy to write out: let T (m, n) be
the number of steps to solve the problem for an m × n array. It takes n time to find the
minimum of row m/2. If this row has minimum at column j, we recurse on submatrices of
size at most m/2-by-j and m/2-by-(n − j). So we can write the following recurrence relation:
T (m, n) ≤ T (m/2, j) + T (m/2, n − j) + n.
This does not directly look the recurrences in the master theorem — it is “2-D” since it depends
upon two variables. You need some inspiration to guess the solution. We will guess that
T (m, n) ≤ n(log m + 1). We can prove this by strong induction on m.
Base case: T (1, n) = n = n(log 1 + 1).
Induction step: