Decrease and Conquer PDF
Decrease and Conquer PDF
Want to do even better than linear complexity? Decrease and conquer re-
duces one problem into one smaller subproblem only, and the most common
case is to reduce the state space into half of its original size. If the combining
step takes only constant time, we get an elegant recurrence relation as:
0.1 Introduction
All the searching we have discussed before never assumed any ordering be-
tween the items, and searching an item in an unordered space is doomed to
have a time complexity linear to the space size. This case is about to change
in this chapter.
Think about these two questions: What if we have a sorted list instead
of an arbitrary one? What if the parent and children nodes within a tree
are ordered in some way? With such special ordering between items in a
data structures, can we increase its searching efficiency and be better than
the blind one by one search in the state space? The answer is YES.
Let’s take advantage of the ordering and the decrease and conquer method-
ology. To find a target in a space of size n, we first divide it into two sub-
spaces and each of size n/2, say from the middle of the array. If the array is
1
2 0. DECREASE AND CONQUER
increasingly ordered, all items in the left subspace are smaller than all items
in the right subspace. If we compare our target with the item in the middle,
we will know if this target is on the left or right side. With just one step,
we reduced our state space by half size. We further repeat this process on
the reduced space until we find the target. This process is called Binary
Search. Binary search has recurrence relation:
Find the Exact Target This is the most basic application of binary
search. We can set two pointers, l and r, which points to the first and
last position, respectively. Each time we compute the middle position m =
(l+r)//2, and check if the item num[m] is equal to the target t.
• If it is smaller than the target, move to the left half by setting the right
pointer to the position right before the middle position, r = m − 1.
• If it is larger than the target, move to the right half by setting the left
pointer to the position right after the middle position, l = m + 1.
0.2. BINARY SEARCH 3
Repeat the process until we find the target or we have searched the whole
space. The criterion of finishing the whole space is when l starts to be larger
than r. Therefore, in the implementation we use a while loop with condition
l≤ r to make sure we only scan once of the searching space. The process
of applying binary search on our exemplary array is depicted in Fig. 1 and
the Python code is given as:
1 def standard_binary_search ( l s t , t a r g e t ) :
2 l , r = 0 , len ( l s t ) − 1
3 w h i l e l <= r :
4 mid = l + ( r − l ) // 2
5 i f l s t [ mid ] == t a r g e t :
6 r e t u r n mid
7 e l i f l s t [ mid ] < t a r g e t :
8 l = mid + 1
9 else :
10 r = mid − 1
11 r e t u r n −1 # t a r g e t i s not found
Applying the first standard binary search will return 3 as the target position,
which is the second 4 in the array. This does not seem like a problem at
first. However, what if you want to know the predecessor or successor (3 or
5) of this target? In a distinct array, the predecessor and successor would
be adjacent to the target. However, when the target has duplicates, the
predecessor is before the first target and the successor is next to the last
target. Therefore, returning an arbitrary one will not be helpful.
Another case, what if our target is 6, and we first want to see if it exists
in the array. If it does not, we would like to insert it into the array and still
keep the array sorted. The above implementation simply returns −1, which
is not helpful at all.
The lower and upper bound of a binary search are the lowest and
highest position where the value could be inserted without breaking the
ordering.
4 0. DECREASE AND CONQUER
• With index 2 as the lower bound, items in i ∈ [0, l−1], a[i] < t, a[l] = t,
and i ∈ [l, n), a[i] ≥ t. A lower bound is also the first position that has
a value v ≥ t. This case is shown in Fig. 2.
• With the upper bound, items in i ∈ [0, u − 1], a[i] ≤ t, a[u] = t, and
i ∈ [u, n), a[i] > t. An upper bound is also the first position that has
a value v > t. This case is shown in Fig. 3.
Figure 4: Binary Search: Lower and Upper Bound of target 5 is the same.
0.2. BINARY SEARCH 5
right side of the state space, making l = mid+1 = 6. Now, in the right state
space, the middle pointer will always have values larger than 4, thus it will
only moves to the left side of the space, which only changes the right pointer
r and leaves the left pointer l touched when the program ends. Therefore, l
will still return our final upper bound index. The Python code is as follows:
1 d e f upper_bound_bs ( nums , t ) :
2 l , r = 0 , l e n ( nums ) − 1
3 w h i l e l <= r :
4 mid = l + ( r − l ) // 2
5 i f t >= nums [ mid ] : # move a s r i g h t a s p o s s i b l e
6 l = mid + 1
7 else :
8 r = mid − 1
9 return l
Bonus For the lower bound, if we return the position as l-1, then we get
the last position that value < target. Similarily, for the upper bound, we
0.2. BINARY SEARCH 7
0.2.2 Applications
Binary Search is a powerful problem solving tool. Let’s go beyond the sorted
array, “How about the array is sorted in someway but not as monotonic as
what we have seen before?” “How about solving continuous or discrete
function, whether it is equation or inequation?”
c a l l i s B a d V e r s i o n ( 3 ) −> f a l s e
c a l l i s B a d V e r s i o n ( 5 ) −> t r u e
c a l l i s B a d V e r s i o n ( 4 ) −> t r u e
Then 4 i s t h e f i r s t bad v e r s i o n .
Analysis and Design In this case, we have a search space in range [1, n].
Think the value at each position is the result from function isBadVersion(i).
Assume the first bad version is at position b, then the values from the po-
sitions are of such pattern: [F,..., F, ..., F, T, ..., T]. We can totally apply
the binary search in the search space [1, n]: to find the first bad version is
the same as finding the first position that we can insert a value True–the
lower bound of value True. Therefore, whenever the value we find is True,
we move to the left space to try to get its first location. The Python code
is given below:
1 def firstBadVersion (n) :
2 l , r = 1, n
3 w h i l e l <= r :
4 mid = l + ( r − l ) // 2
5 i f i s B a d V e r s i o n ( mid ) :
6 r = mid − 1
7 else :
8 l = mid + 1
9 return l
8 0. DECREASE AND CONQUER
target = 8
Output : −1
Analysis and Design In the rotated sorted array, the array is not purely
monotonic. Instead, there will be at most one drop in the array because of
the rotation, which we denote the high and the low item as ah , al respectively.
This drop cuts the array into two parts: a[0 : h + 1] and a[l : n], and both
parts are ascending sorted. If the middle point falls within the left part, the
left side of the state space will be sorted, and if it falls within the right part,
the right side of the state space will be sorted. Therefore, at any situation,
there will always be one side of the state space that is sorted. To check
which side is sorted, simply compare the value of middle pointer with that
of left pointer.
• Otherwise when they equal to each other, which is only possible that
there is no left part left, we have to move to the right part. For
example, when nums=[1, 3], we move to the right part.
With a sorted half of state space, we can check if our target is within the
sorted half: if it is, we switch the state space to the sorted space; otherwise,
we have to move to the other half that is unknown. The Python code is
shown as:
1 d e f R o t a t e d B i n a r y S e a r c h ( nums , t ) :
2 l , r = 0 , l e n ( nums )−1
3 w h i l e l <= r :
4 mid = l + ( r−l ) //2
0.2. BINARY SEARCH 9
5 i f nums [ mid ] == t :
6 r e t u r n mid
7 # Left i s sorted
8 i f nums [ l ] < nums [ mid ] :
9 i f nums [ l ] <= t < nums [ mid ] :
10 r = mid − 1
11 else :
12 l = mid + 1
13 # Right i s s o r t e d
14 e l i f nums [ l ] > nums [ mid ] :
15 i f nums [ mid ] < t <= nums [ r ] :
16 l = mid + 1
17 else :
18 r = mid − 1
19 # L e f t and middle i n d e x i s t h e same , move t o t h e r i g h t
20 else :
21 l = mid + 1
22 r e t u r n −1
Arranging Coins (L441, easy) You have a total of n coins that you
want to form in a staircase shape, where every k-th row must have exactly
k coins. Given n, find the total number of full staircase rows that can be
formed. n is a non-negative integer and fits within the range of a 32-bit
signed integer.
Example 1 :
n = 5
The c o i n s can form t h e f o l l o w i n g rows :
∗
∗ ∗
∗ ∗
Because t h e 3 rd row i s i n c o m p l e t e , we r e t u r n 2 .
Analysis and Design Each row x has x coins, summing it up, we get
1 + 2 + ... + x = x(x+1)
2 . The problem is equvalent to find the last integer x
x(x+1)
that makes 2 ≤ n. Of course, this is just a quadratic equation which
can be easily solved if you remember the formula, such as the following
Python code:
1 import math
2 d e f a r r a n g e C o i n s ( n : i n t ) −> i n t :
3 r e t u r n i n t ( ( math . s q r t (1+8∗n ) −1) // 2 )
12 else :
13 r = mid − 1
14 return l
15 return bisect_right () − 1
With l and r to represent the left and right child of node x, there are
two other definitions other than the binary search tree definition we just in-
troduced: (1)l.key ≤ x.key < r.key and (2) l.key < x.key ≤ r.key. In these
two cases, our resulting BSTs allows us to have duplicates. The exemplary
implementation follow the definition that does not allow duplicates.
0.3.1 Operations
In order to build a BST, we need to insert a series of items in the tree
organized by the search tree property. And in order to insert, we need
to search for a proper position first and then insert the new item while
sustaining the search tree property. Thus, we introduce these operations in
the order of search, insert and generate.
Search The search is highly similar to the binary search in the array. It
starts from the root. Unless the node’s value equals to the target, the search
proceeds to either the left or right child depending upon the comparison
result. The search process terminates when either the target is found or
when an empty node is reached. It can be implemented either recursively or
iteratively with a time complexity O(h), where h is the height of the tree,
which is roughly log n is the tree is balanced enough. The recursive search
is shown as:
1 def search ( root , t ) :
2 i f not r o o t :
3 r e t u r n None
4 i f r o o t . v a l == t :
5 return root
6 e l i f t < root . val :
7 return search ( root . l e f t , t )
8 else :
9 return search ( root . right , t )
Figure 6: The red colored path from the root down to the position where
the key 9 is inserted. The dashed line indicates the link in the tree that is
added to insert the item.
Insert Assuming we are inserting a node with key 9 into the tree shown
in Fig 5. We start from the root, compare 9 with 8, and goes to node 10.
Next, the search process will lead us to the left child of node 10, and this is
where we should put node 9. The process is shown in Fig. 6.
The process itself is easy and clean. Here comes to the implementation.
We treat each node as a subtree: whenever the search goes into that node,
then the algorithm hands over the insertion task totally to that node, and
assume it has inserted the new node and return its updated node. The
main program will just simply reset its left or right child with the return
value from its children. The insertion of new node happens when the search
hits an empty node, it returns a new node with the target value. The
implementation is given as:
1 def i n s e r t ( root , t ) :
2 i f not r o o t :
3 r e t u r n BiNode ( t )
4 i f r o o t . v a l == t :
5 return root
6 e l i f t < root . val :
7 root . l e f t = i n s e r t ( root . l e f t , t )
8 return root
14 0. DECREASE AND CONQUER
9 else :
10 root . right = i n s e r t ( root . right , t )
11 return root
1. When the parent node is None, which means the tree is empty. We
assign the root node with the a new node of the target value.
2. When the target’s value is larger than the parent node’s, the put a
new node as the right child of the parent node.
3. When the target’s value is smaller than the parent node’s, the put a
new node as the left child of the parent node.
Find the Minimum and Maximum Key Because the minimum key is
the leftmost node within the tree, the search process will always traverse to
the left subtree and return the last non-empty node, which is our minimum
node. The time complexity is the same as of searching any key, which is
O(log n).
1 d e f minimum ( r o o t ) :
2 i f not r o o t :
3 r e t u r n None
4 i f not r o o t . l e f t :
5 return root
6 r e t u r n minimum ( r o o t . l e f t )
To find the maximum node, replacing left with right will do. Also, some-
times we need to search two additional items related to a given node: suc-
cessor and predecessor. The structure of a binary search tree allows us to
determine the successor or the predecessor of a tree without ever comparing
keys.
Let us try something else. In the BST shown in Fig. 6, the node 3’s
successor will be node 4. For node 4, its successor will be node 6. For node
7, its successor is node 8. What are the cases here?
• An easy case is when a node has right subtree, its successor is the
minimum node within its right subtree.
• However, if a node does not have a right subtree, there are two more
cases:
The above two rules can be merged as: starting from the target node,
traverse backward to check its parent, find the first two nodes which
are in left child–parent relation. The parent node in that relation will
be our targeting successor. Because the left subtree is always smaller
than a node, when we backward, if a node is smaller than its parent,
it tells us that the current node is smaller than that parent node too.
We write three functions to implement the successor:
• Function findNodeAddParent will find the target node and add a
parent node to each node along the searching that points to their
parents. The Code is as:
1 d e f findNodeAddParent ( r o o t , t ) :
2 i f not r o o t :
3 r e t u r n None
4 i f t == r o o t . v a l :
5 return root
6 e l i f t < root . val :
7 root . l e f t . p = root
8 r e t u r n findNodeAddParent ( r o o t . l e f t , t )
9 else :
10 root . right . p = root
11 r e t u r n findNodeAddParent ( r o o t . r i g h t , t )
• Function reverse will find the first left-parent relation when traverse
backward from a node to its parent.
1 d e f r e v e r s e ( node ) :
2 i f not node o r not node . p :
3 r e t u r n None
4 # node i s a l e f t c h i l d
0.3. BINARY SEARCH TREE 17
The expected time complexity is O(log n). And the worst is when the tree
line up and has no branch, which makes it O(n). Similarily, we can use
inorder traversal:
18 0. DECREASE AND CONQUER
1 d e f p r e d e c e s s o r I n o r d e r ( r o o t , node ) :
2 i f not node :
3 r e t u r n None
4 i f node . l e f t i s not None :
5 r e t u r n maximum( node . l e f t )
6 # Inorder traversal
7 pred = None
8 while root :
9 i f node . v a l > r o o t . v a l :
10 pred = r o o t
11 root = root . right
12 e l i f node . v a l < r o o t . v a l :
13 root = root . l e f t
14 else :
15 break
16 r e t u r n pred
2. Node to be deleted has only one child: Copy the child to the node and
delete the child. For example, to delete node 14, we need to copy node
13 to node 14.
Next, we implement the above three cases in function _delete when a delet-
ing node is given, which will return a processed subtree deleting its root
node.
0.3. BINARY SEARCH TREE 19
Finally, we call the above two function to delete a node with a target key.
1 def d e l e t e ( root , t ) :
2 i f not r o o t :
3 return
4 i f r o o t . v a l == t :
5 root = _delete ( root )
6 return root
7 e l i f t > root . val :
8 root . right = delete ( root . right , t )
9 return root
10 else :
11 root . l e f t = delete ( root . l e f t , t )
12 return root
If we use any of the other two definitions we introduced that allows dupli-
cates, things can be more complicated. For example, if we use the definition
x.lef t.key <= x.key < x.right.key, we will end up with a tree looks like
Fig. 7:
20 0. DECREASE AND CONQUER
Note that the duplicates are not in contiguous levels. This is a big issue
when allowing duplicates in a BST representation as, because duplicates may
be separated by any number of levels, making the detection of duplicates
difficult.
An option to avoid this issue is to not represent duplicates structurally
(as separate nodes) but instead use a counter that counts the number of
occurrences of the key. The previous example will be represented as in Fig. 8:
This simplifies the related operations at the expense of some extra bytes
and counter operations.
To get the answer for range query [0, 5], we just return the value at root
node. If the range is [0, 1], which is on the left side of the tree, we go to the
left branch, and cutting half of the search space. For a range that happens
to be between two nodes, such as [1, 3], which needs node [0, 1] and [2-5],
we search [0, 1] in the left subtree and [2, 3] in the right subtree and combine
them together. Any searching will be within O(log n), relating to the height
of the tree. needs better complexity analysis
Segment tree The above binary tree is called segment tree. From our
analysis, we can see a segment tree is a static full binary trees. ’Static‘ here
means once the data structure is built, it can not be modified or extended.
However, it can still update the value in the original array into the segment
tree. Segment tree is applied widely to efficiently answer numerous dynamic
range queries problems (in logarithmic time), such as finding minimum,
maximum, sum, greatest common divisor, and least common denominator
in array.
Consider an array A of size n and a corresponding segment tree T :
22 0. DECREASE AND CONQUER
4. If the parent node is in range [i, j], then we separate this range at the
middle position m = (i + j)//2; the left child takes range [i, m], and
the right child take the interval of [m + 1, j].
Because in each step of building the segment tree, the interval is divided
into two halves, so the height of the segment tree will be log n. And there
will be totally n leaves and n − 1 number of internal nodes, which makes the
total number of nodes in segment tree to be 2n − 1, which indicates a linear
space cost. Except of an explicit tree can be used to implement segment
tree, an implicit tree implemented with array can be used too, similar to the
case of heap data structure.
0.4.1 Implementation
Implementation of a functional segment tree consists of three core oper-
ations: tree construction, range query, and value update, named as as
_buildSegmentTree(), RangeQuery(), and update(), respectively. We
demonstrate the implementation with Range Sum Query (RSQ) problem,
but we try to generalize the process so that the template can be easily reused
to other range query problems. In our implementation, we use explicit tree
data structure for both convenience and easier to understand. We define a
general tree node data structure as:
1 c l a s s TreeNode :
2 d e f __init__ ( s e l f , v a l , s , e ) :
3 s e l f . val = val
4 self . s = s
5 self .e = e
6 s e l f . l e f t = None
7 s e l f . r i g h t = None
Given nums = [ 2 , 9 , 4 , 5 , 8 , 7 ]
sumRange ( 0 , 2 ) −> 15
update ( 1 , 3 )
sumRange ( 0 , 2 ) −> 9
0.4. SEGMENT TREE 23
Range Query Each query within range [i, j], i < j, i ≥ s, j ≤ e, will be
found on a node or by combining multiple node. In the query process, check
24 0. DECREASE AND CONQUER
• If range [i, j] matches the range [s, e], if it matches, return the value
of the node, otherwise, processed to other cases.
– For the first two cases, a recursive call on that branch will return
our result.
– For the third case, where the range crosses two space, two re-
cursive calls on both children of our current node are needed:
the left one handles range [i, m], and the right one handles range
[m + 1, j]. The final result will be a combination of these two.
Update To update nums[1]=3, all nodes on the path from root to the
leaf node will be affected and needed to be updated with to incorporate the
change at the leaf node. We search through the tree with a range [1, 1] just
like we did within _rangeQuery except that we no longer need the case of
crossing two ranges. Once we reach to the leaf node, we update that node’s
value to the new value, and it backtracks to its parents where we recompute
the parent node’s value according to the result of its children. This operation
takes O(log n) time complexity, and we can do it inplace since the structure
of the tree is not changed.
1 d e f _update ( r o o t , s , e , i , v a l ) :
2 i f s == e == i :
3 root . val = val
4 return
5 m = ( s + e ) // 2
6 i f i <= m:
7 _update ( r o o t . l e f t , s , m, i , v a l )
8 else :
9 _update ( r o o t . r i g h t , m + 1 , e , i , v a l )
0.5. EXERCISES 25
0.5 Exercises
1. 144. Binary Tree Preorder Traversal
1 d e f rangeSumBST ( s e l f , r o o t , L , R) :
2 i f not r o o t :
3 return 0
4 i f L <= r o o t . v a l <= R:
5 r e t u r n s e l f . rangeSumBST ( r o o t . l e f t , L , R) + s e l f .
rangeSumBST ( r o o t . r i g h t , L , R) + r o o t . v a l
6 e l i f r o o t . v a l < L : #l e f t i s not needed
7 r e t u r n s e l f . rangeSumBST ( r o o t . r i g h t , L , R)
8 e l s e : # r i g h t s u b t r e e i s not needed
9 r e t u r n s e l f . rangeSumBST ( r o o t . l e f t , L , R)
0.5.1 Exercises
0.1 35. Search Insert Position (easy). Given a sorted array and a
target value, return the index if the target is found. If not, return the
index where it would be if it were inserted in order.
You can assume that there are no duplicates in the array.
Example 1 :
Input : [ 1 , 3 , 5 , 6 ] , 5
Output : 2
Example 2 :
Input : [ 1 , 3 , 5 , 6 ] , 2
Output : 1
Example 3 :
Input : [ 1 , 3 , 5 , 6 ] , 7
Output : 4
Example 4 :
Input : [ 1 , 3 , 5 , 6 ] , 0
Output : 0
7 l = mid+1
8 e l i f nums [ mid ] > t a r g e t : #move t o t h e l e f t s i d e ,
not mid−1
9 r= mid
10 e l s e : #found t h e t r a g e t
11 r e t u r n mid
12 #where t h e p o s i t i o n s h o u l d go
13 return l
1 # inclusive version
2 d e f s e a r c h I n s e r t ( s e l f , nums , t a r g e t ) :
3 l = 0
4 r = l e n ( nums )−1
5 w h i l e l <= r :
6 m = ( l+r ) //2
7 i f t a r g e t > nums [m] : #s e a r c h t h e r i g h t h a l f
8 l = m+1
9 e l i f t a r g e t < nums [m] : # s e a r c h f o r t h e l e f t h a l f
10 r = m−1
11 else :
12 return m
13 return l
For example ,
C o n s i d e r t h e f o l l o w i n g matrix :
[
[1 , 3 , 5 , 7] ,
[ 1 0 , 11 , 16 , 2 0 ] ,
[ 2 3 , 30 , 34 , 50]
]
Given t a r g e t = 3 , r e t u r n t r u e .
1 class Solution :
2 d e f s e a r c h M a t r i x ( s e l f , matrix , t a r g e t ) :
3 i f not matrix o r t a r g e t i s None :
4 return False
5
6 rows , c o l s = l e n ( matrix ) , l e n ( matrix [ 0 ] )
7 low , h i g h = 0 , rows ∗ c o l s − 1
8
9 w h i l e low <= h i g h :
10 mid = ( low + h i g h ) / 2
11 num = matrix [ mid / c o l s ] [ mid % c o l s ]
12
13 i f num == t a r g e t :
14 r e t u r n True
15 e l i f num < t a r g e t :
16 low = mid + 1
17 else :
18 h i g h = mid − 1
19
20 return False
2. 153. Find Minimum in Rotated Sorted Array (medium) The key here
is to compare the mid with left side, if mid-1 has a larger value, then
that is the minimum