1D Peak Finding
Objective
Given an array A with n elements, find the index i of the peak element A[i] where A[i] >= A[i - 1]
and A[i] >= A[i + 1]. For elements on the boundaries of the array, the element only needs to be greater than or equal to its lone neighbor to be considered a peak. Or, say A[-1] = A[n] = ∞ .
Algorithm
- Take the middle element fo A, A[ n2 ], and compare that element to its neighbors
- If the middle element is greater than or equal to its neighbors, then by definition, that element is a peak. Reture its index n2
- Else, if the element to the left is greater than the middle element, then recurse and use this algorithm on the left half of the array, not including the middle element.
- Else, the element to the right must be greater than the middle element. Recurse and use this algorithm on the right half of the array, not including the middle element.
Runtime Analysis
T(n) = T( n2 ) + c
T(n) = T( n4 ) + c + c
T(n) = T( n8 ) + c + c + c
T(n) = T( n2k ) + ck
Substitute k = logn
T(n) = T( n2logn ) + c logn
= T(1) + c logn
= O(log n)
L = [4, 8, 5, 6, 9, 10, 13, 4, 5 ,6, 0]
print L[len(L) / 2:]
print L[: len(L) / 2]
print len(L) / 2
L[len(L) / 2]
def peakFindStraight(L):
'''
Find a peak in a straight way
Runtime: O(len(L))
'''
if len(L) == 0:
return None
if len(L) == 1:
return 0
for i in xrange(len(L)):
if i == 0:
if L[i] >= L[i + 1]:
return i
elif i == len(L) - 1:
if L[i] >= L[i - 1]:
return i
else:
if L[i] >= L[i + 1] and L[i] >= L[i - 1]:
return i
print peakFindStraight(L)
def peakFinding(L, low, high):
'''
Peak finding recursive way.
It is similar to binary search.
Runtime: O(log(len(L)))
'''
mid = low + (high - low) / 2
if mid == 0 or mid == len(L) - 1 or (L[mid] >= L[mid - 1] and L[mid] >= L[mid + 1]):
return mid
elif L[mid - 1] > L[mid]:
return peakFinding(L, low, mid - 1)
else:
return peakFinding(L, mid + 1, high)
print L
peakindex = peakFinding(L, 0, len(L) - 1)
print L[peakindex]
2D Peak Finding
Objective
Given an n*n matrix M, find the indices of a peak element M[i][j] where the element is greater
than or equal to its neighbors, M[i + 1][j], M[i - 1][j], M[i][j + 1], and M[i][j - 1]. For elements
on the boundaries of the matrix, the element only needs to be greater than or equal to the neighbors
it has to be considered a peak.
Algorithm 2D:
- Pick middle column j = m / 2
- Find global maximum on column j at (i, j)
- Compare (i, j - 1), (i, j), (i, j + 1)
- Pick left columns of (i, j - 1) > (i, j)
- Similarly for right
- (i, j) is a 2D-peak if neither condition holds <- WHY
- Solve the new problem with half the number of columns.
- When you have a single column, find global maximum and you're done.
The complexity is:
If T(n, m) denotes work required to solve problem with n rows and m columns
T(n, m) = T(n, m/2) + Θ(n) (to find global maximum on a column \-- (n rows))
T(n, m) = Θ(n)+⋯+Θ(n)logm
= Θ(n log m ) = Θ(n log n ) if m = n
problemMatrix = [
[ 4, 5, 6, 7, 8, 7, 6, 5, 4, 3, 2],
[ 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 3],
[ 6, 7, 8, 9, 10, 9, 8, 7, 6, 5, 4],
[ 7, 8, 9, 10, 11, 10, 9, 8, 7, 6, 5],
[ 8, 9, 10, 11, 12, 11, 10, 9, 8, 7, 6],
[ 7, 8, 9, 10, 11, 10, 9, 8, 7, 6, 5],
[ 6, 7, 8, 9, 10, 9, 8, 7, 6, 5, 4],
[ 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 3],
[ 4, 5, 6, 7, 8, 7, 6, 5, 4, 3, 2],
[ 3, 4, 5, 6, 7, 6, 5, 4, 3, 2, 1],
[ 2, 3, 4, 5, 6, 5, 4, 3, 2, 1, 0]
]
def getDimensions(array):
'''
Gets the dimensions for a two-dimensional array
Runtime: O(len(array))
'''
rows = len(array)
cols = 0
for row in array:
if len(row) > cols:
cols = len(row)
return (rows, cols)
#test getDimensions
getDimensions(problemMatrix)
class PeakProblem(object):
'''
A class representing an instance of a peak-finding problem
'''
def __init__(self, array, bounds):
'''
A method for initializing an instance of the PeakProblem class.
Takes an array and an argument indicating which rows to include.
Runtime: O(1)
'''
(startRow, startCol, numRow, numCol) = bounds
self.array = array
self.bounds = bounds
self.startRow = startRow
self.startCol = startCol
self.numRow = numRow
self.numCol = numCol
def get(self, location):
'''
Returns the value of the array at the given location, offset by
the coordinates (startRow, startCol).
Runtime: O(1)
'''
(r, c) = location
if not (0 <= r and r < self.numRow):
return 0
if not (0 <= c and c < self.numCol):
return 0
return self.array[self.startRow + r][self.startCol + c]
def getBetterNeighbor(self, location):
'''
If (r, c) has a better neighbor, return the neighbor. Otherwise,
return the location (r, c)
'''
(r, c) = location
best = location
if r - 1 >= 0 and self.get((r-1, c)) > self.get(best):
best = (r - 1, c)
if c - 1 >= 0 and self.get((r, c - 1)) > self.get(best):
best = (r, c - 1)
if r + 1 < self.numRow and self.get((r + 1, c)) > self.get(best):
best = (r + 1, c)
if c + 1 < self.numCol and self.get((r, c + 1)) > self.get(best):
best = (r, c + 1)
return best
def getMaximum(self, locations):
'''
Finds the location in the current problem with the greatest value
'''
(bestLoc, bestVal) = (None, 0)
for loc in locations:
if bestLoc is None or self.get(loc) > bestVal:
(bestLoc, bestVal) = (loc, self.get(loc))
return bestLoc
def isPeak(self, location):
'''
Returns true if the given location is a peak in the current subproblem.
'''
return (self.getBetterNeighbor(location) == location)
def getSubproblem(self, bounds):
'''
Returns a subproblem with the given bounds. The bounds is a quadruple
of numbers: (starting row, starting column, # of rows, # of columns).
'''
(sRow, sCol, nRow, nCol) = bounds
newBounds = (self.startRow + sRow, self.startCol + sCol, nRow, nCol)
return PeakProblem(self.array, newBounds)
def getSubproblemContaining(self, boundList, location):
'''
Returns the subproblem containing the given location. Picks the first
of the subproblems in the list which satisfies that constraint, and
then constructs the subproblem using getSubproblem().
Runtime: O(len(boundList))
'''
(row, col) = location
for (sRow, sCol, nRow, nCol) in boundList:
if sRow <= row and row < sRow + nRow:
if sCol <= col and col < sCol + nCol:
return self.getSubproblem((sRow, sCol, nRow, nCol))
# shouldn't reach here
return self
def getLocationInSelf(self, problem, location):
'''
Remaps the location in the given problem to the same location in
the problem that this function is being called from.
Runtime: O(1)
'''
(row, col) = location
newRow = row + problem.startRow - self.startRow
newCol = col + problem.startCol - self.startCol
return (newRow, newCol)
def printPeakProblem(self):
'''
Print the two-dimensional array
Runtime: O(self.numRow * self.numCol)
'''
for i in range(self.startRow, self.numRow):
for j in range(self.startCol, self.numCol):
print '{:4}'.format(self.get((i, j))),
print
def createProblem(array):
'''
Constructs an instance of the PeakProblem object for the given array,
using bounds derived from the array using the getDimensions function.
Runtime: O(len(arry))
'''
(rows, cols) = getDimensions(array)
return PeakProblem(array, (0, 0, rows, cols))
pb = createProblem(problemMatrix)
print pb.startRow
pb.printPeakProblem()
def crossProduct(list1, list2):
'''
Returns all pairs with one item from the first list and one item from
the second list. (Cartesian product of the two lists)
The code is equivalent to the following list comprehension:
return [(a, b) for a in list1 for b in list2]
but for easier reading and analysis, we have included more explicit code.
'''
answer = []
for a in list1:
for b in list2:
answer.append((a, b))
return answer
mid = pb.numCol // 2
divider = crossProduct(range(pb.numRow), [mid])
print divider
bestLoc = pb.getMaximum(divider)
bestLoc
def algorithm1(problem):
# if it's empty, we're done
if problem.numRow <= 0 or problem.numCol <= 0:
return None
# the recursive subproblem will involve half the number of columns
mid = problem.numCol // 2
# information about the two subproblems
(subStartR, subNumR) = (0, problem.numRow)
(subStartC1, subNumC1) = (0, mid)
(subStartC2, subNumC2) = (mid + 1, problem.numCol - (mid + 1))
subproblems = []
subproblems.append((subStartR, subStartC1, subNumR, subNumC1))
subproblems.append((subStartR, subStartC2, subNumR, subNumC2))
# get a list of all locations in the dividing column
divider = crossProduct(range(problem.numRow), [mid])
# find the maximum in the dividing column
bestLoc = problem.getMaximum(divider)
# see if the maximum value we found on the dividing line has a better
# neighbor (which can't be on the dividing line, because we know that
# this location is the best on the dividing line)
neighbor = problem.getBetterNeighbor(bestLoc)
# this is a peak, so return it
if neighbor == bestLoc:
return bestLoc
# otherwise, figure out which subproblem contains the neighbor, and
# recurse in that half
sub = problem.getSubproblemContaining(subproblems, neighbor)
result = algorithm1(sub)
return problem.getLocationInSelf(sub, result)
# test algorithm1
peak = algorithm1(pb)
if pb.isPeak(peak):
print(str(peak) + " => is a peak")
def algorithm2(problem, location = (0, 0)):
# if it's empty, we're done
if problem.numRow <= 0 or problem.numCol <= 0:
return None
nextLocation = problem.getBetterNeighbor(location)
if nextLocation == location:
# there is no better neighbor, so return this peak
return location
else:
# there is a better neighbor, so move to the neighbor and recurse
return algorithm2(problem, nextLocation)
# test algorithm2
peak2 = algorithm2(pb)
if pb.isPeak(peak2):
print(str(peak2) + " => is a peak")
def algorithm4(problem, bestSeen = None, rowSplit = True):
# if it's empty, we're done
if problem.numRow <= 0 or problem.numCol <= 0:
return None
subproblems = []
divider = []
if rowSplit:
# the recursive subproblem will involve half the number of rows
mid = problem.numRow // 2
# information about the two subproblems
(subStartR1, subNumR1) = (0, mid)
(subStartR2, subNumR2) = (mid + 1, problem.numRow - (mid + 1))
(subStartC, subNumC) = (0, problem.numCol)
subproblems.append((subStartR1, subStartC, subNumR1, subNumC))
subproblems.append((subStartR2, subStartC, subNumR2, subNumC))
# get a list of all locations in the dividing column
divider = crossProduct([mid], range(problem.numCol))
else:
# the recursive subproblem will involve half the number of columns
mid = problem.numCol // 2
# information about the two subproblems
(subStartR, subNumR) = (0, problem.numRow)
(subStartC1, subNumC1) = (0, mid)
(subStartC2, subNumC2) = (mid + 1, problem.numCol - (mid + 1))
subproblems.append((subStartR, subStartC1, subNumR, subNumC1))
subproblems.append((subStartR, subStartC2, subNumR, subNumC2))
# get a list of all locations in the dividing column
divider = crossProduct(range(problem.numRow), [mid])
# find the maximum in the dividing row or column
bestLoc = problem.getMaximum(divider)
neighbor = problem.getBetterNeighbor(bestLoc)
# update the best we've seen so far based on this new maximum
if bestSeen is None or problem.get(neighbor) > problem.get(bestSeen):
bestSeen = neighbor
# return when we know we've found a peak
if neighbor == bestLoc and problem.get(bestLoc) >= problem.get(bestSeen):
return bestLoc
# figure out which subproblem contains the largest number we've seen so far,
# and recurse, alternating between splitting on rows and splitting on columns.
sub = problem.getSubproblemContaining(subproblems, bestSeen)
newBest = sub.getLocationInSelf(problem, bestSeen)
result = algorithm4(sub, newBest, not rowSplit)
return problem.getLocationInSelf(sub, result)
# test algorithm4
peak4 = algorithm4(pb)
if pb.isPeak(peak4):
print(str(peak4) + " => is a peak")