Dokumen - Pub Algorithm Design Techniques
Dokumen - Pub Algorithm Design Techniques
Design Techniques
Recursion
Backtracking
Greedy
Divide and Conquer
Dynamic Programming
By
Narasimha Karumanchi
Copyright© 2019 by .
Designed by ℎ ℎ
All rights reserved. No part of this book may be reproduced in any form or by any electronic
or mechanical means, including information storage and retrieval systems, without written
permission from the publisher or author.
Acknowledgements
First and foremost, I want to thank my wife for all of the support she has given through all
of these years of work. Most of the work occurred on weekends, nights, while on vacation,
and other times inconvenient for my family.
ℎ and ℎ ! It is impossible to thank you adequately for everything you have done,
from loving me unconditionally to raising me in a stable household, where your persistent
efforts and traditional values taught your children to celebrate and embrace life. I could
not have asked for better parents or role-models. You showed me that anything is possible
with faith, hard work and determination.
This book would not have been possible without the help of many people. I would like to
express my gratitude to all the people who had provided support, talked things over, read,
wrote, offered comments, allowed me to quote their remarks and assisted in the editing,
proofreading and designing. In particular, I would like to thank the following personalities:
− ℎ ℎ
M-Tech,
Founder, . , .
Preface
Dear Reader,
Please hold on! I know many people typically, who do not read the of a book.
But I strongly recommend that you read this particular Preface.
The study of algorithms and data structures is central to understanding what computer
science is all about. Learning computer science is not unlike learning any other type of
difficult subject matter. The only way to be successful is through deliberate and
incremental exposure to the fundamental ideas. A novice computer scientist needs practice
and thorough understanding before continuing on to the more complexities of the
curriculum. In addition, a beginner needs to be given the opportunity to be successful and
gain confidence. This textbook is designed to serve as a text for a first course on data
structures and algorithms. We looked at a number of data structures and solved classic
problems that arose. The tools and techniques that you learn here will be applied over and
over as you continue your study of computer science.
In all the chapters, you will see more emphasis on problems and analysis rather than on
theory. In each chapter, you will first see the basic required theory followed by various
problems. I have followed a pattern of improving the problem solutions with different
complexities (for each problem, you will find multiple solutions with different, and reduced,
complexities). Basically, it’s an enumeration of possible solutions. With this approach, even
if you get a new question, it will show you a way to think about the possible solutions. You
will find this book useful for interview preparation, competitive exams preparation, and
campus interview preparations.
For many problems, solutions are provided with different levels of complexity. We
started with the solution and slowly moved towards the possible
for that problem. For each problem, we endeavor to understand how much time the
algorithm takes and how much memory the algorithm uses.
It is recommended that the reader does at least one complete reading of this book to gain a
full understanding of all the topics that are covered. Then, in subsequent readings you can
skip directly to any chapter to refer to a specific topic. Even though many readings have
been done for the purpose of correcting errors, there could still be some minor typos in the
book. If any are found, they will be updated at . . . You can monitor this
site for any corrections and also for new problems and solutions. Also, please provide your
valuable suggestions at: @ . .
I wish you all the best and I am confident that you will find this book useful.
: https://github.com/careermonk/algorithm-design-techniques.git
− ℎ ℎ
M-Tech,
Founder, . , .
Table of Contents
0. Organization of Chapters-------------------------------------------------------------------------------- 15
What is this book about? ------------------------------------------------------------------------------------------- 15
Should I buy this book?--------------------------------------------------------------------------------------------- 16
Organization of chapters ------------------------------------------------------------------------------------------- 16
Some prerequisites--------------------------------------------------------------------------------------------------- 18
1. Introduction to Algorithms Analysis ------------------------------------------------------------------ 19
1.1 Variables ---------------------------------------------------------------------------------------------------------- 19
1.2 Data types -------------------------------------------------------------------------------------------------------- 19
1.3 Data structures--------------------------------------------------------------------------------------------------- 20
1.4 Abstract data types (ADTs) ------------------------------------------------------------------------------------ 20
1.5 What is an algorithm? ------------------------------------------------------------------------------------------- 21
1.6 Why the analysis of algorithms? ------------------------------------------------------------------------------- 21
1.7 Goal of the analysis of algorithms ---------------------------------------------------------------------------- 21
1.8 What is running time analysis?--------------------------------------------------------------------------------- 22
1.9 How to compare algorithms ----------------------------------------------------------------------------------- 22
1.10 What is rate of growth? --------------------------------------------------------------------------------------- 22
1.11 Commonly used rates of growth----------------------------------------------------------------------------- 22
1.12 Types of analysis ----------------------------------------------------------------------------------------------- 24
1.13 Asymptotic notation------------------------------------------------------------------------------------------- 24
1.14 Big-O notation -------------------------------------------------------------------------------------------------24
1.15 Omega-Ω notation--------------------------------------------------------------------------------------------- 26
1.16 Theta- notation ---------------------------------------------------------------------------------------------- 27
1.17 Why is it called asymptotic analysis? ------------------------------------------------------------------------ 28
1.18 Guidelines for asymptotic analysis -------------------------------------------------------------------------- 28
1.19 Simplifying properties of asymptotic notations------------------------------------------------------------ 30
1.20 Commonly used logarithms and summations ------------------------------------------------------------- 30
1.21 Master theorem for divide and conquer recurrences ----------------------------------------------------- 30
1.22 Master theorem for subtract and conquer recurrences --------------------------------------------------- 31
1.23 Variant of subtraction and conquer master theorem ----------------------------------------------------- 31
1.24 Method of guessing and confirming ------------------------------------------------------------------------ 31
1.27 Amortized analysis --------------------------------------------------------------------------------------------- 33
1.28 Algorithms analysis: problems and solutions -------------------------------------------------------------- 34
1.29 Celebrity problem ---------------------------------------------------------------------------------------------- 47
1.30 Largest rectangle under histogram--------------------------------------------------------------------------- 50
1.31 Negation technique -------------------------------------------------------------------------------------------- 53
2. Algorithm Design Techniques-------------------------------------------------------------------------- 58
2.1 Introduction ----------------------------------------------------------------------------------------------------- 58
2.2 Classification----------------------------------------------------------------------------------------------------- 58
2.3 Classification by implementation method ------------------------------------------------------------------- 59
2.4 Classification by design method ------------------------------------------------------------------------------ 59
2.5 Other classifications -------------------------------------------------------------------------------------------- 61
3. Recursion and Backtracking ---------------------------------------------------------------------------- 62
3.1 Introduction ----------------------------------------------------------------------------------------------------- 62
3.2 Storage organization -------------------------------------------------------------------------------------------- 62
3.3 Program execution---------------------------------------------------------------------------------------------- 63
3.4 What is recursion? ---------------------------------------------------------------------------------------------- 68
3.5 Why recursion? -------------------------------------------------------------------------------------------------- 69
3.6 Format of a recursive function-------------------------------------------------------------------------------- 69
3.7 Example ---------------------------------------------------------------------------------------------------------- 70
3.8 Recursion and memory (Visualization) ---------------------------------------------------------------------- 70
3.9 Recursion versus Iteration ------------------------------------------------------------------------------------- 71
3.10 Notes on recursion -------------------------------------------------------------------------------------------- 72
3.11 Algorithms which use recursion----------------------------------------------------------------------------- 72
3.12 Towers of Hanoi ---------------------------------------------------------------------------------------------- 72
3.13 Finding the ℎ odd natural number ----------------------------------------------------------------------- 75
3.14 Finding the ℎ power of 2 ---------------------------------------------------------------------------------- 76
3.15 Searching for an element in an array------------------------------------------------------------------------ 77
3.16 Checking for ascending order of array --------------------------------------------------------------------- 77
3.17 Basics of recurrence relations -------------------------------------------------------------------------------- 78
3.18 Reversing a singly linked list --------------------------------------------------------------------------------- 81
3.19 Finding the ℎ smallest element in BST ------------------------------------------------------------------ 87
3.20 Finding the ℎ largest element in BST-------------------------------------------------------------------- 90
3.21 Checking whether the binary tree is a BST or not -------------------------------------------------------- 91
3.22 Combinations: n choose m ----------------------------------------------------------------------------------- 94
3.23 Solving problems by brute force --------------------------------------------------------------------------- 100
3.24 What is backtracking? ---------------------------------------------------------------------------------------- 101
3.25 Algorithms which use backtracking------------------------------------------------------------------------ 103
3.26 Generating binary sequences ------------------------------------------------------------------------------- 103
3.27 Generating −ary sequences ------------------------------------------------------------------------------ 107
3.28 Finding the largest island ------------------------------------------------------------------------------------ 107
3.29 Path finding problem ---------------------------------------------------------------------------------------- 112
3.30 Permutations --------------------------------------------------------------------------------------------------116
3.31 Sudoku puzzle ------------------------------------------------------------------------------------------------ 123
3.32 N-Queens problem ------------------------------------------------------------------------------------------ 129
4. Greedy Algorithms -------------------------------------------------------------------------------------- 136
4.1 Introduction---------------------------------------------------------------------------------------------------- 136
4.2 Greedy strategy ------------------------------------------------------------------------------------------------ 136
4.3 Elements of greedy algorithms ------------------------------------------------------------------------------ 136
4.4 Do greedy algorithms always work? ------------------------------------------------------------------------ 137
4.5 Advantages and disadvantages of greedy method -------------------------------------------------------- 137
4.6 Greedy applications ------------------------------------------------------------------------------------------- 137
4.7 Understanding greedy technique ---------------------------------------------------------------------------- 138
4.8 Selection sort--------------------------------------------------------------------------------------------------- 141
4.9 Heap sort ------------------------------------------------------------------------------------------------------- 144
4.10 Sorting nearly sorted array ---------------------------------------------------------------------------------- 152
4.11 Two sum problem: A[i] + A[j] = K ----------------------------------------------------------------------- 155
4.12 Fundamentals of disjoint sets ------------------------------------------------------------------------------ 157
4.13 Minimum set cover problem ------------------------------------------------------------------------------- 164
4.14 Fundamentals of graphs ------------------------------------------------------------------------------------ 169
4.15 Topological sort ---------------------------------------------------------------------------------------------- 177
4.16 Shortest path algorithms ------------------------------------------------------------------------------------ 180
4.17 Shortest path in an unweighted graph -------------------------------------------------------------------- 181
4.18 Shortest path in weighted graph-Dijkstra’s algorithm -------------------------------------------------- 184
4.19 Bellman-Ford algorithm ------------------------------------------------------------------------------------ 189
4.20 Overview of shortest path algorithms -------------------------------------------------------------------- 193
4.21 Minimal spanning trees-------------------------------------------------------------------------------------- 193
4.22 Prim's algorithm---------------------------------------------------------------------------------------------- 194
4.23 Kruskal’s algorithm ------------------------------------------------------------------------------------------ 197
4.24 Minimizing gas fill-up stations ----------------------------------------------------------------------------- 200
4.25 Minimizing cellular towers---------------------------------------------------------------------------------- 202
4.26 Minimum scalar product ------------------------------------------------------------------------------------ 203
4.27 Minimum sum of pairwise multiplication of elements-------------------------------------------------- 204
4.28 File merging--------------------------------------------------------------------------------------------------- 205
4.29 Interval scheduling------------------------------------------------------------------------------------------- 210
4.30 Determine number of class rooms ------------------------------------------------------------------------ 213
4.31 Knapsack problem------------------------------------------------------------------------------------------- 215
4.32 Fractional knapsack problem------------------------------------------------------------------------------- 216
4.33 Determining number of platforms at a railway station ------------------------------------------------- 218
4.34 Making change problem ------------------------------------------------------------------------------------ 221
4.35 Preparing songs cassette ------------------------------------------------------------------------------------ 221
4.36 Event scheduling--------------------------------------------------------------------------------------------- 222
4.37 Managing customer care service queue-------------------------------------------------------------------- 224
4.38 Finding depth of a generic tree ----------------------------------------------------------------------------- 224
4.39 Nearest meeting cell in a maze ----------------------------------------------------------------------------- 226
4.40 Maximum number of entry points for any cell in maze------------------------------------------------- 228
4.41 Length of the largest path in a maze----------------------------------------------------------------------- 231
4.42 Minimum coin change problem ---------------------------------------------------------------------------- 231
4.43 Pairwise distinct summands--------------------------------------------------------------------------------- 233
4.44 Team outing to Papikondalu-------------------------------------------------------------------------------- 236
4.45 Finding smallest elements in an array ------------------------------------------------------------------- 240
4.46 Finding ℎ-smallest element in an array -----------------------------------------------------------------240
5. Divide and Conquer Algorithms ---------------------------------------------------------------------- 241
5.1 Introduction ---------------------------------------------------------------------------------------------------- 241
5.2 What is divide and conquer strategy? ----------------------------------------------------------------------- 241
5.3 Do divide and conquer approach always work? ----------------------------------------------------------- 241
5.4 Divide and conquer visualization ---------------------------------------------------------------------------- 241
5.5 Understanding divide and conquer -------------------------------------------------------------------------- 242
5.6 Advantages of divide and conquer -------------------------------------------------------------------------- 243
5.7 Disadvantages of divide and conquer ----------------------------------------------------------------------- 243
5.9 Divide and conquer applications ---------------------------------------------------------------------------- 243
5.8 Master theorem ------------------------------------------------------------------------------------------------ 243
5.9 Master theorem practice questions -------------------------------------------------------------------------- 244
5.10 Binary search --------------------------------------------------------------------------------------------------245
5.11 Merge sort ----------------------------------------------------------------------------------------------------- 250
5.12 Quick sort ----------------------------------------------------------------------------------------------------- 253
5.13 Convert algorithms to divide & conquer recurrences --------------------------------------------------- 259
5.14 Converting code to divide & conquer recurrences ------------------------------------------------------ 261
5.15 Summation of n numbers ----------------------------------------------------------------------------------- 262
5.16 Finding minimum and maximum in an array ------------------------------------------------------------- 263
5.17 Finding two maximal elements ----------------------------------------------------------------------------- 265
5.18 Median in two sorted lists ----------------------------------------------------------------------------------- 267
5.19 Strassen's matrix multiplication----------------------------------------------------------------------------- 270
5.20 Integer multiplication ---------------------------------------------------------------------------------------- 275
5.21 Finding majority element ------------------------------------------------------------------------------------ 280
5.22 Checking for magic index in a sorted array --------------------------------------------------------------- 285
5.23 Stock pricing problem --------------------------------------------------------------------------------------- 287
5.24 Shuffling the array -------------------------------------------------------------------------------------------- 289
5.25 Nuts and bolts problem ------------------------------------------------------------------------------------- 292
5.26 Maximum value contiguous subsequence-----------------------------------------------------------------294
5.27 Closest-pair points in one-dimension --------------------------------------------------------------------- 294
5.28 Closest-pair points in two-dimension --------------------------------------------------------------------- 296
5.29 Calculating ------------------------------------------------------------------------------------------------ 300
5.30 Skyline Problem ---------------------------------------------------------------------------------------------- 301
5.31 Finding peak element of an array -------------------------------------------------------------------------- 309
5.32 Finding peak element in two-dimensional array --------------------------------------------------------- 314
5.33 Finding the largest integer smaller than given element ------------------------------------------------- 321
5.3 Finding the smallest integer greater than given element ------------------------------------------------- 322
5.34 Print elements in the given range of sorted array ------------------------------------------------------- 322
5.35 Finding smallest elements in an array ------------------------------------------------------------------ 323
5.36 Finding ℎ-smallest element in an array ---------------------------------------------------------------- 330
5.37 Finding ℎ smallest element in two sorted arrays ----------------------------------------------------- 340
5.38 Many eggs problem ------------------------------------------------------------------------------------------ 346
5.39 Tromino tiling ------------------------------------------------------------------------------------------------ 347
5.40 Grid search---------------------------------------------------------------------------------------------------- 350
6. Dynamic Programming --------------------------------------------------------------------------------- 358
6.1 Introduction---------------------------------------------------------------------------------------------------- 358
6.2 What is dynamic programming strategy?------------------------------------------------------------------- 358
6.3 Properties of dynamic programming strategy ------------------------------------------------------------- 359
6.4 Greedy vs Divide and Conquer vs DP --------------------------------------------------------------------- 359
6.5 Can DP solve all problems?---------------------------------------------------------------------------------- 359
6.6 Dynamic programming approaches ------------------------------------------------------------------------ 360
6.7 Understanding DP approaches ------------------------------------------------------------------------------ 360
6.8 Examples of DP algorithms --------------------------------------------------------------------------------- 365
6.9 Climbing n stairs with taking only 1 or 2 steps ------------------------------------------------------------ 365
6.10 Tribonacci numbers ----------------------------------------------------------------------------------------- 366
6.11 Climbing n stairs with taking only 1, 2 or 3 steps-------------------------------------------------------- 366
6.12 Longest common subsequence ---------------------------------------------------------------------------- 369
6.13 Computing a binomial coefficient: n choose k ---------------------------------------------------------- 373
6.14 Solving recurrence relations with DP --------------------------------------------------------------------- 378
6.15 Maximum value contiguous subsequence ---------------------------------------------------------------- 379
6.16 Maximum sum subarray with constraint-1 --------------------------------------------------------------- 386
6.17 House robbing ----------------------------------------------------------------------------------------------- 387
6.18 Maximum sum subarray with constraint-2 --------------------------------------------------------------- 388
6.19 SSS restaurants ----------------------------------------------------------------------------------------------- 389
6.20 Gas stations --------------------------------------------------------------------------------------------------- 392
6.21 Range sum query --------------------------------------------------------------------------------------------- 394
6.22 2D Range sum query ---------------------------------------------------------------------------------------- 395
6.23 Two eggs problem-------------------------------------------------------------------------------------------- 398
6.24 E eggs and F floors problem ------------------------------------------------------------------------------- 402
6.25 Painting grandmother’s house fence----------------------------------------------------------------------- 403
6.26 Painting colony houses with red, blue and green -------------------------------------------------------- 405
6.27 Painting colony houses with k colors---------------------------------------------------------------------- 406
6.28 Unlucky numbers --------------------------------------------------------------------------------------------- 408
6.29 Count numbers with unique digits ------------------------------------------------------------------------- 410
6.30 Catalan numbers ---------------------------------------------------------------------------------------------- 412
6.31 Binary search trees with n vertices ------------------------------------------------------------------------- 412
6.32 Rod cutting problem ----------------------------------------------------------------------------------------- 417
6.33 0-1 Knapsack problem--------------------------------------------------------------------------------------- 425
6.34 Making change problem ------------------------------------------------------------------------------------- 433
6.35 Longest increasing subsequence [LIS]--------------------------------------------------------------------- 438
6.36 Longest increasing subsequence [LIS] with constraint -------------------------------------------------- 444
6.37 Box stacking--------------------------------------------------------------------------------------------------- 449
6.38 Building bridges ---------------------------------------------------------------------------------------------- 452
6.39 Partitioning elements into two equal subsets ------------------------------------------------------------- 457
6.40 Subset sum ---------------------------------------------------------------------------------------------------- 459
6.41 Counting boolean parenthesizations----------------------------------------------------------------------- 461
6.42 Optimal binary search trees --------------------------------------------------------------------------------- 465
6.43 Edit distance --------------------------------------------------------------------------------------------------471
6.44 All pairs shortest path problem: Floyd's algorithm ------------------------------------------------------ 476
6.45 Optimal strategy for a game -------------------------------------------------------------------------------- 480
6.46 Tiling ----------------------------------------------------------------------------------------------------------- 484
6.47 Longest palindrome substring ------------------------------------------------------------------------------ 484
6.48 Longest palindrome subsequence -------------------------------------------------------------------------- 494
6.49 Counting the subsequences in a string -------------------------------------------------------------------- 497
6.50 Apple count --------------------------------------------------------------------------------------------------- 501
6.51 Apple count variant with 3 ways of reaching a location ------------------------------------------------ 503
6.52 Largest square sub-matrix with all 1’s --------------------------------------------------------------------- 504
6.53 Maximum size sub-matrix with all 1’s --------------------------------------------------------------------- 510
6.54 Maximum sum sub-matrix ---------------------------------------------------------------------------------- 515
6.55 Finding minimum number of squares to sum ------------------------------------------------------------ 521
6.56 Finding optimal number of jumps ------------------------------------------------------------------------- 521
6.57 Frog river crossing ------------------------------------------------------------------------------------------- 527
6.58 Number of ways a frog can cross a river ------------------------------------------------------------------531
6.59 Finding a subsequence with a total ------------------------------------------------------------------------ 535
6.60 Delivering gifts------------------------------------------------------------------------------------------------ 536
6.61 Circus human tower designing ----------------------------------------------------------------------------- 536
6.62 Bombing enemies -------------------------------------------------------------------------------------------- 536
APPENDIX-I: Python Program Execution -------------------------------------------------------------------- 542
I.1 Compilers versus Interpreters-------------------------------------------------------------------------------- 542
I.2 Python programs ---------------------------------------------------------------------------------------------- 543
I.3 Python interpreter --------------------------------------------------------------------------------------------- 543
I.4 Python byte code compilation ------------------------------------------------------------------------------- 544
I.5 Python Virtual Machine (PVM) ----------------------------------------------------------------------------- 544
APPENDIX-II: Complexity Classes ----------------------------------------------------------------------------- 545
II.1 Introduction --------------------------------------------------------------------------------------------------- 545
II.2 Polynomial/Exponential time ------------------------------------------------------------------------------ 545
II.3 What is a decision problem? -------------------------------------------------------------------------------- 546
II.4 Decision procedure------------------------------------------------------------------------------------------- 546
II.5 What is a complexity class?---------------------------------------------------------------------------------- 546
II.6 Types of complexity classes --------------------------------------------------------------------------------- 546
II.7 Reductions ----------------------------------------------------------------------------------------------------- 548
II.8 Complexity classes: Problems & Solutions---------------------------------------------------------------- 551
Bibliography ---------------------------------------------------------------------------------------------------------- 554
O r g ani zati o n o f Ch apter s | 15
Organization of
Chapters
The algorithms and data structures in this book are presented in the Python programming
language. A unique feature of this book, when compared to the books available on the
subject, is that it offers a balance of theory, practical concepts, problem solving, and
interview questions.
+ +
The book deals with some of the most important and challenging areas of programming
and computer science in a highly readable manner. It covers both algorithmic theory and
programming practice, demonstrating how theory is reflected in real Python programs.
Well-known algorithms and data structures that are built into the Python language are
explained, and the user is shown how to implement and evaluate others.
The book offers a large number of questions, with detailed answers, so that you can practice
and assess your knowledge before you take the exam or are interviewed.
W h a t i s t h i s b oo k a bo u t?
O r g ani zati o n o f Ch apter s | 16
How to understand several classical algorithms and data structures in depth, and
be able to implement these efficiently in Python
Note that this book does not cover numerical or number-theoretical algorithms, parallel
algorithms or multi-core programming.
Although this book is more precise and analytical than many other data structure and
algorithm books, it rarely uses mathematical concepts that are more advanced than those
taught at high school. I have made an effort to avoid using any advanced calculus,
probability, or stochastic process concepts. The book is therefore appropriate for
undergraduate students preparing for interviews.
Organization of chapters
Data structures and algorithms are important aspects of computer science as they form
the fundamental building blocks of developing logical solutions to problems, as well as
creating efficient programs that perform tasks optimally. This book covers the topics
required for a thorough understanding of the concepts such as Recursion, Backtracking,
Greedy, Divide and Conquer, and Dynamic Programming.
S h o u l d I b u y t h i s b oo k ?
O r g ani zati o n o f Ch apter s | 17
path. Algorithms that use this approach are called algorithms, and
backtracking is a form of recursion. Also, some problems can be solved by combining
recursion with backtracking.
In this chapter, we discuss a few tips and tricks with a focus on bitwise operators. Also, it
covers a few other uncovered and general problems.
At the end of each chapter, a set of problems/questions is provided for you to
improve/check your understanding of the concepts. The examples in this book are kept
O r g a n i z a t i o n o f c ha p t e r s
O r g ani zati o n o f Ch apter s | 18
simple for easy understanding. The objective is to enhance the explanation of each concept
with examples for a better understanding.
Some prerequisites
This book is intended for two groups of people: Python programmers who want to beef up
their algorithmics, and students taking algorithm courses who want a supplement to their
algorithms textbook. Even if you belong to the latter group, I’m assuming that you have a
familiarity with programming in general and with Python in particular. If you don’t, the
Python web site also has a lot of useful material. Python is a really easy language to learn.
There is some math in the pages ahead, but you don’t have to be a math prodigy to follow
the text. We’ll be dealing with some simple sums and nifty concepts such as polynomials,
exponentials, and logarithms, but I’ll explain it all as we go along.
Some prerequisites
In tr o du ctio n to A lg or i thm s A n aly si s | 19
Chapter
Introduction to
Algorithms
Analysis 1
The objective of this chapter is to explain the importance of the analysis of algorithms, their
notations, relationships and solving as many problems as possible. Let us first focus on
understanding the basic elements of algorithms, the importance of algorithm analysis, and
then slowly move toward the other topics as mentioned above. After completing this
chapter, you should be able to find the complexity of any given algorithm (especially
recursive functions).
1.1 Variables
Before going to the definition of variables, let us relate them to old mathematical equations.
All of us have solved many mathematical equations since childhood. As an example,
consider the equation given below:
+2 −2 = 1
We don’t have to worry about the use of this equation. The important thing that we need
to understand is that the equation has names ( and ), which hold values (data). That
means the ( and ) are placeholders for representing data. Similarly, in computer
science programming we need something for holding data, and is the way to do
that.
1.1 Variables
In tr o du ctio n to A lg or i thm s A n aly si s | 20
1 . 3 D a t a s t r u ct u r e s
In tr o du ctio n to A lg or i thm s A n aly si s | 21
To simplify the process of solving problems, we combine the data structures with their
operations and we call this (ADTs). An ADT consists of parts:
1. Declaration of data
2. Declaration of operations
Commonly used ADTs : Linked Lists, Stacks, Queues, Priority Queues, Binary Trees,
Dictionaries, Disjoint Sets (Union and Find), Hash Tables, Graphs, and many others. For
example, stack uses LIFO (Last-In-First-Out) mechanism while storing the data in data
structures. The last element inserted into the stack is the first element that gets deleted.
Common operations of it are: creating the stack, pushing an element onto the stack,
popping an element from stack, finding the current top of the stack, finding number of
elements in the stack, etc.
While defining the ADTs, do not worry about the implementation details. They come into
the picture only when we want to use them. Different kinds of ADTs are suited to different
kinds of applications, and some are highly specialized to specific tasks. By the end of this
book, we will go through many of them and you will be in a position to relate the data
structures to the kind of problems they solve.
1 . 5 W h a t i s a n a lg o r i t h m?
In tr o du ctio n to A lg or i thm s A n aly si s | 22
Ideal solution? Let us assume that we express the running time of a given algorithm as a
function of the input size (i.e., ( )) and compare these different functions corresponding
to running times. This kind of comparison is independent of machine time, programming
style, etc.
For the above-mentioned example, we can represent the cost of the car and the cost of the
bicycle in terms of function, and for a given function ignore the lower order terms that are
relatively insignificant (for large value of input size, ). As an example, in the case
below, , 2 , 100 and 500 are the individual costs of some function and approximate to
since is the highest rate of growth.
+ 2 + 100 + 500 ≈
1 . 8 W h a t i s r u n n i ng t i m e a na l y s i s?
In tr o du ctio n to A lg or i thm s A n aly si s | 23
The diagram below shows the relationship between different rates of growth.
D
! e
c
r
e
4 a
s
i
n
2 g
R
a
t
e
s
log
O
log ( !) f
G
r
o
w
t
2 h
log log
1 . 1 1 C o m m o n l y u s e d r a t e s o f g ro w t h
In tr o du ctio n to A lg or i thm s A n aly si s | 24
In general, the first case is called the and the second case is called the
for the algorithm. To analyze an algorithm we need some kind of syntax, and that forms
the base for asymptotic analysis/notation. There are three types of analysis:
Worst case
o Defines the input for which the algorithm takes a long time (slowest time
to complete).
o Input is the one for which the algorithm runs the slowest.
Best case
o Defines the input for which the algorithm takes the least time (fastest time
to complete).
o Input is the one for which the algorithm runs the fastest.
Average case
o Provides a prediction about the running time of the algorithm.
o Runs the algorithm many times, using many different inputs that come
from some distribution that generates these inputs, computes the total
running time (by adding the individual times), and divides by the number
of trials.
o Assumes that the input is random.
<= <=
For a given algorithm, we can represent the best, worst and average cases in the form of
expressions. As an example, let ( ) be the function which represents the given algorithm.
( )= + 500, for worst case
( )= + 100 + 500, for best case
Similarly for the average case. The expression defines the inputs with which the algorithm
takes the average running time (or memory).
1 . 1 2 T y p e s o f a n a l ys i s
In tr o du ctio n to A lg or i thm s A n aly si s | 25
Rate of Growth ( )
( )
Input Size,
Let us see the O−notation with a little more detail. O−notation defined as O( ( )) = { ( ):
there exist positive constants and such that 0 ≤ ( ) ≤ ( ) for all ≥ }. ( ) is
an asymptotic tight upper bound for ( ). Our objective is to give the smallest rate of growth
( ) which is greater than or equal to the given algorithms’ rate of growth ( ).
Generally we discard lower values of . That means the rate of growth at lower values of
is not important. In the figure, is the point from which we need to consider the rate of
growth for a given algorithm. Below , the rate of growth could be different. is called
threshold for the given function.
Big-O visualization
O( ( )) is the set of functions with smaller or the same order of growth as ( ). For
example; O( ) includes O(1), O( ), O( ), etc.
Note: Analyze the algorithms at larger values of only. What this means is, below we
do not care about the rate of growth.
Big-O examples
Example-1 Find upper bound for ( ) = 3 + 8
Solution: 3 + 8 ≤ 4 , for all ≥ 8
∴ 3 + 8 = O( ) with c = 4 and =8
Example-2 Find upper bound for ( ) = + 1
1 . 1 4 B i g - O no ta t i on
In tr o du ctio n to A lg or i thm s A n aly si s | 26
No uniqueness?
There is no unique set of values for and in proving the asymptotic bounds. Let us
consider, 100 + 5 = O( ). For this function there are multiple and values possible.
Solution2: 100 + 5 ≤ 100 + 5 = 105 ≤ 105 , for all ≥ 1, = 1 and = 105 is also a
solution.
Rate of Growth
( )
( ))
Input Size,
The Ω notation can be defined as Ω(g(n)) = {f(n): there exist positive constants c and
0 such that 0 ≤ ( ) ≤ ( ) for all n ≥ 0 }. ( ) is an asymptotic tight lower bound
1 . 1 5 O m e g a -Ω n ot a t i o n
In tr o du ctio n to A lg or i thm s A n aly si s | 27
for ( ). Our objective is to give the largest rate of growth ( ) which is less than or equal
to the rate of growth ( ) of the given algorithm.
Ω Examples
Example-1 Find lower bound for ( ) = 5 .
Solution: , 0 Such that: 0 5 5 = 5 and 0=1
∴ 5 = ( ) with = 5 and 0 = 1
Example-3 2 = ( ), = ( ), = ( ).
Rate of Growth
c ( )
( )
c ( )
Input Size,
1 . 1 6 T h e t a - n o ta t io n
In tr o du ctio n to A lg or i thm s A n aly si s | 28
Examples
Example 1 Find bound for ( ) = − .
Important notes
For analysis (best case, worst case and average), we try to give the upper bound (O) and
lower bound () and average running time (). From the above examples, it should also be
clear that, for a given function (algorithm), getting the upper bound (O) and lower bound
() and average running time () may not always be possible. For example, if we are
discussing the best case of an algorithm, we try to give the upper bound (O) and lower
bound () and average running time ().
In the remaining chapters, we generally focus on the upper bound (O) because knowing
the lower bound () of an algorithm is of no practical importance, and we use the notation
if the upper bound (O) and lower bound () are the same.
for i in range(0,n):
# inner loop executes n times
for j in range(0,n):
print 'i value %d and j value %d' % (i,j) #constant time
Total time = × × = = O( ).
3) Consecutive statements: Add the time complexities of each statement.
n = 100
# executes times
for i in range(0,n):
print 'Current Number :', i #constant time
# outer loop executed n times
for i in range(0,n):
# inner loop executes n times
for j in range(0,n):
print 'i value %d and j value %d' % (i,j) #constant time
Total time = + + = O( ).
4) If-then-else statements: Worst-case running time: the test, plus ℎ the ℎ part
or the part (whichever is larger).
if n == 1: #constant time
print "Wrong Value"
print n
else:
for i in range(0,n): #n times
print 'Current Number :', i #constant time
Total time = + ∗ = O( ).
5) Logarithmic complexity: An algorithm is O( ) if it takes a constant time to cut the
problem size by a fraction (usually by ½). As an example, let us consider the following
program:
def logarithms(n):
i=1
while i <= n:
i= i * 2
print i
logarithms(100)
If we observe carefully, the value of is doubling every time. Initially = 1, in next step
= 2, and in subsequent steps = 4, 8 and so on. Let us assume that the loop is
executing some times. At step 2 = , and at ( + 1) step we come out of the
. Taking logarithm on both sides, gives
2 =
2=
= //if we assume base-2
Total time = O( ).
Note: Similarly, for the case below, the worst case rate of growth is O( ). The same
discussion holds good for the decreasing sequence as well.
def logarithms(n):
i=n
while i >= 1:
i= i // 2
print i
1 . 1 8 G u i d e l i n e s f o r a s y m p to t i c a n a ly s is
In tr o du ctio n to A lg or i thm s A n aly si s | 30
logarithms(100)
Another example: binary search (finding a word in a dictionary of pages)
Look at the center point in the dictionary
Is the word towards the left or right of the center?
Repeat the process with the left or right part of the dictionary until the word is
found.
= =
Arithmetic series
( + 1)
= 1+2+⋯+ =
2
Geometric series
−1
=1+ + …+ = ( ≠ 1)
−1
Harmonic series
1 1 1
= 1 + + …+ ≈
2
Other important formulae
1
= 1 + 2 +⋯+ ≈
+1
1 . 1 9 S i m p l i f y i n g p r o p e r t i e s o f a sy m p t ot i c n ot a t i o n s
In tr o du ctio n to A lg or i thm s A n aly si s | 31
each of which is half the size of the original, and then performs O( ) additional work for
merging. This gives the running time equation:
T( ) = 2 + O( )
The following theorem can be used to determine the running time of divide and conquer
algorithms. For a given program (algorithm), first we try to find the recurrence relation for
the problem. If the recurrence is of the form given below, then we can directly give the
answer without fully solving it fully.
If the recurrence is of the form T( ) = ( ) + ( ), where ≥ 1, > 1, ≥ 0 and
is a real number, then:
1) If > , then ( ) = Θ
2) If =
a. If > −1, then ( ) = Θ
b. If = −1, then ( ) = Θ
c. If < −1, then ( ) = Θ
3) If <
a. If ≥ 0, then ( ) = Θ( )
b. If < 0, then ( ) = O( )
1 . 2 2 M a st e r t h eo r e m f o r su b t r a c t a n d c o nq u e r r e c u r r e n ce s
In tr o du ctio n to A lg or i thm s A n aly si s | 32
impression that it is similar to the divide and conquer method (dividing the problem into
√ subproblems each with size √ ). As we can see, the size of the subproblems at the first
level of recursion is . So, let us guess that T( ) = O( ), and then try to prove that our
guess is correct.
Let’s start by trying to prove an bound T( ) ≤ :
T( ) = √ T(√ ) +
≤ √ . √ √ +
= . √ +
= .c. . +
≤
The last inequality assumes only that 1 ≤ c. . . This is correct if is sufficiently large
and for any constant , no matter how small. From the above proof, we can see that our
guess is correct for the upper bound. Now, let us prove the bound for this recurrence.
T( ) = √ T(√ ) +
≥ √ . √ √ +
= . √ +
= . . . +
≥
The last inequality assumes only that 1 ≥ . . . This is incorrect if is sufficiently large
and for any constant . From the above proof, we can see that our guess is incorrect for
the lower bound.
From the above discussion, we understood that Θ( ) is too big. How about Θ( )? The
lower bound is easy to prove directly:
T( ) = √ T(√ ) + ≥
Now, let us prove the upper bound for this Θ( ).
T( ) = √ T(√ ) +
≤ √ . .√ +
= . +
= ( + 1)
≰
From the above induction, we understood that Θ( ) is too small and Θ( ) is too big. So,
we need something bigger than and smaller than . How about ?
Proving the upper bound for :
T( ) = √ T(√ ) +
≤ √ . .√ √ +
= . .
√
√ +
≤ √
Proving the lower bound for :
T( ) = √ T(√ ) +
≥ √ . .√ √ +
= . .
√
√ +
≱ √
1 . 2 4 M e t ho d o f gu e s s i ng a n d c o n f i r m i ng
In tr o du ctio n to A lg or i thm s A n aly si s | 33
The last step doesn’t work. So, Θ( ) doesn’t work. What else is between and ?
How about ?
Proving upper bound for :
T( ) = √ T(√ ) +
≤ √ . .√ √ +
= . . - . +
≤ , if ≥ 1
Proving lower bound for :
T( ) = √ T(√ ) +
≥ √ . .√ √ +
= . . - . +
≥ , if ≤ 1
From the above proofs, we can see that T( ) ≤ , if ≥ 1 and T( ) ≥ , if
≤ 1. Technically, we’re still missing the base cases in both proofs, but we can be fairly
confident at this point that T( ) = Θ( ).
1 . 2 7 A m o r t i z e d a na l y s i s
In tr o du ctio n to A lg or i thm s A n aly si s | 34
1 . 2 8 A l g o r i t h m s a n a ly s i s : p r o b l e m s a nd s o l u t io n s
In tr o du ctio n to A lg or i thm s A n aly si s | 35
function(20)
We can define the ‘ ’ terms according to the relation = + . The value of ‘ ’ increases by
1 for each iteration. The value contained in ‘ ’ at the iteration is the sum of the
first ‘ ’ positive integers. If is the total number of iterations taken by the program, then
the ℎ loop terminates if:
( )
1 + 2+...+ = > ⟹ = O(√ ).
1 . 2 8 A l g o r i t h m s a n a ly s i s : p r o b l e m s a nd s o l u t io n s
In tr o du ctio n to A lg or i thm s A n aly si s | 36
( ) = (1) + ( − 1)
( ) = (1) + −
1 . 2 8 A l g o r i t h m s a n a ly s i s : p r o b l e m s a nd s o l u t io n s
In tr o du ctio n to A lg or i thm s A n aly si s | 38
(( + 1)(2 + 1) ( + 1)
( )=1+ −
6 2
( ) =( )
Note: We can use the master theorem for this problem.
Problem-14 Consider the following program:
def Fib(n):
if n == 0: return 0
elif n == 1: return 1
else: return Fib(n-1)+ Fib(n-2)
print(Fib(3))
Solution: The recurrence relation for the running time of this program is: ( ) = ( − 1) +
( − 2) + . Note T( ) has two recurrence calls indicating a binary tree. Each step
recursively calls the program for reduced by 1 and 2, so the depth of the recurrence tree
is O( ). The number of leaves at depth is 2 since this is a full binary tree, and each leaf
takes at least O(1) computations for the constant factor. Running time is clearly
exponential in and it is O(2 ).
Problem-15 What is the running time of following program?
def function(n):
count = 0
if n <= 0:
return
for i in range(0, n):
j=1
while j <n:
j=j+i
count = count + 1
print (count)
function(20)
Solution: Consider the comments in the function below:
def function(n):
count = 0
if n <= 0:
return
for i in range(0, n): # Outer loop executes n times
j=1 # Inner loop executes j increase by the rate of i
while j <n:
j=j+i
count = count + 1
print (count)
function(20)
In the above code, inner loop executes / times for each value of . Its running time
is × (∑ni=1 n/i) = O( ).
Problem-16 What is the complexity of ∑ ?
Solution: Using the logarithmic property, = + , we can see that this problem
is equivalent to
= 1+ 2+ ⋯+ = (1 × 2 × … × ) = ( !) ≤ ( )≤
Problem-17 What is the running time of the following recursive function (specified as a
function of the input value )? First write the recurrence formula and then find its
complexity.
def function(n):
if n <= 0:
return
for i in range(0, 3):
function(n/3)
function(20)
Solution: Consider the comments in the below function:
def function(n):
if n <= 0:
return
for i in range(0, 3): #This loop executes 3 times with recursive value of value
function(n/3)
function(20)
We can assume that for asymptotical analysis = for every integer ≥ 1. The
recurrence for this code is ( ) = 3 ( ) + Θ(1). Using master theorem, we get ( ) = Θ( ).
Problem-18 What is the running time of the following recursive function (specified as a
function of the input value )? First write a recurrence formula, and show its solution
using induction.
def function(n):
if n <= 0:
return
for i in range(0, 3): #This loop executes 3 times with recursive value of value
function(n-1)
function(20)
Solution: Consider the comments in the function below:
def function(n):
if n <= 0:
return
for i in range(0, 3): #This loop executes 3 times with recursive value of − 1 value
function(n-1)
function(20)
The statement requires constant time [O(1)]. With the loop, we neglect the loop
overhead and only count three times that the function is called recursively. This implies a
time complexity recurrence:
( ) = , ≤ 1
= + 3 ( − 1), > 1
Using the master theorem, we get ( ) = Θ(3 ).
Problem-19 Write a recursion formula for the running time ( ) of the function whose
code is below.
def function3(n):
if n <= 0:
return
for i in range(0, 3): #This loop executes 3 times with recursive value of n/3 value
function3(0.8 * n)
function3(20)
1 . 2 8 A l g o r i t h m s a n a ly s i s : p r o b l e m s a nd s o l u t io n s
In tr o du ctio n to A lg or i thm s A n aly si s | 40
( ) = (2 ) = 2 √2 + =2 2 +
Solution: Applying the logic of Problem-20 gives: ( )=2 + 1. Using the master
theorem results ( ) = O = O( ). Substituting = gives ( ) = O( ).
Problem-23 Find the complexity of the below function.
import math
count = 0
def function(n):
global count
if n <= 2:
return 1
else:
function(round(math.sqrt(n)))
count = count + 1
return count
print(function(200))
Solution: Consider the comments in the function below:
import math
count = 0
def function(n):
global count
if n <= 2:
return 1
else:
function(round(math.sqrt(n))) #Recursive call with √ value
1 . 2 8 A l g o r i t h m s a n a ly s i s : p r o b l e m s a nd s o l u t io n s
In tr o du ctio n to A lg or i thm s A n aly si s | 41
count = count + 1
return count
print(function(200))
For the above code, the recurrence function can be given as: ( ) = (√ ) + 1. This is same
as that of Problem-21.
Problem-24 Analyze the running time of the following recursive pseudo-code as a function
of .
def function(n):
if (n < 2):
return
else:
counter = 0
for i in range(0,8):
function (n/2)
for i in range(0,n**3):
counter = counter + 1
Solution: Consider the comments in the pseudo-code below and call running time of
function(n) as ( ).
def function(n):
if (n < 2): # Constant time
return
else:
counter = 0 # Constant time
for i in range(0,8): # This loop executes 8 times with n value half in every call
function (n/2)
for i in range(0,n**3): # This loop executes n^3times with constant time loop
counter = counter +1
( ) can be defined as follows:
( ) = 1 < 2,
=8 ( ) + 3 + 1 ℎ .
2
Using the master theorem gives: ( ) = Θ( ) = Θ( ).
Problem-25 Find the complexity of the pseudocode below.
count = 0
def function(n):
global count
count = 1
if n <= 0:
return
for i in range(0, n):
count = count + 1
n = n//2
function(n)
print count
function(200)
Solution: Consider the comments in the pseudocode below:
count = 0
def function(n):
global count
count = 1
1 . 2 8 A l g o r i t h m s a n a ly s i s : p r o b l e m s a nd s o l u t io n s
In tr o du ctio n to A lg or i thm s A n aly si s | 42
if n <= 0:
return
for i in range(1, n): # This loops executes n times
count = count + 1
n = n//2 # Integer Division
function(n) # Recursive call with value
print count
function(200)
The recurrence for this function is ( ) = ( /2) + . Using master theorem we get ( ) =
O( ).
Problem-26 What is the running time of the following program?
def function(n):
for i in range(1, n):
j=1
while j <= n:
j=j*2
print("*")
function(20)
Solution: Consider the comments in the function below:
def function(n):
for i in range(1, n): # This loops executes n times
j=1
while j <= n: # This loops executes times from our logarithms guideline
j=j*2
print("*")
function(20)
Complexity of above program is: O( ).
Problem-27 What is the running time of the following program?
def function(n):
for i in range(0, n/3):
j=1
while j <= n:
j=j+4
print("*")
function(20)
Solution: Consider the comments in the function below:
def function(n):
for i in range(0, n/3): #This loops executes n/3 times
j=1
while j <= n: #This loops executes n/4 times
j=j+4
print("*")
function(20)
The time complexity of this program is: O( ).
Problem-28 Find the complexity of the function below:
def function(n):
if n <= 0:
return
print ("*")
1 . 2 8 A l g o r i t h m s a n a ly s i s : p r o b l e m s a nd s o l u t io n s
In tr o du ctio n to A lg or i thm s A n aly si s | 43
function(n/2)
function(n/2)
print ("*")
function(20)
Solution: Consider the comments in the function below:
def function(n):
if n <= 0: #Constant time
return
print ("*") #Constant time
function(n/2) #Recursion with n/2 value
function(n/2) #Recursion with n/2 value
print ("*")
function(20)
The recurrence for this function is: ( ) = 2 + 1. Using master theorem, we get ( ) =
O( ).
Problem-29 Find the complexity of the function below:
count = 0
def logarithms(n):
i=1
global count
while i <= n:
j=n
while j > 0:
j = j//2
count = count + 1
i= i * 2
return count
print(logarithms(10))
Solution:
count = 0
def logarithms(n):
i=1
global count
while i <= n:
j=n
while j > 0:
j = j//2 # This loop gets executed for times from our logarithms guideline
count = count + 1
i= i * 2 # This loop gets executed for times from our logarithms guideline
return count
print(logarithms(10))
Time Complexity: O( ∗ ) = O( ).
Problem-30 ∑ ( ), where O( ) stands for order is:
(a) O( ) (b) O( ) (c) O( ) (d) O(3 ) (e) O(1.5 )
Solution: (b). ∑ ( ) = O( ) ∑ 1 = O( ).
Problem-31 Which of the following three claims are correct?
I ( + ) = ( ), where and are constants II 2 = O(2 ) III 2 = O(2 )
(a) I and II (b) I and III (c) II and III (d) I, II and III
Solution: (a). (I) ( + ) = + c1* + ... = ( ) and (II) 2 = 2*2 = O(2 )
1 . 2 8 A l g o r i t h m s a n a ly s i s : p r o b l e m s a nd s o l u t io n s
In tr o du ctio n to A lg or i thm s A n aly si s | 44
( ). This indicates that tight lower bound and tight upper bound are the same. That
means, O( ) and ( ) are correct for given recurrence. So option (C) is wrong.
Problem-37 Find the complexity of the function below:
def function(n):
for i in range(1, n):
j=i
while j <i*i:
j=j+1
if j %i == 0:
for k in range(0, j):
print(" * ")
function(10)
Solution:
def function(n):
for i in range(1, n): # Executes n times
j=i
while j <i*i: # Executes n*n times
j=j+1
if j %i == 0:
for k in range(0, j): #Executes j times = (n*n) times
print(" * ")
function(10)
Time Complexity: O( ).
Problem-38 To calculate 9 , give an algorithm and discuss its complexity.
Solution: Start with 1 and multiply by 9 until reaching 9 .
Time Complexity: There are − 1 multiplications and each takes constant time giving a
( ) algorithm.
Problem-39 For Problem-58, can we improve the time complexity?
Solution: Refer to the chapter.
Problem-40 Find the complexity of the function below:
def function(n):
sum = 0
for i in range(0, n-1):
if i > j:
sum = sum + 1
else:
for k in range(0, j):
sum = sum - 1
print (sum)
function(10)
Solution: Consider the − and we can ignore the value of j.
def function(n):
sum = 0
for i in range(0, n-1): # Executes times
if i > j:
sum = sum + 1 # Executes times
else:
for k in range(0, j): # Executes times
sum = sum - 1
1 . 2 8 A l g o r i t h m s a n a ly s i s : p r o b l e m s a nd s o l u t io n s
In tr o du ctio n to A lg or i thm s A n aly si s | 46
print (sum)
function(10)
Time Complexity: O( ).
Problem-41 Find the time complexity of recurrence T( ) = T( ) + T( ) + T( ) + .
Solution: Let us solve this problem by the method of guessing. The total size on each level
of the recurrence tree is less than , so we guess that ( ) = will dominate. Assume for
all < that ≤ T( ) ≤ . Then,
+ + + ≤ T( ) ≤ + + +
( + + + ) ≤ T( ) ≤ ( + + + )
( + ) ≤ T( ) ≤ ( + )
Problem-42 Rank the following functions by the order of growth: ( + 1)!, n!, 4 , ×3 ,
3 + + 20 , ( ) , 4 , 4 , + 200, 20 + 500, 2 , / , 1.
Solution:
Function Rate of Growth
( + 1)! O( !)
! O( !)
4 O(4 )
×3 O( 3 )
3 + + 20 O(3 )
3 O(( ) )
( ) Decreasing rate of growths
2
4 O( )
4 O( )
+ 200 O( )
20 + 500 O( )
2 O( )
/
O( / )
1 O(1)
.
Problem-43 Can we say 3 = O(3 )?
.
Solution: Yes, because 3 < 3 .
Problem-44 Can we say 2 = O(2 )?
Solution: No, because 2 = (2 ) = 8 is not less than 2 .
Problem-45 A perfect square is a number that can be expressed as the product of two
equal integers. Give an algorithm to find out if an integer is a perfect square. For
example, 16 is a perfect square and 15 isn't a perfect square.
Solution: Anytime we square an integer, the result is a perfect square. The numbers 4, 9,
16, and 25 are just a few perfect squares, but there are infinitely more.
Perfect Square Factors
1 1*1
4 2*2
9 3*3
16 4*4
1 . 2 8 A l g o r i t h m s a n a ly s i s : p r o b l e m s a nd s o l u t io n s
In tr o du ctio n to A lg or i thm s A n aly si s | 47
25 5 *5
36 6 *6
49 7 *7
64 8 *8
81 9 *9
100 10 * 10
Initially, let us say = 2. Compute the value × and see if it is equal to the given number.
If it is equal then we are done; otherwise increase the i value. Continue this process until
we reach × greater than or equal to the given number.
Time Complexity: O(√ ). Space Complexity: O(1).
Problem-46 Solve the recurrence T( ) = 2T( − 1) + 2 .
Solution: At each level of the recurrence tree, the number of problems is double that of the
previous level, while the amount of work being done in each problem is half of that of the
previous level. Formally, the level has 2 problems, each requiring 2 work. Thus the
level requires exactly 2 work. The depth of this tree is , because at the level, the
originating call will be T( − ). Thus the total complexity for T( ) is T( 2 ).
Example
For example, in the following relationship matrix, person 2 is a celebrity.
0 1 2 3 4
0 1 1 1 0 1
1 0 1 1 0 1
2 0 0 1 0 0
3 1 0 1 1 0
4 0 1 1 0 1
And, there is no celebrity in the following relationship matrix:
0 1 2 3 4
0 1 0 1 0 1
1 0 1 0 0 1
2 1 0 1 1 0
3 1 0 0 1 0
4 0 1 1 0 1
Brute-force solution
A celebrity is a person who is known by everyone and does not know anyone besides
himself/herself. The matrix has at most ( − 1) elements, and we can compute it by
checking each element.
At this point, we can check whether a person is a celebrity by checking its row and its
column. This brute-force solution checks ( − 1) times.
import random
def celebrity(matrix):
n = len(matrix)
# For all potential celebrities
for i in range(n):
eliminated = False
# For every other person
for j in range(n):
if not eliminated:
if i == j: # Same person
continue
# If the possible celebrity knows someone, it's not a celebrity
# If somebody does not know the possible celebrity, it’s not a celebrity
if matrix[i][j] or not matrix[j][i]:
eliminated = True
if not eliminated:
return i # If no breaks were encountered, we make it here and return the celeb
def main():
matrix = [[random.randint(0, 1)]*5 for i in range(5)]
for i in range(random.randint(0, len(matrix) - 1)):
for j in range(len(matrix)):
matrix[j][i] = 1
matrix[i][j] = 0
for i in range(len(matrix)):
print matrix[i]
celeb = celebrity(matrix)
print "Celebrity:", celeb
if __name__ == "__main__":
main()
Performance
Time Complexity: O( ) Space Complexity: O(1)
An elegant solution
Next, we show how to do this with at the most 3( - 1) checks. This algorithm consists of
two phases:
1. Elimination and
2. Verification
In the elimination phase, we eliminate all but one person from being the celebrity; in the
verification phase we check whether this remaining person is indeed a celebrity. The
elimination phase maintains a list of possible celebrities. Initially, it contains all people.
In each iteration, we delete one person from the list. We exploit the following key
observation:
If person 1 knows person 2, then person 1 is not a celebrity; if person 1 does not know
person 2, then person 2 is not a celebrity.
Thus, by asking person 1 if he knows person 2, we can eliminate either person 1 or person
2 from the list of possible celebrities. We can use this idea repeatedly to eliminate all people
but one, say person .
We now verify by whether is a celebrity: for every other person , we ask person
whether he knows person , and we ask persons whether they know person . If person
always answers no, and the other people always answer yes, then we declare person as
the celebrity. Otherwise, we conclude there is no celebrity in this group.
import random
def celebrity(matrix):
n = len(matrix)
# The first two people, we begin eliminating
u, v = 0, 1
for i in range(2, n + 1):
# u knows someone, not a celeb
if matrix[u][v]:
u=i
# v is not known, not a celeb
else:
v=i
# As we iterated above, someone was always getting replaced/eliminated as
# not a celeb, the last person to get eliminated is obviously not a celeb,
# so the person besides the last person (we're keeping track of 2 people)
# has a chance of being a celebrity, if at least one exists actually.
celeb = None
if u == n:
celeb = v
else:
celeb = u
eliminated = False
for person in range(n):
if person == celeb:
continue
if matrix[celeb][person] or not matrix[person][celeb]:
eliminated = True
if not eliminated:
return celeb
return None
def main():
matrix = [[random.randint(0, 1)]*5 for i in range(5)]
for i in range(random.randint(0, len(matrix) - 1)):
for j in range(len(matrix)):
matrix[j][i] = 1
matrix[i][j] = 0
for i in range(len(matrix)):
print matrix[i]
celeb = celebrity(matrix)
print "Celebrity:", celeb
if __name__ == "__main__":
main()
Performance
The elimination phase requires exactly − 1 checks, since each check reduces the size of
the list by 1. In the verification phase, we perform − 1 checks for the person , and also
check remaining − 1 persons once. This phase requires at the most 2( − 1) checks,
possibly fewer is is not a celebrity. So the total number of checks is 3( − 1).
Time Complexity: O( ).
Space Complexity: O(1).
The first insight is to identify which rectangles to be considered for the solution: those
which cover a contiguous range of the input histogram and whose height equals the
minimum bar height in the range (rectangle height cannot exceed the minimum height in
the range and there’s no point in considering a height less than the minimum height
because we can just increase the height to the minimum height in the range and get a
better solution). This greatly constrains the set of rectangles we need to consider. Formally,
we need to consider only those rectangles with ℎ = − + 1 (0 = = < ) and ℎ ℎ
= ( [ . . ]).
At this point, we can directly implement this solution.
def findMin(A, i, j):
min = A[i]
while i <= j:
if min > A[i]:
min = A[i]
i=i+1
return min
def largestHistrogram(A):
maxArea = 0
print A
for i in range(len(A)):
for j in range(i, len(A)):
minimum_height = A[i]
minimum_height = findMin(A, i, j)
maxArea = max(maxArea, (j-i+1) * minimum_height)
return maxArea
A = [6, 2, 5, 4, 5, 1, 6]
print "largestRectangleArea: ", largestHistrogram(A)
There are only choices for i and j. If we naively calculate the minimum height in the
range [i..j], this will have time complexity O( ).
Instead, we can keep track of the minimum height in the inner loop for j, leading to the
following implementation with O( ) time complexity and O(1) auxiliary space complexity.
def largestHistrogram(A):
maxArea = 0
for i in range(len(A)):
minimum_height = A[i]
for j in range(i, len(A)):
minimum_height = min(minimum_height, A[j])
maxArea = max(maxArea, (j-i+1) * minimum_height)
return maxArea
1 . 3 0 L a r g e s t r e ct a ng l e u n d e r h is t og r a m
In tr o du ctio n to A lg or i thm s A n aly si s | 51
A = [6, 2, 5, 4, 5, 1, 6]
print "largestRectangleArea: ", largestHistrogram(A)
Example
As an example, consider the following histogram. Let us process the elements (bars) one by
one.
Stack= {}
1 2 3 4 5 6 7
Initially, stack is empty. So, insert the first bar on to the stack.
Stack= {1}
1 2 3 4 5 6 7
Second bar has more height than the first one. So, insert it on to the stack.
Stack= {1, 2}
1 2 3 4 5 6 7
Third bar has lesser height than the top of the stack. Pop 2 from the stack. Again the third
bar’s height is lesser than the top of the stack. Hence, pop 1 from the stack. Now, the stack
is empty. So, insert the third bar on to the stack.
1 . 3 0 L a r g e s t r e ct a ng l e u n d e r h is t og r a m
In tr o du ctio n to A lg or i thm s A n aly si s | 52
Stack= {3}
1 2 3 4 5 6 7
Fourth bar has more height than the top of the stack. So, insert it on to the stack.
Stack= {3, 4}
1 2 3 4 5 6 7
Fifth bar has more height than the top of the stack. So, insert it on to the stack.
Stack= {3, 4, 5}
1 2 3 4 5 6 7
Sixth bar has lesser height than the top of the stack. Pop 5 from the stack. Sixth bar has
more height than the top of the stack. So, insert it on to the stack.
Stack= {3, 4, 6}
1 2 3 4 5 6 7
Seventh bar has more height than the top of the stack. So, insert it on to the stack.
Stack= {3, 4, 6, 7}
1 2 3 4 5 6 7
With this strategy, we are able to keep track of the increasing heights of the bars. To get
the maximum rectangle, we would need to look at the maximum area seen so far instead
of just looking at the top of the stack.
This way, all subproblems are finished when the stack becomes empty, or its top element
is less than or equal to the new element, leading to the actions described above. If all
elements have been processed, and the stack is not yet empty, we finish the remaining
subproblems by updating the maximum area with respect to the elements at the top.
def largest_rectangle_area(self, height):
stack=[]; i=0; maxArea=0
while i<len(height):
if stack==[] or height[i]>height[stack[-1]]:
stack.append(i)
else:
curr=stack.pop()
width=i if stack==[] else i-stack[-1]-1
1 . 3 0 L a r g e s t r e ct a ng l e u n d e r h is t og r a m
In tr o du ctio n to A lg or i thm s A n aly si s | 53
maxArea=max(maxArea,width*height[curr])
i-=1
i+=1
while stack!=[]:
curr=stack.pop()
width=i if stack==[] else len(height)-stack[-1]-1
maxArea=max(maxArea,width*height[curr])
return maxArea
At the first impression, this solution seems to be having O( ) complexity. But if we look
carefully, every element is pushed and popped at the most once, and in every step of the
function at least one element is pushed or popped. Since the amount of work for the
decisions and the update is constant, the complexity of the algorithm is O( ) by amortized
analysis.
Space Complexity: O( ) [for stack].
1 . 3 1 N e ga t i o n t e c hn i q ue
In tr o du ctio n to A lg or i thm s A n aly si s | 54
1 1
2 1
3 1
Now if we try inserting 2, since the counter value of 2 is already 1, we can say the element
has appeared twice.
Time Complexity: O( ). Space Complexity: O( ).
Negation technique
Let us assume that the array elements are positive numbers and all the elements are in
the range 0 to − 1. For each element [ ], go to the array element whose index is [ ]. That
means select [ [ ]] and mark - [ [ ]] (negate the value at [ [ ]]). Continue this process
until we encounter the element whose value is already negated. If one such element exists,
then we say duplicate elements exist in the given array. As an example, consider the array,
= {3, 2, 1, 2, 2, 3}.
Initially, 3 2 1 2 2 3
0 1 2 3 4 5
0 1 2 3 4 5
0 1 2 3 4 5
0 1 2 3 4 5
0 1 2 3 4 5
At step-4, observe that [ ( [3])] is already negative. That means we have encountered
the same value twice.
1 . 3 1 N e ga t i o n t e c hn i q ue
In tr o du ctio n to A lg or i thm s A n aly si s | 55
import math
def check_duplicates_negation_technique(A):
for i in range(0,len(A)):
if(A[abs(A[i])] < 0):
print("Duplicates exist:", A[i])
return
else:
A[A[i]] = - A[A[i]]
print("No duplicates in given array.")
A = [3,2,1,2,2,3]
check_duplicates_negation_technique(A)
Time Complexity: O( ). Since only one scan is required. Space Complexity: O(1).
Notes:
This solution does not work if the given array is read only.
This solution will work only if all the array elements are positive.
If the elements range is not in 0 to − 1 then it may give exceptions.
6 8
Minimum depth: 2
Depth: 3
2 7
3
Depth: 4
Recursive solution
The algorithm is similar to the algorithm of finding depth (or height) of a binary tree, except
here we are finding minimum depth. One simplest approach to solve this problem would
be by using recursion. But the question is when do we stop it? We stop the recursive calls
when it is a leaf node or .
Algorithm:
Let be the pointer to the root node of a subtree.
If the is equal to , then the minimum depth of the binary tree would be
0.
If the is a leaf node, then the minimum depth of the binary tree would be 1.
If the is not a leaf node and if left subtree of the is None, then find the
minimum depth in the right subtree. Otherwise, find the minimum depth in the
left subtree.
1 . 3 2 M i n i m u m d e p t h o f a b i na r y t r e e
In tr o du ctio n to A lg or i thm s A n aly si s | 56
If the is not a leaf node and both left subtree and right subtree of the are
not , then recursively find the minimum depth of left and right subtree. Let it
be ℎ and ℎ ℎ respectively.
To get the minimum height of the binary tree rooted at root, we will take minimum
of ℎ and ℎ ℎ and 1 for the node.
class Solution:
def minimumDepth(self, root):
# If root (tree) is empty, minimum depth would be 0
if root is None:
return 0
# If root is a leaf node, minimum depth would be 1
if root.left is None and root.right is None:
return 1
# If left subtree is None, find minimum depth in right subtree
if root.left is None:
return self.minimumDepth(root.right)+1
# If right subtree is None, find minimum depth in left subtree
if root.right is None:
return self.minimumDepth(root.left) +1
# Get the minimum depths of left and right subtrees and add 1 for current level.
return min(self.minimumDepth(root.left), self.minimumDepth(root.right)) + 1
# Approach two
class Solution:
def minimumDepth(self, root):
if root == None:
return 0
if root.left == None or root.right == None:
return self.minimumDepth(root.left) + self.minimumDepth(root.right)+1
return min(self.minimumDepth(root.right), self.minimumDepth(root.left))+1
Time complexity: O( ), as we are doing pre order traversal of tree only once.
Space complexity: O( ), for recursive stack space.
1 . 3 2 M i n i m u m d e p t h o f a b i na r y t r e e
In tr o du ctio n to A lg or i thm s A n aly si s | 57
while queue:
current, depth = queue.pop(0)
if current.left is None and current.right is None:
return depth
if current.left:
queue.append((current.left, depth+1))
if current.right:
queue.append((current.right, depth+1))
Time complexity: O( ), as we are doing lever order traversal of the tree only once.
Space complexity: O( ), for queue.
Symmetric question: Maximum depth of a binary tree: Given a binary tree, find its
maximum depth. The maximum depth of a binary tree is the number of nodes along the
shortest path from the root node down to the farthest leaf node. For example, maximum
depth of following binary tree is 4. Careful observation tells us that it is exactly same as
finding the depth (or height) of the tree.
6 8 Depth: 2
Depth: 3
2 7
3 Maximum depth: 4
1 . 3 2 M i n i m u m d e p t h o f a b i na r y t r e e
A l go ri thm D esig n Tech ni qu es | 58
Chapter
Algorithm
Design
Techniques 2
2.1 Introduction
Given an algorithmic problem, where do you even start? It turns out that most of the
algorithms follow several well-known techniques and we call them algorithmic techniques.
Usually we start with brute force approach. If the problem statement is clear, we can get
the solution with brute force approach.
Before solving a new problem, the general tendency is to look for the similarity of the
current problem to other problems for which we have solutions (reducing one problem to
another). This helps us in getting the solution easily.
In this chapter, we will see different ways of classifying the algorithms and in subsequent
chapters we will focus on a few of them (Greedy, Divide and Conquer, Dynamic
Programming).
In this book, we will go over these techniques, which are key to both sequential and parallel
algorithms, and focus on one of them, divide and conquer, which turns out to be
particularly useful for parallel algorithm design. We will also talk about asymptotic cost
analysis and how to analyze algorithms.
Brute force essentially means checking all possible configurations for a problem. It is an
exhaustive search of all the possible solutions for a problem. It is often easy to implement
and will almost definitely find a solution (if there is one). The tradeoff here is the time
required.
The other place where the brute force approach can be very useful is when writing a test
code to check the correctness of more efficient algorithms. Even though inefficient for large
inputs the brute force approach could work well for testing small inputs. The brute force
approach is usually the simplest solution to a problem, but not always.
2.2 Classification
There are many ways of classifying algorithms and a few of them are shown below:
Implementation method
Design method
Other classifications
2 . 1 I n t r o d u c t io n
A l go ri thm D esig n Tech ni qu es | 59
Some problems are suited for recursive and others are suited for iterative. For example, the
problem can be easily understood in recursive implementation. Every
recursive version has an iterative version, and vice versa.
If the parallel algorithms are distributed on to different machines, then we call such
algorithms algorithms.
Deterministic or non-deterministic
algorithms solve the problem with a predefined process, whereas −
algorithms guess the best solution at each step through the use of heuristics.
Exact or approximate
As we have seen, for many problems we are not able to find the optimal solutions. That
means, the algorithms for which we are able to find the optimal solutions are called
algorithms. In computer science, if we do not have the optimal solution, we give
approximation algorithms.
Approximation algorithms are generally associated with NP-hard problems (refer to the
chapter for more details).
Greedy method
ℎ work in stages. In each stage, a decision is made that is good at that
point, without bothering about the future consequences. Generally, this means that some
2 . 3 C l a s s i f i ca t io n b y i m p l e m e nt a t i o n m e t h o d
A l go ri thm D esig n Tech ni qu es | 60
is chosen. It assumes that the local best selection also makes for the
optimal solution.
Dynamic programming
Dynamic programming (DP) and memoization work together. The difference between DP
and divide and conquer is that in the case of the latter there is no dependency among the
subproblems, whereas in DP there will be an overlap of subproblems. By using
memoization [maintaining a table for already solved subproblems], DP reduces the
exponential complexity to polynomial complexity (O( ), O( ), etc.) for many problems.
Linear programming
Linear programming is not a programming language like C++, Java, or Visual Basic. Linear
programming can be defined as:
A linear program consists of a set of variables, a linear objective function indicating the
contribution of each variable to the desired outcome, and a set of linear constraints
describing the limits on the values of the variables. The to a linear program is a set
of values for the problem variables that results in the best -- -- value of
the objective function and yet is consistent with all the constraints.
Formulation is the process of translating a real-world problem into a linear program. Once
a problem has been formulated as a linear program, a computer program can be used to
solve the problem. In this regard, solving a linear program is relatively easy. The hardest
part about applying linear programming is formulating the problem and interpreting the
solution.
2 . 4 C l a s s i f i ca t io n b y d es i g n m et h o d
A l go ri thm D esig n Tech ni qu es | 61
Classification by complexity
In this classification, algorithms are classified by the time they take to find a solution based
on their input size. Some algorithms take linear time complexity (O( )) and others take
exponential time, and some never halt. Note that some problems may have multiple
algorithms with different complexities.
Randomized algorithms
A few algorithms make choices randomly. For some problems, the fastest solutions must
involve randomness. Example: Quick sort.
Note: In the next few chapters we discuss the Greedy, Divide and Conquer, and Dynamic
Programming] design methods. These methods are emphasized because they are used more
often than the other methods to solve problems.
2 . 5 O t h e r c l a ss i f i c a t i o n s
Recu r si on an d Backtr acki n g | 62
Chapter
Recursion
and
Backtracking 3
3.1 Introduction
In this chapter, first we would have a quick look at program execution—how Python runs
program. Then, we will look at one of the important topics, “ ”, which will be used
in almost every chapter, and also its relative “ ”.
Heap Segment
Stack Segment
Data Segment
Static Segment
Code Segment
Static segment
As shown above, the static storage is divided into two parts: segment and
segment.
Code segment
In this part, the programs code is stored. This will not change throughout the execution of
the program. In general, this part is made read-only and protected. Constants may also be
placed in the static area depending on their type.
3 . 1 I n t r o d u c t io n
Recu r si on an d Backtr acki n g | 63
Data segment
In simple terms, this part holds the global data. In this part, the program’s static data
(except code) is stored. In general, this part is editable (like global variables and static
variables come under this category). This includes the following:
Global variables
Numeric and string-valued constant literals
Local variables that retain value between calls (e.g., static variables)
Stack segment
If a language supports recursion, then the number of instances of a variable that may exist
at any time is unlimited (at least theoretically). In this case, static allocation is not useful.
As an example, let us assume that a function () is called from another function (). In the
code below, the () has a local variable . After executing (), if () tries to get ,
then it should be able to get its old value. That means, it needs a mechanism for storing
the current state of the function, so that once it comes back from calling function it restores
that context and uses its variables.
def A():
count=10
B()
count=count + 20
. . .. . .
def B():
b=0
. . .. . .
To solve these kinds of issues, stack allocation is used. When we call a , push a
new activation record (also called a frame) onto the run-time stack, which is particular to
the . In the next section, we will have a look at activation records with a detailed
example.
Heap segment
If we want to increase the temporary space dynamically, then the and allocation
methods are not enough. We need a separate allocation method for dealing with these kinds
of requests. strategy addresses this issue.
Heap allocation method is required for dynamically allocated pieces of linked data
structures and dynamically resized objects. Heap is an area of memory which is allocated
dynamically. Like a stack, it may grow and shrink during runtime.
But unlike a stack, it is not a (Last In First Out) which is more complicated to manage.
In general, all programming languages implementation will have both heap-allocated and
stack allocated memory.
3 . 3 P r o g r a m e xe c u t io n
Recu r si on an d Backtr acki n g | 64
3 . 3 P r o g r a m e xe c u t io n
Recu r si on an d Backtr acki n g | 65
1 def function1():
2 print("function1 line 1")
3 print("function1 line 2")
4 print("function1 line 3")
5
6 def function2(): __main__
7 print("function2 line 1") COMPLETED
8 print("function2 line 2")
9 print("function2 line 3") Runtime stack
10
11 def function3():
12 print("function3 line 1")
13 function2() # function3 line 2
14 function1() # function3 line 3
15 print("function3 line 4")
16
17 def test():
18 function3() # test line 1
19
20 test() ####
3 . 3 P r o g r a m e xe c u t io n
Recu r si on an d Backtr acki n g | 66
19
20 test() ####
3 . 3 P r o g r a m e xe c u t io n
Recu r si on an d Backtr acki n g | 67
3 . 3 P r o g r a m e xe c u t io n
Recu r si on an d Backtr acki n g | 68
6
7 def test():
8 v=4
9 function(v, v+1) # test line 1
10
11 test() ####
12
When calling a function with parameters, the formal parameters become local variables on
the stack that are initialized with the values of the actual parameters. For example, when
( , + 1) is called, the values of the actual parameters , + 1 are 4 and 5, so those
are the values that are copied into the formal parameters of function, namely the variables
a and b. (Try stepping through the function call at line 9, and see how a and b appear on
the stack, with the values 4 and 5 inside.)
Returning from a function pops all variables that are local to that function—that is, we
keep popping until we've popped the topmost call frame. As a reminder, the number inside
each call frame also tells us what line number is to be test back to in the caller—the
function that called the one we are returning from.
As in previous section, we are simplifying things by not showing the call frames for the
print function. In fact, the print function will place a call frame on the stack—along with
its formal parameters and any local variables it uses. Since they are all removed as soon
as print does its work and returns, we can safely leave out those details from the
animation—but it is important to keep in mind that we have simplified things a little bit
from what happens in reality.
In this example, all the variables have different names— , , , —so it is easy to see that
they are all different.
3 . 4 W h a t i s r e cu r s i o n ?
Recu r si on an d Backtr acki n g | 69
We can see that smaller dolls fit into the bigger Russian dolls, until the one that is the
smallest which cannot contain another.
What do Russian nesting dolls have to do with ℎ ? Just as one Russian doll has
within it a smaller Russian doll, which has an even smaller Russian doll within it, all the
way down to a tiny Russian doll that is too small to contain another, we'll see how to design
an algorithm to solve a problem by solving a smaller instance of the same problem, unless
the problem is so small that we can just solve it directly. We call this technique .
It is important to ensure that the recursion terminates. Each time the function calls itself
with a slightly simpler version of the original problem, the sequence of smaller problems
must eventually converge on the base case.
Recursion is the most useful for tasks that can be defined in terms of similar subtasks. For
example, sort, search, and traversal problems often have simple recursive solutions.
3 . 5 W h y r e c u r s i o n?
Recu r si on an d Backtr acki n g | 70
3.7 Example
As an example, consider the factorial function. We indicate the factorial of by !. ! is the
product of all integers 1 through . For example, 5! = 5 × 4 × 3 × 2 × 1.
You might wonder why we would possibly care about the factorial function. It is very useful
when we are trying to count the different orders which are used to arrange things. For
example, how many different ways can we arrange things? We have choices for the first
thing. For each of these choices, we are left with − 1 choices for the second thing, so
that we have × ( − 1) choices for the first two things, in order.
Now, for each of these first two choices, we have n-2 choices for the third thing, giving us
× ( − 1) × ( − 2) choices for the first three things, in order, and so on, until we get down
to just two things remaining, and then just one thing remaining. So, we have × ( −
1) × ( − 2) . . . 2 × 1 ways to arrange things in order. And that product is just ! (
factorial).
The definition of recursive factorial looks like:
! = 1, if = 0 or 1
! = ∗ ( − 1)! if > 1
This definition can easily be converted to recursive implementation. Here the problem is
determining the value of !, and the subproblem is determining the value of ( − )!. In the
recursive case, when is greater than 1, the function calls itself to determine the value
of ( − )! and multiplies that with . The recursive (or general) case is where the magic
happens. This is where we feed the problem back into itself, where the function calls itself.
In the base case, when is 0 or 1, the function simply returns 1. The base case is the part
of the function that stops the recursion. It's generally something we already know, so it can
be met without making any more recursive calls. Without a base case, the recurse function
will continue forever.
3.7 Example
Recu r si on an d Backtr acki n g | 71
def print_func(n):
if n == 0: # this is the terminating base case
return 0
else:
print n
return print_func(n-1) # recursive call to itself again
print(print_func(4))
For this example, if we call the print function with n=4, visually our memory assignments
may look like:
print_func(4
)
print_func(3
)
Returns 0 print_func(2
)
print_func(1
Returns 0 )
print_func(0
Returns 0 to main function Returns 0
)
Returns 0
Now, let us consider our factorial function. The visualization of factorial function with =
4 will look like:
4!
4×3!
3×2!
4*6=24 is returned 2×1!
3*2=6 is returned 1
2*1=2 is returned
Returns 24 to main function Returns 1
Recursive algorithms
Terminates when a base case is reached.
Each recursive call requires extra space on the stack frame (memory).
If we get infinite recursion, the program may run out of memory and result in stack
overflow.
Solutions to some problems are easier to formulate recursively.
Iterative algorithms
Terminates when a condition is proven to be false.
Each iteration does not require extra space.
An infinite loop could loop forever since there is no extra memory being created.
Iterative solutions to a problem may not always be as obvious as a recursive
solution.
3 . 9 R e c u r s i on v e r s us I t e ra t io n
Recu r si on an d Backtr acki n g | 72
Largest disk
3 . 1 0 N o t es o n r ec u r s i o n
Recu r si on an d Backtr acki n g | 73
Example
Following is a sequence of representations for solving a Tower of Hanoi puzzle with three
disks. As a first step, move the top disk of the first tower to the second tower.
3 . 1 2 T o w e r s o f H a no i
Recu r si on an d Backtr acki n g | 74
This completes the puzzle. This presentation shows that a puzzle with 3 disks has taken
2 − 1 = 7 steps. A Tower of Hanoi puzzle with n disks can be solved in minimum 2 −
1 steps.
Algorithm
To give an algorithm for the Tower of Hanoi, first we need to understand how to solve this
puzzle with lesser number of disks, say 1 or 2. We mark three towers with name, ,
and (only to help moving the disks). If we have only one disk, then it
can easily be moved from source to destination tower.
If we have 2 disks:
3 . 1 2 T o w e r s o f H a no i
Recu r si on an d Backtr acki n g | 75
So now, we are in a position to design an algorithm for the Tower of Hanoi with more than
two disks. We divide the stack of disks into two parts. The largest disk ( disk) is in one
part and all the other ( − 1) disks are in the second part.
Our ultimate aim is to move disk from source to destination and then put all the other
( − 1) disks onto it. We can imagine to apply the same in a recursive way for all given set
of disks.
Move the top − 1 disks from to tower,
Move the disk from to tower,
Move the − 1 disks from tower to tower.
Transferring the top − 1 disks from to tower can again be thought of as
a fresh problem and can be solved in the same manner. Once we solve with
three disks, we can solve it with any number of disks with the above algorithm.
def move_tower(numberOfDisks, fromTower, toTower, withTower):
if numberOfDisks >= 1:
move_tower(numberOfDisks-1, fromTower, withTower, toTower)
move_disk(fromTower, toTower)
move_tower(numberOfDisks-1, withTower, toTower, fromTower)
def move_disk(fromTower,toTower):
print("Moving disk from ", fromTower, " to ", toTower)
move_tower(3,"Source","Destination","Auxiliary")
ℎ
3.13 Finding the odd natural number
: Given a positive integer , find odd natural number.
Note here that this can be solved very easily by simply outputting 2 × ( − 1) + 1 for a
given . The purpose here, however, is to illustrate the basic idea of recursion rather than
solving the problem.
Algorithm
Algorithm: Odd(positive integer k)
Input: k, a positive integer
Output: k-th odd natural number (the first odd being 1)
if k = 1, then return 1;
else return Odd(k-1) + 2.
Here the computation of Odd(k) is reduced to that of Odd for a smaller input value, that is
Odd(k-1). Odd(k) eventually becomes Odd(1) which is 1 by the first line. For example, to
compute Odd(3), Odd(k) is called with k = 2. In the computation of Odd(2), Odd(k) is called
with k = 1. Since Odd(1) = 1, 1 is returned for the computation of Odd(2), and Odd(2) =
Odd(1) + 2 = 3 is obtained. This value 2 for Odd(2) is now returned to the computation of
Odd(3), and Odd(3) = Odd(2) + 2 = 5 is obtained.
def Odd(k):
if k == 1:
return 1
else:
return Odd(k-1) + 2
print Odd(3)
Time Complexity: O( ).
Space Complexity: O( ) for recursive stack.
Algorithm
Algorithm: power_of_2(natural number k)
Input: k, a natural number
Output: power of 2
if k = 0, then return 1;
else return 2*power_of_2(k - 1)
Here the computation of power_of_2(k) is reduced to that of power_of_2 for a smaller input
value, that is power_of_2(k-1). power_of_2(k) eventually becomes power_of_2(0) which is 1
by the first line. For example, to compute power_of_2(3), power_of_2(k) is called with k = 2.
This value 4 for power_of_2(2) is now returned to the computation of power_of_2(3), and
power_of_2(3) = 2 * power_of_2(2) = 8 is obtained.
def power_of_2(k):
if k == 0:
return 1
else:
return 2*power_of_2(k-1)
print power_of_2(3)
Time Complexity: O( ).
Space Complexity: O( ) for recursive stack.
Algorithm
Algorithm: linear_search(A, i, j, k)
Input: A is an array, i and j are positive integers, i j, \
and k is the key to be searched for in A.
Output: If k is in A between indexes i and j, then output its index, else output -1.
if i ≤ j , then {
if A(i) = k, then return i;
else return linear_search(A, i+1, j, k)
}
else return -1
Here the computation of linear_search(A, i, j, k) is reduced to that of linear_search for a
next input value, that is linear_search(A, i+1, j, k). For example, to search for 5 in the given
array [3, -1, 5, 10, 9, 19, 14, 12, 8], linear_search(A, i, j, k) is called with i=0, j =8, and k =
5.
Algorithm
Algorithm: is_sorted(A)
Input: A is an array.
3 . 1 5 S e a r c h i n g fo r a n e le m e n t i n a n a r r a y
Recu r si on an d Backtr acki n g | 78
Output: If the elements of A are increasing order return True else return False.
if len(A) = 1 then return True
else
# Check if first two elements are in increasing order and
# recursively call for is_sorted(A[1:])
return A[0] <= A[1] and is_sorted(A[1:])
Here the computation of is_sorted(A) is reduced to that of is_sorted for a next input value,
that is is_sorted(A[1:]). For example, to check for sortedness of the given array [127, 220,
246, 277, 321, 454, 534, 565, 933], is_sorted(A) is called.
In the computation of is_sorted(A), is_sorted(A) is called with subarray A[1:] as len(A) is not
equal to 1 and first two elements A[0] and A[1] are in increasing order. And in the
computation of is_sorted(A[1:]), is_sorted(A) is called with subarray A[2:] as len(A[1:]) is not
equal to 1 and first two elements of subarray A[1:], A[1] and A[2], are in increasing order.
The process continues till the last element of the array which will have the subarray size
equal to 1. If the subarray size becomes 1, it returns True. While performing the recursive
operations, if the first elements of the subarray are not in the increasing order, it will False
and same value will be passed back to all the recursive calls till the main function.
def is_sorted(A):
# Base case
if len(A) == 1:
return True
else:
return A[0] <= A[1] and is_sorted(A[1:])
A = [127, 220, 246, 277, 321, 454, 534, 565, 933]
print(is_sorted(A))
Time Complexity: O( ).
Space Complexity: O( ) for recursive stack space.
3 . 1 7 B a s i cs o f r e cu r r e n c e r e la t i o ns
Recu r si on an d Backtr acki n g | 79
= [ ( − 3) + 2( − 2) − 1] + 2( − 1) + 2 − 2 # ( − 2) = ( − 3) + 2( − 2) − 1
= ( − 3) + 2( − 2) + 2( − 1) + 2 − 3
...
= ( − ) + 2( − + 1) +. . . + 2 −
...
= ( − ) + 2( − + 1) +. . . + 2 −
= 0 + 2 + 4 +. . . + 2 – # because (0) = 0
= 2 + 4 +. . . + 2 −
= × ×( ) ( )
– # arithmetic progression formula 1+. . . + =
=
Applications
Recurrence relations are a fundamental mathematical tool since they can be used to
represent mathematical functions/sequences that cannot be easily represented non-
recursively. An example is the sequence. Another one is the famous ′
function. Here we are mainly interested in applications of recurrence relations in the design
and analysis of algorithms.
3 . 1 7 B a s i cs o f r e cu r r e n c e r e la t i o ns
Recu r si on an d Backtr acki n g | 80
3 . 1 7 B a s i cs o f r e cu r r e n c e r e la t i o ns
Recu r si on an d Backtr acki n g | 81
4 15 7 40 None
Head
3 . 1 8 R e v e r s i n g a s i ng l y l i n k e d l i s t
Recu r si on an d Backtr acki n g | 82
Arrays overview
One memory block is allocated for the entire array to hold the elements of the array. The
array elements can be accessed in constant time by using the index of the particular
element as the subscript.
3 2 1 2 2 3
Index 0 1 2 3 4 5
Advantages of arrays
Simple and easy to use
Faster access to the elements (constant access)
Disadvantages of arrays
Preallocates all needed memory up front and wastes memory space for indices in
the array that are empty.
Fixed size: The size of the array is static (specify the array size before using it).
One block allocation: To allocate the array itself at the beginning, sometimes it
may not be possible to get the memory for the complete array (if the array size is
big).
Complex position-based insertion: To insert an element at a given position, we
may need to shift the existing elements. This will create a position for us to insert
3 . 1 8 R e v e r s i n g a s i ng l y l i n k e d l i s t
Recu r si on an d Backtr acki n g | 83
the new element at the desired position. If the position at which we want to add an
element is at the beginning, then the shifting operation is more expensive.
Dynamic arrays
Dynamic array (also called , , , or ) is a
random access, variable-size list data structure that allows elements to be added or
removed. One simple way of implementing dynamic arrays is to initially start with some
fixed size array. As soon as that array becomes full, create the new array double the size of
the original array. Similarly, reduce the array size to half if the elements in the array are
less than half.
3 . 1 8 R e v e r s i n g a s i ng l y l i n k e d l i s t
Recu r si on an d Backtr acki n g | 84
O( ), if array is not
Deletion in the
O( ) full (for shifting the O( )
middle
elements)
O( ) (for
Space wasted 0 O( )
pointers)
4 15 7 40 None
Head
Following is a type declaration for a linked list of integers:
#Node of a Singly Linked List
class Node:
#constructor
def __init__(self):
self.data = None
self.next = None
#method for setting the data field of the node
def set_data(self,data):
self.data = data
#method for getting the data field of the node
def get_data(self):
return self.data
#method for setting the next field of the node
def set_next(self,next):
self.next = next
#method for getting the next field of the node
def get_next(self):
return self.next
#returns true if the node points to another node
def has_next(self):
return self.next != None
3 . 1 8 R e v e r s i n g a s i ng l y l i n k e d l i s t
Recu r si on an d Backtr acki n g | 85
count = count + 1
current = current.get_next()
return count
Time Complexity: O( ), for scanning the list of size .
Space Complexity: O(1), for creating a temporary variable.
Recursive solution
We can find it easier to start from the top down, by asking and answering tiny questions:
What is the reverse of None (the empty list)?
None.
What is the reverse of a one element list?
The element itself.
What is the reverse of an element list?
Divide the list into two parts - first node and the rest of the linked list.
Call reverse, for the rest of the linked list.
Link the rest to the first.
Fix head pointer.
# node of a singly linked list
class Node:
#constructor
def __init__(self, data):
self.data = data
self.next = None
def print_list(head):
while head is not None:
print "-->", head.data
head = head.next
def reverse_list_recursive(head):
if head is None or head.next is None:
return head
p = head.next
head.next = None
revrest = reverse_list_recursive(p)
p.next = head
return revrest
node1, node2, node3, node4, node5 = Node(1), Node(2), Node(3), Node(4), Node(5)
node1.next = node2
node2.next = node3
node3.next = node4
node4.next = node5
head = node1
print_list(head)
head = node1
head = reverse_list_recursive(head)
print_list(head)
Time Complexity: O( ).
Space Complexity: O( ), for recursive stack.
3 . 1 8 R e v e r s i n g a s i ng l y l i n k e d l i s t
Recu r si on an d Backtr acki n g | 86
Iterative solution
This algorithm reverses this singly linked list in place, in O( ). The function uses three
pointers to walk the list and reverse link direction between each pair of nodes. Three
references are needed to reverse a list: previous node, current node, and next node.
… 15 7 40 …
To reverse a node, we have to store previous element. We can use the simple following
statement to reverse the current element's direction:
current.next = previous
However, to iterate over the list, we have to store next node before the execution of the
statement above because as reversing the current element's next reference, we don't know
the next element anymore, that's why a third reference is needed.
# node of a singly linked list
class Node:
#constructor
def __init__(self, data):
self.data = data
self.next = None
def print_list(head):
while head is not None:
print "-->", head.data
head = head.next
# iterative version
def reverse_list_iterative(head):
prev = None
current = head
while(current is not None):
nextNode = current.next
current.next = previous
previous = current
current = nextNode
head = previous
return head
node1, node2, node3, node4, node5 = Node(1), Node(2), Node(3), Node(4), Node(5)
node1.next = node2
node2.next = node3
node3.next = node4
node4.next = node5
head = node1
print_list(head)
head = node1
head = reverse_list_iterative(head)
print_list(head)
Time Complexity: O( ).
Space Complexity: O(1).
3 . 1 8 R e v e r s i n g a s i ng l y l i n k e d l i s t
Recu r si on an d Backtr acki n g | 87
< < ℎ
If is smaller than the number of elements in the left subtree, the smallest element
must belong to the left subtree. If is larger, then the smallest element is in the right
subtree.
Example
In the following BSTs, the left binary tree is a binary search tree and the right binary tree
is not a binary search tree (at node 5 it’s not satisfying the binary search tree property).
Element 2 is less than 5 but on the right subtree of 5.
6 5
3 8 3 6
1 4 7 9 1 4 2 8
result = kth_smallest_in_BST(node1, 5)
print result.data
Time Complexity: O( ).
Space Complexity: O(1).
root = BSTNode(4)
input = [3, 2, 1, 6, 5, 8, 7, 9, 10, 11]
for x in input:
insert_bst(root, BSTNode(x))
print kth_smallest_in_BST(root, 1)
print kth_smallest_in_BST(root, 5)
print kth_smallest_in_BST(root, 10)
Time Complexity: This takes O(depth of node) time, which is O(log n) in the worst case on
a balanced BST, O( ) in the worst-case on an unbalanced BST, or O( ) on average for a
random BST.
Space Complexity: A BST requires O( ) storage, and it takes another O( ) to store the
information about the number of elements. All BST operations take O(depth of node) time,
and it takes O(depth of node) extra time to maintain the "number of elements" information
for insertion, deletion or rotation of nodes. Therefore, storing information about the number
of elements in the left subtree keeps the space and time complexity of a BST.
2 8
1 9
Consider the following simple program. For each node, check if the node on its left is
smaller and check if the node on its right is greater. This approach is wrong as this will
return true for binary tree below. Checking only at current node is not enough and the
binary search tree property should be satisfied for every pair of nodes.
'''Binary Search Tree Node'''
class BSTNode:
def __init__(root, data):
root.left = None
root.right = None
root.data = data
def is_BST(root):
if root is None:
return True
# false if left is > than root
if root.left is not None and root.left.data > root.data:
return False
# false if right is < than root
if root.right is not None and root.right.data < root.data:
return False
# false if, recursively, the left or right is not a BST
if not is_BST(root.left) or not is_BST(root.right):
return False
# passing all that, it's a BST
return True
# create BST
node1, node2, node3, node4, node5 = \
BSTNode(6), BSTNode(2), BSTNode(8), BSTNode(1), BSTNode(9)
node1.left, node1.right = node2, node3
node2.left, node2.right = node4, node5
root = node1
print is_BST(root) # returns True but it should be False
Time Complexity: O( ), but algorithm is incorrect.
Space Complexity: O( ), for runtime stack space.
3 . 2 1 C h e c k i n g w h et h e r t he b i n a r y t r e e i s a BS T o r n ot
Recu r si on an d Backtr acki n g | 92
assumed that we have helper functions _ () and _ () that return the min or
max integer value from a non-empty tree.
'''Binary Search Tree Node'''
class BSTNode:
def __init__(root, data):
root.left = None
root.right = None
root.data = data
def find_min(root):
current = root
if current is None:
return None
while current.left is not None:
current = current.left
return current
def find_max(root):
current = root
if current is None:
return None
while current.right is not None:
current = current.right
return current
# create BST
node1, node2, node3, node4, node5 = \
BSTNode(6), BSTNode(2), BSTNode(8), BSTNode(1), BSTNode(9)
node1.left, node1.right = node2, node3
node2.left, node2.right = node4, node5
root = node1
print is_BST(root) # returns False
3 . 2 1 C h e c k i n g w h et h e r t he b i n a r y t r e e i s a BS T o r n ot
Recu r si on an d Backtr acki n g | 93
# create BST
node1, node2, node3, node4, node5 = \
BSTNode(9), BSTNode(2), BSTNode(10), BSTNode(1), BSTNode(6)
node1.left, node1.right = node2, node3
node2.left, node2.right = node4, node5
root = node1
print is_BST(root) # returns True
Time Complexity: O( ). In a BST, we spend O( ) time (in the worst case) for finding the
maximum element in the left subtree and O( ) time for finding the minimum element in
the right subtree. In the above algorithm, for every element we keep finding the maximum
element in the left subtree and minimum element in right subtree which will cost
O(2 )≈O( ). Since there are such elements, the overall time complexity is O( ).
Space Complexity: O( ) for runtime stack space.
3 . 2 1 C h e c k i n g w h et h e r t he b i n a r y t r e e i s a BS T o r n ot
Recu r si on an d Backtr acki n g | 94
3 . 2 2 C o m b i n a t i o n s : n c h oo s e m
Recu r si on an d Backtr acki n g | 95
!
= =
!( − )!
For example, from the debate team with a membership of 10 ( = 10) we are to select three
members to participate in the competition next week ( = 3). In how many ways can this
task be accomplished? Since selecting Prem, then Ram, then Rahim would give the same
results as Ram, then Rahim, and then Prem (the competition team would still be the same)
we use the combination formula:
!
= 10 =
( − )! !
10!
=
(10 − 7)! 3!
10 × 9 × 8 × 7 × 6 × 5 × 4 × 3 × 2 × 1
=
7×6×5×4×3×2×1×3×2×1
10 × 9 × 8 × 7 × 6 × 5 × 4 × 3 × 2 × 1
=
7×6×5×4×3×2×1×3×2×1
10 × 9 × 8
=
3×2×1
720
=
6
= 120
3 . 2 2 C o m b i n a t i o n s : n c h oo s e m
Recu r si on an d Backtr acki n g | 96
1, =0
( , ) = 1, =
( −1, )+ ( −1, − 1), ℎ
This formulation does not require the computation of factorials. In fact, the only
computation needed is addition.
This can be converted to code easily with recursion as shown below:
def n_choose_m(n , m):
if m == 0 or m == n :
return 1
return n_choose_m(n-1 , m-1) + n_choose_m(n-1 , m) # recursive call
print(n_choose_m(5,2)) # 10
This will work for non-negative integer inputs and with . However, this ends up
repeating many instances of recursive calls, and being very slow.
The problem here with efficiency is the same as with Fibonacci. Many recursive calls to the
function get recomputed many times. To calculate (5,2) recursively we call (4,1) + (4,2)
which calls (3,0) + (3,1) and (3,1) + (3,2). This continues and in the end we make the
following calls these many times.
The execution tree for (5,2) is shown below:
C(5,2)
C(4,1) C(4,2)
Performance
With recursive implementation, the recurrence for the running time can be given as:
O(1), =0
T( ,) = O(1), =
T( − 1, ) + T( − 1, − 1), ℎ
This is very similar to the above recursive definition. In fact, we can show that ( , ) =
O( ), which is not a very good running time at all. Again the problem with the direct
recursive implementation is that it does far more work than is needed because it solves the
same subproblems many times.
3 . 2 2 C o m b i n a t i o n s : n c h oo s e m
Recu r si on an d Backtr acki n g | 97
!
m elements from a set n elements. The solution can be achieved by generating ( )! !
combinations, each of length , where is the number of elements in the given array.
For example, how can the numbers 1 to 5 be taken in sets of three (that is, what is 5 choose
3)?
1 2 3
1 2 4
1 2 5
1 3 4
1 3 5
1 4 5
2 3 4
2 3 5
2 4 5
3 4 5
Algorithm
Following pseudocode would generate all combinations each of length between 1 and
where is the number of elements in the input.
1. For each of the input characters
a. Put the current character in output string and print it.
b. If there are any remaining characters, generate combinations with those
remaining characters.
def combinations(elems, s, idx, result):
for i in range(idx, len(elems)):
s+=elems[i]
result.append(s)
combinations(elems, s, i+1, result)
s=s[0:-1]
result = []
combinations('123', '', 0, result)
print result
Example
Let us see how it works for the input string ‘123’. The initial call to the function
is ( = ′123′, = ′′, = 0, ).
Now, for each of the character in input string, add it to the result list of combinations and
recursively call the function with the remaining characters. The first character of the input
is 1. Hence, add it to s, and then add s to the result list.
result: 1
The remaining characters of the inputs are: 23, and in the code it is being tracked with the
index idx. Hence recursively call the function.
3 . 2 2 C o m b i n a t i o n s : n c h oo s e m
Recu r si on an d Backtr acki n g | 98
For this call, the first character (character pointed by index idx) is 2. Hence, add it to s,
and then add s to the result list.
result 1, 12
The remaining characters of the inputs are: 3. Hence, recursively call the function.
For this call, the first character (pointed by idx) is 3. Hence, add it to s, and then add s to
the result list.
result 1, 12, 123
With the next index idx, recursively call the function.
The current index idx is beyond the length of the input string. Hence, it is the end of the
processing for this recursive call. So, we need to return to the calling function with s to 12.
For this call too, we are done with the recursive call to combinations function as there are
no more characters to add. Hence, return to the calling function with s to 1.
The next possible value for i is 2, and the character at index 2 is 3. Hence, add it to s, and
then add s to the result list.
result 1, 12, 123, 13
3 . 2 2 C o m b i n a t i o n s : n c h oo s e m
Recu r si on an d Backtr acki n g | 99
There are no further characters as the idx is 3. Hence, return to the calling function with
s = 1.
combinations(elems='123', s='', idx=0, result)
At this point, we are done with all the combinations starting with 1.
Next, let us generate the combinations starting with 2. The next possible value for i is 1,
and the character at index 1 is 2. Hence, add it to s, and then add s to the result list.
result 1, 12, 123, 13, 2
The next recursive call would be ( = ′123′, = ′2′, = 2, ).
For this call, the first character (pointed by idx) is 3. Hence, add it to s, and then add s to
the result list.
result 1, 12, 123, 13, 2, 23
The next recursive call would be ( = ′123′, = ′2′, = 2, ).
The current index idx is beyond the length of the input string. Hence it is the end of the
processing for this recursive call. So, we would need to return to the calling function with
s to 2.
combinations(elems='123', s='', idx=0, result)
For this call too, we are done with the recursive call to combinations function as there were
no more characters to add. Hence, return to the calling function with s to ‘’.
At this point, we are done with all the combinations starting with 2.
Next, let us generate the combinations starting with 2. The next possible value for i is 2,
and the character at index 2 is 3. Hence, add it to s, and then add s to the result list.
result 1, 12, 123, 13, 2, 23, 3
The next recursive call would be ( = ′123′, = ′3′, = 3, ).
The current index idx is beyond the length of the input string. Hence it is the end of the
processing for this recursive call. So, we need to return to the calling function with s to ‘’.
At this point, we are done with generating the combinations starting with 3. Since there
are no more characters in , we are done with generating all combinations with all
possible sizes. Hence, the result list would now have all the combinations.
result 1, 12, 123, 13, 2, 23, 3
Performance
Time complexity
!
Running time of the algorithm is O ∑ ( )! !
as we are generating all m-combinations
with lengths between 1 and n (that is, all m-combinations with m from 1 to n). If we generate
!
only one set of m-combinations, the running time of the algorithm is O ( )! !
.
3 . 2 3 S o l v i n g p r o b l e m s b y b r ut e f o r ce
Recu r si on an d Backtr acki n g | 101
force will find it. Similarly, if the set is known to contain all solutions, then all solutions
will eventually be found. Thus, in a nutshell, the application of brute force needs to devise
a way to generate a set that contains solutions, and a way to test each element of the
generated set.
Discard
non-solutions
We describe the basic brute force strategy for solving a problem as follows: generate the
elements of a set that is known to contain solutions -- that is, a superset of the solution
set -- and test each element of that set. To avoid the waste of storing a large number of
elements that are not solutions, we test each candidate as it is generated, and keep only
the solutions. If we regard the test as a filter that passes solutions and discards non-
solutions, this approach can be represented by the diagram.
The superset generator produces the elements of the appropriate superset and passes each
one to the filter. The filter determines whether each element is a solution, adding it to the
list of solutions if it is, and discarding it if it is not. If the goal is to find a single solution,
or if the problem is known to have only one solution, then the filter can shut down the
process after finding the first solution. If, on the other hand, we want all solutions, then
the process must continue until the superset generator has exhausted all possibilities.
Another name for the technique we call 'brute force' is solution by superset.
The brute force strategy can be applied, in some form, to nearly any problem, but it's rarely
an attractive option. Nevertheless, for very small problems, or problems for which no
alternative is known, brute force is sometimes the method of choice.
Root
1: Option-1 2: Option-1
11: Bad end 12: Bad end 21: Good end 22: Bad end
3.24 What is backtracking?
Recu r si on an d Backtr acki n g | 102
Example
Starting at Root, your options are 1 and 2. You choose 1.
At Option-1, your options are 11 and 12. You choose 11.
11 is bad. Go back to 1.
At 1, you have already tried 11, and it failed. Try 12.
12 is bad. Go back to 1.
At 1, you have no options left to try. Go back to Root.
At Root, you have already tried 1. Try 2.
At 2, your options are 21 and 22. Try 21.
21 is good. Congratulations!
Backtracking allows us to deal with situations in which a raw brute-force approach would
explode into an impossible number of options to consider. Backtracking is a sort of refined
brute force. At each node, we eliminate choices that are obviously not possible and proceed
to recursively check only those that have potential.
In this example we drew a picture of a tree. The tree is an abstract model of the possible
sequences of choices we can make. There is also a data structure called a tree, but usually
we don't have a data structure to tell us what choices we have. If we do have an actual tree
data structure, backtracking on it is called depth-first tree searching.
Let's take a situation. Suppose you are standing in front of three tunnels, one of which is
having a bag of gold at its end, but you don't know which one. So you'll try all three. First
go into tunnel 1. If that is not the one, then come out of it, and go into tunnel 2, and again
if that is not the one, come out of it and go into tunnel 3. So basically, in backtracking we
attempt solving a subproblem, and if we don't reach the desired solution, then undo
whatever we did for solving that subproblem, and try solving another subproblem.
What is interesting about backtracking is that we back up only as far as needed to reach
a previous decision point with an as-yet-unexplored alternative. In general, that will be at
the most recent decision point. Eventually, more and more of these decision points will
have been fully explored, and we will have to backtrack further and further. If we backtrack
all the way to our initial state and have explored all alternatives from there, we can conclude
that the particular problem is unsolvable. In such a case, we will have done all the work of
the exhaustive recursion and known that there is no viable solution possible.
Example
Root
1: Option-1 2: Option-1
11: Bad end 12: Bad end 21: Bad end 22: Bad end
Notes on backtracking
Sometimes the best algorithm for a problem is to try all possibilities.
This is always slow, but there are standard tools that can be used to help.
Tools: algorithms for generating basic objects, such as
o binary strings [2 possibilities for -bit string],
o permutations [ !],
!
o combinations !( )!
,
o general strings [ −ary strings of length has possibilities], etc...
Backtracking speeds the exhaustive search by pruning.
3 . 2 5 A l g o r i t h m s w h i c h us e ba c k t r a c k i n g
Recu r si on an d Backtr acki n g | 104
To generate all possible binary sequences of length , first, we need to design a way to
enumerate the solution space. As discussed, recursion uses stack as an auxiliary data
structure to store the activation records. In a stack, the order in which the data arrives is
important.
A pile of plates in a cafeteria is a good example of a stack. The plates are added to the stack
as they are cleaned and they are placed on the top. When a plate is required it is taken
from the top of the stack. The first plate placed on the stack is the last one to be used.
Basically, the thing we put in last is what we pop out first from the stack. It follows the last
in first out (LIFO) or first in last out (FILO) strategy.
Push D Pop D
D top
top top
C C C
B B B
A A A
For example, how do we generate all bit sequences of length two ( = 2)? Let us get started
with the first bit. For this, we have two options: 0, and 1. First, fix the bit 0 by pushing it
to stack, and then in the second attempt, we will fix the bit 1 in the first position.
?
0
Stack
For the second bit too, we have two options: 0, and 1. First, push 0 for the second bit. Now,
the total number of bits in the stack is equal to . Hence, print the sequence.
0
0
Stack
Next, pop 0, and push 1 for the second bit, and then print the sequence.
1
0
Stack
Now, we are done with all bit sequences starting with bit 0. Next, we have to generate all
bit sequences starting with bit 1. So, we need to backtrack to the first bit and repeat the
process.
?
1
Stack
For the second bit, first push bit 0 on to the stack, and print the sequence; and then pop
bit 0, push bit 1, and print the sequence.
0 1
1 1
3 . 2 6 G e n e r a t i n g b i na r y se q ue n c es
Recu r si on an d Backtr acki n g | 105
Stack Stack
We can represent the above solution space with tree as shown below.
Root
Example
Starting at Root, your options are 0 and 1. You choose 0.
For second bit, your options are 0 and 1. You choose 0.
00 is a valid sequence. Print the sequence 00. Go back to 0.
At 0, you have already tried 0. Try 1.
01 is a valid sequence. Print the sequence 01. Go back to 0.
At 0, you have no options left to try. Go back to Root.
At Root, you have already tried 0. Try 1.
For second bit, your options are 0 and 1. Try 0.
10 is a valid sequence. Print the sequence 10. Go back to 1.
At 1, you have already tried 0. Try 1.
11 is a valid sequence. Print the sequence 11. Go back to 1.
At 1, you have no options left to try. Go back to Root.
At Root, you have no options left to try.
Done with generating all binary sequences of length 2!
The next question will be, how do we convert the above discussion to code?
First, we set 0 for the first bit followed by two possibilities for the second bit. Next, we set
1 for the first bit followed by two possibilities for the second bit.
result[0] = “0”
result[1] = “0”
print result
result[1] = “1”
print result
result[0] = “1”
result[1] = “0”
print result
result[1] = “1”
print result
The above code looks very repetitive. We can move the second bit operations to a loop and
iterate through all possibilities (0, and 1 in this case). As, a result the code would be
reduced to:
possibilities = [“0”, “1”]
result[0] = “0”
for secondBit in possibilities:
result[1] = secondBit
print result
result[0] = “1”
for secondBit in possibilities:
result[1] = secondBit
3 . 2 6 G e n e r a t i n g b i na r y se q ue n c es
Recu r si on an d Backtr acki n g | 106
print result
Observing the above code snippet, it clearly tells us that block of code is repeated for the
first bit as well. So, we can further reduce the repetitive code by moving the common code
under a loop.
possibilities = ["0", "1"]
result = [None]*2
for firstBit in possibilities:
result[0] = firstBit
for secondBit in possibilities:
result[1] = secondBit
print result
This looks good for the 2-bit binary sequences. What if we want to generate binary
sequences with bits?
possibilities = ["0", "1"]
result = [None]*n
for firstBit in possibilities
result[0] = firstBit
for secondBit in possibilities:
result[1] = secondBit
for secondBit in possibilities:
result[1] = secondBit
for secondBit in possibilities:
result[1] = secondBit
print result
……
Oh! it really looks odd, right? We have to do something to reduce this repeated code for
each bit position. One possibility would be, converting the above repetitive code to a
recursive function.
def append(x, L):
return [x + element for element in L]
def bit_strings(n):
if n == 0:
return []
if n == 1:
return ["0", "1"]
else:
return (append("0", bit_strings(n-1)) + append("1", bit_strings(n-1)))
print bit_strings(4)
Alternatively:
def bit_strings (n):
if n == 0:
return []
if n == 1:
return ["0", "1"]
return [ bit + bitstring for bit in ["0", "1"] for bitstring in bit_strings (n-1)]
print bit_strings (4)
3 . 2 6 G e n e r a t i n g b i na r y se q ue n c es
Recu r si on an d Backtr acki n g | 107
For the above two problems, backtracking algorithm applied is fairly
straight forward because the calls are not subject to any constraint. We are
not backtracking from an unwanted result. We are merely backtracking to
return to a previous state without filtering out unwanted output.
3.27 Generating −a r y s eq u en c e s
Recu r si on an d Backtr acki n g | 108
1 0 0 0 1
0 1 0 1 1
The diagram below depicts three regions of the matrix; for each region, the component cells
forming the region are marked with an X:
Region-1: X X 0 0 0 Region size: 5
0 X X 0 0
0 0 X 0 1
1 0 0 0 1
0 1 0 1 1
Explanation
We’ll try to think of this problem recursively. Here are some facts that help build the
intuition.
Base case
3 . 2 8 F i n d i n g t h e l a r ge s t i s la n d
Recu r si on an d Backtr acki n g | 109
When you try to write a recursive method, always start from the base cases. So, what are
the base cases?
For any cell at location ( , ) there are 3 possibilities:
( , ) may be out of bounds,
the cell ( , ) may be 0, or
the cell ( , ) may be 1
In the first two cases, the size of the region containing the cell ( , ) is zero because there is
no filled cell. For the last case, we need to go into recursion.
− 1, − 1 − 1, − 1, + 1
, −1 , , +1
+ 1, − 1 + 1, + 1, + 1
To find the number of full cells connected to it requires the evaluation of region_size() for
each of the surrounding eight cells, hence the recursive rule that:
region_size (i,j) = 1 + region_size (i,j-1)
+ region_size (i,j+1)
+ region_size (i+1,j+1)
+ region_size (i+1,j)
+ region_size (i+1,j-1)
+ region_size (i-1,j+1)
+ region_size (i-1,j)
+ region_size (i-1,j-1)
However this is not correct. When evaluating _ ( , − 1) the original ( , ) cell will
be counted again since it is connected to the ( , − 1) cell. Hence, as each cell is visited, it
is marked as having been `visited' and will not be counted again.
Here is what we have so far:
if (the cell is outside the grid) then return 0
if (the cell is 0 or visited) then return 0
else mark the cell, and return 1 + the counts of the cell’s eight neighbors.
_ () calls itself eight times, each time a different neighbor of the current cell is
visited. The cells are visited in a clockwise manner starting with the neighbor above and to
the left.
As the size of the problem diminishes will you reach the base cases?
3 . 2 8 F i n d i n g t h e l a r ge s t i s la n d
Recu r si on an d Backtr acki n g | 110
Every time the routine visits a filled cell, it marks it it visits its neighbors. Eventually
all of the filled cells in the blob will be marked and the routine will encounter nothing but
base cases.
If a cell is not be marked before the recursive calls, then the cell will be counted more than
once since it is a neighbor of each of its eight neighbors. In fact a much worse problem
would occur. When each neighbor of the cell is visited, _ () is called again on the
current cell. Thus if the cell was still not visited, an infinite sequence of calls would be
generated.
Alternative coding
To simplify the coding, we would define the 8 possible directions for a cell and iterate
through those instead of repeating the above function for 8 times.
class ConnectedCells(object):
def __init__(self, matrix):
self.max = -1
self.matrix = matrix
self.cur_region_size = 0
self.directions = [(0, 1), (0, -1), (1, 0), (-1, 0), (1, 1), (1, -1), (-1, 1), (-1, -1)]
def solution(self):
m, n = len(self.matrix), len(self.matrix[0])
visited = [[False for _ in xrange(n)] for _ in xrange(m)]
for i in xrange(m):
for j in xrange(n):
if not visited[i][j] and self.matrix[i][j] == 1:
self.cur_region_size = 0
self.region_size(visited, i, j, m, n)
return self.max
def region_size(self, visited, i, j, m, n):
visited[i][j] = True
self.cur_region_size += 1
self.max = max(self.max, self.cur_region_size)
# for each of the neighbors, if it is not visited, call the function recursively
for dir in self.directions:
i1 = i + dir[0]
j1 = j + dir[1]
if 0 <= i1 < m and 0 <= j1 < n and not visited[i1][j1] and self.matrix[i1][j1] == 1:
self.region_size(visited, i1, j1, m, n)
if __name__ == "__main__":
matrix = [[1,1,0,0,0],
[0,1,1,0,0],
[0,0,1,0,1],
[1,0,0,0,1],
[0,1,0,1,1]]
# region_size
s = ConnectedCells(matrix)
3 . 2 8 F i n d i n g t h e l a r ge s t i s la n d
Recu r si on an d Backtr acki n g | 112
Performance
Running time of the algorithm is O( ). With the () function, we are traversing all
elements of the matrix.
Explanation
We can now outline a backtracking algorithm that returns an array containing the path in
a coordinate form ( , ), where i, and j are the cell positions in the matrix. For example, the
solution for the above problem is: (0,0) (0,1) (1,1) (1,2) (1,2) (3,2) (3,3)
Source
Destination
The simplest idea is to start from a position (0, 0), and try moving in the right direction if
the neighbor cell (0, 1) has 1. If this move leads to the final destination (lower right cell),
add this position to the solution set. Similarly, move in the downward direction if the
neighbor cell (1, 0) has 1. If this move leads to the final destination (lower right cell), add
this position to the solution set.
1
Continue this process for the new position (either (0, 1), or (1, 0)) until either the final
destination cell is reached or all possible paths are explored. If there is no path from source
to destination, return saying that there is no path for this matrix by starting at cell (0, 0)
to the destination cell ( − 1, − 1).
Algorithm
If we have reached the destination point,
return an array containing only the position of the destination
else
1. Move in the right direction and check if this leads to a solution
Example
Let us see how it works for the following matrix.
0 1 2 3 4
0 1 1 1 1 0
1 0 1 0 1 0
2 0 1 0 1 0
3 0 0 0 1 0
4 1 1 1 1 1
Assume the the source position of the matrix is (0, 0). For this cell, we have two possibilities.
We can either move downward (cell (1, 0)) or right direction (cell (0, 1)). Let us consider a
cell (1, 0) as a first try. But, it is marked 0.
0 1 2 3 4
0 1 1 1 1 0
1 0 1 0 1 0
2 0 1 0 1 0
3 0 0 0 1 0
4 1 1 1 1 1
Hence, we cannot use this cell for further processing. So, backtrack to cell (0, 0), and try
moving to the right cell (0, 1).
0 1 2 3 4
0 1 1 1 1 0
1 0 1 0 1 0
2 0 1 0 1 0
3 0 0 0 1 0
4 1 1 1 1 1
For cell (1, 0), we have two chances: cell (1, 1), and cell (0, 2). Considering the cell (1, 1)
would give us the following matrix status:
0 1 2 3 4
0 1 1 1 1 0
1 0 1 0 1 0
2 0 1 0 1 0
3 0 0 0 1 0
4 1 1 1 1 1
For cell (1, 1), we have two chances: cell (2, 1), and cell (1, 2). Considering the cell (2, 1)
would give us the following matrix status:
0 1 2 3 4
0 1 1 1 1 0
1 0 1 0 1 0
2 0 1 0 1 0
3 0 0 0 1 0
4 1 1 1 1 1
For cell (2, 1), we have two chances: cell (3, 1), and cell (2, 2). Considering the cell (3, 1)
would give us the following matrix status:
0 1 2 3 4
0 1 1 1 1 0
1 0 1 0 1 0
2 0 1 0 1 0
3 0 0 0 1 0
4 1 1 1 1 1
But the cell (3, 1) is marked 0. Hence, we cannot use this cell for further processing. So,
backtrack to cell (2, 1), and try moving to the right cell (2, 2).
0 1 2 3 4
0 1 1 1 1 0
1 0 1 0 1 0
2 0 1 0 1 0
3 0 0 0 1 0
4 1 1 1 1 1
Cell (2, 2) is also marked 0. So, backtrack to cell (1, 1), and try moving to the right cell (1,
2).
0 1 2 3 4
0 1 1 1 1 0
1 0 1 0 1 0
2 0 1 0 1 0
3 0 0 0 1 0
4 1 1 1 1 1
The current cell (1, 2) is also marked 0. So, backtrack to cell (0, 1), and try moving to cell
(0, 2).
0 1 2 3 4
0 1 1 1 1 0
1 0 1 0 1 0
2 0 1 0 1 0
3 0 0 0 1 0
4 1 1 1 1 1
For cell (0, 2), we have two chances: cell (1, 2), and cell (0, 3). Considering the cell (1, 2)
would give us the following matrix status:
0 1 2 3 4
0 1 1 1 1 0
1 0 1 0 1 0
2 0 1 0 1 0
3 0 0 0 1 0
4 1 1 1 1 1
The current cell (1, 2) is marked 0. So, backtrack to cell (0, 2), and try moving to the right
cell (0, 3).
0 1 2 3 4
0 1 1 1 1 0
1 0 1 0 1 0
2 0 1 0 1 0
3 0 0 0 1 0
4 1 1 1 1 1
This process continues and one of the possible path from source to destination which the
algorithm would give us is:
0 1 2 3 4
0 1 1 1 1 0
1 0 1 0 1 0
2 0 1 0 1 0
3 0 0 0 1 0
4 1 1 1 1 1
Performance
For the first cell, we have 2 possibilities (cells (1, 0), and (0, 2)). For the second cell we
would check 2 possible cells (cells (1, 1), and (0, 2)). Notice that, even though few cells are
not valid, we would still need to check for their validity. Hence for each of the cell, we have
2 possible cells to check.
Notice that, as per the problem statement we can either move downward or to the right.
With this information, we can formalize the recurrence equation as follows:
Let ( ) be the time complexity to find a path from source to destination in a matrix of
size × . As seen above, for a cell we have two possible moves. Also, if we move downward,
we cannot move back upward or to the left. Hence, the matrix size reduces to − 1 × − 1.
Similarly, if we move right, we cannot move back to the left or upward, also cannot move
up; and the matrix size reduces to − 1 × − 1.
Hence, ( ) can be written in terms of ( − 1) as:
( ) = 2 × ( − 1)+1
It is not difficult to derive this recurrence. The overall running time of the algorithm is
( ) = O(2 ).
Space complexity
Space complexity of the algorithm is O(n), and it is because of the recursive runtime stack.
3.30 Permutations
: Given a string of characters S, generate all permutations of S. That is,
give an algorithm for printing all possible permutations of the characters in a string S.
Assume that the length of S is n and that characters in S are drawn from some finite
alphabet Σ. All the permutations of S are n-sequences where each element is drawn from
Σ. For this problem, the filter checks each element of the possible sets to determine if it
contains the proper characters and in the correct numbers to be a permutation of S. If the
cardinality of Σ is C, then we have possible sets of which ! are permutations.
For example, a family of three (mother, father, and child) wants to take a picture in a
birthday event. For this example, for each of the position, we have three options (mother,
father, or child). So, we have a total of 3 × 3 × 3 = 3 = 27 possible sets. But, for a
permutation, we cannot keep same person in two different places. That is, once we place
mother at first place, we cannot keep her in the remaining two places. So, here are the
different ways of lining them up:
father mother child
father child mother
mother father child
mother child father
child father mother
child mother father
formula.
=
( −
!
)!
A permutation of a given set of items is a certain rearrangement of the elements. It can be
shown that an array A of length n has n! permutations. For example, the array [1, 2, 3] has
the following permutations:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
The solution is reached by generating ! strings, each of length , where n is the length of the input string.
A generator function that generates all permutations of the input elements. If the input
contains duplicates, then some permutations may be visited with multiplicity greater than
one.
Unlike combinations, two permutations are considered distinct if they contain the same
characters but in a different order. Assume that each occurrence of a repeated character
is a distinct character. That is, if the input is “aaa”, the output should be six repetitions of
“aaa”. The permutations can be in any order.
3 . 3 0 P e r m u t a t i on s
Recu r si on an d Backtr acki n g | 117
this problem up into smaller problems? One way to do it is as follows. In order to list all
the permutations of [A, B, C], we can split our work into three groups of permutations:
The other nice thing to note is that when we list all permutations that start with A, they
are nothing but strings that are formed by attaching A to the front of all permutations of
"BC". This is nothing but another permutation problem!
For each letter that we choose for the first (leftmost) position, we need to write all the
permutations beginning with that letter before we change the first letter. Likewise, if we
pick up a letter for the second position, we need to write out all permutations beginning
with this two letter sequence before changing the letters in either the first or second
position.
In other words , we can define the permutation process as picking a letter for a given
position and performing the permutation process starting at the next position to the right
before coming back to change the letter we just picked.
In essence, we see the need for a loop in the algorithm:
for (each possible starting letter)
list all permutations that start with that letter
As each letter from the input string can appear only once in each permutation, "all
allowable characters” can’t be defined as every letter in the input string. "All allowable
characters" mean all letters in the input string that haven't already been chosen for a
position to the left of the current position (a position less than n). We need to check this
scenario algorithmically. We can check each candidate letter for a position n against all the
letters in positions less than n to determine whether it had been used. We can eliminate
these inefficient scans by maintaining an array of boolean values corresponding to the
positions of the letters in the input string and using this array to mark a letter as used or
unused, as appropriate.
Implementation
3 . 3 0 P e r m u t a t i on s
Recu r si on an d Backtr acki n g | 118
The typical problem that we need to solve is the following. Let A ⊆ {1, 2, . . . , n} be an
arbitrary subset of size m. Let B = {1, 2, . . . , n} - A. The elements in A have been placed in
the first m slots of an array. We now need to generate all the permutations of elements in
B.
The pseudocode for our solution is
for each element x in B do
place x in position m+1
recursively generate all permutations of B - {x}
We now need to figure out what information we need to pass to each recursive call. There
are several ways to do this. One simple option is to keep two arrays, one called to
keep track of the actual permutation being generated and the other called (as in the
above code) which keeps track of the subset of elements whose permutations need to be
generated. We may also want to send in m, the number of elements which have already
been placed. So the header of the recursive version of function will look like:
def genPerms(B, soFar):
Helper function genPerms recursively generate permutations. So, our recursive algorithm
requires two pieces of information, the elements that have not yet been permuted and the
partial permutation built up so far. We thus phrase this function as a wrapper around a
recursive function with extra parameters.
def permutations(S):
soFar = [] # initially nothing is placed in it
B=S # initially B contains everything
for perm in genPerms(B, soFar):
print perm
We still need to figure out the base cases. Clearly, when there are no elements in B, then
we are done. This can be checked by testing if B (which equals the number of elements in
B is zero) is empty. So, if ( ) == 0 then it means that an entire permutation has been
generated in the array and it is time to print this out. The code that implements the
above idea is given below.
def permutations(S):
soFar = [] # initially nothing is placed in it
B = S # initially B contains everything
for perm in genPerms(soFar, B):
print perm
# The function takes in two arguments, the elements to permute and the partial
# permutation created so far, and then produces all permutations that start with the given
# sequence and end with some permutations of the unpermuted elements.
def genPerms(soFar, B):
# Base case: If there are no more elements to permute, then the answer will
# be the permutation we have created so far.
if len(B) == 0:
yield soFar
# Otherwise, try extending the permutation we have created so far by each of the
# elements we have yet to permute.
else:
for x in range(0, len(B)):
# First parameter: Place the element from B into position m+1 in perms
# Second parameter: Make a temporary copy of B without the element x
# Extend the current permutation by the xth element, then remove
3 . 3 0 P e r m u t a t i on s
Recu r si on an d Backtr acki n g | 119
# the xth element from the set of elements we have not yet
# permuted. We then iterate across all the permutations that have
# been generated this way and hand each one back to the caller.
for perm in genPerms(soFar + [B[x]], B[0:x] + B[x+1:]):
yield perm
permutations(['A', 'B', 'C', 'A'])
Example
As an example, consider the list [1, 2, 3], and trace the functions to see how it generates
the permutations. The initial call would be:
permutations([1, 2, 3])
This in turn calls with = [] and = [1, 2, 3]
genPerms([],[1, 2, 3]):
Now, for each of the elements in B, keep that element at the beginning, and permute the
remaining elements recursively. The first element ( = 0) of B is 1. Hence, move this element
to .
Next, for this function, the first element ( = 0) of B is 2. So, move this element 2 to
array, and recursively call the function.
For this function call, the first element ( = 0) of B is 3. So, move this element 3 to
array, and recursively call the function.
Now, the array B is empty. So, the base condition of the function yields the first
permutation, and prints the value of , i.e. [1, 2, 3].
Next, in the recursive tree, it goes back to genPerms(soFar=[1], B=[2, 3]), and tries to add
3 to .
Next, the first element of B is 2. So, move this element 2 to array, and recursively
call the function.
Now, the array B is empty. So, the base condition of the function yields the
second permutation, and prints the value of , i.e. [1, 3, 2].
With this, we are done with all the permutations starting with element 1. Next, let us
enumerate all the permutations starting with element 2. The second element ( = 1) of B is
2. Hence, move this character to .
… …
Next, the first element of B is 1. So, move this element 1 to array, and recursively
call the function.
Next, the first element of B is 3. So, move this element 3 to array, and recursively
call the function.
Now, the array B is empty. So, the base condition of the function yields the third
permutation, and prints the value of , i.e. [2, 1, 3]. Next, in the recursive tree, it goes
back to ( = [2], = [1, 3]), and tries to add 3 to .
Next, the first element of B is 1. So, move this element 1 to array, and recursively
call the function.
Now, the array B is empty. So, the base condition of the function yields the fourth
permutation, and prints the value of , i.e. [2, 3, 1]. With this, we are done with all the
permutations starting with element 2. Next, let us enumerate all the permutations starting
with element 3. The third element ( = 2) of B is 3. Hence, move this character to .
…
Recu r si on an d Backtr acki n g | 122
Next, the first element of B is 1. So, move this element 1 to array, and recursively
call the function.
Next, the first element of B is 2. So, move this element 2 to array, and recursively
call the function.
Now, the array B is empty. So, the base condition of the function yields the fifth
permutation, and prints the value of , i.e. [3, 1, 2]. Next, in the recursive tree, it goes
back to ( = [3], = [1, 2]), and tries to add 2 to .
Next, the first element of B is 1. So, move this element 1 to array, and recursively
call the function.
Now, the array B is empty, and the base condition of the function yields the sixth
permutation, and prints the value of , i.e. [3, 2, 1]. With this, we are done with all the
permutations starting with element 3.
This completes the processing of all permutations for each of the positions. Hence, it goes
back to the original caller function and ends the processing.
In this exhaustive traversal, we try every possible combination. There are ! ways to
rearrange the characters in a string of length n and this prints all of them.
This is an important example and worth spending time to understand. The permutation
pattern is at the heart of many recursive algorithms— finding anagrams, solving Sudoku
puzzles, optimally matching classes to classrooms, or scheduling for the best efficiency can
all be done using an adaptation of the general permutation code.
Performance
Time complexity
Note that the running time of this program, in terms of the number of times a permutation
is printed, is exactly n!, so it is as efficient as it can be since it necessarily does n! things.
Running time of the algorithm: O(n!).
The backtracking algorithm applied here is fairly straight forward because
the calls are not subject to any constraint. We are not backtracking from
an unwanted result, we are merely backtracking to return to a previous
state without filtering out unwanted output.
Example
Consider the classic 9 × 9 Sudoku puzzle in the following figure. The goal is to fill in the
empty cells such that every row, every column and every 3 × 3 block contains the digits 1
through 9.
6 1 4 5
8 3 5 6
2 1
8 4 7 6
6 3
7 9 1 4
5 2
7 2 6 9
4 5 8 7
The solution to the puzzle is given in the following figure and it satisfies the following
constraints:
The digits to be entered are 1, 2, 3, 4, 5, 6, 7, 8, and 9.
A row is 9 cells wide. A filled-in row must have one of each digit. That means each
digit appears only once in the row. There are 9 rows in the grid, and the same
applies to each of them.
A column is 9 cells tall. A filled-in column must have one of each digit. That means
each digit appears only once in the column. There are 9 columns in the grid, and
the same applies to each of them.
A block contains 9 cells in a 3 × 3 layout. A filled-in block must have one of each
digit. That means each digit appears only once in the box. There are 9 blocks in
the grid, and the same applies to each of them.
9 6 3 1 7 4 2 5 8
1 7 8 3 2 5 6 4 9
2 5 4 6 8 9 7 3 1
8 2 1 4 3 7 5 9 6
4 9 6 8 5 2 3 1 7
7 3 5 9 6 1 8 2 4
5 8 9 7 1 3 4 6 2
3 1 7 2 4 6 9 8 5
6 4 2 5 9 8 1 7 3
and solve it was developed in 1989, the best puzzles are still reckoned to be devised by
human skill and judgement. [Vova]
The Sudoku puzzles which are published for entertainment invariably have unique
solutions. A Sudoku puzzle is said to be well-formed if it has a unique solution. Another
challenging research problem is to determine how few cells need to be filled for a Sudoku
puzzle to be well-formed. Well-formed Sudoku with 17 preset symbols exist. It is unknown
whether or not there exists a well-formed puzzle with only 16 preset symbols. [Sean]
Algorithm
Each Sudoku puzzle has a unique solution. Very simple Sudoku puzzles can be solved
using elementary logic, such as noting that if a blank space has eight different digits in its
surrounding row, column, and 3 × 3 square, then that blank must contain the other digit.
More difficult puzzles require more complex logic. For very difficult puzzles most people
reach a point in the solution process at which they make an intelligent guess about a new
entry in the matrix, and follow the consequences of that guess to the solution (or to a
demonstrable inconsistency, then backtrack to the guessed entry, and guess differently).
For the backtracking algorithm, our strategy is defined as follows:
1. Number the cells from 0 to 80
2. Find a cell i with zero value in the grid
3. If there is no such cell, return true
4. For digits from 1 to 9:
a. If there is no conflict for digit at cell i:
i. Assign digit to cell i and recursively try to fill in rest of grid
ii. If recursion successful, return true
iii. If not successful, remove digit from cell i and try another digit
5. If all digits have been tried and nothing worked, return false to trigger backtracking
6. Continue the steps 2 to 5 until either solution is found or return saying NO
SOLUTION possible for the given grid of elements.
To simplify the implementation, let us assume the grid is numbered from 0 to 80 which
makes a total of 81 cells for the 9 × 9 Sudoku puzzle. To place a digit at any cell, we need
to validate it across each row, each column, and also each 3 × 3 square.
Next question would be, how do we validate a row, a column or a square in a grid with 81
cells?
The list of indexes of each row can be calculated with the following simple formula. Notice
that, in the following code snippet would give us the indexes of a row in each
iteration. For example, in the first iteration, it would return first row indexes [0, 1, 2, 3, 4,
5, 6, 7, 8, 9], and in the second iteration, it would return second row indexes [9, 10, 11,
12, 13, 14, 15, 16, 17], and so on.
Similarly, the list of indexes of each column can be calculated with the following formula.
Also, in the following code snippet would give us the indexes of a row in each
iteration. For example, in the first iteration, it would return first column indexes [0, 9, 18,
27, 36, 45, 54, 63, 72], and in the second iteration, it would return the second column
indexes [1, 10, 19, 28, 37, 46, 55, 64, 73], and so on.
On the similar lines, the list of indexes of each square can be calculated with the following
formula. The list, , in the following code snippet would give us the indexes of a square
block in each iteration. For example, in the first iteration, it would return first square block
indexes [0, 1, 2, 9, 10, 11, 18, 19, 20], and in the second iteration, it would return second
square block indexes [3, 4, 5, 12, 13, 14, 21, 22, 23], and so on.
for eachSq in range(9):
trialSq = [ x+cols for x in range(3) ] +
[ x+9+cols for x in range(3) ] +
[ x+18+cols for x in range(3) ]
With this data, we would traverse through all the cells of the grid starting from cell 0, and
identify the cell which has 0. Assume that 0 in the grid indicates the untracked cell and
we would need to fill it with a number from 1 to 9. Let us assume, the cell which has 0 is
identified as i. For this cell, we will try placing 1 and check if it is a valid number by
validating it against each row, each column, and also each 3 × 3 square block. If it is not a
valid number, try placing number 2 in cell i, and validate it again against each row, each
column, and also each 3 × 3 square block. If it is a valid number, move to the next cell
which has zero and continue the same process. If none of the numbers are valid for the cell
i, then we backtrack to the previous cell, and try replacing the number of that cell with
another number; and continue.
# global variable
grid_size = 81
def isFull (grid):
return grid.count(0) == 0
# can be used more purposefully
def getTrialCelli(grid):
for i in range(grid_size):
if grid[i] == 0:
print 'Trialling cell', i
return i
def isValid(trialVal, trialCelli, grid):
cols = 0
# validate square
for eachSq in range(9):
trialSq = [ x+cols for x in range(3) ] +
[ x+9+cols for x in range(3) ] +
[ x+18+cols for x in range(3) ]
cols +=3
if cols in [9, 36]:
cols +=18
if trialCelli in trialSq:
for i in trialSq:
if grid[i] != 0:
if trialVal == int(grid[i]):
print 'Square',
return False
# validate row
for eachRow in range(9):
trialRow = [ x+(9*eachRow) for x in range (9) ]
if trialCelli in trialRow:
for i in trialRow:
if grid[i] != 0:
if trialVal == int(grid[i]):
print 'Row',
return False
# validate column
for eachCol in range(9):
trialCol = [ (9*x)+eachCol for x in range (9) ]
if trialCelli in trialCol:
for i in trialCol:
if grid[i] != 0:
if trialVal == int(grid[i]):
print 'Column',
return False
print 'is legal.', 'So, set cell', trialCelli, 'with value', trialVal
return True
def setCell(trialVal, trialCelli, grid):
grid[trialCelli] = trialVal
return grid
def clearCell( trialCelli, grid ):
grid[trialCelli] = 0
print 'Clear cell', trialCelli
return grid
def hasSolution (grid):
if isFull(grid):
print '\nSOLVED'
return True
else:
trialCelli = getTrialCelli(grid)
trialVal = 1
solution_found = False
while ( solution_found != True) and (trialVal < 10):
print 'Trial value', trialVal,
if isValid(trialVal, trialCelli, grid):
grid = setCell(trialVal, trialCelli, grid)
if hasSolution (grid) == True:
solution_found = True
return True
else:
clearCell( trialCelli, grid )
print '++'
trialVal += 1
return solution_found
printGrid(sampleGrid, 0)
if hasSolution (sampleGrid):
printGrid(sampleGrid, 0)
else:
print 'NO SOLUTION'
if __name__ == "__main__":
main()
Performance
Many recursive searches can be modelled as a tree. In Sudoku, you have 9 possibilities
each time you try out a new cell. At the maximum, you have to put solutions into all 81
fields. At this point it can help drawing it up in order to see that the resulting search space
is a tree with a depth of 81 and a branching factor of 9 at each node of each layer, and
each leaf is a possible solution. Given these numbers, the search space is 9 .
In other words, there are 9 rows, 9 columns and 9 square blocks, and we only need to
check for each, if each of the numbers [1 .. 9] is contained. Then there is a limited number
of total combinations to distribute numbers over a 9 × 9 grid (9 , including the invalid
ones).
But given any Sudoku with k pre-set numbers, you can with 100% certainty say that you
will need at most tries. Hence, the overall running time of the algorithm, ( ), is
O( ).
Space complexity
Space complexity of the algorithm is O( 2 ), and it is because of the recursive runtime stack.
2
It is because of the fact that, in the recursive tree, the maximum depth is 81 which is .
Example
Some examples of solution for this problem for different N's:
4 Queens 5 Queens 6 Queens
Q
Q
Q Q
Q
Q Q
Q
Q Q
Q
Q Q
Q
Q
Explanation
N-queens problem is a computationally expensive problem − NP-complete, what makes it
very famous problem in computer science. Since the 1960's, with rapid developments in
computer science, this problem has been used as an example of backtracking algorithms.
There are some practical applications to the queens puzzle, such as parallel memory
storage schemes, VLSI testing, traffic control, and deadlock prevention.
Backtracking solution
We can solve this problem with the help of backtracking. The idea is very simple. We start
from the first row and place the queen in each square of the first row and recursively
explore remaining rows to check if they lead to the solution. If current configuration doesn’t
3 . 3 2 N - Q u ee n s p r o b l e m
Recu r si on an d Backtr acki n g | 130
result in a solution, we backtrack. Before exploring any square, we ignore the square if two
queens threaten each other.
The backtracking solution for this problem is based on building a search tree, where each
node represents a valid position of a queen on the chessboard. Nodes at the first level
correspond to one queen on the × board. Nodes at the second level represent boards
containing two queens in valid locations, and so on. When a tree of depth N is found, then
we have a solution for positioning N queens on the board.
Partial search tree for 6-queen problem is shown in the figure below. As the search
progresses down the tree, more queens are added to the board. In this example, we assume
that queens are added on successive rows of the board. So as the search progresses down,
we cover more rows of the board. When the search reaches a leaf at the level, a solution
has been found.
The program stops when it reaches a terminal leaf (success), or when all the subtrees have
been visited without ever reaching a terminal leaf (no solutions).
The idea of the backtracking algorithm is simple. We have a recursive algorithm that tries
to build a solution part by part, and when it gets into a dead end, it has either built a
solution or it needs to go back (backtrack) and try picking different values for some of the
parts. We check whether the solution we have built is a valid solution only at the deepest
level of recursion –when we have all the parts picked out.
Like most recursive algorithms, the execution of an N-queens backtracking algorithm can
be illustrated using a recursion tree. The root of the recursion tree corresponds to the
original invocation of the algorithm; edges in the tree correspond to recursive calls. A path
from the root down to any node shows the history of a partial solution to the N-queens
problem, as queens are added to successive rows. The leaves correspond to partial
solutions that cannot be extended, either because there is already a queen on every row,
or because every position in the next empty row is in the same row, column, or diagonal as
an existing queen. The backtracking algorithm simply performs a depth-first traversal of
this tree.
3 . 3 2 N - Q u ee n s p r o b l e m
Recu r si on an d Backtr acki n g | 131
Q Q Q Q
Q Q Q Q
Q Q
Q Q
Q Q
The program that we write will actually permit a varying number of queens. The number
of queens must always equal the size of the chess board. For example, if we have six queens,
then the board will be a six by six chess board.
So initially we are having × unattacked cells where we need to place N queens. Let's
place the first queen at a cell ( , ). So now the number of unattacked cells is reduced, and
the number of queens to be placed is − 1. Place the next queen at some unattacked cell.
This again reduces the number of unattacked cells and number of queens to be placed
becomes − 2. Continue doing this, as long as following conditions hold.
1. The number of unattacked cells is not 0.
2. The number of queens to be placed is not 0.
If the number of queens to be placed becomes 0, then it's over. We found a solution. But if
the number of unattacked cells become 0, then we need to backtrack, i.e. remove the last
placed queen from its current cell, and place it at some other cell. We do this recursively.
A high level overview of how to use backtracking to solve the N queens problem:
1. Place a queen in the first column and first row
2. Place a queen in the second column such that it does not attack the queen in the
first column
3. Continue placing non-attacking queens in the remaining columns
4. If all N queens have been placed, a solution is found. Remove the queen in the
column, and try incrementing the row of the queen in the − 1 column
5. If it’s a dead end, remove the queen, increment the row of the queen in the previous
column
6. Continue doing this until the queen in the 1st column exhausts all options and is
in the row N
3 . 3 2 N - Q u ee n s p r o b l e m
Recu r si on an d Backtr acki n g | 132
Example
Let us see how it works for = 4.
Place queen at cell (0, 0):
0 1 2 3
0 Q
1
2
3
With this first queen, we cannot place the second queen in the same row, same column,
and also same diagonal. Hence, place the second queen at cell (1, 2):
0 1 2 3
0 Q
1 Q
2
3
We can place the third queen at cell (3, 1):
0 1 2 3
0 Q
1 Q
2
3 Q
For placing the fourth queen, there are no more valid cells. Hence, we need to backtrack
one step.
0 1 2 3
0 Q
1 Q
2
3
For placing the third queen, there are no more valid cells. Hence, we need to backtrack one
step further.
0 1 2 3
0 Q
1
2
3
We can place the second queen at cell (1, 3).
0 1 2 3
0 Q
1 Q
2
3
We can place the third queen at cell (2, 1):
0 1 2 3
0 Q
1 Q
2 Q
3
For placing the fourth queen, there are no more valid cells. Hence, we need to backtrack
one step.
3 . 3 2 N - Q u ee n s p r o b l e m
Recu r si on an d Backtr acki n g | 133
0 1 2 3
0 Q
1 Q
2
3
We can place third queen at cell (2, 1):
0 1 2 3
0 Q
1 Q
2
3 Q
For placing the fourth queen, there are no more valid cells. Hence, we need to backtrack
one step.
0 1 2 3
0 Q
1 Q
2
3
For placing the third queen, there are no more valid cells. Hence, we need to backtrack one
step further.
0 1 2 3
0 Q
1
2
3
This process continues, and one of the final valid placement of the queens which the
algorithm would give is:
0 1 2 3
0 Q
1 Q
2 Q
3 Q
Also, notice that there could be multiple valid solutions for the N-queens problem. For
example, another valid placement of 4 queens would be:
0 1 2 3
0 Q
1 Q
2 Q
3 Q
def N_queens(N):
queenRow = [-1] * N
def isLegal(row, col):
# a position is legal if it's on the board (which we can assume
# by way of our algorithm) and no prior queen (in a column < col)
# attacks this position
for qcol in xrange(col):
qrow = queenRow[qcol]
if ((qrow == row) or
(qcol == col) or
(qrow+qcol == row+col) or
(qrow-qcol == row-col)):
3 . 3 2 N - Q u ee n s p r o b l e m
Recu r si on an d Backtr acki n g | 134
return False
return True
def printSolution(queenRow):
board = [(["- "] * N) for row in xrange(N)]
for col in xrange(N):
row = queenRow[col]
board[row][col] = "Q "
return "\N".join(["".join(row) for row in board])
def solve(col):
if (col == N):
return printSolution(queenRow)
else:
# try to place the queen in each row in turn in this col,
# and then recursively solve the rest of the columns
for row in xrange(N):
if isLegal(row,col):
queenRow[col] = row # place the queen and hope it works
solution = solve(col+1)
if (solution != None):
# it did work
return solution
queenRow[col] = -1 # pick up the wrongly-placed queen
# shoot, can't place the queen anywhere
return None
return solve(0)
print N_queens(4)
Performance
For the first queen, we have × possibilities. To place the second queen we would check
× possible cells. Notice that, even though few cells are not valid, we would still need to
check for their validity. So for each of the N queens, we have × cells to check. Hence,
the running time of this approach would be O( ).
But, the algorithm we have used above does not check all × possible checks. The naive
algorithm ignores an obvious constraint that can save us a huge amount of effort:
No two queens can be in the same column, row, or diagonal.
Space complexity
For this improved algorithm, space complexity is O(N). The algorithm uses an auxiliary
array of length N to store just N positions.
Time complexity
The method takes O(N) time as it iterates through array every time.
For each invocation of the method, there is a loop which runs for O(N) time.
In each iteration of this loop, there is invocation which is O(N) and a
recursive call with a smaller argument.
Let ( ) be the overall running time of the algorithm. If we add up all this up and define
the runtime as ( ), then
( ) = O( )+ × ( − 1)
If you draw a recursion tree using this recurrence, the final term will be something like +
!. By the definition of Big O, this can be reduced to O( !) running time.
3 . 3 2 N - Q u ee n s p r o b l e m
Recu r si on an d Backtr acki n g | 135
The time complexity of the above backtracking algorithm can be further improved by using
Branch and Bound. In backtracking solution we backtrack when we hit a dead end but in
branch and bound, after building a partial solution, we figure out that there is no point
going any deeper as we are going to hit a dead end.
The above observations of smaller-size problems show that the number of
solutions increases exponentially with increasing N. Alternatively, search-
based algorithms have been developed. For example, a backtracking
search will systematically generate all possible solution sets for a given
solution – for example sorting. Sorting numbers with backtracking is a
very bad decision, one should definitely use quicksort/mergesort! One
should use backtracking only when there is no other known/easy way to
solve a particular problem.
Use backtracking to solve problems with a small input or with a small set
of possible solutions, otherwise your CPU will really hate you!
3 . 3 2 N - Q u ee n s p r o b l e m
G r eedy A l go ri thm s | 136
Chapter
Greedy
Algorithms 4
4.1 Introduction
Let us start our discussion with simple theory that will give us an understanding of the
Greedy technique. In the game of ℎ , every time we make a decision about a move, we
have to think about the future consequences. Whereas, in the game of (or
), our action is based on the immediate situation. This means that in some cases
making a decision that looks right at that moment gives the best solution ( ), but in
other cases it doesn’t.
Optimal substructure
A problem exhibits optimal substructure when an optimal solution to the problem contains
optimal solutions to the subproblems, that means we can solve subproblems and build up
the solutions to solve larger problems.
4 . 1 I n t r o d u c t io n
G r eedy A l go ri thm s | 137
Its main disadvantage is that for many problems there is no greedy algorithm. That means,
in many cases there is no guarantee that making locally optimal improvements in a locally
optimal solution gives the optimal global solution. Another difficult part of greedy
algorithms is that for greedy algorithms we have to work much harder to understand
correctness issues. Even with the correct algorithm, it is hard to prove why it is correct.
Proving that a greedy algorithm is correct is more of an art than a science.
Greedy algorithms are fast and are used in practice in many cases. Therefore, if it is proved
that they yield the global optimum for a certain problem, they will become the method of
choice. Following are few applications which can be solved optimally with the greedy
technique.
4 . 4 D o g r e e d y a l go r i t h m s a l w a ys w o r k ?
G r eedy A l go ri thm s | 138
As we see, Huffman coding compresses data by using fewer bits to encode more frequently
occurring characters so that not all characters are encoded with 8 bits.
Definition
Given a set of characters from the alphabet A [each character c ∈ A] and their associated
frequency (c), find a binary code for each character c ∈ A, such that
∑ ∈ freq(c)|binarycode(c)| is minimum, where | binarycode(c)| represents the length of
binary code of character c. That means the sum of the lengths of all character codes should
be minimum [the sum of each character’s frequency multiplied by the number of bits in
the representation].
The basic idea behind the Huffman coding algorithm is to use fewer bits for more frequently
occurring characters. The Huffman coding algorithm compresses the storage of data using
variable length codes. We know that each character takes 8 bits for representation. But in
general, we do not use all of them. Also, we use some characters more frequently than
others. While reading a file, the system generally reads 8 bits at a time to read a single
character. But this coding scheme is inefficient. The reason for this is that some characters
are more frequently used than other characters. Let's say that the character ′ ′ is used 10
times more frequently than the character ′ ′. Instead, it would then be advantageous for
us to use a 7-bit code for e and a 9-bit code for because that could reduce our overall
message length.
On average, using Huffman coding on standard files can reduce them anywhere from 10%
to 30% depending on the character frequencies. The idea behind the character coding is to
give longer binary codes for less frequent characters and groups of characters. Also, the
4 . 7 U n d e r s t a n d i n g g r e e d y t e c h n iq u e
G r eedy A l go ri thm s | 139
character coding is constructed in such a way that no two character codes are prefixes of
each other.
Example
Let's assume that after scanning a file we find the following character frequencies:
Character Frequency
12
2
7
13
14
85
Given this, create a binary tree for each character that also stores the frequency with which
it occurs (as shown below).
The algorithm works as follows: In the list, find the two binary trees that store minimum
frequencies at their nodes. Connect these two nodes at a newly created common node that
will store no character but will store the of the frequencies of all the nodes connected
below it. So, our picture looks like this:
21 27
48
21 27
4 . 7 U n d e r s t a n d i n g g r e e d y t e c h n iq u e
G r eedy A l go ri thm s | 140
133
48 f-85
21 27
b-2 c-7
Once the tree is built, each leaf node corresponds to a letter with a code. To determine the
code for a particular node, traverse from the root to the leaf node. For each move to the
left, append a 0 to the code, and for each move to the right, append a 1. As a result, for the
above generated tree, we get the following codes:
Letter Code
a 001
b 0000
c 0001
d 010
e 011
f 1
Thus, we saved 399 − 238 = 161 bits, or nearly 40% of the storage space.
From the above discussion, it is clear that Huffman's algorithm is an example of a greedy
algorithm. It's called greedy because the two smallest nodes are chosen at each step, and
this local decision results in a globally optimal encoding tree.
4 . 7 U n d e r s t a n d i n g g r e e d y t e c h n iq u e
G r eedy A l go ri thm s | 141
If there are elements to be sorted, the process mentioned above should be repeated − 1
times to get required sorted result. For better performance, in the second step, comparison
starts from the second element because after the first step, the required number is
automatically placed at first. In case of sorting in ascending order, the smallest element
will be at first and in case of sorting in descending order, the largest element will be at first.
Similarly, in the third step, comparison starts from the third element and so on.
Selection sort is an in-place sorting algorithm. Selection sort works well for small files. It
is used for sorting the files with very large values and small keys. This is because selection
is made based on keys, and swaps are made only when required.
Algorithm
The selection sort improves on the bubble sort by making only one exchange for every pass
through the list. In order to do this, a selection sort looks for the smallest value as it makes
a pass and, after completing the pass, places it in the proper location.
As with a bubble sort, after the first pass, the largest item is in the correct place. After the
second pass, the next largest is in place. This process continues and requires − 1 passes
to sort items, since the final item must be in place after the − 1 pass.
1. Find the minimum value in the list
2. Swap it with the value in the current position
3. Repeat this process for all the remaining elements until the entire array is sorted
This algorithm is called since it repeatedly the smallest element.
Example
Following the steps shows the entire sorting process. On each pass, the smallest remaining
item is selected and then placed in its proper location. Consider the following array as an
example.
54 26 93 17 77 31 44 55 20
For the first position in the sorted list, the whole list is scanned sequentially. The first
position where 54 is stored presently, we search the whole list and find that 17 is the lowest
value. So we replace 54 with 17. After one iteration, 17, which happens to be the minimum
value in the list, appears in the first position of the sorted list.
17 26 93 54 77 31 44 55 20
Sorted list
For the second position, where 26 is residing, we start scanning the rest of the list in a
linear manner. We find that 20 is the second lowest value in the list and it should appear
at the second place. We swap these values. After two iterations, two least values are
positioned at the beginning in a sorted manner.
17 26 93 54 77 31 44 55 20
Sorted list
The same process is applied to the rest of the items in the array. Following is a pictorial
depiction of the entire sorting process:
17 20 93 54 77 31 44 55 26
Sorted list
17 20 26 54 77 31 44 55 93
Sorted list
17 20 26 31 77 54 44 55 93
Sorted list
17 20 26 31 44 54 77 55 93
Sorted list
17 20 26 31 44 54 77 55 93
Sorted list
17 20 26 31 44 54 55 77 93
Sorted list
17 20 26 31 44 54 55 77 93
Sorted list
17 20 26 31 44 54 55 77 93
Sorted list
The main advantage of the selection sort is that it performs well on a small list.
Furthermore, because it is an in-place sorting algorithm, no additional temporary storage
is required beyond what is needed to hold the original list. The primary disadvantage of the
selection sort is its poor efficiency while dealing with a huge list of items. Similar to the
bubble sort, the selection sort requires number of steps for sorting elements.
Additionally, its performance is easily influenced by the initial ordering of the items before
the sorting process. Because of this, the selection sort is only suitable for a list of few
elements that are in random order.
Advantages
Easy to implement
In-place sort (requires no additional storage space)
Disadvantages
Doesn't scale well: O( )
Implementation
Now, let us see some programming aspects of selection sort.
def selection_sort( A ):
for i in range( len(A) ):
smallest = i
for j in range( i + 1 , len(A) ):
if A[j] < A[smallest]:
smallest = j
A[smallest], A[i] = A[i], A[smallest]
A = [54, 26, 93, 17, 77, 31, 44, 55, 20]
selection_sort(A)
print(A)
Performance
To find the minimum element from the array of elements, − 1 comparisons are required.
After putting the minimum element in its proper position, the size of an unsorted array
reduces to − 1, and then − 2 comparisons are required to find the minimum in the
unsorted array. This process continues and requires − 1 passes to sort items, since the
final item must be in place after the − 1 pass.
Worst case complexity : O( )
Best case complexity : O( )
Average case complexity : O( )
Worst case space complexity: O(1) auxiliary
What is a heap?
A heap is a tree with some special properties. The basic requirement of a heap is that the
value of a node must be ≥ (or ≤) than the values of its children. This is called ℎ .
A heap also has the additional property that all leaves should be at ℎ or ℎ − 1 levels (where
ℎ is the height of the tree) for some ℎ > 0 ( ). That means the heap
should form a (as shown below).
us say the root node has 1) then we get a complete sequence from 1 to the number of nodes
in the tree. While traversing we should give numbering for NULL pointers also. A binary
tree is called if all leaf nodes are at height ℎ or ℎ − 1 and also without
any missing number in the sequence.
2 3
4 5
In the examples below, the left tree is a heap (each element is greater than its children) and
the right tree is not a heap (since 11 is greater than 2).
7 11
3 6 5 2
1 2 4 6 7 4 3
Types of heaps
Based on the property of a heap we can classify heaps into two types:
Min heap: The value of a node must be less than or equal to the values of its
children
1
3 2
5 4 6 7
Max heap: The value of a node must be greater than or equal to the values of its
children
7
5 6
1 4 2 3
Binary heaps
In a binary heap each node may have up to two children. In practice, binary heaps are
enough and we concentrate on binary min-heaps and binary max-heaps for the remaining
discussion.
7 5 6 1 4 2 3
1 2 3 4 5 6 7
Note: For the remaining discussion let us assume that we are using max heap.
Declaration of heap
class Heap:
def __init__(self):
self.heapList = [0] # Elements in Heap
self.size = 0 # Size of the heap
Time Complexity: O(1).
Parent of a node
For a node at location, its parent is at location. In the previous example, the element
6 is at ℎ location and its parent is at location.
def parent(self, index):
"""
Parent will be at math.floor(index/2). Since integer division
simulates the floor function, we don't explicitly use it
"""
return index // 2
Time Complexity: O(1).
Children of a node
Similar to the above discussion, for a node at location, its children are at 2 ∗ and 2 ∗
+ 1 locations. For example, in the above tree the element 6 is at the third location and its
children 2 and 5 are at 6 (2 ∗ = 2 ∗ 3) and 7 (2 ∗ + 1 = 2 ∗ 3 + 1) locations.
def left_child(self, index): def right_child(self, index):
""" array begins at index 1 """ return 2 * index + 1
return 2 * index
Time Complexity: O(1). Time Complexity: O(1).
Heapifying an element
After inserting an element into a heap, it may not satisfy the heap property. In that case,
we need to adjust the locations of the heap to make it a heap again. This process is called
ℎ . In max-heap, to heapify an element, we have to find the maximum of its
children and swap it with the current element and continue this process until the heap
property is satisfied at every node. In min-heap, to heapify an element, we have to find the
minimum of its children and swap it with the current element and continue this process
until the heap property is satisfied at every node.
31
1 21
9 10 12 18
3 2 8 7
Observation
One important property of a heap is that, if an element is not satisfying the heap property,
then all the elements from that element to the root will have the same problem. In the
example above, element 1 is not satisfying the heap property and its parent 31 is also having
the same issue. Similarly, if we heapify an element, then all the elements from that element
to the root will also satisfy the heap property automatically. Let us go through an example.
In the above heap, the element 1 is not satisfying the heap property. Let us try heapifying
this element.
31
1 21
9 10 12 18
3 2 8 7
To heapify 1, find the maximum of its children and swap with that.
31
10 21
9 1 12 18
3 2 8 7
We need to continue this process until the element satisfies the heap properties. Now, swap
1 with 8.
31
10 21
9 8 12 18
3 2 1 7
Now the tree is satisfying the heap property. In the above ℎ process, since we are
moving from top to bottom, this process is sometimes called . Similarly, if we
start ℎ from any other node to root, we call that process as we are
moving from bottom to top.
def percolate_down(self,i):
while (i * 2) <= self.size:
minimum_child = self.min_child(i)
if self.heapList[i] > self.heapList[minimum_child]:
tmp = self.heapList[i]
self.heapList[i] = self.heapList[minimum_child]
self.heapList[minimum_child] = tmp
i = minimum_child
def min_child(self,i):
if i * 2 + 1 > self.size:
return i * 2
else:
if self.heapList[i*2] < self.heapList[i*2+1]:
return i * 2
else:
return i * 2 + 1
def percolate_up(self,i):
while i // 2 > 0:
if self.heapList[i] < self.heapList[i // 2]:
tmp = self.heapList[i // 2]
self.heapList[i // 2] = self.heapList[i]
self.heapList[i] = tmp
i = i // 2
Time Complexity: O( ). Heap is a complete binary tree and in the worst case we start at
the root and come down to the leaf. This is equal to the height of the complete binary tree.
Deleting an element
To delete an element from a heap, we are allowed to delete the root element of the tree. This
is the only operation (maximum element) supported by standard heap. After deleting the
root element, copy the last element of the heap to root and delete the last element.
After replacing the last element, the tree may not satisfy the heap property. To make it a
heap again, call the _ function on the root element.
Copy the first element into some variable
Copy the last element into the first element location (root element)
_ the first element
Inserting an element
Insertion of an element is similar to the heapify and deletion process.
Before going through the code, let us look at an example. We have inserted the element 29
at the end of the heap and this is not satisfying the heap property.
31
10 21
9 8 12 18
3 2 1 7 29
For the new element 29, it is not satisfying the heap property as it is less than its parent
element 12. To heapify this element (29), we need to swap it with its parent. Swapping the
elements 29 and 12 gives:
31
10 21
9 8 29 18
3 2 1 7 12
At node 29, it is not satisfying the heap property as it is less than its parent element 21.
So, swap 29 with 21:
31
10 29
9 8 21 18
3 2 1 7 12
Now the tree is satisfying the heap property. Since we are following the bottom-up
approach, this process is called .
def insert(self, k):
self.heapList.append(k)
self.size = self.size + 1
self.percolate_up(self.size)
Time Complexity: O( ). In the worst case we start at the last element and go up till the
root. This is equal to the height of the complete binary tree.
Observation
Leaf nodes always satisfy the heap property and do not need to care for them. The leaf
elements are always at the end and to heapify the given array it should be enough if we
heapify the non-leaf nodes. Now let us concentrate on finding the first non-leaf node. The
last element of the heap is at location ℎ → − 1, and to find the first non-leaf node it is
enough to find the parent of the last element.
31
10 29
9 8 21 18
def build_heap(self,A):
i = len(A) // 2
self.size = len(A)
self.heapList = [0] + A[:]
while (i > 0):
self.percolate_down(i)
i=i-1
Time Complexity: The linear time bound of building heap can be shown by computing the
sum of the heights of all the nodes. For a complete binary tree of height ℎ containing =
2 – 1 nodes, the sum of the heights of the nodes is – ℎ – 1 = − − 1. That means,
building the heap operation can be done in linear time (O( )) by applying a _
function to the nodes in reverse level order.
Heap sort
One main application of heap ADT is sorting (heap sort). The heap sort algorithm inserts
all elements (from an unsorted array) into a heap, then removes them from the root of a
heap until the heap is empty. Note that heap sort can be done in place with the array to be
sorted. Instead of deleting an element, exchange the first element (maximum) with the last
element and reduce the heap size (array size). Then, we heapify the first element. Continue
this process until the number of remaining elements is one.
def heap_sort( A ):
# convert A to heap
length = len( A ) - 1
leastParent = length / 2
for i in range ( leastParent, -1, -1 ):
percolate_down( A, i, length )
# flatten heap into sorted array
for i in range ( length, 0, -1 ):
if A[0] > A[i]:
swap( A, 0, i )
percolate_down( A, 0, i - 1 )
# modfied percolate_down to skip the sorted elements
def percolate_down( A, first, last ):
largest = 2 * first + 1
while largest <= last:
# right child exists and is larger than left child
if ( largest < last ) and ( A[largest] < A[largest + 1] ):
largest += 1
# right child is larger than parent
if A[largest] > A[first]:
swap( A, largest, first )
# move down to largest child
first = largest
largest = 2 * first + 1
else:
return # force exit
def swap( A, x, y ):
temp = A[x]
A[x] = A[y]
A[y] = temp
Time complexity: As we remove the elements from the heap, the values become sorted (since
maximum elements are always ). Since the time complexity of both the insertion
algorithm and deletion algorithm is O( ) (where is the number of items in the heap),
the time complexity of the heap sort algorithm is O( ).
Performance
Worst case performance: ( )
Best case performance: ( )
Average case performance: ( )
Worst case space complexity: ( ) total, (1) auxiliary
4 . 1 0 S o r t i n g n ea r l y so r t e d a r r a y
G r eedy A l go ri thm s | 153
20 20
15
12 12
10
9
8
6 6
5 5
4
3
1
0
0 2 4 6 8 10 12
A simple solution would be to use an efficient sorting algorithm to sort the whole array
again. The worst-case time complexity of this approach will be O(nlogn) where n is the size
of the input array. This method also does not use the fact that array is k-sorted. We can
also use insertion sort that will correct the order in just O(nk) time. Insertion sort performs
really well for small values of k but it is not recommended for large value of k.
As discussed in heapsort section, creating a min-heap with sorted ascending elements
would take linear time. That is, for creating a min-heap with n increasing elements would
take O(n). Because, with sorted elements just appending the elements to min-heap would
suffice, and it does not need any heapifying process as the elements were already in proper
order.
We can solve this problem with a min heap as well and the algorithm is defined as follows.
The idea is to construct a min-heap of size k and insert first k elements into the heap. Then
we remove minimum from the heap, insert next element from the array into the heap and
continue the process till both array and heap are exhausted.
Algorithm
1. Build a min-heap with first elements of the given array.
2. For each element X of the remaining − elements:
a. Delete minimum element from the min-heap.
b. Add X to min-heap.
3. Delete all elements of the min-heap one by one until it is empty.
class MinHeap:
def __init__(self):
self.A = [0]
self.size = 0
4 . 1 0 S o r t i n g n ea r l y so r t e d a r r a y
G r eedy A l go ri thm s | 154
def percolate_up(self,i):
while i // 2 > 0:
if self.A[i] < self.A[i // 2]:
tmp = self.A[i // 2]
self.A[i // 2] = self.A[i]
self.A[i] = tmp
i = i // 2
def insert(self,k):
self.A.append(k)
self.size = self.size + 1
self.percolate_up(self.size)
def percolate_down(self,i):
while (i * 2) <= self.size:
minChild = self.min_child(i)
if self.A[i] > self.A[minChild]:
tmp = self.A[i]
self.A[i] = self.A[minChild]
self.A[minChild] = tmp
i = minChild
def min_child(self,i):
if i * 2 + 1 > self.size:
return i * 2
else:
if self.A[i*2] < self.A[i*2+1]:
return i * 2
else:
return i * 2 + 1
def delete_min(self):
retval = self.A[1]
self.A[1] = self.A[self.size]
self.size = self.size - 1
self.A.pop()
self.percolate_down(1)
return retval
def build_heap(self, A):
i = len(A) // 2
self.size = len(A)
self.A = [0] + A[:]
while (i > 0):
self.percolate_down(i)
i=i-1
def sort_nearly_sorted(self, A, k):
# create heap for k elements
self.build_heap(A[:k])
result = []
# step2: insert remaining n-k elements
for X in range(k, len(A)):
result.append(self.delete_min())
self.insert(A[X])
# step3: pop all remaining elements
while (self.size > 0):
result.append(self.delete_min())
return result
4 . 1 0 S o r t i n g n ea r l y so r t e d a r r a y
G r eedy A l go ri thm s | 155
h = MinHeap()
A = [1, 3, 5, 6, 8, 9, 12, 4, 6, 12, 20]
print h.sort_nearly_sorted(A, 7)
Performance
First step of the algorithm would take O(k) as the size of the min-ℎ is . The second step
of the algorithm would consume O(( − )× ) as we need to keep updating the minimum
element in min−ℎ with the lesser elements from − elements. The third step of the
algorithm would take O(klogk) as each deletion of minimum element would need logk time,
and we perform n such delete operations.
Time Complexity: O(k+ +( − ) ). Since n is greater than k, the overall running time
of the algorithm is O( ).
Space Complexity: O(k) for auxiliary heap of size k.
Algorithm
1. Sort the given array of elements. We could use any sorting algorithm, say quick
sort or heap sort.
2. For the sorted array, maintain two indices, left and right initialized to first and
last indexes of the array respectively.
a. left = 0
b. right = len(A)-1
3. Continue this step until left index is less than right index.
a. If A[left] + A[right] = K:
i. Print A[left], A[right]
ii. Increase left index (or decrease right index)
b. If A[left] + A[right] > K:
i. Decrease right index
c. If A[left] + A[right] < K:
i. Increase left index
def pair_sum_k(A, K):
A.sort()
left = 0
right = len(A) - 1
while(left < right):
if(A[left] + A[right] == K):
print A[left], A[right]
left += 1
elif(A[left] + A[right] < K):
left += 1
else:
right -= 1
A = [1, 2, 6, 3, 5, 7, 8, 4, 0, 6, 10, -8, 12]
pair_sum_k(A, 12)
Performance
The first step of the algorithm would take O(nlogn) for sorting the array elements. The
second step of the algorithm would consume O(1) for initialization of the left and right
indexes. The third step of the algorithm would take O(n).
Total running time of the algorithm is: O(nlogn+1+n) ≈ O( ).
Space Complexity: O(1).
4 . 1 2 F u n d a m e n ta l s o f d i s jo i n t s e t s
G r eedy A l go ri thm s | 158
3 5 …. 2 3
0 1 −2 −1
4 . 1 2 F u n d a m e n ta l s o f d i s jo i n t s e t s
G r eedy A l go ri thm s | 159
keep the parent of the element. Therefore, using an array which stores the parent of each
element solves our problem.
To differentiate the root node, let us assume that its parent is the same as that of the
element in the array. Based on this representation, MAKESET, FIND, UNION operations
can be defined as:
MAKESET ( ): Creates a new set containing a single element and in the array
update the parent of as . That means root (set name) of is .
UNION( , ): Replaces the two sets containing and by their union and in the
array updates the parent of as .
X Y Y
1 4 6 7 X 6 7
2 5 1 4
2 5
FIND(X): Returns the name of the set containing the element . We keep on
searching for ’ set name until we come to the root of the tree.
1 4
2 X
0 1 ………. -2 -1
0 ……… -2
1 -1
. Array
Parent
To perform a UNION on two sets, we merge the two trees by making the root of one tree
point to the root of the other.
0 1 2 3 4 5 6
0 1 2 3 4 5 6
Parent Array
4 . 1 2 F u n d a m e n ta l s o f d i s jo i n t s e t s
G r eedy A l go ri thm s | 160
After UNION(5,6):
0 1 2 3 4 6
5
0 1 2 3 4 5 6
Parent Array
After UNION(1,2):
0 2 3 4 6
1 5
0 1 2 3 4 5 6
Parent Array
After UNION(0,2)
2 3 4 6
0 1 5
0 1 2 3 4 5 6
Parent Array
One important thing to observe here is, UNION operation is changing the parent of the root
only, but not for all the elements in the sets. Due to this, the time complexity of UNION
operation is O(1). A FIND( ) on element is performed by returning the root of the tree
containing . The time to perform this operation is proportional to the depth of the node
representing . Using this method, it is possible to create a tree of depth – 1 (Skew Trees).
The worst-case running time of a FIND is O( ) and consecutive FIND operations take
O( ) time in the worst case.
class DisjointSet:
def __init__(self, n):
self.S = []
self.MAKESET(n)
4 . 1 2 F u n d a m e n ta l s o f d i s jo i n t s e t s
G r eedy A l go ri thm s | 161
else:
return self.FIND(self.S[X])
ds = DisjointSet(7)
ds.UNION(5, 6)
ds.UNION(1, 2)
ds.UNION(0, 2)
UNION by Size (also called UNION by Weight): Make the smaller tree a subtree of
the larger tree
UNION by Height (also called UNION by Rank): Make the tree with less height a
subtree of the tree with more height
UNION by size
In the earlier representation, for each element we have stored (in the parent array) for
the root element and for other elements, we have stored the parent of . But in this approach
we store the negative of the size of the tree (that means, if the size of the tree is 3 then store
−3 in the parent array for the root element). For the previous example (after UNION(0,2)),
the new representation will look like:
2 3 4 6
0 1 5
2 2 -3 -1 -1 6 -2
Parent Array
Assume that the size of one element set is 1 and would get −1 in the parent array.
class DisjointSet:
def __init__(self, n):
self.MAKESET(n)
def MAKESET(self, n):
self.S = [-1 for x in range(n)]
def FIND(self, X):
if( self.S[X] < 0 ):
return X
else:
return self.FIND(self.S[X])
def UNION(self, root1, root2):
4 . 1 2 F u n d a m e n ta l s o f d i s jo i n t s e t s
G r eedy A l go ri thm s | 162
if self.FIND(root1) == self.FIND(root2):
return
if(self.S[root2] < self.S[root1] ):
self.S[root2] += self.S[root1]
self.S[root1] = root2
else:
self.S[root1] += self.S[root2]
self.S[root2] = root1
ds = DisjointSet(7)
ds.UNION(5, 6)
ds.UNION(1, 2)
ds.UNION(0, 2)
print ds.FIND(5), ds.FIND(1), ds.FIND(2)
Note: There is no change in FIND operation implementation.
2 3 4 6
0 1 5
2 2 -2 -1 -1 6 -2
Parent Array
UNION by Height
class DisjointSet:
def __init__(self, n):
self.MAKESET(n)
def MAKESET(self, n):
self.S = [-1 for x in range(n)]
def FIND(self, X):
if( self.S[X] < 0 ):
return X
else:
return self.FIND(self.S[X])
def UNION(self, root1, root2):
if self.FIND(root1) == self.FIND(root2):
return
if(self.S[root1] < self.S[root2] ):
self.S[root2] = root1
elif self.S[root1] == self.S[root2] :
self.S[root2] = self.S[root2] - 1
self.S[root1] = root2
else:
self.S[root1] = root2
4 . 1 2 F u n d a m e n ta l s o f d i s jo i n t s e t s
G r eedy A l go ri thm s | 163
print self.S
ds = DisjointSet(7)
ds.UNION(5, 6)
ds.UNION(1, 2)
ds.UNION(0, 2)
print ds.FIND(5), ds.FIND(1), ds.FIND(2)
Note: For FIND operation there is no change in the implementation.
Path compression
FIND operation traverses a list of nodes on the way to the root. We can make later FIND
operations efficient by making each of these vertices point directly to the root. This process
is called ℎ . For example, in the FIND( ) operation, we travel from to the
root of the tree. The effect of path compression is that every node on the path from to the
root has its parent changed to the root.
1 1
2 3 2 3
4 5 6 7 4 5 6 7
With path compression, the only change that is made to the FIND function is that [ ] is
made equal to the value returned by FIND. That means, after the root of the set is found
recursively, is made to point directly to it. This happens recursively to every node on the
path to the root.
return X
else:
self.S[X]= self.FIND(self.S[X])
return self.S[X]
def UNION(self, root1, root2):
if self.FIND(root1) == self.FIND(root2):
return
if(self.S[root1] < self.S[root2] ):
self.S[root2] = root1
elif self.S[root1] == self.S[root2] :
self.S[root2] = self.S[root2] - 1
self.S[root1] = root2
else:
self.S[root1] = root2
Note: Path compression is compatible with UNION by size but not with UNION by height
as there is no efficient way to change the height of the tree.
Summary
Performing union-find operations on a set of objects.
Algorithm Worst-case time
Quick-find
Quick-union
Quick-union by size/height +
Path compression +
Quick-union by size/height + Path compression ( + )
=
∈
We say that a subset s ∈ F covers all elements in X. Our goal is to find a minimum size
subset ⊆ whose members cover all of X.
=
∈
4 . 1 3 M i n i m u m s e t co v e r p r o b l e m
G r eedy A l go ri thm s | 165
The cost of the set-covering is the size of C, which defines as the number of sets it contains,
and we want |C| to be minimum.
Example
Consider Tata motors need to buy a certain amount of varied supplies and there are
suppliers who offer various deals for different combinations of materials:
Supplier 1: 4 ton of steel + 500 tiles for Rs 1 Million
Supplier 2: 5 ton of steel + 2000 tiles for Rs 2 Million
etc…
You could use set covering to find the best way to get all the materials while minimizing
cost.
An example of set-covering is shown in figure below. Suppose = {1, 2, 3, 4, 5, 6}, = {5,
6, 8, 9}, = {1, 4, 7, 10}, = {2, 5, 7, 8, 11}, = {3, 6, 9, 12} and = {10, 11}. We can see
that the optimal solution has size 3 because the union of sets , , and contains all
elements. So, the minimum size set cover is C = { , , } and it has the size of 3.
1 2 3
4 5 6
7 8 9
10 11 12
Greedy approximation
Recall that a greedy algorithm is one that makes the choice at each stage. The greedy
algorithm selects the set containing the largest number of uncovered points at each step,
until all the points have been covered. That is, at each stage, the greedy algorithm picks
the set S ∈ F that covers the greatest number of elements that are not yet covered.
The description of this algorithm is as follows. First, start with an empty set C. Let C contain
a cover being constructed. Let U contain, at each stage, the set of remaining uncovered
elements. While there exist remaining uncovered elements, choose the set S from F that
covers as many uncovered elements as possible, put that set in C and remove these covered
elements from U. When all the elements are covered, C contains a subfamily of F that
covers X and the algorithm terminates.
For the example, the greedy algorithm will first pick because covers the maximum
number of uncovered elements {1, 2, 3, 4, 5, 6}, which is 6.
Next, it will pick since it covers maximum number of uncovered elements (uncovered
elements are 7, 8, 11), which is 3, leaving 3 more elements uncovered. Then it will select
4 . 1 3 M i n i m u m s e t co v e r p r o b l e m
G r eedy A l go ri thm s | 166
and , which cover 2 (uncovered elements covered by are 9, and 12) and 1 (remaining
uncovered element covered by is 10) uncovered elements, respectively. At this point,
every element in X will be covered.
So, by greedy algorithm, C = { , , , }, the set size of the solution is, |C| = 4. Whereas,
the optimum solution is, C = { , , } with cost |C| = 3.
def set_cover(U, subsets):
"""Find a family of subsets that covers the universal set"""
allElements = set(e for s in subsets for e in s)
# Check the subsets cover the U
if allElements != U:
return None
covered = set()
cover = []
# Greedily add the subsets with the most uncovered points
while covered != allElements:
subset = max(subsets, key=lambda s: len(s - covered))
cover.append(subset)
covered |= subset
return cover
def main():
s1 = set([1, 2, 3, 4, 5, 6])
s2 = set([5, 6, 8, 9])
s3 = set([1, 4, 7, 10])
s4 = set([2, 5, 7, 8, 11])
s5 = set([3, 6, 9, 12])
s6 = set([10, 11])
U = set(range(1, 13))
subsets = [s1, s2, s3, s4, s5, s6]
cover = set_cover(U, subsets)
print(cover)
if __name__ == '__main__':
main()
Greedy strategy produces an approximation algorithm which may not be an optimal
solution.
Greedy approximation
The greedy algorithm for weighted set cover builds a cover by repeatedly choosing a set
that minimizes the weight divided by the number of elements in that are not yet covered
by chosen sets. It stops and returns the chosen sets when they form a cover.
4 . 1 3 M i n i m u m s e t co v e r p r o b l e m
G r eedy A l go ri thm s | 167
Algorithm set-cover( , ):
1. Initialize with empty set:
= []
2. Define ( ): Indicates the number of elements in .
( )=
∈
3. Repeat until ( ) = ( ):
a. Choose ∈ minimizing the price per element:
( ∪ { }) − ( )
b. Let = ∪ { }.
4. Return .
Example
Suppose = {1, 2, 3, 4, 5, 6}, = {5, 6, 8, 9}, = {1, 4, 7, 10}, = {2, 5, 7, 8, 11}, = {3,
6, 9, 12}, and = {10, 11}. Also, the weights of these subsets were, w = {1, 2, 3, 4, 5, 6}.
So, size of , ( ) is 12 as we have 12 different elements among all subsets.
The first step of the algorithm is initialization of the set-cover C = []. Since is empty, the
value of ( ) would be 0. Now, for each of the set in , calculate the price per element.
( )
Price per element for set = ( ) ( )
= = =
( )
Price per element for set = ( ) ( )
= = =
( )
Price per element for set = ( ) ( )
= = =
( )
Price per element for set = ( ) ( )
= = =
( )
Price per element for set = ( ) ( )
= = =
( )
Price per element for set = ( ) ( )
= = =3
Among all these values, the minimum is and is associated with set . Hence, add to
and delete it from F.
=[ ]
For the second iteration, the remaining sets were: to .
( )
Price per element for set = ( ∪ ) ( )
= ∪
= =
( )
Price per element for set = ( ∪ ) ( )
= ∪
= =
( )
Price per element for set = ( ∪
= = =
) ( ) ∪
( )
Price per element for set = ( ∪
= = =
) ( ) ∪
( )
Price per element for set = ( ∪
= = =3
) ( ) ∪
Among all these values, the minimum is and is associated with set . Hence, add to
.
=[ , ]
For the third iteration, the remaining sets were: to .
( )
Price per element for set = ( ∪ ) ( )
= ∪
= =
4 . 1 3 M i n i m u m s e t co v e r p r o b l e m
G r eedy A l go ri thm s | 168
( )
Price per element for set = ( ∪
= = = =2
) ( ) ∪
( )
Price per element for set = ( ∪ ) ( )
= ∪
= = =5
( )
Price per element for set = ( ∪ ) ( )
= ∪
= =3
Among all these values, the minimum is 1.5 and is associated with set . Hence, add to
.
=[ , , ]
For the fourth iteration, the remaining sets were: to .
( )
Price per element for set = ( ∪ ) ( )
= ∪
= = =4
( )
Price per element for set = ( ∪ ) ( )
= ∪
= = =5
( )
Price per element for set = ( ∪
= = = , indicates does not add
) ( ) ∪
any element to .
Among all these values, the minimum is 4 and is associated with set . Hence, add to
.
=[ , , , ]
For the fifth iteration, the remaining sets were: to .
( )
Price per element for set = ( ∪
= = = =5
) ( ) ∪
( )
Price per element for set = ( ∪
= = = , indicates does not add
) ( ) ∪
any element to .
Among all these values, the minimum is 4 and is associated with set . Hence, add to
.
=[ , , , , ]
After this iteration, size of ( ( ) = 12) and size of ( ( ) = 12) are equal, and it is the end
of algorithm.
infinity = float("infinity")
def set_cover(F, w):
udict = {}
C = list()
s = [] # During the process, F will be modified. Make a copy for F.
for index, item in enumerate(F):
s.append(set(item))
for j in item:
if j not in udict:
udict[j] = set()
udict[j].add(index)
pq = PriorityQueue()
cost = 0
coveredElements = 0
for index, item in enumerate(s): # add all sets to the priority queue
if len(item) == 0:
pq.add(index, infinity)
else:
pq.add(index, float(w[index]) / len(item))
while coveredElements < len(udict):
a = pq.extract_min() # get the most cost-effective set
4 . 1 3 M i n i m u m s e t co v e r p r o b l e m
G r eedy A l go ri thm s | 169
C.append(a) # a: set id
cost += w[a]
coveredElements += len(s[a])
# Update the sets that contain the new covered elements
for m in s[a]: # m: element
for n in udict[m]: # n: set id
if n != a:
s[n].discard(m)
if len(s[n]) == 0:
pq.add(n, infinity)
else:
pq.add(n, float(w[n]) / len(s[n]))
s[a].clear()
pq.add(a, infinity)
return C, cost
if __name__ == "__main__":
F = [[1, 2, 3, 4, 5, 6], [5, 6, 8, 9], [1, 4, 7, 10], [2, 5, 7, 8, 11], \
[3, 6, 9, 12], [10, 11]]
w = [1, 2, 3, 4, 5, 6]
C, cost = set_cover(F, w)
print "Selected subsets:", C
print "Cost:", cost
A B
D C
4 . 1 4 F u n d a m e n ta l s o f g r a p h s
G r eedy A l go ri thm s | 170
C D F
B A E
When an edge connects two vertices, the vertices are said to be adjacent to each
other and the edge is incident on both vertices.
The egree of a vertex is the number of edges incident on it.
A subgraph is a subset of a graph’s edges (with associated vertices) that form a
graph.
A path in a graph is a sequence of adjacent vertices. Simple path is a path with no
repeated vertices. In the graph below, the dotted lines represent a path from to
.
C D F
B A E
A cycle is a path where the first and last vertices are the same. A simple cycle is a
cycle with no repeated vertices or edges (except the first and last vertices).
C D F
B A E
A B
D C
7 5
5 9
E
9
15 8
D F G
6 11
4 . 1 4 F u n d a m e n ta l s o f g r a p h s
G r eedy A l go ri thm s | 171
Graphs with relatively few edges (generally if it edges < | | log | |) are called
ℎ and graphs with relatively few of the possible edges missing are called
graphs.
Applications of graphs
Since graphs are powerful abstractions, they are very important in modeling data. In fact,
many problems can be reduced to known graph problems. Few of them include the
following:
Social network graphs (to tweet or not to tweet): Graphs that represent who knows
whom, who communicates with whom, who influences whom or other
relationships in social structures. An example is the graph of who follows
whom.
The power grid, the Internet, and the water network are all examples of graphs
where vertices represent connection points, and edges are the wires or pipes
between them.
Transportation networks: In road networks, vertices are intersections and edges
are the road segments between them, and for public transportation networks
vertices are stops, and edges are the links between them. Such networks are used
by many map programs such as Google maps.
Computer networks: Local area network, Internet, Web
Databases: For representing ER (Entity Relationship) diagrams in databases, for
representing dependency of tables in databases.
Dependence graphs: Graphs can be used to represent dependencies among items.
Such graphs are often used in large projects.
Graph representations
To manipulate graphs, we need to represent them in some useful form. Basically, there are
two ways of doing this:
Adjacency matrix representation
Adjacency list representation
4 . 1 4 F u n d a m e n ta l s o f g r a p h s
G r eedy A l go ri thm s | 172
self.id = id
def set_visited(self):
self.visited = True
def __str__(self):
return str(self.id)
class Graph:
def __init__(self, numVertices, cost = 0):
self.adjMatrix = [[-1]*numVertices for _ in range(numVertices)]
self.numVertices =numVertices
self.vertices = []
for i in range(0,numVertices):
newVertex = Vertex(i)
self.vertices.append(newVertex)
Description
In this method, we use a matrix with size × . The values of matrix are boolean. Let us
assume the matrix is . The value [ , ] is set to 1 if there is an edge from vertex u to
vertex v and 0 otherwise.
In the matrix, each edge is represented by two bits for undirected graphs. That means, an
edge from u to v is represented by 1 value in both [u, v] and [ , ]. To save time, we
can process only half of this symmetric matrix. Also, we can assume that there is an “edge”
from each vertex to itself. So, [u, u] is set to 1 for all vertices. If the graph is a directed
graph then we need to mark only one entry in the adjacency matrix. As an example,
consider the directed graph below.
A B
D C
4 . 1 4 F u n d a m e n ta l s o f g r a p h s
G r eedy A l go ri thm s | 173
def get_vertex_ID(self):
return self.id
def set_visited(self):
self.visited = True
def __str__(self):
return str(self.id)
class Graph:
def __init__(self, numVertices, cost = 0):
self.adjMatrix = [[-1]*numVertices for _ in range(numVertices)]
self.numVertices =numVertices
self.vertices = []
for i in range(0,numVertices):
newVertex = Vertex(i)
self.vertices.append(newVertex)
def get_vertices(self):
vertices = []
for vertxin in range(0, self.numVertices):
vertices.append(self.vertices[vertxin].get_vertex_ID())
return vertices
def print_matrix(self):
for u in range(0, self.numVertices):
row = []
for v in range(0, self.numVertices):
row.append(self.adjMatrix[u][v])
print row
def get_edges(self):
edges = []
for v in range(0,self.numVertices):
4 . 1 4 F u n d a m e n ta l s o f g r a p h s
G r eedy A l go ri thm s | 174
if __name__ == '__main__':
G = Graph(5)
G.set_vertex(0,'a')
G.set_vertex(1, 'b')
G.set_vertex(2, 'c')
G.set_vertex(3, 'd')
G.set_vertex(4, 'e')
print 'Graph data:'
G.add_edge('a', 'e', 10)
G.add_edge('a', 'c', 20)
G.add_edge('c', 'b', 30)
G.add_edge('b', 'e', 40)
G.add_edge('e', 'd', 50)
G.add_edge('f', 'e', 60)
print G.print_matrix()
print G.get_edges()
The adjacency matrix representation is good if the graphs are dense. The matrix requires
O(V ) bits of storage and O(V ) time for initialization. If the number of edges is proportional
to V , then there is no problem because V steps are required to read the edges. If the graph
is sparse, the initialization of the matrix dominates the running time of the algorithm as it
takes O(V ).
4 . 1 4 F u n d a m e n ta l s o f g r a p h s
G r eedy A l go ri thm s | 175
Description
Considering the same example which is used for adjacency matrix representation, the
adjacency list representation can be given as:
A
B D
B
C
C
A D
Since vertex A has an edge for B and D, we have added them in the adjacency list for A and
the same process has to be applied for all the other vertices as well.
class Vertex:
def __init__(self, node):
self.id = node
self.adjacent = {}
# Set distance to infinity for all nodes
self.distance = None
# Mark all nodes unvisited
self.visited = False
# Predecessor
self.previous = None
def add_neighbor(self, neighbor, weight=0):
self.adjacent[neighbor] = weight
def get_connections(self):
return self.adjacent.keys()
def get_vertex_ID(self):
return self.id
def get_weight(self, neighbor):
return self.adjacent[neighbor]
def set_distance(self, dist):
self.distance = dist
def get_distance(self):
return self.distance
def set_previous(self, prev):
self.previous = prev
def set_visited(self):
self.visited = True
def __str__(self):
return str(self.id) + ' adjacent: ' + str([x.id for x in self.adjacent])
class Graph:
def __init__(self):
self.vertDictionary = {}
4 . 1 4 F u n d a m e n ta l s o f g r a p h s
G r eedy A l go ri thm s | 176
self.numVertices = 0
def __iter__(self):
return iter(self.vertDictionary.values())
def add_vertex(self, node):
self.numVertices = self.numVertices + 1
newVertex = Vertex(node)
self.vertDictionary[node] = newVertex
return newVertex
def get_vertex(self, n):
if n in self.vertDictionary:
return self.vertDictionary[n]
else:
return None
def add_edge(self, frm, to, cost = 0):
if frm not in self.vertDictionary:
self.add_vertex(frm)
if to not in self.vertDictionary:
self.add_vertex(to)
self.vertDictionary[frm].add_neighbor(self.vertDictionary[to], cost)
#For directed graph do not add this
self.vertDictionary[to].add_neighbor(self.vertDictionary[frm], cost)
def get_vertices(self):
return self.vertDictionary.keys()
def set_previous(self, current):
self.previous = current
def get_previous(self, current):
return self.previous
def get_edges(self):
edges = []
for v in G:
for w in v.get_connections():
vid = v.get_vertex_ID()
wid = w.get_vertex_ID()
edges.append((vid, wid, v.get_weight(w)))
return edges
if __name__ == '__main__':
G = Graph()
G.add_vertex('a')
G.add_vertex('b')
G.add_vertex('c')
G.add_vertex('d')
G.add_vertex('e')
G.add_edge('a', 'b', 4)
G.add_edge('a', 'c', 1)
G.add_edge('c', 'b', 2)
G.add_edge('b', 'e', 4)
G.add_edge('c', 'd', 4)
G.add_edge('d', 'e', 4)
print 'Graph data:'
print G.get_edges()
4 . 1 4 F u n d a m e n ta l s o f g r a p h s
G r eedy A l go ri thm s | 177
For this representation, the order of edges in the input is . This is because they
determine the order of the vertices on the adjacency lists. The same graph can be
represented in many different ways in an adjacency list. The order in which the edges
appear on the adjacency list affects the order in which the edges are processed by
algorithms.
From the above discussion, you must have got some basics of graphs data
structure. Now, let us focus on greedy algorithms which use this graphs
data structure.
Examples
A DAG can be used to represent prerequisites in a university course, constraints on
operations to be carried out in building construction, or depict dependencies of a library.
4 . 1 5 T o p o l o g i ca l s o r t
G r eedy A l go ri thm s | 178
Consider a source code structure where you are building several DLLs (dynamically linked
libraries) and they have dependencies on each other. For example, DLL A has references to
DLLs B, C, and D (may be the code has import statements which references the DLLs B,
C, and D). So, to build DLL A, one must have built DLLs B, C and D.
Let's mark a dependency edge from each of B, C and D to A implying that A depends on
the other three and can only be built once each of the three are built. Technically speaking,
( , ) => An edge from implies that DLL can be built only when DLL is already
built.
Build DLL A
After constructing a graph of these DLLs and dependency edges, you can conclude that a
successful build is possible only if the resulting graph is acyclic. How does the build system
decide in which order to build these DLLs? It sorts them topologically. These kinds of
dependency graphs are being used in many package management tools. For example, −
in Ubuntu uses topological sorting to obtain the best possible sequence in which a set
of debian packages can be installed/removed.
As another example, constraints for a small house construction process are given below.
Note that no order is imposed between and , but the plumbing
works cannot be started until the walls are constructed.
Foundations
Walls works
Interior decorations
A topological-sort of a DAG is a linear ordering of the vertices such that appears before
whenever there is an edge < , >.
For the above DAG, the following sequences are valid topological orderings:
4 . 1 5 T o p o l o g i ca l so r t
G r eedy A l go ri thm s | 179
And in the graph below: 7, 5, 3, 11, 8, 2, 9, 10 and 3, 5, 7, 8, 11, 2, 9, 10 are both topological
orderings.
7 5 3
11 8
2 9 10
In case we’re using adjacency matrix we need space to store the graph. To find the
vertices with no predecessors, we have to scan the entire graph which will cost us O( )
time. And we’ll have to do that |V| times. This will be O( ) time consuming algorithm
and for dense graphs this will be quite an ineffective algorithm.
What about the adjacency list? There we need | | space to store a directed graph. How
fast can we find a node with no predecessor? Practically we’ll need O(E) time. Thus in the
worst case we have again O( ) time consuming programs.
We just need to store both incoming and outgoing edges and slightly modify the adjacency
lists to get the topological ordering,. First we easily find the nodes with no predecessors.
Then, using a queue, we can keep the nodes with no predecessors and on each DeQueue
(delete from queue) we can remove the edges from the node to all the other nodes. This is
going to be the best approach among the three.
Initially, is computed for all vertices, starting with the vertices which are having
indegree 0. That is, consider the vertices which do not have any prerequisite. To keep track
of vertices with indegree zero, we can use a queue.
All vertices of indegree 0 are placed on queue. While the queue is not empty, a vertex is
removed, and all edges adjacent to have their indegrees decremented. A vertex is put on
the queue as soon as its indegree falls to 0. The topological ordering is the order in which
the vertices DeQueue.
The time complexity of this algorithm is O(| | + | |) if adjacency lists are used.
def topological_sort(graph):
topologicalSortedList = [] #result
zeroInDegreeVertexList = [] #node with 0 in-degree/inbound neighbours
inDegree = { u : 0 for u in graph } #inDegree/inbound neighbours
#Step 1: Iterate graph and build in-degree for each vertex
#Time complexity: O(V+E) - outer loop goes V times and inner loop goes E times
for u in graph:
for v in graph[u]:
inDegree[v] += 1
#Step 2: Find vertex(s) with 0 in-degree
for k in inDegree:
#print(k,inDegree[k])
if (inDegree[k] == 0):
zeroInDegreeVertexList.append(k)
#Step 3: Process nodes with in-degree = 0
while zeroInDegreeVertexList:
v = zeroInDegreeVertexList.pop(0) #order in important!
topologicalSortedList.append(v)
#Step 4: Update in-degree
4 . 1 5 T o p o l o g i ca l so r t
G r eedy A l go ri thm s | 180
Topological sort is not possible if the graph has a cycle, since for thetwo
vertices and on the cycle, precedes and precedes .
4 . 1 6 S h o r t e s t pa t h a l go r i t h m s
G r eedy A l go ri thm s | 181
Example
As an example, consider the following graph and its adjacency list representation.
4 . 1 7 S h o r t e s t pa t h i n a n un w e i g ht e d gr a p h
G r eedy A l go ri thm s | 182
4 . 1 7 S h o r t e s t pa t h i n a n un w e i g ht e d gr a p h
G r eedy A l go ri thm s | 183
1
2 B
2 D
2 D
For nodes , , and , their neighbors were already processed. Hence, no change in table
data. This process would be continued as long as queue is not empty. But, the queue would
be empty after processing these three nodes. Note that, node is not reachable from node
. Hence, it was not updated in the distance table.
Vertex Distance[ ] Previous vertex which gave Distance[ ] Queue content
0 -
1
None -
1
2
2
2
4 . 1 7 S h o r t e s t pa t h i n a n un w e i g ht e d gr a p h
G r eedy A l go ri thm s | 184
g.add_edge("c", "f", 1)
g.add_edge("d", "f", 1)
g.add_edge("d", "g", 1)
g.add_edge("e", "g", 1)
g.add_edge("g", "f", 1)
unweighted_shortest_path(g, "a")
Performance
Running time: O(| | + | |), if adjacency lists are used. In loop, we are checking the
outgoing edges for a given vertex and the sum of all examined edges in the while loop is
equal to the number of edges which gives O(| |).
If we use matrix representation, the complexity is O(| | ), because we need to read an entire
row in the matrix of length | | to find the adjacent vertices for a given vertex.
Example
The ’ algorithm can be better understood through an example, which explains
each step that is taken and how is being calculated. As in the unweighted shortest
path algorithm, here too we need the table. The algorithm works by keeping the
shortest distance of vertex from the source in the table. The value [ ]
holds the distance from to . The shortest distance from the source to itself is zero. The
table for all other vertices is set to ∞ to indicate that those vertices are not yet
processed.
Vertex Distance[v] Previous vertex which gave Distance[v]
(source) 0 -
∞ -
4 . 1 8 S h o r t e s t pa t h i n we i g h te d g r a p h -D i j k s t r a ’ s a l go r i t h m
G r eedy A l go ri thm s | 185
∞ -
∞ -
∞ -
After the algorithm finishes, the table will have the shortest distance from source
to every other vertex . To simplify the understanding of ’ algorithm, let us
assume that the given vertices are maintained in two sets. Initially, the first set contains
only the source element and the second set contains all the remaining elements. After the
iteration, the first set contains vertices which are the closest to the source. These
vertices are the ones for which we have already computed the shortest distances from the
source.
The weighted graph below has 5 vertices from − . The value between the two vertices is
known as the or ℎ between two vertices. For example, the edge cost between
and is 1. Dijkstra’s algorithm can be used to find the shortest path from source to all
the remaining vertices in the graph.
4
A B
4
1 2 E
C D
4
Initially, the table would look like:
Vertex Previous vertex which gave
Distance[v] Priority Queue
v Distance[v]
0 -
∞ -
(0, ), (∞, ), (∞, ), (∞, ),
∞ -
(∞, )
∞ -
∞ -
The distance table entries would be kept in priority queue to select a
vertex for processing in each iteration. From the priority queue, the
vertex with minimum distance would be selected for processing.
For the first step, the minimum distance in the priority queue is 0 and it is with node .
Hence, select node for processing by deleting it from the priority queue. From node , we
can reach nodes and . So, in the update the reachability of nodes and
with their costs of reaching from node .
4
0 - A B
(4, ),
4 A 4
(1, ),
1 A
(∞, ), 1 2
∞ - E
(∞, )
∞ -
C D
4
ℎ ℎ ,
4 . 1 8 S h o r t e s t pa t h i n we i g h te d g r a p h -D i j k s t r a ’ s a l go r i t h m
G r eedy A l go ri thm s | 186
Now, let us select the minimum distance among all. The node with minimum distance in
the priority queue is . That means, we have to reach other nodes from these two nodes (
and ). For example, node can be reached from and also from . In this case, we have
to select the one which gives the lowest cost. Since reaching through is giving the
minimum cost (1 + 2), we update the for node with cost 3 and the node
from which we got this cost as .
4
A B
0 - 4
3 C (3, ),
1 A (5, ), 1 2 E
5 C (∞, )
-1 -
C D
4
ℎ ℎ ,
The only vertex remaining is . To reach , we have to see all the paths through which we
can reach and select the one which gives the minimum cost. We can see that using node
(in the priority queue the minimum distance 3 is with node ) as the intermediate vertex
through would get the minimum cost.
4
A B
0 - 4
3 C 2
(5, ), 1 E
1 A
(7, )
5 C
7 B
C D
4
The next minimum distance in the priority queue is 5 and it is with node . Delete it from
the priority queue and update the distances of its neighbor nodes. Node has only one
neighbor with cost 4 and the distance for reaching D from source node A is 5. So, to reach
E, the total cost would be 9 which is more than current cost for reaching E. Hence, the
priority queue would not be updated.
4
A B
4
0 -
3 C 1 2 E
1 A
(7, )
5 C
7 B C D
4
The only remaining element in the priority queue is node with distance 7. Since it does
not have any neighbors, there won’t be any further processing and it is the end of ’
algorithm. The final minimum cost tree which Dijkstra’s algorithm generates is:
A B
4
1 2 E
4
C D
4 . 1 8 S h o r t e s t pa t h i n we i g h te d g r a p h -D i j k s t r a ’ s a l go r i t h m
G r eedy A l go ri thm s | 187
4 . 1 8 S h o r t e s t pa t h i n we i g h te d g r a p h -D i j k s t r a ’ s a l go r i t h m
G r eedy A l go ri thm s | 188
Performance
The time complexity of Dijkstra’s algorithm is dependent upon the internal data structures
used for implementing the queue and representing the graph. When using an adjacency
list to represent the graph and an unordered array to implement the queue, the time
complexity is O( ) where is the number of vertices in the graph.
However, using an adjacency list to represent the graph and a min-heap to represent the
queue, the time complexity can go as low as O( ), where is the number of edges. In
Dijkstra’s algorithm, the efficiency depends on the number of delete operations (
extract_min operations) and updates for priority queue ( updates) that are used. The term
comes from updates (each update takes ) for the heap.
It is possible to get an even lower time complexity by using more complicated and memory
intensive internal data structures (Fibonacci heap).
Space Complexity: O( ), for maintaining the priority queue.
C
For the first step, the minimum distance in the priority queue is 0 and it is with node .
Hence, select node for processing by deleting it from the priority queue. From node , we
can reach nodes and . So, in the update the reachability of nodes and
with their costs of reaching from node .
4
A B
0 -
(4, ), 1 -2
4 A
(1, )
1 A
C
Now, let us select the minimum distance among all. The node with minimum distance in
the priority queue is . That means, we have to reach other nodes from these two nodes (
and ). For example, node can be reached from and also from . In this case, we have
to select the one which gives the lowest cost. Since reaching through is giving the
minimum cost (1 − 2), we update the for node with cost −1 and the node
from which we got this cost as .
4
A B
0 -
−1 C (−1, ) 1 -2
1 A
C
4 . 1 8 S h o r t e s t pa t h i n we i g h te d g r a p h -D i j k s t r a ’ s a l go r i t h m
G r eedy A l go ri thm s | 189
The only remaining element in the priority queue is node with distance -1. Hence, select
node for processing by deleting it from the priority queue. From node , we can reach
nodes and . Node has the distance 0 and has distance 1. Through node B, the
distance to node would be 3 (−1 + 4) which is greater than 0. Hence, we do not update
node distance. But for node , the new distance through node would be −1 (1 + −2)
and so update the distance and its previous node as .
4
A B
0 -
−1 C (−1, ) 1 -2
−1 B
C
This process would continue indefinitely and is not possible to determine the shortest path
from source node to nodes , and .
Algorithm
The idea of the algorithm is fairly simple:
4 . 1 9 B e l l m a n - Fo r d a l go r i t h m
G r eedy A l go ri thm s | 190
Relaxation formula
Relaxation is the most important step in Bellman-Ford. It is what increases the accuracy
of the distance to any given vertex. Relaxation works by continuously shortening the
calculated distance between vertices, comparing that distance with other known distances.
The formula for relaxation remain the same as Dijkstra’s Algorithm. Initialize the queue
with . Then, at each stage, a vertex , and find all vertices w adjacent to such
that,
+ ℎ ( , ) < old distance to w
Update w old distance and path, and place on a queue if it is not already there. A bit can be
set for each vertex to indicate presence in the queue and repeat the process until the queue
is empty.
The detection of negative cycles is important, but the main contribution of
Example
Given the following directed graph and using vertex as the source (setting its distance to
0), we initialize all the other distances to ∞.
3
A B
4
3
6 1 E
1
C D
2
Initially, the table would look like:
Vertex v Distance[v] Previous vertex which gave Distance[v]
0 -
∞ -
∞ -
∞ -
∞ -
Take one vertex at a time say A, and relax all the edges in the graph. Point worth noticing
is that we can only relax the edges which are outgoing from the vertex A. Rest of the edges
will not make much of a difference. Also, it is useful to maintain a list of edges handy.
(A, C, 6), (A, D, 3), (B, A, 3), (C, D, 2), (D, C, 1), (D, B, 1), (E, B, 4), (E, D, 2)
Relax edge (A, C, 6): Distance to node is ∞ which is greater than the distance of +
weight of edge to (0 + 6 < ∞).
3
A B
4
0 -
∞ - 3
6 1 E
6 A
∞ -
∞ - 1
C D
2
Relax edge (A, D, 3): Distance to node is ∞ which is greater than the distance of +
weight of edge to (0 + 3 < ∞).
3
A B
4
0 -
3
∞ - 6 1 E
6 A
3 A
1
∞ - C D
2
Relax edge (B, A, 3): Distance to node is 0 which is less than the distance of + weight
of edge to (0 < ∞ + 3). Hence, no update to distance for node .
Relax edge (C, D, 2): Distance to node is 3 which is less than the distance of + weight
of edge to (3 < 6 + 2). Hence, no update to distance for node .
Relax edge (D, C, 1): Distance to node is 6 which is greater than the distance of +
weight of edge to (6 < 3 + 1). Hence, update to distance for node with 4.
3
A B
0 - 4
∞ -
4 D 3
6 1 E
3 A
∞ -
1
C D
2
Relax edge (D, B, 1): Distance to node is ∞ which is greater than the distance of +
weight of edge to (∞ > 3 + 1). Hence, update to distance for node .
3
A B
0 - 4
4 D
D 3
4 6 1 E
3 A
∞ -
1
C D
2
Relax edge (E, B, 4): Distance to node is ∞ which is less than distance of + weight of
edge to (4< ∞ + 4). Hence, no update to distance for node .
4 . 1 9 B e l l m a n - Fo r d a l go r i t h m
G r eedy A l go ri thm s | 192
Relax edge (E, D, 2): Distance to node is 3 which is less than distance of + weight of
edge to (3< ∞ + 3). Hence, no update to distance for node . This completes the first
pass.
3
A B
4
0 -
4 D 3
6 1 E
4 D
3 A
∞ - 1
C D
2
Continue this process for |V|-1 times.
So, why to process relax edges for |V| – 1 times?
The argument would be, that the shortest path in the graph with |V| vertices cannot be
lengthier than |V| – 1. If we relax all the edges, we will cover all the possibilities and will
be left with all the shortest paths.
Also, if we analyze the above process, we understand that the cost to reach each vertex can
be updated times (where is the number of incoming edges to this vertex). It might be
possible that the first cost is so less that it is not changed by the subsequent operations.
In each pass of Bellman Ford algorithm, we can start from any node. No
4 . 1 9 B e l l m a n - Fo r d a l go r i t h m
G r eedy A l go ri thm s | 193
Performance
As described above, Bellman-Ford algorithm makes relaxations for every iteration, and
there are O(| |) iterations. Therefore, the worst-case running time of Bellman-Ford
algorithm is O(| | . | |).
However, in some scenarios, the number of iterations can be much lower. For certain
graphs, only one iteration is needed, and so in the best case scenario, only O(| |) time is
needed. An example of a graph that needs only one round of relaxation is a graph where
each vertex connects only to the next one in a linear fashion, like the graph below.
Bellman Ford algorithm is not a greedy algorithm. But, we would need this
for Floyd-Warshall’s all pair shortest path algorithm and that would be
covered in chapter.
C D
For this simple graph, we can have multiple spanning trees as shown below.
A B A B A B A B
C D C D C D C D
4 . 2 0 O v e r v i e w o f s ho r t e st p a t h a lg o r i th m s
G r eedy A l go ri thm s | 194
Example
Prim's algorithm shares a similarity with the shortest path algorithms (Dijkstra's
algorithm). As in Dijkstra's algorithm, Prim's algorithm too maintains the and
ℎ in a table. The only exception is that since the definition of in Prim's
algorithm is different, the updating statement also changes a little. The update statement
is simpler than before.
4.22 Prim's algorithm
G r eedy A l go ri thm s | 195
The weighted graph below has 5 vertices from − . The value between the two vertices is
known as the or ℎ between two vertices. For example, the edge cost between
and is 1.
4
A B
4
1 2 E
C D
4
Initially, the table would look like:
Vertex v Distance[v] Previous vertex which gave Distance[v] Priority Queue
0 -
∞ -
∞ - (0, )
∞ -
∞ -
The distance table entries would be kept in priority queue to select a vertex
for processing in each iteration. From the priority queue, the node with
minimum distance would be selected for processing.
The first step is to choose a vertex to start with. This will be the vertex . For the first
step, the minimum distance in the priority queue is 0 and it is with node . Hence, select
node for processing by deleting it from the priority queue. From node , we can reach
nodes and . These two nodes are not yet processed and not in the priority queue. So, in
the , update the reachability of nodes and with their costs of reaching
from node .
4
A B
0 - 4
4 A
(4, ),
1 A 1 2 E
(1, )
∞ -
∞ -
C D
4
ℎ ℎ ,
Now, let us select the minimum distance among all. The node with minimum distance in
the priority queue is . That means, we have to reach other nodes from these two nodes (
and ). For example, node can be reached from and also from . In this case, we have
to select the one which gives the lowest cost. Since reaching through is giving the
minimum cost (1 + 2), we update the for node with cost 3 and the node
from which we got this cost as .
4
A B
0 -
4
3 C (3, ),
1 A (5, ), 1 2
5 C (∞, ) E
-1 -
C D
4
A B
4
1 2 E
4
C D
pq.add(v.get_distance(), v.get_vertex_ID())
MST = []
while not pq.empty():
t = pq.extract_min()
currentVert = G.get_vertex(t[1])
MST.append((currentVert.get_previous(), currentVert.get_vertex_ID()))
for nextVert in currentVert.get_connections():
newCost = currentVert.get_weight(nextVert) + currentVert.get_distance()
if nextVert in pq and newCost<nextVert.get_distance():
nextVert.set_previous(currentVert)
nextVert.set_distance(newCost)
pq.replace_key(nextVert,newCost)
print MST
#create an empty graph
G = Graph()
#add vertices to the graph
for i in ["a", "b", "c", "d", "e"]:
G.add_vertex(i)
#add edges to the graph - need one for each edge to make them undirected
#since the edges are unweighted, make all cost 1
G.add_edge("a", "b", 4)
G.add_edge("a", "c", 1)
G.add_edge("b", "e", 4)
G.add_edge("c", "b", 2)
G.add_edge("c", "d", 4)
G.add_edge("d", "e", 4)
prim(G, "a")
Performance
The entire implementation of this algorithm is identical to that of Dijkstra's algorithm. The
time complexity of Prim's algorithm is dependent upon the internal data structures used
for implementing the queue and representing the graph. When using an adjacency list to
represent the graph and an unordered array to implement the queue, the time complexity
is O( ), where is the number of vertices in the graph.
However, using an adjacency list to represent the graph and a min-heap to represent the
queue, the time complexity can go as low as O( ), where is the number of edges. In
Prim's algorithm, the efficiency depends on the number of delete operations ( extract_min
operations) and updates for priority queue ( updates) that are used. The term comes
from updates (each update takes ) for the heap.
If the graphs are dense, we can go for adjacency list representation of the graph and an
unordered array for the queue. That would have O( ) running time. If the graphs are
sparse, adjacency list with binary heaps would be a good choice with O( ) running
time.
Space Complexity: O( ), for maintaining the priority queue.
4 . 2 3 K r u s k a l ’ s a l go r i t h m
G r eedy A l go ri thm s | 198
The algorithm starts with V different trees (V is the vertices in the graph). While
constructing the minimum spanning tree, Kruskal’s algorithm selects an edge that has
minimum weight and then adds that edge if it doesn’t create a cycle. So, initially, there are
|V| single-node trees in the forest. Adding an edge merges two trees into one. When the
algorithm is completed, there will be only one tree, and that is the minimum spanning tree.
There are two ways of implementing Kruskal’s algorithm:
By using Disjoint Sets: Using UNION and FIND operations
By using Priority Queues: Maintains weights in priority queue
The appropriate data structure is the UNION/FIND algorithm [for implementing forests].
Two vertices belong to the same set if and only if they are connected in the current spanning
forest. Each vertex is initially in its own set. If and are in the same set, the edge is
rejected because it forms a cycle. Otherwise, the edge is accepted, and a UNION is
performed on the two sets containing and .
Example
To understand Kruskal's algorithm let us consider the following example.
7 8
A C B
7 5
5 9
E
9
15 8
D F G
6 11
Now, let us perform Kruskal’s algorithm on this graph. The first step is to create a set of
edges with weights, and arrange them in an ascending order of weights (costs).
Edge AD BE DF AC CE EF BC DC EG FG DE
Cost 5 5 6 7 7 8 8 9 9 11 15
Now, we start adding edges to the spanning tree (subgraph) beginning from the one which
has the least weight. Throughout, we shall keep check on the spanning properties remain
intact. In case, by adding one edge, the spanning tree property does not hold then we shall
consider not to include the edge in the spanning tree.
The least cost is 5 and edges involved are AD and BE. We add them (dotted lines). Adding
them does not violate spanning tree properties, so we continue to our next edge selection.
7 8
A C B
7 5
5 9
E
9
15 8
D F G
6 11
4 . 2 3 K r u s k a l ’ s a l go r i t h m
G r eedy A l go ri thm s | 199
Next minimum cost is 6, and the associated edge is DF. We add it as it does not create a
cycle.
7 8
A C B
7 5
5 9
E
9
15 8
D F G
6 11
Next minimum cost in the table is 7, and associated edges are AC and CE. Adding them
does not violate spanning tree properties, so we continue to our next edge selection.
7 8
A C B
7 5
5 9
E
9
15 8
D F G
6 11
Next minimum cost in the table is 8, and the associated edges are EF and BC. Observe that
adding the edges EF and BC will create a cycle in the graph and we ignore them. In the
process we shall ignore/avoid all edges that create a cycle.
Next minimum cost in the table is 9, and the associated edges are DC and EG. Observe
that adding the edge DC will create a cycle in the graph and we ignore it. But, adding the
edge EG does not violate spanning tree properties, so add EG. With these last two edges,
we have included all the nodes of the graph and have minimum cost spanning tree.
7 8
A C B
7 5
5 9
E
9
15 8
D F G
6 11
def kruskal(graph):
for vertice in graph['vertices']:
make_set(vertice)
minimum_spanning_tree = set()
edges = list(graph['edges'])
edges.sort()
for edge in edges:
fr, to, weight = edge
if find(fr) != find(to):
union(fr, to)
4 . 2 3 K r u s k a l ’ s a l go r i t h m
G r eedy A l go ri thm s | 200
minimum_spanning_tree.add(edge)
return minimum_spanning_tree
graph = {
'vertices': ['A', 'B', 'C', 'D', 'E', 'F', 'G'],
'edges': set([
('A', 'C', 7), ('A', 'D', 5),
('B', 'C', 8), ('B', 'E', 5),
('C', 'D', 9), ('C', 'E', 7),
('D', 'E', 15), ('D', 'F', 6),
('E', 'F', 8), ('E', 'G', 9),
('F', 'G', 11),
])
}
print kruskal(graph)
Performance
The edges have to be sorted first and it takes O(ElogE) where it dominates the runtime for
verifying whether the edge in consideration is a safe edge or not which would take O( ElogV).
Greedy solution
The optimal strategy is the obvious greedy one. Starting with a full tank of gas, Professor
Modaiah should go to the farthest gas station he can get to within miles of Vijayawada.
He should fill up there and then go to the farthest gas station he can get to within miles
from where he has filled up, fill up there, and so on.
Another way is, Professor Modaiah should check at each gas station, whether he can make
it to the next gas station without stopping at this one. If he can, he should skip this one. If
he cannot, fill up. Professor Modaiah doesn't need to know how much gas his car has or
how far the next station is, to implement this approach, since he can determine which is
the next station at which he’ll need to stop.
4 . 2 4 M i n i m i z i n g g a s f i l l - u p st a t i o n s
G r ee dy A l go ri thm s | 201
This problem has optimal substructure. Suppose there are possible gas stations.
Consider an optimal solution with stations and whose first stop is at the gas station.
Then the rest of the optimal solution must be an optimal solution to the subproblem of the
remaining − stations. Otherwise, if there were a better solution to the subproblem,
i.e., one with fewer than − 1 stops, we could use it to come up with a solution with fewer
than stops for the full problem, contradicting our supposition of optimality.
Example
As per the above algorithm, stop if and only if you don’t have enough gas to make it to the
next gas station, and if you stop, fill the tank up all the way.
As an example, consider a route with 5 gas stations on the way to the destination. The gas
stations are separated with the following distances (in kilometers):
Gas station number 0 1 2 3 4 5
Distance between gas stations (in kms) 0 20 37 55 75 95
Note that, for the first gas station the distance is zero. Distance from station 2 to 3 is 18kms
(55-37). So, they are separated by 18 kms. For example, assume that the car can hold gas
for travelling a maximum of 40kms.
in the beginning, the car is filled up with full gas. Starting from the source location, see
whether we can reach the first station. The distance to the next station is 20kms and the
car has gas for 40kms.
Repeat the same steps for this second station too. The distance from the first station to
second station is 17kms (37-20) and the car has gas remaining for 20kms as it has
consumed gas for reaching this first station. So, skip this gas station and move to the next
station.
At the second station, car has gas remaining for 3kms as it has consumed gas for reaching
this second station (car gas balance at first station was 20 and distance for reaching this
second station is 17kms. Hence, the remaining gas balance of the car is 3). From second
station, the distance to next station is 18kms (55-37). But, car has gas only for 3kms. So,
we cannot reach the next station without filling up the gas at this station. Hence, fill up
gas at this second station and make a note of it. Once the car is filled up with full gas, go
to the next station.
At the third station, car has the gas remaining for 22kms as it has consumed gas for
reaching this second station (car gas balance at second station was 40 and distance for
reaching this third station is 18kms. Hence, the remaining gas balance in the car is 22).
From the third station, the distance to the next station is 20kms (75-55) and the car has
gas remaining for 22kms. So, skip this gas station and move to the next station.
At the fourth station, car has gas remaining for 2kms as it has consumed gas for reaching
this fourth station (car gas balance at the third station was 22 and distance for reaching
this fourth station is 20kms. Hence, the remaining gas balance in the car is 2). From the
fourth station, the distance to next station is 20kms (95-75). But, car has gas only for
2kms. So, we cannot reach the next station without filling up the gas at this station. Hence,
fill up gas at this fourth station and make a note of it. Once the car is filled up with full
gas, go to the final station.
It can be seen that, during the journey we have filled the gas for twice.
def min_refill_gas_stops(stationDist, carMaxGasCapacity):
curPos = 0
minRefill = 0
carRemainingGas = carMaxGasCapacity
numRefillStations = len(stationDist)
stops = []
4 . 2 4 M i n i m i z i n g g a s f i l l - u p st a t i o n s
G r eedy A l go ri thm s | 202
while(curPos<numRefillStations):
while(curPos < numRefillStations and stationDist[curPos] <= carRemainingGas):
curPos += 1
if (curPos >= numRefillStations):
return minRefill, stops
curPos -= 1
minRefill += 1
carRemainingGas = stationDist[curPos] + carMaxGasCapacity
if (stationDist[curPos] < carRemainingGas):
minRefill += 1
stops.append(curPos)
return minRefill, stops
print "Minimum refills required for reaching destination:", min_refill_gas_stops([0, \
20, 37, 55, 75, 95], 40)
Performance
Time Complexity: O( ), as there is only one scan of the array.
Space Complexity: O(1) if only the filled-up gas stops are required to return. If we want to
return to the location of all such stops, we need an array which would consume O( ) in the
worst case.
Tower
9kms
9kms
Greedy solution
The optimal solution to this problem is to use greedy strategy.
Tower Tower
9kms
9kms
Uncovered house
First uncovered house 9kms
Place tower 9kms away from away from the previous tower
the first uncovered house
4 . 2 5 M i n i m i z i n g c e l l u la r t o w e r s
G r eedy A l go ri thm s | 203
Performance
Time Complexity: O( + )≈O( ). Running time of the algorithm is dominated by
sorting. If the distances are small integers, and you know the range K, you could use
counting sort to do the sort in O( ) time, thereby reducing the overall time to O( ).
Space Complexity: O(1) if only the minimum cellular tower count is required to return. If
we want to returns the location of all cellular towers, we need an array which consumes
O( ) in the worst case.
Suppose we are allowed to permute the elements of each vector as we wish. Choose two
permutations such that the scalar product of two new vectors is the smallest possible, and
output that minimum scalar product.
4 . 2 6 M i n i m u m s c a la r p r o d u ct
G r eedy A l go ri thm s | 204
Example
Let us consider the two vectors X = [1, 2, -4], and Y = [-2, 4, 1]. For these two sequences
there are many possible dot products. Among all those dot products, the minimum will be:
−4 × 4 + 1 × 1 + 2 × −2 = −19.
Greedy solution
The goal is, given two sequences , ,..., and , ,..., , we need to find a
ℎ such that the dot product is minimum. How do we find
the minimum dot product values among many?
We claim that it is safe to multiply a maximum element of X by a minimum element of Y.
We illustrate this by an example. Assume that = 4 and that = { , , , } and
= { , , , }.
Consider a candidate solution: + + +
Here, is multiplied by and is multiplied by . Let’s show that if we these two
pairs, total value can only decrease. Indeed, the difference between dot products of these
two solutions is equal to:
( + + + )-( + + + + - -
=
)
= ( − )( − )
It is non-negative, since = and = .
So, the minimum sum of products occurs only when we multiply a smaller number in X
with the larger number in Y and add all such occurrences. Indirectly, we need to sort X in
ascending order and Y in descending order.
X = [1, 2, -4]
Y = [-2, 4, 1]
print minimum_dot_product(X, Y)
Greedy solution
This is an extension of the previous problem ( ) and the solution is
also on the similar lines. Since, we need to minimize the sum product of consecutive pair
4 . 2 7 M i n i m u m s u m o f pa i r w i s e m u l t i p li c a t io n o f e le m e n t s
G r eedy A l go ri thm s | 205
of elements, we have to divide the array elements into two sets: In the first set, elements
would be in ascending order and in the second set elements would be in decreasing order.
As a result, the sum of product of these numbers would be the minimum as we have already
seen above.
So, to make it little simpler, we sort the given array. Then, we traverse the first half (till )
elements from the beginning and in parallel traverse, the second half in reverse. The sum
of products of these two index elements would give us the minimum.
def minimum_pairwise_product(A):
# Sort A in ascending order
A.sort()
min_product = 0
j = len(A)-1
for i in range( len(A)//2 ):
min_product += A[i]*A[j]
j -= 1
return min_product
A = [6, 2, 9, 4, 5, 1, 6, 7]
print minimum_pairwise_product(A)
Time Complexity: O( ), for sorting the given array.
Space Complexity: O(1) [for iterative algorithm].
Note: Given two files and with sizes and , the complexity of merging them into a
single file is O( + ) as they have to be merged sequentially by reading one line at a time.
To solve this problem, let us try different ways of merging the files and see whether they
work, and are optimal or not?
15
10 5 100 50 20 15
115
15 100
10 5 50 20 15
165
115 50
15 100
10 5 20 15
185
165 20
115 50
15 100
10 5 15
Finally, merging the files with sizes 185 and 15 would produce the final single file with size
200.
{200}
200
185 15
165 20
115 50
15 100
10 5
15 150 35
10 5 100 50 20 15
Similarly, merge the output in pairs and this step produces [below, the third element does
not have a pair element, so keep it the same]:
{165,35}
165
15 150 35
10 5 100 50 20 15
Finally, a single file with size 200 would be produced after merging the files 165 and 35.
{200}
200
165 35
15 150 20 15
10 5 100 50
Greedy solution
Using the Greedy algorithm, we can reduce the total time for merging the given files. Let
us consider the following algorithm.
Algorithm:
1. Store file sizes in a priority queue. The key of elements are file lengths.
2. Repeat the following until there is only one file:
a. Extract two smallest elements and .
b. Merge and , and insert this new file in the priority queue.
Alternative algorithm:
1. Sort the file sizes in ascending order.
2. Repeat the following until there is only one file:
a. Take the first two elements (smallest) and .
b. Merge and and insert this new file in the sorted list.
To check the above algorithm, let us trace it with the previous example. The given array is:
= {10,5,100,50,20,15}
As per the above algorithm, after sorting the list, it becomes: {5, 10, 15, 20, 50, 100}. We
need to merge the two smallest files (5 and 10 size files) and as a result we get the following
list of files. In the list below, 15 indicates the cost of merging two files with sizes 5 and 10.
{15,15,20,50,100}
15
5 10 15 20 50 100
Next, merging the two smallest elements (15 and 15) produces: {20,30,50,100}.
30
15 15
5 10 20 50 100
50
30 20
15 15
5 10 50 100
{100,100}
100
50 50
30 20
15 15
5 10 100
Finally, a single file with size 200 would be produced after merging the files 100 and 100.
{200}
200
100 100
50 50
30 20
15 15
5 10
def merge_files( F ):
# sort the files based on their lenghs
F.sort()
merge_time_for_two_files = F[0] + F[1]
total_merge_time = merge_time_for_two_files
for i in range(2, len(F)):
merge_time_for_two_files = merge_time_for_two_files + F[i]
total_merge_time += merge_time_for_two_files
return total_merge_time
Performance
Time Complexity: The algorithm takes O( ) running time for sorting and O( ) time for
merging the files. The overall running time of this algorithm is dominated by sorting, and
it is O( ).
M O N K
Optimal solution
4 . 2 9 I n t e r v a l sc h e d u l i n g
G r eedy A l go ri thm s | 211
Optimal solution
Greedy solution
What is the optimal solution for this scheduling problem?
Now, let us concentrate on the optimal greedy solution. This problem can be solved
optimally with a simple greedy strategy of scheduling requests based on the earliest finish
time i.e., from the set of intervals that always select the one with the earliest finish time.
Algorithm:
Sort intervals according to the end times
For every consecutive interval {
– If the left-most end is after the right-most end of the last selected interval
then select this interval
– Otherwise we skip it and go to the next interval
}
Example
As an example, consider the following set of intervals:
[(1, 6), (1, 2) , (8, 9) , (5, 8) , (3, 4) , (5, 7), (6, 8)]
As the first step of the algorithm, sort the intervals based on their end times. The sorted
intervals would look like:
4 . 2 9 I n t e r v a l sc h e d u l i n g
G r eedy A l go ri thm s | 212
[(1, 2), (3, 4), (1, 6), (5, 7), (5, 8), (6, 8), (8, 9)]
Now, scan through each of the intervals in the sorted list and see whether they overlap or
not. If they overlap, ignore the interval and go to the next interval. If they do not overlap,
add it to a set X which has the intervals which are not overlapped by any other in the set.
For the initialization, assume ℎ time as 0. This ℎ time indicates the finish time of
the latest interval added to the set.
finish=0
The first interval is (1, 2) and has the start time (1) which is greater than the previous ℎ
(0). It indicates that, this operation would get started after the end of the previous
operation. Hence, there won’t be any overlap; add this interval to set, and update the ℎ
with an end time of this newly added interval.
X= [(1, 2)], finish=2
Next, the interval (3, 4) has the start time (3) which is greater than the previous interval
ℎ time (2). Hence, there won’t be any overlap; add this interval to set, and update the
ℎ with an end time of this newly added interval.
X= [(1, 2), (3, 4)], finish=4
The next interval to be considered is (1, 6) and has the start time (1) which is less than the
previous interval ℎ time (4). So, there is an overlap. Hence, ignore this interval and go
to the next.
Next, the interval (5, 7) has the start time (3) which is greater than the previous interval
ℎ time (4). Hence, there won’t be any overlap; add this interval to set, and update the
ℎ with an end time of this newly added interval.
X= [(1, 2), (3, 4), (5, 7)], finish=7
The next interval to be considered is (5, 8) and has the start time (5) which is less than the
previous interval ℎ time (7). So, there is an overlap. Hence, ignore this interval and go
to the next.
For the next interval (6, 8) too, the start time (6) is less than the previous interval ℎ
time (7). So, ignore this interval and go to the next.
Next, the interval (8, 9) has the start time (8) which is greater than the previous interval
ℎ time (7). Hence, there won’t be any overlap; add this interval to set, and update the
ℎ with an end time of this newly added interval.
X= [(1, 2), (3, 4), (5, 7), (9, 9)], finish=9
This completes the processing of all intervals and hence the algorithm. From the
processing, it can be seen that a maximum of four intervals can be run in parallel.
class Interval(object):
def __init__(self, start, finish):
self.start = start
self.finish = finish
def __repr__(self):
return str((self.start, self.finish))
def schedule_intervals(I):
I.sort(lambda x, y: x.finish - y.finish)
X = []
finish = 0
for i in I:
if finish <= i.start:
finish = i.finish
4 . 2 9 I n t e r v a l sc h e d u l i n g
G r eedy A l go ri thm s | 213
X.append(i)
return X
if __name__ == '__main__':
I = []
I.append(Interval(1, 6))
I.append(Interval(1, 2))
I.append(Interval(3, 4))
I.append(Interval(5, 7))
I.append(Interval(5, 8))
I.append(Interval(8, 9))
X = schedule_intervals(I)
print "Maximum subset",X, "and has", len(X), "intervals"
Performance
Running time of the algorithm is dominated by the sorting.
Total running time = Time for sorting + Time for scanning
= O( )+O( )
= O( )
B C D
E F G
Maximizing the number of classes in the first room results in having { , , , } in one room,
and classes , , and each in their own rooms, for a total of 4. The optimal solution is to
put in one room, { , , } in another, and { , , } in another, for a total of 3 rooms.
4 . 3 0 D e t e r m i n e n u m b e r o f c la s s ro o m s
G r eedy A l go ri thm s | 214
This problem can be solved optimally with a simple greedy strategy of scheduling classes
based on start time, i.e., from the set of classes, and determine the maximum number of
overlaps.
So, process the classes in increasing order of start timings. Assume that we are processing
class . If there is a room such that has been assigned to an earlier class, and can
be assigned to without overlapping previously assigned classes, then assign to .
Otherwise, put in a new room. Does this algorithm solve the problem?
This algorithm solves the interval-coloring problem. Note that if the greedy algorithm
creates a new room for the current class , then because it examines classes in the order
of start times, the start point must intersect with the last class in all of the current
rooms. Thus, when greedy strategy creates the last room, , it is because the start time of
the current class intersects with − 1 other classes. But we know that for any single point
in any class it can only intersect with at the most s other class, so it must then be that
≤ . As is a lower bound on the total number needed, and greedy is feasible, it is thus
also optimal.
class ClassTimings(object):
def __init__(self, start, finish):
self.start = start
self.finish = finish
def __repr__(self):
return str((self.start, self.finish))
class ClassRoom(object):
def __init__(self, roomNumber = 1, finish=0):
self.roomNumber = roomNumber
self.finish = finish
def schedule_classes(I):
I.sort(lambda x, y: x.start - y.start)
classRooms = []
classRooms.append(ClassRoom())
finish = 0
for i in I:
scheduled = False
roomNumber = 1
for c in classRooms:
if c.finish <= i.start:
print "Scheduling (", i.start, i.finish, ") in classroom ", c.roomNumber
c.finish = i.finish
scheduled = True
break
if (scheduled == False):
roomCount = len(classRooms) + 1
finish = i.finish
classRooms.append(ClassRoom(roomCount, finish))
print "Adding new classroom", roomCount
print "Scheduling (", i.start, i.finish, ") in classroom ", roomCount
return roomCount
if __name__ == '__main__':
I = []
I.append(ClassTimings(1, 6))
I.append(ClassTimings(5, 8))
I.append(ClassTimings(6, 8))
I.append(ClassTimings(1, 2))
I.append(ClassTimings(3, 4))
4 . 3 0 D e t e r m i n e n u m b e r o f c la s s ro o m s
G r eedy A l go ri thm s | 215
I.append(ClassTimings(5, 7))
I.append(ClassTimings(8, 9))
schedule_classes(I)
Performance
For this algorithm, it is clear that presorting the class timings according to start times
would take O( ). In the sorted array, picking class with earliest start time can be done
in O(1) time.
Also, we need to keep track of the finish time of last lecture in each classroom to select a
classroom for the new class. With classrooms, checking conflict takes O( ) time. With
priority queues, checking conflict takes O( ) time. Total running time of the algorithm =
O( + )= O( + ) = O( ). Overall running time of the algorithm is
dominated by sorting time which is O( ).
A large variety of resource allocation problems can be cast in the framework of a knapsack
problem. The general idea is to think of the capacity of the knapsack as the available
amount of a resource and the types of items as activities this resource can be allocated.
Two quick examples are the allocation of an advertising budget to the promotions of
individual products and the allocation of ones effort to the preparation of final exams in
different subjects.
There are numerous versions to this problem. Based on the number of items, knapsack
problems can be categorized as:
Unbounded knapsack problem: The main feature of this version of the problem is
that there are infinitely many items of each type.
Bounded knapsack problem: This version of the problem is identical to the
unbounded problem except that there might be bounds (lower and/or upper) on
the size of each type.
0-1 knapsack problem: This is a special instance of the bounded problem in which
all the lower bounds are equal to zero and all the upper bounds are equal to 1. In
4 . 3 1 K n a p sa c k p r o b l e m
G r eedy A l go ri thm s | 216
other words, the problem involves the selection of a sub-set of a given set of distinct
items.
Another way to categorize the knapsack problems is based on the nature of the items:
Fractional knapsack: In this case, items can be broken into smaller pieces, and
allowed to select fractions of items.
0-1 knapsack problem: Either select a full item or leave it. This is a special instance
of the bounded problem in which all the lower bounds are equal to zero and all the
upper bounds are equal to 1. In other words, the problem involves the selection of
a sub-set of a given set of distinct items. Ultimately saying, either select a full item
or leave it.
: Given a set of items, each with a weight and a value, determine
a subset of items to be included in a collection, so that the total weight is less than or equal
to a given limit and the total value is as large as possible.
Algorithm
According to the problem statement, there are items in the store, and:
weight of item is ,
value of item is >0 and,
capacity of the knapsack is C.
In this version of knapsack problem, items can be broken into smaller pieces. So, the thief
may take a fraction of item.
0≤ ≤1
The item contributes the weight . to the total weight in the knapsack and
profit . to the total profit. Hence, the objective of this algorithm is to
( . )
4 . 3 2 F r a c t i o na l k n a p sa c k p r o b l e m
G r eedy A l go ri thm s | 217
( . )≤
It is clear that an optimal solution must fill the knapsack exactly, otherwise we could add
a fraction of one of the remaining items and increase the overall profit. Thus, an optimal
solution can be obtained by:
( . )=
In this context, first we need to sort those items according to the value of , so that ≤ .
Only the last item needs to be broken up because sorting by guarantees that the
current item is the optimum one to take. So, we should take as much of it as we can, until
the knapsack cannot contain it, and will be the whole item. When the knapsack cannot
contain it, we break enough of it to fill the remaining capacity with the last item.
Example
Let us consider a knapsack with capacity = 8 and the list of provided items are shown in
the following table:
Item A B C D
Value 35 10 18 12
Weight 5 1 3 3
Profit 7 10 6 4
As the provided items are not sorted based on . After sorting, the items are as shown in
the following table.
Item B A C D
Value 10 35 18 12
Weight 1 5 3 3
Profit 10 7 6 4
After sorting all the items according to , the item with maximum profit would appear at
the beginning of the sorted list. So, select the item B. The weight of item B is 1 and
knapsack capacity is 8. So, whole of item B is chosen, as the weight of B is less than the
capacity of the knapsack. Next, whole of item A is chosen, as the available capacity of the
knapsack (8 − 1 = 7) is greater than the weight of A (5).
Now, item is chosen as the next item. However, the whole item cannot be chosen as the
remaining capacity of the knapsack is 2 which is less than the weight of (weight of the
item is 3). Hence, fraction of item (i.e. = ) is chosen.
Now, the capacity of the knapsack is equal to the total weights of the selected items. Hence,
no more item can be selected.
The total weight of the selected items is 1 + 5 + 3 × = 8.
4 . 3 2 F r a c t i o na l k n a p sa c k p r o b l e m
G r eedy A l go ri thm s | 218
This is the optimal solution. We cannot gain more profit selecting any other different
combination of items.
def fractional_knapsack(weights, values, C):
"Given problem (weights, values, C), return best fractional_knapsack solution"
n = len(weights)
assert n==len(values)
x = [0]*n
totalProfit = 0
# Idea: take the highest profit items first, only the last item taken
# may be fractional_knapsack.
used = 0
for profit, i in sorted([(float(values[i])/weights[i], i) for i in range(n)], reverse=True):
if used+weights[i] <= C:
used += weights[i]
x[i] = 1
totalProfit += profit
else:
x[i] = float(C-used)/weights[i]
totalProfit += x[i]*values[i]
# remainder of x remains 0
break
return x, totalProfit
weights = [1, 5, 3, 3]
values = [10, 35, 18, 12]
selectedItems, profit = fractional_knapsack(weights, values, 8)
print "Selected items:", selectedItems, "with profit",profit
Analysis
If the provided items are already sorted into a decreasing order of , then the ℎ loop
takes a time in O( ). Therefore, the total time including the sort is in O( ).
Time Complexity: O( ), for sorting and O( ) for greedy selections.
4 . 3 3 D e t e r m i n i n g n u m b e r o f p la t fo r m s a t a r a i l w a y s ta t io n
G r eedy A l go ri thm s | 219
Greedy solution
Let’s take the same example as described above. Calculating the number of platforms is
done by determining the maximum number of trains at the railway station at any time.
First, sort all the arrivals and departures times in an array. Then, save the corresponding
arrivals and departures in the array also. After sorting, our array will look like this:
0900 0915 0930 1030 1045 1100 1145 1300
A A D A A D D D
Now modify the array by placing 1 for and -1 for . The new array will look like this:
1 1 -1 1 1 -1 -1 -1
Finally make a cumulative array out of this:
1 2 1 2 3 2 1 0
Our solution will be the maximum value in this array. Here it is 3.
Note: If we have a train arriving and another departing at the same time, then put the
departure time first in the sorted array.
def number_of_platforms_required(arrivals, departures):
arrivals.sort()
departures.sort()
i=0
j=0
merged_list = []
while (i < len(arrivals) and j < len(departures)):
if arrivals[i] < departures[j]:
merged_list.append('A')
i += 1
else:
merged_list.append('D')
j += 1
while (i < len(arrivals)):
merged_list.append('A')
4 . 3 3 D e t e r m i n i n g n u m b e r o f p la t fo r m s a t a r a i l w a y s ta t io n
G r eedy A l go ri thm s | 220
i += 1
while (j < len(departures)):
merged_list.append('D')
j += 1
max_overlapped_intervals = 0
number_of_overlaps = 0
for i in range(len(merged_list)):
if merged_list[i] == 'A':
number_of_overlaps += 1
else:
number_of_overlaps -= 1
if number_of_overlaps > max_overlapped_intervals:
max_overlapped_intervals = number_of_overlaps
return max_overlapped_intervals
# array of train arrivals and departures
arrivals = [900, 915, 1030, 1045]
departures = [930, 1300, 1100, 1145]
number_of_platforms = number_of_platforms_required(arrivals, departures)
print number_of_platforms
Time Complexity: O( ) for sorting the arrival and departure timings of the trains.
Space Complexity: O( ), for storing the merged sorted arrival and departure timings.
Compare the current elements in arrivals (arrivals[i]) and departures (departures[j]) arrays
and pick whichever is smaller; and increase the pointer of that array whose value is picked.
If time is picked from the arrivals array, increase the total number of trains on the station
(indicates the number of overlaps) and if time is picked from the departures array, decrease
the total number of trains on the station. While doing the above process, we keep the count
of maximum number of stations seen so far. At the end this maximum value would be
returned.
def number_of_platforms_required(arrivals, departures):
arrivals.sort()
departures.sort()
i=0
j=0
max_overlapped_intervals = 0
number_of_overlaps = 0
while (i < len(arrivals) and j < len(departures)):
if arrivals[i] < departures[j]:
i += 1
number_of_overlaps += 1
if number_of_overlaps > max_overlapped_intervals:
max_overlapped_intervals = number_of_overlaps
else:
number_of_overlaps -= 1
j += 1
return max_overlapped_intervals
4 . 3 3 D e t e r m i n i n g n u m b e r o f p la t fo r m s a t a r a i l w a y s ta t io n
G r eedy A l go ri thm s | 221
Greedy solution
The greedy algorithm is not optimal for the problem of converting currency with the
minimum number of coins when the denominations are 1, 5, 10, 20, 25, and 50. In order
to make 40 rupees, the greedy algorithm would use three coins of 25, 10, and 5 rupees. The
optimal solution is to use two 20 rupee coins.
( )= []
The cost reflects the fact that before we read song , we must first scan past all the earlier
songs on the tape. If we change the order of the songs on the tape, we change the cost of
accessing the songs, with the result that some songs become more expensive to read, but
others become cheaper. Different song orders are likely to result in different expected costs.
If we assume that each song is equally likely to be accessed, which order should we use if
we want the expected cost to be as less as possible?
Greedy solution
The answer is simple. We should store the songs in the shortest to longest order. Storing
the short songs at the beginning reduces the forwarding times for the remaining songs. So,
we should sort the songs according to their length and then store.
4 . 3 4 M a k i n g c h a ng e p r o b l e m
G r eedy A l go ri thm s | 222
def arrange_songs( A ):
# sort the songs based on their lengths
A.sort()
def forward_time_of_song(A, song_number):
if song_number <= 0 or song_number > len(A):
return -1
return sum(A[:song_number-1])
# array of songs with their lengths in minutes
A = [3, 6, 9, 3, 5, 1, 4 , 7, 19]
arrange_songs(A)
waiting_time_of_song = forward_time_of_song(A, 3)
print waiting_time_of_song
Time Complexity: O( ) for sorting the songs according to their length.
Space Complexity: O(1).
Greedy solution
This problem can be solved with greedy technique. The setting is that we have events,
each of which takes unit time, and a convention center on which we would like to schedule
them in as profitable manner as possible. Each event has a profit associated with it, as well
as a deadline; if the event is not scheduled by the deadline, then we don’t get the profit.
Because each event takes the same amount of time, we will think of a ℎ as
consisting of a sequence of event “slots” 0, 2, 3, . . . where ( ) is the event scheduled in
slot .
More formally, the input is a sequence ( , ),( , ), ( , ) · · · ,( , ) where is a
non-negative real number representing the profit obtainable from event , and is the
deadline for event . Notice that, even if some event deadlines are bigger than , we can
schedule them in a slot less than as each event takes only one unit of time.
Algorithm
1. Sort the events according to their profits in the decreasing order.
2. Now, for each of the events:
o Schedule event in the latest possible free slot meeting its deadline.
o If there is no such slot, do not schedule event .
Example
Let us consider that the capacity of the knapsack = 8 and the list of provided items are
shown in the following table:
Event, 0 1 2 3
Time (deadline), 1 3 4 4
Profit, 3 8 8 10
First step of the algorithm is to sort the events according to their profits.
4 . 3 6 E v e n t sc h e d u l i n g
G r eedy A l go ri thm s | 223
Event, 3 1 2 0
Time (deadline), 4 3 4 1
Profit, 10 8 8 3
Now, for each of the event , we would need to schedule it in the latest possible free slot
meeting its deadline. The first event to be considered is = 3, and its deadline is 4. Since
the first slot is free, we can select it for scheduling.
Event, 3 1 2 0
Time (deadline), 4 3 4 1
Profit, 10 8 8 3
Schedule 3
Next, for event = 1, the deadline is 3, and has the free slot before 3. So, schedule it in the
next slot.
Event, 3 1 2 0
Time (deadline), 4 3 4 1
Profit, 10 8 8 3
Schedule 3 1
Next, for event = 2, the deadline is 4, and has the free slot before 4. So, schedule it in the
next slot.
Event, 3 1 2 0
Time (deadline), 4 3 4 1
Profit, 10 8 8 3
Schedule 3 1 2
Next, for event = 0, the deadline is 1, but there is no free slot before 1. So, we cannot
schedule it. Hence, the final schedule of the events is:
Event, 3 1 2 0
Time (deadline), 4 3 4 1
Profit, 10 8 8 3
Schedule 3 1 2
The total profit with this schedule is: 10 + 8 + 8 = 26.
class Event(object):
def __init__(self, deadline, profit):
self.deadline = deadline
self.profit = profit
def __repr__(self):
return str((self.deadline, self.profit))
def schedule_events(E):
E.sort(lambda x, y: y.profit - x.profit)
X = []
totalProfit = 0
slot = 0
for i in E:
if slot <= i.deadline:
totalProfit += i.profit
X.append(i)
slot += 1
return X, totalProfit
if __name__ == '__main__':
4 . 3 6 E v e n t sc h e d u l i n g
G r eedy A l go ri thm s | 224
E = []
E.append(Event(1, 3))
E.append(Event(3, 8))
E.append(Event(4, 8))
E.append(Event(4, 10))
X, profit = schedule_events(E)
print "Schedule", X, "got the profit", profit
Performance
The sort takes O( ) and the scheduling takes O( ) for events. So the overall running
time of the algorithm is O( ) time.
Greedy solution
This problem can be solved easily using greedy technique. Since our objective is to reduce
the total waiting time, select the customer whose service time is less. That is, if we process
the customers in the increasing order of service time then we can reduce the total waiting
time.
def arrange_service_requests( A ):
# sort the service requests based on their service times
A.sort()
def waiting_time_of_service_request(A, request_number):
if request_number <= 0 or request_number > len(A):
return -1
return sum(A[:request_number-1])
def total_wait_time_all_customers(A):
total_wait_time = 0
for i in range(1, len(A)+1):
total_wait_time += waiting_time_of_service_request(A, i)
return total_wait_time
# array of requests with their service times
A = [3, 16, 9, 3, 5, 1, 4 , 7, 19]
arrange_service_requests(A)
print "Total wait time of all customers:", total_wait_time_all_customers(A)
waiting_time = waiting_time_of_service_request(A, 3)
print waiting_time
Time Complexity: O( ) for sorting the service requests based on their service times.
Space Complexity: O(1).
4 . 3 7 M a na g i ng c u st o m e r ca r e se r v i c e q u e u e
G r eedy A l go ri thm s | 225
Example
For example, for the parent array [-1, 0, 1, 6, 6, 0, 0, 2, 7]:
-1 0 1 6 6 0 0 2 7
0 1 2 3 4 5 6 7 8
parent[0] = -1, therefore node 0 is the root of the tree.
parent[1] = parent[5] = parent[6] =0, therefore node with value 0 would be parent
node for nodes with values 1, 5 and 6.
parent[2] = 1, therefore node with value 1 would be parent node for node with value
2.
parent[3] = parent[4] = 6 implies that node with value 6 would be parent node for
nodes with values 3 and 4.
parent[7] = 2 therefore node with value 2 would be parent node for node with values
7.
parent[8] = 7 therefore node with value 7 would be parent node for node with values
8.
Its corresponding tree can be depicted as:
5 1 6
2 3 4
4 . 3 8 F i n d i n g d e p t h o f a g e ne r i c t r e e
G r eedy A l go ri thm s | 226
j = parent[j]
if(current_depth > max_depth):
max_depth = current_depth
return max_depth
parent=[-1, 0, 1, 6, 6, 0, 0, 2, 7]
print "Depth of given generic tree is:", find_depth_in_generic_tree(parent)
Time Complexity: O( ). For skew trees we will be re-calculating the same values.
Space Complexity: O(1).
4 . 3 9 N e a r e st m e et i n g ce l l i n a m a ze
G r eedy A l go ri thm s | 227
Input for this problem would be a list of values of the edge array. Edge contains the cell
number that can be reached from of cell in one step and edge is -1 if the cell doesn’t
have an exit.
Example
As an example, consider a maze with 23 cells and with the following edges.
4, 4, 1, 4, 13, 8, 8, 8, 0, -1, 14, 9, 15, 11, -1, 10, 15, 22, 22, 22, 22, -1, 21
Each element of the edges array contains the cell number that can be reached from of cell
in one step, which means that edge indicates the parent of cell . So, we can treat edge
array as a parent array. Hence, we can draw an arrow from cell to edge i ( value in the
edge array).
In other words, finding the nearest meeting point of two cells is nothing but finding the
least common ancestor of two nodes in the graph. For simplicity, let us assume there were
no cycles in the graph.
Let us draw the above data pictorially. In the following pictorial representation of graph,
the nearest meeting point for two cells 2 and 3 is 4. Nearest meeting point of two cells 14
and 16 is 14. Also, the nearest meeting point of two cells 7 and 16 is -1 as there is no
connectivity between these two cells.
-1
13
-1
14
3 4 11 18 21
10 22
1 0 9
17 19 20
15
2 8 -1
12 16
5 6 7
Traverse once through both the cells till they reach -1 (head of directed acyclic graph) and
find the lengths. Take the difference of these lengths. Among these, determine the cell
which has a longer length. Starting from that cell, move those cells with difference by using
the edges array. From there onwards, move both cells at the same time using the edges
array as long as they are different.
def nearest_meeting_point(edges, cell1, cell2):
# cell1_count and cell2_count represent distance from root
c1 = cell1
cell1_count = 0
while edges[c1] != -1:
c1 = edges[c1]
cell1_count += 1
c2 = cell2
4 . 3 9 N e a r e st m e et i n g ce l l i n a m a ze
G r eedy A l go ri thm s | 228
cell2_count = 0
while edges[c2] != -1:
c2 = edges[c2]
cell2_count += 1
edges = [4, 4, 1, 4, 13, 8, 8, 8, 0, -1, 14, 9, 15, 11, -1, 10, 15, 22, 22, 22, 22, -1, 21]
print nearest_meeting_point (edges, 2, 3)
print nearest_meeting_point (edges, 14, 16)
print nearest_meeting_point (edges, 7, 16)
Time Complexity: O(ℎ), where ℎ is the height of the directed acyclic graph. In the worst-
case height would be equal to .
Space Complexity: O(1).
4 . 4 0 M a x i m u m n u m b e r o f en t r y p o i nt s f o r a n y c e l l i n m a ze
G r eedy A l go ri thm s | 229
edges = [4, 4, 1, 4, 13, 8, 8, 8, 0, -1, 14, 9, 15, 11, -1, 10, 15, 22, 22, 22, 22, -1, 21]
max_entry_points(edges)
Time Complexity: O( ), for two nested loops.
Space Complexity: O(1).
4 . 4 0 M a x i m u m n u m b e r o f en t r y p o i nt s f o r a n y c e l l i n m a ze
G r eedy A l go ri thm s | 230
edges = [4, 4, 1, 4, 13, 8, 8, 8, 0, -1, 14, 9, 15, 11, -1, 10, 15, 22, 22, 22, 22, -1, 21]
max_entry_points(edges)
Time Complexity: O( ).
Space Complexity: O( ), for hash table.
4 . 4 0 M a x i m u m n u m b e r o f en t r y p o i nt s f o r a n y c e l l i n m a ze
G r eedy A l go ri thm s | 231
if edges[i] == -1:
continue
edges[edges[i]%n] += n
for i in range(0,len(edges)):
if(edges[i]/n > max):
max = edges[i]/n
max_entry_point_cell =i
print "Cell", max_entry_point_cell, "has", max-1, "entrypoints."
edges = [4, 4, 1, 4, 13, 8, 8, 8, 0, -1, 14, 9, 15, 11, -1, 10, 15, 22, 22, 22, 22, -1, 21]
max_entry_points(edges)
Notes:
This solution does not work if the given array is read only.
This solution will work only if the elements of the array are positive. In the
implementation, we have ignored -1 to fix this issue.
If the elements range is not in 0 to − 1 then it may give exceptions.
Time Complexity: O( ).
Space Complexity: O(1).
4 . 4 1 L e n g t h o f t h e l a rg e st p a th i n a ma z e
G r eedy A l go ri thm s | 232
For example, let us consider the set of denominations {1, 5, 10, 25, 50, 100}. Also assume
that we have infinite supply of coins for each denomination. To make change for 37, we
can have four combinations
{25, 10, 1, 1}
{1, 1, , …..37 times}
{10, 10, 10, 5, 1, 1}
{5, 5, 5, 5, 5, 5, 5, 1, 1}
{25, 5, 5, 1, 1}
….
Among these, the minimum number of coins to make change for 37 is '4' and the
combination of coins are {25, 10, 1, 1}.
Greedy solution
Assume that, in India, the coins in use are: 1 paise, 5 paise, 10 paise, 25 paise, 50 paise,
and 100 paise.
Coin Value
1 1 paise
2 5 paise
3 10 paise
4 25 paise
5 50 paise
6 100 paise
Suppose a customer puts in a bill and purchases an item for 63 paise. What is the smallest
number of coins you can use to get the change? The answer is six coins: two 25 paise, one
10 paise, and three 1 paise. How did we arrive at the answer of six coins? We start with
the largest coin in our collection of coins (a quarter) and use as many of those as possible,
then we go to the next lowest coin value and use as many of those as possible. This
approach is called a greedy method because we try to solve as big a piece of the problem
as possible right away.
Coin Value
1 1 paise
2 5 paise
3 10 paise
4 21 paise
5 25 paise
6 50 paise
7 100 paise
So, greedy algorithm for this minimum coin change problem can be defined as:
1. Sort the coin denominations in the decreasing order ( > > . . . > ).
2. Select the coin such that: ≤ < . Add coin to the list of selected coins.
3. Problem reduces to coin-changing – paise.
4. Repeat step 2 until the amount becomes zero.
5. Return the list of selected coins.
The greedy method works fine when we are using 1 paise, 5 paise, 10 paise, 25 paise, 50
paise, and 100 paise coins, but suppose India adds one more coin with value 21 paise. So,
the possible coins are 1 paise, 5 paise, 10 paise, 21 paise, 25 paise, 50 paise, and 100
paise. In this instance our greedy method fails to find the optimal solution for 63 paise in
change. With the addition of the 21 paise coin, the greedy method would still find the
solution to be six coins. However, the optimal answer is three 21 paise coins.
4 . 4 2 M i n i m u m c o i n c ha ng e p r o b l e m
G r eedy A l go ri thm s | 233
So, for solving the making change problem, a greedy algorithm repeatedly selects the largest
coin denomination available that do not exceed the remainder. A greedy algorithm is
simple, but it is not guaranteed to find a solution when one exists, and it is not guaranteed
to find a minimal solution. It works only for few sets of coins as discussed above.
def get_change(amount):
"""Changing money optimally.
"""
coins = [100, 1, 25, 5, 50, 10] # must be sorted
coins.sort(reverse=True)
count = 0
selectedCoins = []
for coin in coins:
if amount < coin:
continue
# Update count with the number of coins 'are held' in the amount.
count += amount // coin
selectedCoins.append([coin] * (amount // coin))
# Put remainder to the residuary amount.
amount %= coin
return count, selectedCoins
if __name__ == "__main__":
n = 37
print(get_change(n))
Performance
A correct algorithm should always return the minimum number of coins. It turns out that
the greedy algorithm is correct for only some denomination selections, but not for all.
Time Complexity: O( ), for sorting the denominations in the decreasing order. If the
coins were already in sorted order, the running time of the algorithm will be O( ) as the
number of coins is added once for every denomination.
Space Complexity: O(1).
4 . 4 3 P a i r w i s e d i s t i n c t su m m a n d s
G r eedy A l go ri thm s | 234
print get_summands(23)
if __name__ == '__main__':
main()
Performance
In the above code, the selected are maintained in an array. To determine if “
not in summands” ( ( − ) ) is true, we have to check the values in
one at a time.
Time complexity of this operation is it is an upper bound. In the worst case, all the elements
would be added to (for example, consider pairwise sum for element 15). And, for
each iteration, the elements in would be checked.
Next question would be, how many such elements are added to array?
To determine this, we define the ‘ ’ terms according to the relation = + . The value of
‘ ’ increases by 1 for each iteration. The value contained in ‘ ’ at the iteration is the sum
of the first ‘ ’ positive integers. If is the total number of iterations taken by the program,
then the ℎ loop terminates if:
( )
1 + 2+...+ = > ⟹ = O(√ ).
So, the outer loop is getting executed for approximately = √ times and for each of the
iteration, the inner check, ( ( − ) ), would take O( ) time to confirm whether
current elements exist in the summands.
def get_summands(n):
# In python, sets are implemented with dictionary.
summands=set()
i=1
while(i<=n):
if (n-i) not in summands:
summands.add(i)
n-=i
i+=1
return summands
def main():
print get_summands(23)
if __name__ == '__main__':
main()
4 . 4 3 P a i r w i s e d i s t i n c t su m m a n d s
G r eedy A l go ri thm s | 236
Performance
With the use of data structure, the inner ℎ average time complexity would come
down to O(1). Hence, the overall time complexity of this approach would be O( × 1) = O(√ ).
4.44 Team outing to Papikondalu
: Consider the famous tourist place in India, Papikondalu. Apart from
enjoying the view of the hills, valley and waterfalls, tourists can engage in activities like
boating, trekking, etc... Assume there are tourists from a company and an infinite
number of double boats (can carry maximum of two persons) with a maximum capacity of
.
Each boat is light and narrow with pointed ends and no keel, propelled with a paddle or
paddles. To make all the tourists happy, assume that all their weights are less than . Give
an algorithm to seat the tourists with minimum number of double boats.
Greedy solution
The problem can be solved by using a greedy algorithm. Assume that the weights of
tourists are ={ , ,..., }. The greedy algorithm is defined as follows.
The heaviest tourist is called . Other tourists who can be seated with in the boat
are called (ℎ ). All the other remaining tourists are also called .
The idea is that, for the heaviest , we should find the heaviest who can be seated
with him. So, we seat together the heaviest and the heaviest . Let us note that
the thinner the heaviest bulky is, the fatter can be. Thus, the division between
and will change over time — as the heaviest bulky gets closer to the pool of .
In the above algorithm, to find the bulky or lanky, it would be easy if the weights are in
sorted order. Otherwise, we might be spending more time in finding them. So, let us sort
the tourists weights in the increasing order (decreasing order too works well).
from collections import deque
def get_boats(W, k):
n = len(W)
lanky = deque()
bulky = deque()
for i in xrange(n - 1):
if W[i] + W[-1] <= k:
lanky.append(W[i])
else:
bulky.append(W[i])
bulky.append(W[-1])
boats = 0
while (lanky or bulky):
if len(lanky) > 0:
lanky.pop()
4 . 4 4 T e a m o u t i ng to P a p i k o n da l u
G r eedy A l go ri thm s | 237
bulky.pop()
boats += 1
if (not bulky and lanky):
bulky.append(lanky.pop())
while (len(bulky) > 1 and bulky[-1] + bulky[0] <= k):
lanky.append(bulky.popleft())
return boats
W = (5, 20, 21, 28, 39, 40, 65, 89, 98, 105)
print get_boats(W, 110)
Example
As an example, assume that the following is the tourist’s weights (in increasing order).
W = (5, 20, 21, 28, 39, 40, 65, 89, 98, 105)
Total number of tourists are 10. The initialization for the algorithm is to create two de-
queues: One for bulks ( dequeue) and other for thinners ( dequeue).
bulky []
lanky []
Now, scan through each of the weights, starting from the lowest, and check whether the
current lanky and bulky weights together is less than or not. The lanky is the first weight
(pointed by index ) in the list as that is the smallest among all. The bulky is the last weight
in the list as that is the largest among all. The sum of the first and last weights is 110 (5 +
105) which is equal to . So, these lanky and bulky can be seated together in a boat. Update
this information in lanky dequeue.
bulky []
lanky [5]
The next weight is 20. The bulky weight is 105. Sum of these two weights is > (20 + 105
> 110). So, this tourist cannot be seated with tourist whose weight is 105. Add this weight
to dequeue.
bulky [20]
lanky [5]
Similarly, add the weights 21, 28, 39 40, 65, 89, 98 to the dequeue.
bulky [20, 21, 28, 39, 40, 65, 89, 98, 105]
lanky [5]
Now, for each of the bulky, we try to find the lanky who can be seated with this bulky.
Currently we could find only one lanky (whoever is in dequeue), and that is the tourist
with weight 5. Delete that lanky from dequeue and the heaviest bulky (last element
of dequeue) from the dequeue. This completes the seating of one bulky and one
lanky. So, we can increase the number of boats being used for seating the tourists.
bulky [20, 21, 28, 39, 40, 65, 89, 98]
lanky []
boats 1
The next bulky to be processed is the tourist with weight 98 (last element of dequeue).
So, delete that weight from dequeue. Since the lanky dequeue is empty, for this bulky,
we try to find another bulky (from the beginning of dequeue) who can be seated with
98. The beginning element of bulky is 20. Sum of these two weights (20 + 98 > 110). Hence,
we cannot make these two tourists sit together. So, we have to keep bulky weight 98 in a
separate boat.
bulky [20, 21, 28, 39, 40, 65, 89]
lanky []
4 . 4 4 T e a m o u t i ng to P a p i k o n da l u
G r eedy A l go ri thm s | 238
boats 2
The next bulky to be processed is the tourist with weight 89 (last element of dequeue).
So, delete that weight from dequeue. Since the dequeue is empty, for this bulky,
we try to find bulks (from the beginning of dequeue) who can be seated with 89. The
beginning element of bulky is 20. Sum of these two weights (20 + 89 < 110). So, we make
make two tourists sit together. Hence, delete the weight 20 from dequeue and move
it to dequeue. Similarly, we can move the bulky 21 to dequeue. But, for weight
28, 28 + 89 > 110. Hence, don’t move it to lanky dequeue.
bulky [28, 39, 40, 65, 89]
lanky [20, 21]
boats 2
As a result, the bulky 89 and lanky 21 (the heaviest in dequeue) can be seated
together. So, delete them and increase the number of boats.
bulky [28, 39, 40, 65]
lanky [20]
boats 3
Next, the bulks 28, 39, 40 can be moved to lanky dequeue.
bulky [65]
lanky [20, 28, 39, 40]
boats 3
The remaining bulky in dequeue is 65. For this bulky the heaviest lanky (40) can be
matched as their total weight is less than (65 + 40 < 110).
bulky []
lanky [20, 28, 39]
boats 4
Since the bulky dequeue is empty, we can make the heaviest in dequeue as bulky
and move it to bulky .
bulky [39]
lanky [20, 28]
boats 4
Next, the bulky 39 can be matched with the heaviest in lanky (28). Hence remove them and
increase the boats.
bulky []
lanky [20]
boats 5
Next, the only remaining lanky is 20 and since there is no bulky for this, it has to be seated
in a separate boat and it is the end of the algorithm.
bulky []
lanky []
boats 6
Performance
The total time complexity of this solution is O( ). The outer ℎ loop performs O( ) steps
since in each step, one or two tourists are seated in a boat. The inner while loop in each
step changes a bulky into a lanky. As at the beginning, there are O( ) bulks and with each
step at the outer ℎ loop only one lanky become a bulky, the overall total number of
steps of the inner while loop has to be O( ).
4 . 4 4 T e a m o u t i ng to P a p i k o n da l u
G r eedy A l go ri thm s | 239
Notice that the tourists weights are in increasing order. If the input weights are not in the
increasing order, we need to sort the array which would cost O( ).
Example
Let us consider the same example; the following is 10 tourists weights (in increasing order).
W = (5, 20, 21, 28, 39, 40, 65, 89, 98, 105)
The initialization for the algorithm is to point one index at the beginning of the weights
array and the other index at the end of the weights array.
boats 0
i 0
j 9
The two weights pointed by indexes i and j are <= (5 + 105 ≤ 110). Hence we can put
them in a boat.
boats 1
i 1
j 8
Next, weights pointed by indexes i and j are <= (20 + 98 ≤ 110). Hence we cannot put
them in the same boat. So, put 98 in a separate boat.
boats 2
i 1
j 7
Next, weights pointed by indexes i and j are <= (20 + 89 ≤ 110). Hence we can put them
in a boat.
boats 3
i 2
j 6
Next, weights pointed by indexes i and j are <= (21 + 65 ≤ 110). Hence we can put them
in a boat.
boats 4
i 3
j 5
4 . 4 4 T e a m o u t i ng to P a p i k o n da l u
G r eedy A l go ri thm s | 240
Next, weights pointed by indexes i and j are <= (21 + 65 ≤ 110). Hence we can put them
in a boat.
boats 4
i 3
j 5
Next, weights pointed by indexes i and j are <= (28 + 40 ≤ 110). Hence we can put them
in a boat.
boats 5
i 4
j 4
Now, indexes i and j are pointing to the same element 39 which indicates the reaming
elements to be processed is equal to 1. Hence, put it in a separate boat.
boats 6
i 5
j 5
Performance
The time complexity is O( ), because with each step of the loop, at least one tourist is
seated.
(such as for Simple Knapsack). However for other problems (such as the
scheduling problem above, and finding a minimum cost spanning tree) they
always find an optimal solution. For these problems, greedy algorithms are
great.
Refer
: Finding
chapter for discussion on this.
smallest elements in an array
Refer
: Finding
chapter for discussion on this.
-smallest element in an array
Above two problems have multiple solutions defined of which few are
greedy. We kept them in chapter as the final solutions
were based on strategy.
4.45 Finding s m a l l e s t e l e m e n ts i n a n a r r a y
D i vi de an d Co n qu er Al g or i th m s | 241
Chapter
Divide and
Conquer
Algorithms 5
5.1 Introduction
In the chapter, we have seen that for many problems the Greedy strategy failed to
provide optimal solutions. Among those problems, there are some that can be easily solved
by using the (D & ) technique. Divide and Conquer is an important
algorithm design technique based on recursion.
The algorithm works by recursively breaking down a problem into two
or more subproblems of the same type, until they become simple enough to be solved
directly. The solutions to the subproblems are then combined to give a solution to the
original problem.
5 . 1 I n t r o d u c t io n
D i vi de an d Co n qu er Al g or i th m s | 242
A problem of size
Subproblems
…………..
Combine sub-solutions
for solution to problem
So, he gathered them together and showed them seven sticks that he had tied together and
told them that anyone who could break the bundle would inherit everything. They all tried,
but no one could break the bundle. Then the old man untied the bundle and broke the
sticks one by one. The brothers decided that they should stay together and work together
and succeed together. The moral for problem solvers is different. If we can't solve the
problem, divide it into parts, and solve one part at a time.
Below are a few other real-time problems which can easily be solved with divide and
conquer strategy. For all these problems, we can find the subproblems which are similar
to the original problem.
Looking for a name in a phone book: We have a phone book with names in
alphabetical order. Given a name, how do we find whether that name is there in
the phone book or not?
Breaking a stone into dust: We want to convert a stone into dust (very small
stones).
Finding the exit in a hotel: We are at the end of a very long hotel lobby with a long
series of doors, with one door next to us. We are looking for the door that leads to
the exit.
Finding our car in a parking lot.
5 . 5 U n d e r s t a n d i n g d i v i d e a n d co nq u e r
D i vi de an d Co n qu er Al g or i th m s | 243
Parallelism: Since divide and conquer technique allows us to solve the subproblems
independently, this allows for execution in multi-processor machines, especially shared-
memory systems where the communication of data between processors does not need to
be planned in advance, because different subproblems can be executed on different
processors.
Memory access: Divide and conquer algorithms naturally tend to make efficient use of
memory caches. This is because once a subproblem is small, all its subproblems can be
solved within the cache, without accessing the slower main memory.
Binary search
Merge sort and Quick sort
Median finding
Min and max finding
Matrix multiplication
Closest Pair problem
Finding peak in one dimensional array
Finding peak in two dimensional array (matrix)
5 . 6 A d v a n ta ge s o f d i v i d e a n d co nq u e r
D i vi de an d Co n qu er Al g or i th m s | 244
1) If > , then ( ) = Θ
2) If =
a. If > −1, then ( ) = Θ
b. If = −1, then ( ) = Θ
c. If < −1, then ( ) = Θ
3) If <
a. If ≥ 0, then ( ) = Θ( )
b. If < 0, then ( ) = O( )
As an example, a merge sort algorithm operates on two subproblems, each of which is half
the size of the original, and then performs O( ) additional work for merging. This gives the
running time equation:
T( ) = 2 + O( )
The following theorem can be used to determine the running time of divide and conquer
algorithms. For a given program (algorithm), first we try to find the recurrence relation for
the problem. If the recurrence is of the form below, then we can directly give the answer
without fully solving it.
Problem-1 ( ) = 3 ( /2) +
Solution: ( ) = 3 ( /2) + => ( ) =Θ( ) (Master Theorem Case 3.a)
Problem-2 ( ) = 4 ( /2) +
Solution: ( ) = 4 ( /2) + => ( ) = Θ( ) (Master Theorem Case 2.a)
Problem-3 ( ) = ( /2) +
Solution: ( ) = ( /2) + => Θ( ) (Master Theorem Case 3.a)
Problem-4 ( ) = 2 ( /2) +
Solution: ( ) = 2 ( /2) + => Does not apply ( is not constant)
Problem-5 ( ) = 16 ( /4) +
Solution: ( ) = 16 ( /4) + => ( ) = Θ( ) (Master Theorem Case 1)
Problem-6 ( ) = 2 ( /2) +
Solution: ( ) = 2 ( /2) + => ( ) = Θ( ) (Master Theorem Case 2.a)
Problem-7 ( ) = 2 ( /2) + /
Solution: ( ) = 2 ( /2) + / => ( ) = Θ( ) (Master Theorem Case 2.b)
5 . 9 M a st e r t he o r e m p r a ct i c e q u es t i on s
D i vi de an d Co n qu er Al g or i th m s | 245
.
Problem-8 ( ) = 2 ( /4) +
Solution: ( ) = 2 ( /4) + .
=> ( ) = Θ( .
) (Master Theorem Case 3.b)
Problem-10 ( ) = 6 ( /3) +
Solution: ( ) = 6 ( /3) + => ( ) = Θ( ) (Master Theorem Case 3.a)
Problem-11 ( ) = 64 ( /8) −
Solution: ( ) = 64 ( /8) − => Does not apply (function is not positive)
Problem-12 ( ) = 7 ( /3) +
Solution: ( ) = 7 ( /3) + => ( ) = Θ( ) (Master Theorem Case 3.as)
Problem-13 ( ) = 4 ( /2) +
Solution: ( ) = 4 ( /2) + => ( ) = Θ( ) (Master Theorem Case 1)
Problem-14 ( ) = 16 ( /4) + !
Solution: ( ) = 16 ( /4) + ! => ( ) = Θ( !) (Master Theorem Case 3.a)
Problem-15 ( ) = √2 ( /2) +
Solution: ( ) = √2 ( /2) + => ( ) = Θ(√ ) (Master Theorem Case 1)
Problem-16 ( ) = 3 ( /2) +
Solution: ( ) = 3 ( /2) + => ( ) = ( ) (Master Theorem Case 1)
Problem-17 ( ) = 3 ( /3) + √
Solution: ( ) = 3 ( /3) + √ => ( ) = Θ( ) (Master Theorem Case 1)
Problem-18 ( ) = 4 ( /2) +
Solution: ( ) = 4 ( /2) + => ( ) = ( ) (Master Theorem Case 1)
Problem-19 ( ) = 3 ( /4) +
Solution: ( ) = 3 ( /4) + => ( ) = Θ( ) (Master Theorem Case 3.a)
Problem-20 ( ) = 3 ( /3) + /2
Solution: ( ) = 3 ( /3) + /2 => ( ) = Θ( ) (Master Theorem Case 2.a)
5 . 1 0 B i n a r y se a r c h
D i vi de an d Co n qu er Al g or i th m s | 246
There are certain ways of organizing the data that improves the searching process. That
means, if we keep the data in proper order, it is easy to search the required element. Sorting
is one of the techniques for making the elements ordered. In this chapter we will see
different searching algorithms.
Following are the types of searches which will be discussed in this section.
A = [34,46,93,127,277,321,454,565,1220]
print(ordered_linear_search(A,565))
Time complexity of this algorithm is O( ). This is because in the worst case we need to scan
the complete array. But in the average case it reduces the complexity even though the
growth rate is the same.
Space Complexity: O(1).
Note: For the above algorithm we can make further improvement by incrementing the index
at a faster rate (say, 2). This will reduce the number of comparisons for searching an
element in the sorted list.
5 . 1 0 B i n a r y se a r c h
D i vi de an d Co n qu er Al g or i th m s | 247
Binary search
Let us consider the problem of searching a word in a dictionary. Typically, we directly go
to some approximate page [say, middle page] and start searching from that point. If the
that we are searching is the same, then the search is complete. If the page is before
the selected pages, then apply the same process for the first half; otherwise apply the same
process to the second half. Binary search also works in the same way. The algorithm
applying such a strategy is referred to as ℎ algorithm.
to be searched ℎ ℎ
( )
= +
Example
As said above, for a binary search to work, the input array is required to be sorted. We
shall learn the process of binary search with an example. The following is our sorted array
and let us assume that we need to search the location of value 30 using binary search.
0 1 2 3 4 5 6 7 8 9
9 13 18 25 26 30 32 34 41 43
Sorted array
0 1 2 3 4 5 6 7 8 9
9 13 18 25 26 30 32 34 41 43
mid
Now we compare the value stored at location 4, with the value being searched, i.e. 30. We
find that the value at location 4 is 26, which is not a match. As the value is greater than
26 and we have a sorted array, we also know that the target value must be in the upper
portion of the array.
0 1 2 3 4 5 6 7 8 9
9 13 18 25 26 30 32 34 41 43
mid
We change our low to + 1 and find the new mid value again.
= + 1
(ℎ ℎ − )
= +
2
5 . 1 0 B i n a r y se a r c h
D i vi de an d Co n qu er Al g or i th m s | 248
Our new mid is 7 now. We compare the value stored at location 7 with our target value 30.
0 1 2 3 4 5 6 7 8 9
9 13 18 25 26 30 32 34 41 43
mid
The value stored at location 7 is not a match, rather it is more than what we are looking
for. So, the value must be in the lower part from this location.
0 1 2 3 4 5 6 7 8 9
9 13 18 25 26 30 32 34 41 43
mid
Hence, we calculate the again. This time it is 5.
0 1 2 3 4 5 6 7 8 9
9 13 18 25 26 30 32 34 41 43
mid
We compare the value stored at location 5 with our target value. We find that it is a match.
0 1 2 3 4 5 6 7 8 9
9 13 18 25 26 30 32 34 41 43
mid
Binary search halves the searchable items and thus reduces the count of comparisons to
be made to very less numbers.
# Iterative Binary Search Algorithm
def binary_search_iterative(elements, value):
low = 0
high = len(elements)-1
while low <= high:
mid = (low+high)//2
return -1
A = [534,246,933,127,277,321,454,565,220]
print(binary_search_iterative(A,277))
5 . 1 0 B i n a r y se a r c h
D i vi de an d Co n qu er Al g or i th m s | 249
return -1
if(high == -1):
high = len(elements)-1
if low == high:
if elements[low] == value: return low
else: return -1
mid = low + (high-low)//2
if elements[mid] > value:
return binary_search_recursive(elements, value, low, mid-1)
elif elements[mid] < value:
return binary_search_recursive(elements, value, mid+1, high)
else: return mid
A = [534,246,933,127,277,321,454,565,220]
print(binary_search_recursive(A,277))
Analysis
Let us assume that input size is and ( ) defines the solution to the given problem. The
elements are in sorted order. In binary search, we take the middle element and check
whether the element to be searched is equal to that element or not. If it is equal, then we
return that element.
If the element to be searched is greater than the middle element, then we consider the right
sub-array for finding the element and discard the left sub-array. Similarly, if the element
to be searched is less than the middle element, then we consider the left sub-array for
finding the element and discard the right sub-array.
What this means is, in both the cases we are discarding half of the sub-array and
considering the remaining half only. Also, at every iteration we are dividing the elements
into two equal halves. As per the above discussion, in every iteration, we divide the problem
into 2 subproblems with each of size and solve one T( ) subproblem. The total recurrence
algorithm for this problem can be given as:
( )=2 +O(1)
5 . 1 0 B i n a r y se a r c h
D i vi de an d Co n qu er Al g or i th m s | 250
1 0
1000 ≈ 10
100000 ≈ 20
1,000,000,000 ≈ 30
… …
So, it is much better to have a term than an term. Binary search will be faster for
large problem sizes.
Time
Input size,
Linear Binary search
search faster faster
Algorithm
Because we are using divide-and-conquer to sort, we need to decide what our subproblems
are going to look like. The full problem is to sort an entire array. Let us say that a
5 . 1 1 M e r g e so r t
D i vi de an d Co n qu er Al g or i th m s | 251
Algorithm Merge-sort(A):
by finding the number of the position midway between and ℎ.
Do this step the same way we found the midpoint in binary search:
( )
= + .
Example
To understand merge sort, let us walk through an example:
54 26 93 17 77 31 44 55
We know that merge sort first divides the whole array iteratively into equal halves unless
the atomic values are achieved. We see here that an array of 8 items is divided into two
arrays of size 4.
54 26 93 17 77 31 44 55
This does not change the sequence of appearance of items in the original. Now we divide
these two arrays into halves.
54 26 93 17 77 31 44 55
We further divide these arrays and we achieve the atomic value which can no more be
divided.
54 26 93 17 77 31 44 55
Now, we combine them in exactly the same manner as they were broken down.
We first compare the element for each array and then combine them into another array in
a sorted manner. We see the elements 54 and 26; in the target array of 2 values we put 26
first, followed by 54.
Similarly, we compare 93 and 17 and in the target array of 2 values we put 17 first, followed
by 93. On the similar lines, we change the order of 77 and 31 whereas 44 and 55 are placed
sequentially.
26 54 17 93 31 77 44 55
In the next iteration of the combining phase, we compare lists of two data values, and
merge them into an array of found data values placing all in a sorted order.
17 26 54 93 31 44 55 77
After the final merging, the array should look like this:
5 . 1 1 M e r g e so r t
D i vi de an d Co n qu er Al g or i th m s | 252
17 26 31 44 54 55 77 93
The overall flow of the above discussion can be depicted as:
54 26 93 17 77 31 44 55
54 26 93 17 77 31 44 55
54 26 93 17 77 31 44 55
54 26 93 17 77 31 44 55
26 54 17 93 31 77 44 55
17 26 54 93 31 44 55 77
17 26 31 44 54 55 77 93
Implementation
def merge_sort(A):
if len(A)>1:
mid = len(A)//2
lefthalf = A[:mid]
righthalf = A[mid:]
merge_sort(lefthalf)
merge_sort(righthalf)
i=j=k=0
while i<len(lefthalf) and j<len(righthalf):
if lefthalf[i]<righthalf[j]:
A[k]=lefthalf[i]
i=i+1
else:
A[k]=righthalf[j]
j=j+1
k=k+1
while i<len(lefthalf):
A[k]=lefthalf[i]
i=i+1
k=k+1
while j<len(righthalf):
A[k]=righthalf[j]
j=j+1
k=k+1
A = [54, 26, 93, 17, 77, 31, 44, 55]
merge_sort(A)
print(A)
Analysis
5 . 1 1 M e r g e so r t
D i vi de an d Co n qu er Al g or i th m s | 253
In merge-sort the input array is divided into two parts and these are solved recursively.
After solving the subarrays, they are merged by scanning the resultant subarrays. In merge
sort, the comparisons occur during the merging step, when two sorted arrays are combined
to output a single sorted array. During the merging step, the first available element of each
array is compared and the lower value is appended to the output array. When either array
runs out of values, the remaining elements of the opposing array are appended to the
output array.
How do we determine the complexity of merge-sort? We start by thinking about the three
parts of divide-and-conquer and how to account for their running times. We assume that
we are sorting a total of elements in the entire array.
The divide step takes constant time, regardless of the subarray size. After all, the divide
step just computes the midpoint of the indices and ℎ . Recall that in big-
notation, we indicate constant time by (1).
The conquer step, where we recursively sort two subarrays of approximately elements
each, takes some amount of time, but we shall account for that time when we consider the
subproblems. The combine step merges a total of elements, taking ( ) time.
If we think about the divide and combine steps together, the (1) running time for the
divide step is a low-order term when compared with the ( ) running time of the combine
step. So let us think of the divide and combine steps together as taking ( ) time. To make
things more concrete, let us say that the divide and combine steps together take time
for some constant .
Let us assume ( ) is the complexity of merge-sort with elements. The recurrence for the
merge-sort can be defined as:
( ) = 2 ( ) + ( )
2
Using master theorem, we can determine the time complexity as, ( ) = ( ).
For merge-sort, there is no running time difference between the best, average and worst
cases, as the division of input arrays happens irrespective of the order of the elements.
Merge-sort is a recursive algorithm and each recursive step puts another frame on the
runtime stack. Sorting 32 items will take one more recursive step than 16 items, and it is
in fact the size of the stack that is referred to, when the space requirement is said to be
O( ).
Worst case complexity : ( )
Best case complexity : ( )
Average case complexity : ( )
Space complexity: ( ), for runtime stack space
Algorithm
The recursive algorithm consists of four steps:
1) If there are one or no elements in the list to be sorted, return.
2) Pick an element in the list to serve as the point. Usually the first element in
the list is used as a .
3) Split the list into two parts - one with elements larger than the and the other
with elements smaller than the .
4) Recursively repeat the algorithm for both halves of the original list.
In the above algorithm, the important step is partitioning the list into two sublists. The
basic steps to partition a list are:
1. Select the first element as a in the list.
2. Start a pointer (the pointer) at the second item in the list.
3. Start a pointer (the ℎ pointer) at the last item in the list.
4. While the value at the pointer in the list is lesser than the value, move the
pointer to the right (add 1). Continue this process until the value at the
pointer is greater than or equal to the value.
5. While the value at the ℎ pointer in the list is greater than the value, move
the ℎ pointer to the left (subtract 1). Continue this process until the value at the
ℎ pointer is lesser than or equal to the value.
6. If the pointer value is greater than or equal to the ℎ pointer value, then swap
the values at these locations in the list.
7. If the pointer and ℎ pointer don’t meet, go to step 1.
Example
Following example shows that 50 will serve as our first pivot value. The partition process
will happen next. It will find the point and at the same time move other items to
the appropriate side of the list, either lesser than or greater than the value.
50 25 92 16 76 30 43 54 19
5 . 1 2 Q u i c k s o rpivot
t
D i vi de an d Co n qu er Al g or i th m s | 255
Partitioning begins by locating two position markers—let’s call them and ℎ —at the
beginning and end of the remaining items in the list (positions 1 and 8 in figure). The goal
of the partition process is to move items that are on the wrong side with respect to the pivot
value while converging on the split point also. The figure given below shows this process
as we locate the position of 50.
50 25 92 16 76 30 43 54 19
50 25 92 16 76 30 43 54 19
50 25 92 16 76 30 43 54 19
50 25 92 16 76 30 43 54 19
Now, continue moving left and right pointers. 19 < 50, move pointer to right:
50 25 19 16 76 30 43 54 92
50 25 19 16 76 30 43 54 92
50 25 19 16 76 30 43 54 92
50 25 19 16 76 30 43 54 92
50 25 19 16 76 30 43 54 92
50 25 19 16 76 30 43 54 92
50 25 19 16 43 30 76 54 92
50 25 19 16 43 30 76 54 92
50 25 19 16 43 30 76 54 92
pivot right
left
76 > 50, stop from moving pointer:
50 25 19 16 43 30 76 54 92
pivot right
left
76 > 50, move ℎ pointer to left:
50 25 19 16 43 30 76 54 92
30 25 19 16 43 50 76 54 92
30 25 19 16 43 50 76 54 92
Implementation
def quick_sort(A,low,high):
if low<high:
partition_point = partition(A,low,high)
quick_sort(A,low,partition_point-1)
quick_sort(A,partition_point+1,high)
def partition(A,low,high):
pivot = A[low]
left = low+1
right = high
done = False
while not done:
while left <= right and A[left] <= pivot:
left = left + 1
temp = A[low]
A[low] = A[right]
A[right] = temp
return right
A = [50,25,92,16,76,30,43,54,19]
quick_sort(A,0,len(A)-1)
print(A)
Analysis
Let us assume that T(n) be the complexity of quick sort with elements. Recurrence for
( ) depends on two subproblem sizes which depend on partition element. If pivot is
smallest element, then exactly ( − 1) items will be in left part and (n − ) in right part. Let
us call it as −partition. Since each element has equal probability of selecting it as
the probability of selecting element is .
Best case
In , if the number of elements is greater than 1 then they are divided into two
equal sublists, and the algorithm is recursively invoked on the sublists. After solving the
subproblems, we don’t need to combine them. This is because in they are already
in sorted order. But, we need to scan the complete elements to partition the elements. Best
case for quick sort occur if the partition happens at the middle of the list. The recurrence
equation of best case is:
2 + O( ), >1
( )= 2
0 , =1
Applying master theorem of D & C for this recurrence gives O( ) complexity.
Worst case
In the worst case, quick sort divides the input list into two sublists and one of them
contains only one element. That means, the other sublist has − 1 elements to be sorted.
Let us assume that the input size is and ( ) defines the solution to the given problem.
So we need to solve ( − 1), (1) subproblems. But to divide the input into two sublists,
quick sort needs one scan of the input elements (this takes O( )).
After solving these subproblems, the algorithm takes only a constant time to combine these
solutions. The total recurrence algorithm for this problem can be given as:
( ) = ( − 1) + O(1) + O( )
( )
This is clearly a summation recurrence equation. So, ( ) = = O( ).
Average case
In the average case of , we do not know where the happens. For this
reason, we take all possible values of locations, add all their complexities and
divide with to get the average case complexity.
1
( ) = ( ℎ − )+ +1
1
= ( − 1) + ( − ) + +1
2
= ( )+ +1
T( ) = 2 ( )+ +
( − 1) ( − 1) = 2 ( ) + ( − 1) + ( − 1)
( ) − ( − 1) ( − 1) = 2 ( )+ + − (2 ( ) + ( − 1) + ( − 1))
( ) − ( − 1) ( − 1) = 2 ( − 1) + 2
( ) = ( + 1) ( − 1) + 2
Divide with ( + 1):
= O(1) + 2 ∑
= O(1) + O(2 )
( )
= O( )
+1
( ) = O ( + 1) = O( )
Time Complexity, ( ) = O( ).
Performance
Worst case time complexity: O( )
Best case time complexity: O( )
Average case time complexity: O( )
Worst case space complexity: O(1)
There are two ways of adding randomization in quick sort: either by randomly placing the
input data in the array or by randomly choosing an element in the input data for pivot. The
second choice is easier to analyze and implement. The change will only be done at the
algorithm.
In normal quick sort, element is always the leftmost element in the list to be sorted.
Instead of always using [ ] as , we will use a randomly chosen element from the
subarray [ . . ℎ ℎ] in the randomized version of quick sort. It is done by exchanging
element [ ] with an element chosen at random from [ . . ℎ ℎ]. This ensures that the
element is equally likely to be any of the ℎ ℎ − + 1 elements in the sublist.
Since the pivot element is randomly chosen, we can expect the split of the input list to be
reasonably well balanced on average. This can help in preventing the worst-case behavior
of quick sort which occurs in unbalanced partitioning.
Even though the randomized version improves the worst case complexity, its worst case
complexity is still O( ). One way to improve − is to choose the pivot
for partitioning more carefully than by picking a random element from the array. One
common approach is to choose the pivot as the median of a set of 3 elements randomly
selected from the array.
5 . 1 3 C o n v e rt a l go r i t h m s t o d i v i d e & con q u e r r e cu r r e n c e s
D i vi de an d Co n qu er Al g or i th m s | 260
Solution: Let us assume that the input size of the given algorithm is and ( ) defines
the solution to the given problem. As per the description, the algorithm divides the problem
into 5 subproblems with each of size . So, we need to solve 5T( ) subproblems.
After solving these subproblems, the given array (linear time) is scanned to combine these
solutions. The total recurrence algorithm for this problem can be given as:
( )=5 + O( ).
Using the Master theorem, we can determine the time complexity of the given algorithm
as O( ) ≈ O( ) ≈ O( ).
: Consider an algorithm solves problems of size by recursively solving
two subproblems of size − 1 and then combining the solutions in constant time. What is
the complexity of this algorithm?
Solution: Let us assume that the input size is n and ( ) defines the solution to the given
problem. As per the description of algorithm we divide the problem into 2 subproblems with
each of size n − 1. So, we have to solve 2T( − 1) subproblems.
After solving these subproblems, the algorithm takes only a constant time to combine these
solutions. The total recurrence algorithm for this problem can be given as:
( ) = 2 ( − 1) + O(1)
Using the Master theorem, we can determine the time complexity of the given algorithm
as O 2 = O(2 ).
After solving the subproblems, the algorithm takes quadratic time to combine these
solutions. The total recurrence algorithm for this problem can be given as:
( )=9 + O( )
Using & Master theorem, we could derive the complexity of given algorithm as
O( ).
: Consider the modified version of binary search. Let us assume that the
array is divided into 3 equal parts (ternary search) instead of 2 equal parts. Write the
recurrence for this ternary search and find its complexity.
Solution: As we have seen, binary search has the recurrence relation: ( ) = + O(1).
In the recurrence relation, instead of 2 we have to use 3. This indicates that we are dividing
the array into 3 sub-arrays with equal size and considering only one of them. So, the
recurrence for the ternary search can be given as:
( )= +O(1)
Using Master theorem (of & ), we get the complexity as O( ) ≈ O( ) (we don’t have
to worry about the base of as they are constants).
: For the previous problem, what if we divide the array into two sets of
sizes approximately one-third and two-thirds?
5 . 1 3 C o n v e rt a l go r i t h m s t o d i v i d e & con q u e r r e c u r r e n c e s
D i vi de an d Co n qu er Al g or i th m s | 261
Solution: We now consider a slightly modified version of ternary search in which only one
comparison is made, two partitions are created, one of roughly elements and the other
of . Here, the worst case comes when the recursive call is on the larger element part.
So, the recurrence corresponding to this worst case is:
( )= + O(1)
Using Master theorem (of D & C), we can derive the complexity as O( ). It is interesting
to note that we will get the same results for general -ary search (as long as is a fixed
constant which does not depend on ) as approaches infinity.
Solution: The above code snippet has the input size and for this code assume that ( )
defines the solution to the given problem, as per the given code, after printing the character
and dividing the problem into 2 subproblems with each of size and solving them. So, we
need to solve 2T( ) subproblems. After solving these subproblems, the algorithm is not
doing anything for combining the solutions. The total recurrence algorithm for this problem
can be given as:
( )=2 +O(1)
Using Master theorem of & , we would get the complexity of given code
as O ≈ O( ) = O( ).
def find_in_infinite_series(A):
l=r=1
while( A[r] != ‘$’):
l=r
r=r×2
while( (r – l > 1 ):
mid = (r – l)/2 + l
if( A[mid] == ‘$’):
r = mid
else: l = mid
5 . 1 4 C o n v e rt i n g co d e to d i v i d e & co n q u e r r e c u r r e n c e s
D i vi de an d Co n qu er Al g or i th m s | 262
Naive solution
This is one of the simplest solution which most of us are aware of. To find the sum of all
elements in the given array, simply scan through all the elements of the array and keep
appending the current element to a variable which maintains the sum of all the previous
elements seen so far.
def summation(A):
s=0
for i in range(len(A)):
s = s+ A[i]
return s
A = [3, 4, 2, 1, 5, 8, 7, 6]
print summation(A)
Time Complexity: O( ), as we need to scan the complete array.
Space Complexity: O(1).
Performance
For the above algorithm, the recurrence formula can be written as:
2 + 1, >2
2
( ) = 1, =2
0, =1
With master theorem, we can derive the running time of this recurrence as ( ) = O( ).
Compared to Naïve method, in divide and conquer approach, the number of comparisons
is less. However, using the asymptotic notation, both of the approaches are represented by
O( ).
Space Complexity: O(1).
With the same strategy, we can find the maximum, minimum, and average
of elements in the give array.
Naive solution
To find the maximum and minimum numbers in a given array of size , the following
algorithm can be used. In this straight forward method, the maximum and minimum
number can be found separately. To find the maximum and minimum numbers, the
following straightforward algorithm can be used.
def maxmin(A):
if not A:
return None, None
minimum = A[0]
maximum = A[0]
for i in range(1, len(A)):
if A[i] < minimum:
minimum = A[i]
if A[i] > maximum:
maximum = A[i]
return minimum, maximum
print maxmin([3, 42, 29, 1, 45, 9, 69, 19])
The number of comparisons in method is 2 − 2. The number of comparisons can
be reduced using the divide and conquer approach.
5 . 1 6 F i n d i n g m i n i m u m a n d m a x i m u m in a n a r r a y
D i vi de an d Co n qu er Al g or i th m s | 264
In the divide and conquer approach, the array is divided into two halves. Then, using
recursive approach maximum and minimum numbers in each half are found. Later, return
the maximum of two maxima of each half and the minimum of two minima of each half.
As we are dealing with subproblems, we state each subproblem as computing minimum
and maximum of a subarray A[start . . end]. Initially, = 0 and = ( ) − 1, but
these values change as we recurse through subproblems.
To compute minimum and maximum of A[start . . end]:
by splitting into two subarrays A[start . .r] and A[r+1 . . end], where r is the
halfway point of A[start . . end].
by recursively computing minimum and maximum of the two subarrays
A[start . .r] and A[r+1 . . end].
by computing the overall minimum as the min of the two recursively
computed minimum, similar to the overall maximum.
The divide and conquer algorithm we developed for this problem is motivated by the
following observation. Suppose we know the maximum and minimum element in both of
the roughly sized partitions of a -element ( ≥ 2) list. Then in order to find the maximum
and the minimum element of the entire list, we simply need to see which of the two
maximum elements is the larger, and which of the two minimums is the smaller. We
assume that in a 1-element list the sole element is both the maximum and the minimum
element. With this in mind, we present the following code for the max/min problem.
def maxmin(A):
n = len(A)
if (n == 1):
return A[1], A[1]
elif (n == 2):
if( A[0] < A[1]):
return A[0], A[1]
else:
return A[1], A[0]
else:
min_left, max_left = maxmin(A[:n/2])
min_right, max_right = maxmin(A[n/2:])
if (min_left < min_right):
minimum = min_left
else:
minimum = min_right
if (max_left < max_right):
maximum = max_right
else:
maximum = max_left
return (minimum, maximum)
print maxmin([3, 42, 29, 1, 45, 9, 69, 19])
Performance
Let ( ) be the number of comparisons performed by the procedure. When = 1
clearly there are no comparisons. Thus we have (1) = 0. Similarly, (2) = 1. Otherwise
when > 2 clearly: ( ) = 2 ( ) + 2.
5 . 1 6 F i n d i n g m i n i m u m a n d m a x i m u m in a n a r r a y
D i vi de an d Co n qu er Al g or i th m s | 265
2 + 2, >2
2
( ) = 1, =2
0, =1
Since performs two recursive calls on partitions of roughly half of the total size of
the list, it makes two further comparisons to sort out the max/min for the entire list. (Of
course, to be pedantic there should be floors and ceilings in the recursive function, and
something should be said about the fact that the following proof is only for which are
powers of two and how this implies the general result. This is omitted.)
Therefore, with = 2 (notice that it is valid for any value. But, to simplify the proof, we
assume = 2 ):
( ) = 2 +2
2
= 2
2 +2
2
= 2 (2 ) + 2
= 2[2 (2 ) + 2] + 2
= 2 (2 ) + 2 + 2
= 2 (2 ) + 2 + 2 + 2
= 2 2 ( ) +2 +⋯+2 + 2 +2
= 2 (2) + 2[2 + ⋯ + 2 + 2 + 1]
= 2 + 2[2 − 1]
= 2 + 2.2 −2
= 3. 2 −2
= 3
−2
2
showing the desired result. By the principle of mathematical substitution we are done.
Naive solution
To find the two maximal elements in a given array of size , the following algorithm can
be used. In this straight forward method, the two maximal elements can be found
separately.
def max_max(A):
if not A:
return None, None
# if A contains only 1 item return -1 (a dummy item) and that item
if len(A) == 1:
return (-1, A[0])
elif len(A) == 2:
if A[0] <= A[1]:
5 . 1 7 F i n d i n g t w o m a x i m a l e l e me n t s
D i vi de an d Co n qu er Al g or i th m s | 266
5 . 1 7 F i n d i n g t w o m a x i m a l e l e me n t s
D i vi de an d Co n qu er Al g or i th m s | 267
Performance
Let ( ) be the number of comparisons performed by the procedure. When = 1
clearly there are no comparisons. Thus we have (1) = 0. Similarly, (2) = 1. Otherwise
when > 2 clearly: ( ) = 2 ( ) + 2.
2 + 2, >2
2
( ) = 1, =2
0, =1
performs two recursive calls on partitions of roughly half of the total size of the list
and then makes two further comparisons to sort out the max/max for the entire list. This
recurrence is pretty much same as procedure.
Naive solution
This problem could easily be solved by merging the two arrays into a single array. We can
use the process. Use procedure of and keep track of the count
while comparing the elements of two arrays. If the count becomes (assuming both arrays
of equal size with elements each), we have reached the median. Take the average of the
elements at indexes − 1 and in the merged array.
Time Complexity: This algorithm runs in O( ) time, and then the median could be retrieved
by grabbing the average of middle element(s) in O(1) time.
What if the size of the two lists are not the same?
5 . 1 8 M e d i a n i n t w o so r t e d l i s ts
D i vi de an d Co n qu er Al g or i th m s | 268
The solution is similar to the previous brute force algorithm. Let us assume that the lengths
of two lists are and . In this case, we need to stop when the counter reaches .
Time Complexity: O( ).
What if the size of the two lists are not the same?
The above solution works fine with arrays of the same length, what about those with
different lengths? Consider the following example:
A = [1, 2, 4, 7, 9]
B = [3, 5, 6, 8]
median(A) = 4
median(B) = 5.5
5 . 1 8 M e d i a n i n t w o so r t e d l i s ts
D i vi de an d Co n qu er Al g or i th m s | 269
A’s local median is smaller than B’s, so discard the first element from A ([1]) and the last
element from B ([8]).
A = [1, 2, 4, 7, 9] = [2, 4, 7, 9]
B = [3, 5, 6, 8] = [3, 5, 6]
median(A) = 5.5
median(B) = 5
B’s local median is smaller than A’s, so discard the last element from A ([9]) and the first
element from B ([3])
A = [2, 4, 7, 9] = [2, 4, 7]
B = [ 3, 5, 6] = [5, 6]
median(A) = 4
median(B) = 5.5
A’s local median is smaller than B’s, so discard the first element from A ([2]) and the last
element from B ([6]).
A = [2, 4, 7] = [4, 7]
B = [5, 6] = [5]
What we should do is look at the minimal problem set in order to come up with a solution
that takes the smallest amount of time. For the above example, where A is an array with 2
elements and B is an array with a single element, there are 3 possible results:
If B[0] < A[0]: median = A[0]
If B[0] > A[1]: median = A[1]
Otherwise: median = B[0]
For the above example, median is B[0] and it is 5.
This works for the general case since any values outside the middle 2 can be discarded at
this point. An odd number of values in A can also be broken down like this:
( [ ] [ ])
If B[0] < A[0]: median =
[ ] [ ]
If B[0] > A[2]: median =
[ ] [ ]
Otherwise: median =
There need not be only 2 or 3 elements in A for this to work as we’re only interested in the
middle elements of A.
Algorithm:
1. Find the medians of the given sorted input arrays [] and []. Assume that those
medians are and .
2. If and are equal then return (or ).
3. If is greater than , then the final median will be below two subarrays.
a. From first element of to .
b. From to last element of .
4. If is greater than , then median is present in one of the two subarrays
below.
a. From to last element of .
b. From first element of to .
5. Repeat the above process until the size of both the subarrays becomes 2.
5 . 1 8 M e d i a n i n t w o so r t e d l i s ts
D i vi de an d Co n qu er Al g or i th m s | 270
6. If size of the two arrays is 2, then use the formula below to get the median:
Median = ( ( [0], [0]) + ( [1], [1])/2
def median(A, B):
m = len(A) + len(B)
if m % 2 == 1:
return kth(A, B, m // 2)
else:
return float(kth(A, B, m // 2) + kth(A, B, m // 2 - 1)) / 2
def kth(A, B, k):
if not A:
return B[k]
if not B:
return A[k]
midA, midB = len(A) // 2, len(B) // 2
if midA + midB < k:
if A[midA] > B[midB]:
return kth(A, B[midB + 1:], k - midB - 1)
else:
return kth(A[midA + 1:], B, k - midA - 1)
else:
if A[midA] > B[midB]:
return kth(A[:midA], B, k)
else:
return kth(A, B[:midB], k)
A = [1, 2, 3, 4, 5, 6, 7]
B = [2, 3, 4, 5, 6, 8, 9]
print median(A, B)
Performance
Let and be the medians of the respective lists (which can be easily found since
both lists are sorted). If == , then that is the overall median of the union and
we are done. Otherwise, the median of the union must be between and . Suppose
that < (the opposite case is entirely similar), then we need to find the median
of the union of the following two sets:
{ | >= }{ | <= }
So, we can do this recursively by resetting the of the two arrays. The algorithm
tracks both arrays (which are sorted) using two indices. These indices are used to access
and compare the median of both arrays to find where the overall median lies.
Time Complexity: O( ) since we are considering only half of the input and throwing away
the remaining half.
, = , ,
5 . 1 9 S t r a s s e n' s ma t r i x m u lt i p l i c a t io n
D i vi de an d Co n qu er Al g or i th m s | 271
Naive approach
First, let us discuss naive method and its complexity. Here, we are calculating C = A × B.
Using Naive method, two matrices ( and ) can be multiplied if the order of these matrices
are p × q and q × r. Following is the algorithm.
Algorithm: Matrix-Multiplication (A, B, C)
for i = 1 to p do
for j = 1 to r do
C[i,j] := 0
for k = 1 to q do
C[i,j] := C[i,j] + A[i,k] × B[k,j]
To multiply a matrix by another matrix we need to do the of rows and columns.
What does that mean? Let us see with an example:
To work out the answer for the first row and first column:
123 7 8 58
× 9 10 =
456
1112
The " " is where we multiply matching members, then sum up:
(1, 2, 3) • (7, 9, 11) = 1×7 + 2×9 + 3×11 = 58
We match the 1st members (1 and 7), multiply them, likewise for the 2nd members (2 and
9) and the 3rd members (3 and 11), and finally sum them up.
Want to see another example? Here it is for the first row and second column:
123 7 8 58 64
× 9 10 =
456
1112
(1, 2, 3) • (8, 10, 12) = 1×8 + 2×10 + 3×12 = 64
We can do the same thing for the second row and first column:
(4, 5, 6) • (7, 9, 11) = 4×7 + 5×9 + 6×11 = 139
And for the second row and second column:
(4, 5, 6) • (8, 10, 12) = 4×8 + 5×10 + 6×12 = 154
And we get:
123 7 8 58 64
× 9 10 =
456 139 154
1112
Performance
Here, we assume that integer operations take O(1) time. There are three loops in this
algorithm and one is nested in the other. Hence, the algorithm takes O( ) time to execute.
5 . 1 9 S t r a s s e n' s ma t r i x m u lt i p l i c a t io n
D i vi de an d Co n qu er Al g or i th m s | 272
From the given definition of , , the result sub matrices can be computed as follows:
, = , × , + , × ,
, = , × , + , × ,
, = , × , + , × ,
, = , × , + , × ,
Here the symbols + and × are taken to mean addition and multiplication (respectively) of
× matrices.
Strassen's algorithm
Fortunately, it turns out that one of the eight matrix multiplications is redundant (found
by Strassen). Volker Strassen is a German mathematician born in 1936. He is well known
for his works on probability, but in the computer science and algorithms he’s mostly
recognized because of his algorithm for matrix multiplication that’s still one of the main
methods that outperforms the general matrix multiplication algorithm.
Strassen firstly published this algorithm in 1969 and proved that the algorithm isn’t the
optimal one. Stassen’s matrix multiplication algorithm is a divide-and-conquer algorithm
that beats the bound O( ). Stassen’s algorithm takes two × matrices and produces
multiplication of those two matrices.
Consider the following series of seven × matrices:
= , + , ×( , + , )
= , − , ×( , + , )
= , − , ×( , + , )
= , + , × ,
= , ×( , − , )
= , ×( , − , )
= + , × ,
Each equation above has only one multiplication. Ten additions and seven multiplications
are required to compute through . Given through , we can compute the elements
of the product matrix C as follows:
, = + − +
5 . 1 9 S t r a s s e n' s ma t r i x m u lt i p l i c a t io n
D i vi de an d Co n qu er Al g or i th m s | 273
, = +
, = +
, = + − +
5 . 1 9 S t r a s s e n' s ma t r i x m u lt i p l i c a t io n
D i vi de an d Co n qu er Al g or i th m s | 274
5 . 1 9 S t r a s s e n' s ma t r i x m u lt i p l i c a t io n
D i vi de an d Co n qu er Al g or i th m s | 275
Repeated addition
There are various methods of obtaining the product of two numbers. One such algorithm
is repeated addition. Let’s start by thinking about a simple problem like 4 x 3 (“four times
three”). What does it mean? The way many of us learned to multiply in school was to think
of 4 x 3 as meaning the same thing as “four of the quantity three.” By that I mean, if you
have a box with three chocolates in it, then 4 x 3 is the total number of chocolates contained
in four boxes where each box contained three chocolates. In other words, 4 x 3 is the same
as 3 + 3 + 3 + 3, which of course is 12. So, we can just think of multiplication as adding
some number together some other number of times, right? Multiplication is just repeated
addition.
5 . 2 0 I n t e g e r m u l t i p l i ca t io n
D i vi de an d Co n qu er Al g or i th m s | 276
The carry digit from the leftmost column is put in front of the result without further
computation. Altogether, we have done basic operations, namely one addition of digits
per column.
As we have seen above, addition of two numbers A and B, each with digits, would take
O( ) addition of digits. If we have two numbers A and B, each with digits, and if we use
repeated addition to multiply them, it would take a total of O( ) additions as there are
additions of number B to number A and each of such addition takes addition of digits.
Naïve approach
Many of us are familiar with a quite efficient algorithm for integer multiplication from the
grade-school years. This standard integer multiplication routine of two -digit numbers
involves multiplications of an -digit number by a single digit, plus the addition of
numbers, which have at the most 2 digits.
Let us first focus on how to multiply an -digit number with a single digit and count the
number of operations being performed during that process.
Let us now analyze the number of basic operations used by multiplication of two n-digit
numbers A and B. In case one is shorter than the other, we can pad it with zeros at the
front. For each digit of we need to do one multiplication × . This needs 2 × basic
operations, as we saw above. Because there are digits in B, × multiplication needs
× (2 × ) = 2 × basic operations.
5 . 2 0 I n t e g e r m u l t i p l i ca t io n
D i vi de an d Co n qu er Al g or i th m s | 277
digits, and then for numbers of any length. Let the first number be A, and the second be
B.
The simplest case is, of course, the multiplication of two numbers consisting of one digit
each ( = 1). Multiplying them needs a single basic operation, namely one multiplication
of digits, which immediately gives the result.
The next case we look at is the case = 2, that is, the multiplication of two numbers A
and B having two digits each. We split them into halves, that is, into their digits:
A = x × 10 + y and B = a × 10 + b
For example, we split the numbers A = 89 and B = 32 like this:
x = 8, y = 9, and a = 3, b = 2.
We can now rewrite the product × in terms of the digits:
× = ( × 10 + ) × ( × 10 + ) = ( × ) × 100 + ( × + × ) × 10 + ×
Continuing the example = 89 and = 32, we get
89 × 32 = (8×3) ×100 + (8×2 + 9×3) ×10 + 9×2 = 2848
Writing the product × of the two-digit numbers A and B as above shows how it can be
computed using multiplications of one-digit numbers, followed by additions. This is
precisely what naïve multiplication does.
Observation is that, we split the numbers into two equal parts. Consider the above integers
and split each of them in two parts.
A = 523 and B = 31:
523 = 52 × 10 + 3
31 = 3 × 10 + 5
Now, we can multiply A and B as:
523 × 31 = (52 × 10 + 3) × (3 × 10 + 5)
= (52 × 3) × 10 + (52 × 5 + 3 × 3) × 10 + (3 × 5)
In general, a number A with digits can be represented as:
A = × 10 +
where =
= , left half of digits of A
= , right half of digits of A
For example,
7687554564 = 76875 × 10 + 54564
where = =4
a = 76875, left half of digits
b = 54564, right half of digits
Now, let us focus on general case of multiplying two -digit numbers. Let the "left half" of
the A be and the "right half" of the A be . Assign and similarly. With this notation,
we can set the stage for solving the problem in a divide and conquer fashion. These digit
numbers A and B can be represented as:
A = × 10 +
where =
= , left half of digits of A
5 . 2 0 I n t e g e r m u l t i p l i ca t io n
D i vi de an d Co n qu er Al g or i th m s | 278
×
× ×
+ × ×
× ( × + × ) ×
That image is just a picture of the idea, but more formally, the derivation works as follows:
A×B = ( × 10 + )× ( × 10 + )
= × × 10 + × × 10 + × × 10 + ×
= × × 10 +( × + × ) × 10 + ×
And we already reason that this last value is equal to the product of A and B. Thus, in
order to multiply a pair of -digit numbers, we can recursively multiply four pairs of -digit
numbers. The rest of the operations involved are all O( ) operations. Multiplying by 10
may look like a multiplication (and hence not O( )), but really it's just a matter of appending
zeroes onto the number, which takes O( ) time.
def multiply_dc(A,B):
if len(str(A)) == 1 or len(str(B)) == 1:
return A*B
n = max(len(str(A)), len(str(B)))
#calculates the size of the numbers in base 10
m=max(len(str(A)), len(str(B)))//2
#split the digit sequences about the middle
cut=pow(10,m)
a_left, a_right=A//cut, A%cut
b_left, b_right=B//cut, B%cut
#divide and conquer
p1=multiply_dc(a_left, b_left)
p2=multiply_dc(a_left, b_right)
p3=multiply_dc(a_right, b_left)
p4=multiply_dc(a_right, b_right)
return p1*pow(10, 2*m) + (p2+p3)*pow(10, m) + p4
print multiply_dc(523, 523)
Performance
Let T( ) denote the number of digit multiplications needed to multiply two -digits numbers.
Written in this manner, we have broken down the problem of the multiplication of two -
digit numbers into 4 multiplications of - bit numbers plus 3 additions. Thus, we can
compute the running time T( ) as follows:
T( ) = 4T( ) + O( )
5 . 2 0 I n t e g e r m u l t i p l i ca t io n
D i vi de an d Co n qu er Al g or i th m s | 279
The 4T( ) term arises from conquering the smaller problems; the O( ) is the time to combine
these problems into the final solution (using additions and shifts). Unfortunately, when we
solve this recurrence, the running time is still O( ), by the master theorem. So, it seems
that we have not gained anything compared with naïve multiplication.
A×B = × × 10 +( × + × ) × 10 + ×
With this notation, we can set the stage for solving the problem in a divide and conquer
fashion. Now, the question is, can we optimize this solution in any way? In particular, is
there any way to reduce the number of multiplications done? Karatsuba, a Russian
computer scientist, came up with a solution for this problem in 1960. Karatsuba algorithm
is the first multiplication algorithm with better time complexity than naive multiplication.
Logical thinking of this algorithm is very much similar to Stassen’s matrix multiplication
algorithm. Ultimately, both tries to reduce the number of multiplications are needed to
perform the computation.
Karatsuba’s goal was to decrease the number of multiplications from 4 to 3. His clever
guess work revealed the following:
Let,
P1 = ×
P2 = ( + )×( + )
P3 = ×
Now, note that
P2 – P1 – P3 = ( + )×( + )− × − ×
= × + × + × + × − × − ×
= × + ×
Then we have the following:
A×B = P1 × 10 + [P2 – P1 – P3] × 10 + P3
= × × 10 + ( × + × ) × 10 + ×
So, what's the big deal about this anyway?
Now, consider the work necessary in computing P1, P2 and P3. Both P1 and P3 are -digit
multiplications. But, P2 is a bit more complicated to compute. We do two digit additions,
(this takes O( ) time), and then one -digit multiplication. Potentially, +1 digits.
After that, we do two subtractions, and another two additions, each of which still takes
O( ) time. Thus, our running time T( ) obeys the following recurrence relation:
T( ) = 3T( ) + O( )
Although this seems to be slower initially because of some extra precomputing before doing
the multiplications, this will save time for very large integers.
Why won't this save time for small multiplications?
The hidden constant in the O( ) of the second recurrence is much larger. It consists of 6
additions/subtractions whereas the O( ) in the first recurrence consists of 3
additions/subtractions.
5 . 2 0 I n t e g e r m u l t i p l i ca t io n
D i vi de an d Co n qu er Al g or i th m s | 280
def karatsuba(A,B):
if len(str(A)) == 1 or len(str(B)) == 1:
return A*B
#calculates the size of the numbers in base 10
m=max(len(str(A)), len(str(B)))//2
#split the digit sequences about the middle
cut=pow(10,m)
a_left, a_right=A//cut, A%cut
b_left, b_right=B//cut, B%cut
#divide and conquer
p1=karatsuba(a_left, b_left)
p2=karatsuba((a_left + a_right),(b_left + b_right))
p3=karatsuba(a_right, b_right)
return p1*pow(10, 2*m) + (p2-p1-p3)*pow(10, m) + p3
print karatsuba(523, 453)
Performance
Let T( ) denote the number of digit multiplications needed to multiply two n-digits
numbers. The recurrence (since the algorithm does 3 multiplications on each step) for this
algorithm can be given as:
T( ) = 3T( ) + O( )
By using the master theorem, we could derive the running time of this algorithm as T( ) =
O( = . ), a solid improvement.
So we see: ’ method indeed requires much less computational effort, at least
when counting only multiplication of digits, as we have done. A precise analysis also has
to take the additions and subtractions of intermediate results into account. This will show
that naive multiplication is actually faster for numbers with only a few digits. But as
numbers get longer, Karatsuba’s method becomes superior because it produces less
intermediate results. It depends on the properties of the particular computer and the
representation of long numbers inside it when exactly Karatsuba’s method is faster than
naive multiplication.
Examples
class BSTNode:
'''A node in a binary search tree has left and right subtrees.'''
def __init__(self, data, left=None, right=None):
self.left = left
self.right = right
self.data = data
self.count = 1
class BinarySearchTree:
'''Represents a binary search tree.'''
def __init__(self):
'''Create a new search tree.'''
self.root = None
self.size = 0
def insert(self, data):
'''Put data into the search tree.'''
size = self.size + 1
# if tree is empty
if self.root is None:
self.root = BSTNode(data)
return
# search for data and its would-be parent
p, q = self.find_and_parent(data)
if p is not None:
p.count += p.count
return
# make data a child of q
if data < q.data:
assert q.left is None
q.left = BSTNode(data)
else:
assert q.right is None
q.right = BSTNode(data)
def find(self, data):
'''Find data, return None if data is not present.'''
p, _ = self.find_and_parent(data)
if p is None: return None
else: return p.data
def find_and_parent(self, data):
'''Search for data, returning the node containing data and its parent.
If data doesn't exist, we return None and data's would-be parent.'''
q = None # parent
p = self.root # current node
while p is not None and p.data != data:
q=p
if data < p.data:
p = p.left
else:
p = p.right
return p, q
def majority_element(self, root):
'''Recursively build the inorder list.'''
if root is not None:
self.majority_element(root.left)
def majority_element(A):
dict = {}
for i in A:
dict[i] = dict.get(i, 0) + 1
if dict[i] > len(A)/2:
return i
return None
A = [1, 2, 3, 4, 3, 3, 3, 3, 3, 2, 2, 3, 7, 3, 3, 3]
print majority_element(A)
Time Complexity: O( ).
Space Complexity: O( ).
Think of it this way, every time the counter becomes zero, we’re effectively throwing away
the list up to that point because there is no majority in the list before us. If there were any,
the counter would be positive because it would have to occur more times (increment) than
not (decrement) to be positive.
def majority_element(A):
count = 0
element = -1
n = len(A)
if n == 0:
return
for i in range(n):
if(count == 0) :
element = A[i]
count = 1
elif(element == A[i]):
count += 1
else:
count -= 1
# check if elements appears for more than n/2 times
count = 0
for i in range(n):
if(element == A[i]):
count += 1
if count > n//2:
return element
return None
A = [1, 2, 3, 4, 3, 3, 3, 3, 3, 2, 2, 3, 7, 3, 3, 3]
print majority_element(A)
Time Complexity: O( ).
Space Complexity: O(1).
Example
If the given array is [−1, 0, 2, 5, 7, 9, 11, 12, 19], the answer for this input array would be 2 as
[2] = 2. Also, it is possible to have multiple values for i for which [ ] = . For example,
for the input array [0, 1, 2, 3, 4, 9, 11, 12, 19], the result would be: 0, 1, 2, 3, and 4.
5 . 2 2 C h e c k i n g fo r m a g ic i n d e x i n a so r t e d a r r a y
D i vi de an d Co n qu er Al g or i th m s | 286
Time Complexity: O( ).
Space Complexity: O(1).
5 . 2 2 C h e c k i n g fo r m a g ic i n d e x i n a so r t e d a r r a y
D i vi de an d Co n qu er Al g or i th m s | 287
high = mid-1
else: # A[mid] < mid:
low = mid+1
return -1
A = [-1, 0, 2, 5, 7, 9, 11, 12, 19]
print check_for_magic_index(A)
Time Complexity: O( ).
Space Complexity: O( ).
5.23 Stock pricing problem
: Consider the stock price of . in consecutive days. The
input consists of an array with stock prices of the company. We know that the stock price
will not be the same on all the days. In the input stock prices, there may be dates where
the stock is high when we can sell the current holdings, and there may be days when we
can buy the stock. Now our problem is to find the day on which we can buy the stock and
the day on which we can sell the stock so that we can make maximum profit.
By opting for the divide and conquer technique we can get ( ) solution. Divide the
input list into two parts and recursively find the solution in both the parts. Here, we get
three cases:
and both are in the earlier time period.
and both are in the later time period.
is in the earlier part and is in the later part of the time
period.
The first two cases can be solved with recursion. The third case needs care. This is because
is one side and is on other side. In this case we need to find the
minimum and maximum prices in the two sub-parts and this we can solve in linear-time.
def stock_strategy(A, start, stop):
n = stop - start
# edge case 1: start == stop: buy and sell immediately = no profit at all
if n == 0:
return 0, start, start
if n == 1:
return A[stop] - A[start], start, stop
mid = start + n/2
# the "divide" part in Divide & Conquer: try both halves of the array
maxProfit1, buy1, sell1 = stock_strategy(A, start, mid-1)
maxProfit2, buy2, sell2 = stock_strategy(A, mid, stop)
maxProfitBuyIndex = start
maxProfitBuyValue = A[start]
for k in range(start+1, mid):
if A[k] < maxProfitBuyValue:
maxProfitBuyValue = A[k]
maxProfitBuyIndex = k
maxProfitSellIndex = mid
maxProfitSellValue = A[mid]
for k in range(mid+1, stop+1):
if A[k] > maxProfitSellValue:
maxProfitSellValue = A[k]
maxProfitSellIndex = k
# those two points generate the maximum cross border profit
maxProfitCrossBorder = maxProfitSellValue - maxProfitBuyValue
# and now compare our three options and find the best one
if maxProfit2 > maxProfit1:
if maxProfitCrossBorder > maxProfit2:
return maxProfitCrossBorder, maxProfitBuyIndex, maxProfitSellIndex
else:
return maxProfit2, buy2, sell2
else:
if maxProfitCrossBorder > maxProfit1:
return maxProfitCrossBorder, maxProfitBuyIndex, maxProfitSellIndex
else:
return maxProfit1, buy1, sell1
def stock_strategy_with_divide_and_conquer(A):
return stock_strategy(A, 0, len(A)-1)
Performance
Algorithm is used recursively on two problems of half the size of the input,
and in addition ( ) time is spent searching for the maximum and minimum prices. So,
5.23 Stock pricing problem
D i vi de an d Co n qu er Al g or i th m s | 289
Example
As an example, consider the array A = [11, 12, 13, 14, 15, 16, 17, 18]. After shuffling, the
array should be [11, 15, 12, 16, 13, 17, 14, 18]. In this example, = 11, = 14, = 15,
and = 18. So, the first element to be shifted is .
0 1 2 3 4 5 6 7
A 11 12 13 14 15 16 17 18
The next question would be, how to shift the element 15? For this, we don’t have to compare
its values; simply keep shifting the element 15 to left until it finds the correct place. First,
exchange 14 and 15.
0 1 2 3 4 5 6 7
A 11 12 13 15 14 16 17 18
Next, exchange 13 and 15:
0 1 2 3 4 5 6 7
A 11 12 15 13 14 16 17 18
Next, exchange 12 and 15:
0 1 2 3 4 5 6 7
A 11 15 12 13 14 16 17 18
Now, the element 15 has got its correct place.
In the next iteration, shift the next element 16 to the left until it gets correct place.
Exchange 14 and 16:
0 1 2 3 4 5 6 7
A 11 15 12 13 16 14 17 18
Exchange 13 and 16:
0 1 2 3 4 5 6 7
A 11 15 12 16 13 14 17 18
Now, the element 16 has got its correct place.
In the next iteration, shift the next element 17 to the left until it gets correct place.
Exchange 13 and 16:
0 1 2 3 4 5 6 7
A 11 15 12 16 13 17 14 18
Now, element 16 has got its correct place.
For the last element, there is no need of shift operation as the previous shifts ensures that
that they are in proper places already.
0 1 2 3 4 5 6 7
A 11 15 12 16 13 17 14 18
def shuffle(A):
n = len(A)//2
i =0; q =1; k = n
while (i<n):
j=k
while j > i+ q:
A[j], A[j-1] = A[j-1], A[j]
j -= 1
i += 1; k += 1; q += 1
A = [11, 12, 13, 14, 15, 16, 17, 18]
shuffle(A)
print A
Performance
In the above brute force algorithm, for the first element , there were − 1 shift operations,
for the second element , we have − 2 shift operations, and so on.. The total number of
shift operations are:
( )
T( )= −1 + −2…1 =
Example
As an example, consider the array A = [11, 12, 13, 14, 15, 16, 17, 18]. After shuffling, the
array should be [11, 15, 12, 16, 13, 17, 14, 18]. In this example, = 11, = 14, = 15,
and = 18. The first step of the algorithm is to split the array into two equal halves.
Midpoint of an array can be calculated by using the following formula.
−
= +
2
7−0
=0 + =3
2
0 1 2 3 4 5 6 7
A 11 12 13 14 15 16 17 18
Now, we need to exchange the elements around the center. For this operation, we need to
find the midpoints of the left and right subarrays.
3−0
=0 + =1
2
7−4
ℎ =4 + =5
2
With these midpoints, exchange the elements around the center. As a result, the array
would become:
0 1 2 3 4 5 6 7
A 11 12 15 16 13 14 17 18
Next, solve these subarrays recursively. For the first part, split array into two equal parts.
0 1 2 3
A 11 12 15 16
Then, exchange the elements around the center. Since the subarrays have two elements,
we can exchange the second element of the first subarray and the first element of the
second subarray.
0 1 2 3
A 11 15 12 16
This completes the recursive part of subarray A[0:3]. Hence, return the combined result of
A[0:1] and A[2:3].
0 1 2 3
A 11 15 12 16
Now, let us do the same for right subarray A[4:7]. Next, solve these subarrays recursively.
For the first part, split array into two equal parts.
4 5 6 7
A 13 14 17 18
Then, exchange the elements around the center. Since the subarrays have only two
elements, we can exchange the second element of the first subarray and the first element
of the second subarray.
4 5 6 7
A 13 17 14 18
This completes the recursive part of subarray A[4:7]. Hence, return the combined result of
A[4:5] and A[6:7].
4 5 6 7
A 13 17 14 18
Now, in the last step of the algorithm, combine the results of subarrays A[0:3], and A[4:7].
The final resultant array would be:
0 1 2 3 4 5 6 7
A 11 15 12 16 13 17 14 18
def shuffle(A, start, end):
#Array center
mid = start + (end-start)/2
left_mid = 1 + start + (mid-start)//2
if(start == end): # Base case when the array has only one element
return
k=1
i = left_mid
while(i<=mid):
# Swap elements around the center
A[i], A[mid + k] = A[mid + k], A[i]
i += 1
k += 1
shuffle (A, start, mid) # Recursively call the function on the left and right
shuffle (A, mid + 1, end) # Recursively call the function on the right
A = [11, 12, 13, 14, 15, 16, 17, 18]
shuffle(A, 0, len(A)-1)
print A
Performance
In the above divide-and-conquer algorithm, we split the array into two equal parts,
exchange the elements around the center, and solve the subarrays recursively. Exchanging
the elements around the center would take O( ). The size of each subproblem is . Hence,
the recurrence relation for this algorithm can be written as:
( )= 2 ( ) +
This is pretty same as the merge sort recurrence. By using the master theorem, we can
derive the running time of this algorithm as T( ) = O( ).
Space Complexity: O(1).
: Given a set of nuts of different sizes and bolts such that there is a
one-to-one correspondence between the nuts and bolts, find for each nut its corresponding
bolt. Assume that we can compare only nuts to bolts (cannot compare nuts to nuts and
bolts to bolts).
: We are given a box which contains bolts and nuts. Assume
there are nuts and bolts and that each nut matches exactly one bolt (and vice versa).
By trying to match a bolt and a nut we can see which one is bigger, but we cannot compare
two bolts or two nuts directly. Design an efficient algorithm for matching the nuts and
bolts.
This algorithm first performs a partition by picking the last element of bolts array as ,
rearranges the array of nuts and returns the partition index such that all nuts smaller
than [ ] are on the left side and all nuts greater than [ ] are on the right side. It is
not compulsory to select the last element as pivot. We can select any element as pivot. Next
using the [ ] we can partition the array of bolts. This pair of partitioning operations can
be implemented easily in O( ) time. It leaves the bolts and nuts nicely partitioned so that
the “ " bolt and nut are aligned with each other and all the other bolts and nuts are on
the correct side of these pivots – smaller nuts and bolts precede the pivots, and larger nuts
and bolts follow the pivots. Now we apply this partitioning recursively on the left and right
sub-array of nuts and bolts.
Algorithm
1. Pick last element of bolts as _ ; _ = ( ) − 1.
2. Using this [ _ ], rearrange the array of into three groups of elements:
a. Find the smaller than [ _ ]
b. Nut that matches [ _ ]. Let us say the matched index is _ .
So, [ _ ] is equal to [ _ ]
c. Finally, the nuts larger than [ ]
5 . 2 5 N u t s a n d b o l ts p r o b l e m
D i vi de an d Co n qu er Al g or i th m s | 294
5 . 2 6 M a x i m u m v a l ue c on t i gu o us s u b s eq u e nc e
D i vi de an d Co n qu er Al g or i th m s | 295
Example
Since the points are in one dimension, all the points are in a line. Following are two
examples which depict the points. The following plot has the points (2,2), (7,2), (5,2), (4,2),
(9,2), (11,2) with the same y co-ordinate values.
1
0.5
0
0 2 4 6 8 10 12
X-axis
The following second plot has the points (2,2), (2,7), (2,5), (2,4), (2,9), (2,11) with the same
x co-ordinate values.
2; 7
6
2; 5
4 2; 4
2 2; 2
0
0 0.5 1 1.5 2 2.5
X-axis
Algorithm
From the above example plots, it is clear that points are on a single line. To find the closest
points, we just need to find the pair of elements whose difference is less. Let us assume
that we have sorted the points. If the input points have the same x co-ordinates, then we
have to sort based on y co-ordinates, otherwise x co-ordinates. After sorting we can go
through them to find the consecutive points with the least difference.
from operator import itemgetter
def closest_points_1d(points):
# if points have same y values
if points[0][1] == points[1][1]:
points.sort(key=itemgetter(0), reverse=False)
min = float("infinity")
x = float("infinity")
for i in range(1, len(points)-1):
5 . 2 7 C l o s e st - pa i r p o i n ts i n o n e- d i m e n si o n
D i vi de an d Co n qu er Al g or i th m s | 296
The complexity of sorting is O( ). So, the problem in one dimension can be solved in
O( ) time which is mainly dominated by sorting time.
Time Complexity: O( ).
Space Complexity: O(1).
( , )= ( − ) −( − )
The above equation calculates the distance between two points =( , ) and =( , ).
Calculate the distances between all the pairs of points. From points there are n
ways of selecting 2 points. ( = O( )).
After finding distances for all possibilities, we select the one which is giving the
minimum distance and this takes O( ).
The overall time complexity is O( ).
from operator import itemgetter
from math import sqrt, pow
def distance(a, b):
return sqrt(pow(a[0] - b[0],2) + pow(a[1] - b[1],2))
def closest_points_2d(points):
minimum = float("infinity")
for i in range(0, len(points)-1):
for j in range(i+1, len(points)):
5 . 2 8 C l o s e st - pa i r p o i n ts i n t w o - d i m e n si o n
D i vi de an d Co n qu er Al g or i th m s | 297
d = distance(points[i], points[j])
if d < minimum:
minimum = d
x=i
y=j
return points[x], points[y]
A = [[12,30], [40, 50], [5, 1], [12, 10], [3,4]]
first_point, second_point = closest_points_2d(A)
print "Closest pair is:", first_point, second_point, \
"and the distance is", distance(first_point, second_point)
Time Complexity: O( ).
Space Complexity: O(1).
Algorithm:
1) Sort the given points in (given set of points) based on their −coordinates.
Partition into two subsets, and , about the line through median of . This
step is the part of the & technique.
2) Find the closest-pairs in S and S and call them and recursively.
3) Now, steps 4 to 8 form the combining component of the & technique.
4) Let us assume that = ( , ).
5) Eliminate points that are farther than apart from .
6) Consider the remaining points and sort based on their -coordinates.
7) Scan the remaining points in the order and compute the distances of each point
to all its neighbors that are distanced no more than 2 × (that's the reason for
sorting according to ).
8) If any of these distances is less than , then update .
x co-ordinates of points
Line passes through the median point and divides the set into 2 equal parts
5 . 2 8 C l o s e st - pa i r p o i n ts i n t w o - d i m e n si o n
D i vi de an d Co n qu er Al g or i th m s | 298
2 × area
x co-ordinates of points
Line passes through the median point and divides the set into 2 equal parts
Let = ( , ), where L is the solution to the first subproblem and R is the solution to
the second subproblem. The possible candidates for the closest-pair, which are across the
dividing line, are those which are less than δ distance from the line. So, we need only the
points which are inside the 2 × δ area across the dividing line as shown in the figure. Now,
to check all points within distance δ from the line, consider the following figure.
2 2
From the above diagram, we can see that a maximum of 8 points can be placed inside the
square within a distance not less than . That means, we need to check only the distances
which are within the 7 positions in the sorted list. This is similar to the one above, but with
the difference that in the above combining of subproblems, there are no vertical bounds.
So, we can apply the 8-point box tactic over all the possible boxes in the 2 × area with the
dividing line as the middle line. As there can be a maximum of such boxes in the area,
the total time for finding the closest pair in the corridor is O( ).
5 . 2 8 C l o s e st - pa i r p o i n ts i n t w o - d i m e n si o n
D i vi de an d Co n qu er Al g or i th m s | 299
print closest_points([(0,0),(7,6),(2,20),(12,5),(16,16),(5,8),\
(19,7),(14,22),(8,19),(7,29),(10,11),(1,13)])
Analysis
5 . 2 8 C l o s e st - pa i r p o i n ts i n t w o - d i m e n si o n
D i vi de an d Co n qu er Al g or i th m s | 300
Step-1 and step-2 take O( ) for sorting and finding the minimum recursively.
Step-4 takes O(1).
Step-5 takes O( ) for scanning and eliminating.
Step-6 takes O( ) for sorting.
Step-7 takes O( ) for scanning.
The total complexity: ( ) = O( ) + O(1) + O( ) + O( ) + O( ) ≈ O( ).
5.29 Calculating
: Given two positive integers and , give an algorithm to calculate .
5 . 2 9 C a l c u la t i ng
D i vi de an d Co n qu er Al g or i th m s | 301
( ) = ( ) + 1
2
Using master theorem, we get T( ) = O( ).
Our buildings can be specified by three real numbers: coordinates of left and right end,
and height. It is natural to represent the skyline as a list of heights, ordered from left to
right, also mentioning the coordinates where the heights change.
Given the exact locations and shapes of rectangular buildings in a 2-dimensional city.
There is no particular order for these rectangular buildings. Assume that the bottom of all
the buildings lies on a fixed horizontal line (bottom edges are collinear). The input is a list
of triples; one per building. A building is represented by the triple ( , ℎ , ) where
denotes the -position of the left edge, denotes the -position of the right edge, and ℎ
denotes the building’s height.
ℎ
ℎ
ℎ
ℎ ℎ
Give an algorithm that computes the skyline (in 2 dimensions) of these buildings,
eliminating hidden lines.
ℎ
ℎ
ℎ
ℎ
ℎ
The output is a collection of points which describes the path of the skyline. In some versions
of the problem this collection of points is represented by a sequence of numbers , , ...,
, such that the point represents a horizontal line drawn at height if is even, and it
represents a vertical line drawn at position if is odd. In our case the collection of points
will be a sequence of , , ..., pairs of ( , ℎ ) where ( , ℎ ) represents the ℎ height of
the skyline at position .
ℎ
ℎ
ℎ ℎ
Example
In the diagram below, there are 8 buildings, represented from left to right by the triplets (1,
14, 7), (3, 9, 10), (5, 17, 12), (14, 11, 18), (15, 6, 27), (20, 19, 22), (23, 15, 30) and (26, 14,
29).
19
17
15
14
14
11
9
1 3 5 7 10 12 14 15 18 20 22 23 26 27 29 30
In the diagram below the skyline is drawn around the buildings and it is represented by
the sequence of position-height pairs (1, 14), (5, 17), (12, 0), (14, 11), (18, 6), (20, 19), (22,
6), (23, 15) and (30, 0).
19
17
15
14
11
6 6
0 0
1 5 12 14 18 20 22 23 30
Algorithm
1. For each building :
a. Iterate on the range of [ .. ], where is the left, is the right coordinate
of the
b. For every element of this range, check if ℎ > ℎ [ ], that is if
building is taller than the current height-value at position , replace
ℎ [ ] with ℎ .
For the example, the heights auxiliary array would look like:
1 14
2 14
3 14
4 14
5 17
6 17
7 17
8 17
9 17
10 17
11 17
12 0
13 0
14 11
15 11
16 11
17 11
18 6
19 6
20 19
21 19
22 6
23 15
24 15
25 15
26 15
27 15
28 15
29 15
30 0
Once we check all the buildings, the ℎ array stores the heights of the tallest
buildings at every position. There is one more thing to do: convert the ℎ array to
the expected output format, which is to a sequence of position-height pairs. It's also easy:
just map each and every index to an ( , ℎ [ ]) pair.
def sky_line(buildings):
auxHeights = [0]*100
rightMostBuildingRi=0
for i, building in enumerate(buildings):
left = int(building[0])
height = int(building[1])
right = int(building[2])
for i in range(left,right):
if(auxHeights[i]<height):
auxHeights[i]=height
if(rightMostBuildingRi<right):
rightMostBuildingRi=right
prev = 0
for i in range(1,rightMostBuildingRi-1):
if prev!=auxHeights[i]:
print i, " ", auxHeights[i]
prev=auxHeights[i]
print rightMostBuildingRi, " ", auxHeights[rightMostBuildingRi]
buildings = [(1, 14, 7), (3, 9, 10), (5, 17, 12), (14, 11, 18), \
(15, 6, 27), (20, 19, 22), (23, 15, 30), (26, 14, 29)]
sky_line(buildings)
Performance
Let's have a look at the time complexity of this algorithm. Assume that, indicates the
number of buildings in the input sequence and indicates the maximum coordinate (right
most building ). From the above code, it is clear that for every new input building, we are
traversing from ( ) to ℎ ( ) to update the heights. In the worst case, with equal-
size buildings, each having = 0 left and = − 1 right coordinates, that is, every
building spans over the whole [0. . ) interval. Thus the running time of setting the height
of every position is O( × ). The overall time-complexity is O( × ), which is a lot larger
than O( ) if > .
Naïve algorithm
The main weakness of the auxiliary height map approach is that the sheer number of points
to deal within the application code, maybe we can reduce the number of points in play.
Now that we think about it, the skyline is made up of horizontal line segments, interrupted
in only a few places. In fact, the only time the skyline can change its ℎ position (of a point
( , ℎ )) is at the left or right side of a building. It is clear now that if we find the height of
the skyline at each of these “critical points” on the x axis, then we will have completely
determined the shape of the skyline. At each critical point, you just go up or down to the
new height and draw a line segment to the right until you reach the next critical point.
A straightforward algorithm would start with a single building, insert the other buildings
one by one into the picture and update the skyline. So, the straightforward algorithm for
this problem uses induction on n (number of buildings). Let us assume that indicates
the skyline for i buildings. The base step is for = 1 and the skyline can be obtained directly
from . As an induction step, we assume that we know (the skyline for − 1
buildings), and then must show how to add the building to the skyline.
We scan the skyline from the left to right stopping at the first -coordinate that
immediately precedes and then we extract the part of the skyline overlapping with as
a set of strips ( , ℎ ), ( , ℎ ), …,( , ℎ ) such that < and = (or is last). In
this set of strips, a strip will have its ℎ replaced by ℎ if ℎ > ℎ (because the strip is now
covered by ). If there is no then we add an extra strip (replacing the last 0 in the old
skyline) ( , ℎ , , 0). Also, we check whether two adjacent strips have the same height; if
so, they are merged together into one strip. This process can be viewed as a merging of
and .
Algorithm
This time, instead of printing the buildings onto a height map array with an entry for each
pixel, let’s print the buildings onto an array with an entry for each critical point! This will
eliminate the problem of dealing with too many points, because we’re now dealing with the
minimum number of points necessary to determine the skyline.
For each building (with as the left, and as the right coordinate of the ):
For each critical point ( , ℎ ):
If . >= and . < :
.ℎ gets the max of ℎ and the previous value of .ℎ
Performance
Unfortunately, this isn’t an asymptotic improvement in the worst case. It’s still O( ) given
something like the following configuration:
Since the building may obscure up to − 1 lines in the skyline formed by the first − 1
buildings, updating the list needs O( ) time in the worst case. So, in the worst case, this
algorithm would need to examine all the − 1 triples when adding (this is certainly the
case if is so wide that it encompasses all other triples). Likewise, adding would need
to examine − 2 triples, and so on. This implies that the algorithm is O(n)+O(n-1)+ … +O(1)
= O( ). This results in O( ) time in total.
1 7
Compute (recursively) the skyline for each set, and then merge the two skylines. Inserting
the buildings one after the other is not the fastest way to solve this problem as we've seen
it in the above naïve approach. If, however, we merge pairs of buildings into skylines first,
merge pairs of these skylines into bigger skylines (and not two sets of buildings) next, and
then merge pairs of these bigger skylines into even bigger ones. Since the problem size is
halved in every step - after steps, we can compute the final skyline.
Result skyline, C = ( , h , , h , …, , 0)
Clearly, we merge the list of ’s and ’s just like in the standard Merge algorithm. But, in
addition to that, we have to decide on the correct height in between these boundary values.
We can keep two variables (say, ℎ1 and ℎ2), one, to store the current height in the first set
of buildings and the other, to keep the current height in the second set of buildings.
Basically we simply pick the greater of the two to put in the gap.
When comparing the head entries (ℎ1, ℎ2) of the two skylines, we introduce a new strip
(and append to the output skyline) whose -coordinate is the minimum of the -coordinates
of the entries and whose height is the maximum of ℎ1 and ℎ2.
Example
For example, consider the merging process for two buildings shown below. For the first
building, with the base condition, we could get the skylines as (1, 14), and (7, 0). Similarly,
for the second building, the skylines would be (3, 9), and (10, 0).
14
1 7 3 10
Now, how do we merge [(1, 14), (7, 0)] with [(3, 9), (10, 0)]?
14
1 3 7 10
Let us initialize i, and j pointing to the first critical points in the left and right skylines.
(1, 14) (7, 0) (3, 9) (10, 0)
0 1 0 1
i j
For this case, 1 is less than 3 and 14 is greater than 9. Hence, the first resultant critical
point should be (1, 14) where 1 is the minimum of (1, 3); and 14 is the maximum of (9, 14).
So, we are done with the first critical point out of 4.
(1, 14) (7, 0) (3, 9) (10, 0)
0 1 0 1
i j
The next critical point is (3, 9) as 3 is less than 7. So, the next new critical point to be
added is (3, 14). But, this new height is 14, which is <= the last critical point height (14).
So, it means that, this height is already being covered with the last critical point. Hence,
we can ignore this.
(1, 14) (7, 0) (3, 9) (10, 0)
0 1 0 1
i j
The next critical point is (7, 0) as 7 is less than 10. So, the next new critical point to be
added is (7, 9) where 7 is the minimum of (7, 10); and 9 is the maximum of (0, 9). So, we
are done with third critical point out of 4.
(1, 14) (7, 0) (3, 9) (10, 0)
0 1 0 1
i j
The only remaining critical point to be processed is (10, 0). Hence, add it to the final list of
critical points. So, after merging the left and right skylines, the resultant skylines are
(1, 14) (7, 9) (10, 0)
0 1 2
class Solution:
# @param {integer[][]} buildings
# @return {integer[][]}
def getSkyline(self, buildings):
if buildings==[]:
return []
if len(buildings)==1:
return [[buildings[0][0],buildings[0][1]],[buildings[0][2],0]]
mid=(len(buildings)-1)/2
leftSkyline=self.getSkyline(buildings[0:mid+1])
rightSkyline=self.getSkyline(buildings[mid+1:])
return self.merge(leftSkyline,rightSkyline)
def merge(self, leftSkyline, rightSkyline):
i=0
j=0
resultSkyline=[]
h1=None
h2=None
while i<len(leftSkyline) and j<len(rightSkyline):
if leftSkyline[i][0]<rightSkyline[j][0]:
h1=leftSkyline[i][1]
new=[leftSkyline[i][0],max(h1,h2)]
if resultSkyline==[] or resultSkyline[-1][1]!=new[1]:
resultSkyline.append(new)
i+=1
elif leftSkyline[i][0]>rightSkyline[j][0]:
h2=rightSkyline[j][1]
new=[rightSkyline[j][0],max(h1,h2)]
if resultSkyline==[] or resultSkyline[-1][1]!=new[1]:
resultSkyline.append(new)
j+=1
else:
h1=leftSkyline[i][1]
h2=rightSkyline[j][1]
new=[rightSkyline[j][0],max(h1,h2)]
if resultSkyline==[] or resultSkyline[-1][1]!=new[1]:
resultSkyline.append([rightSkyline[j][0],max(h1,h2)])
i+=1
j+=1
while i<len(leftSkyline):
if resultSkyline==[] or resultSkyline[-1][1]!=leftSkyline[i][1]:
resultSkyline.append(leftSkyline[i][:])
i+=1
while j<len(rightSkyline):
if resultSkyline==[] or resultSkyline[-1][1]!=rightSkyline[j][1]:
resultSkyline.append(rightSkyline[j][:])
j+=1
return resultSkyline
buildings = [[1, 14, 7], [3, 9, 10], [5, 17, 12], [14, 11, 18],
[15, 6, 27], [20, 19, 22], [23, 15, 30], [26, 14, 29]]
sky_line = Solution()
print sky_line.getSkyline(buildings)
Performance
This algorithm has a structure similar to − . Let ( ) denote the running time of
this algorithm for n buildings. Since merging two skylines of size takes O(n), we find that
(n) satisfies the recurrence (n)= 2 ( )+O(n). This is just like − . Thus, we
conclude that running time of the divide-and-conquer algorithm for the skyline problem is
O(nlogn).
Examples
To understand the problem statement well, let us plot the elements with array indexes as
X-axis and values of corresponding indexes as Y-axis.
Example-1:
35, 5, 20, 2, 40, 25, 80, 25, 15, 40
35, 20, 40, 80, 40
In the following graph, we can observe that elements 20, 40, and 80 are greater than its
neighbors. Element 40 is the last element and is greater than its previous element 15.
90
80 80
70
60
50
40 40 40
35
30
25 25
20 20
15
10
5 2
0
0 2 4 6 8 10 12
5 . 3 1 F i n d i n g p e a k e l e m e nt o f a n a r r a y
D i vi de an d Co n qu er Al g or i th m s | 310
Example-2:
1, 2, 3, 4, 5, 6, 7, 8, 9, 10
10
Since the elements are in ascending order, only the last element is satisfying the peak
element definition.
12
10 10
9
8 8
7
6 6
5
4 4
3
2 2
1
0
0 2 4 6 8 10 12
Observations:
If an array has all the same elements, every element is a peak element.
Every array has a peak element.
An array might have many peak elements but we are finding only one.
If the array is in the ascending order, then the last element of the array will be the
peak element.
If the array is in the descending order, then the first element of the array will be
the peak element.
5 . 3 1 F i n d i n g p e a k e l e m e nt o f a n a r r a y
D i vi de an d Co n qu er Al g or i th m s | 311
Note: If the input has only one peak, the above algorithm works and gives the peak element.
In this case, the peak element would be the maximum element in the array.
60
54
50
40
30
20
10
0
0 2 4 6 8 10 12
5 . 3 1 F i n d i n g p e a k e l e m e nt o f a n a r r a y
D i vi de an d Co n qu er Al g or i th m s | 312
index = i
peak = curr
print peak
if A[len(A)-1] > A[len(A)-2]:
print A[len(A)-1]
A = [35, 5, 20, 2, 40, 25, 80, 25, 15, 40]
print A, "\n"
find_peaks(A)
Time Complexity: O( ).
Space Complexity: O(1).
Algorithm
Given an array A with elements:
Take the middle element of A, A[ ], and compare that element to its neighbors
If the middle element is greater than or equal to its neighbours, then by definition,
that element is a peak element. Return its index .
Else, if the element to the left is greater than the middle element, then recursively
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, then
recursively use this algorithm on the right half of the array, not including the
middle element.
5 . 3 1 F i n d i n g p e a k e l e m e nt o f a n a r r a y
D i vi de an d Co n qu er Al g or i th m s | 313
90
80 80
70
60
50
40 40 40
35
30
20 20
10
0
0 2 4 6 8 10 12
there is no further processing needed, because all these elements are greater than both
their neighbors.
What if the selected element is not a peak element? If we select the indexes 2, 4, 6, 8, or 9;
they are not peak elements. A careful observation of the plot indicates that for any non-
peak element, the peak element would be on left if the selected element is less than its left
element. On the similar lines, the peak element would be on right if the selected element is
less than its right element. For example, the element with index 9 is less than its right
element and peak element is on its right side. Similarly, the element with index 8 is less
than its left element and peak element is on its left.
So, for non-peak elements, we check on the left side, if it is less than its left element and
check on the right side if the element is less than its right element.
If the element is less than both its left and right elements, we can select that which is either
to the left side or right side.
To simplify the algorithm and to get complexity, instead of randomly selecting the
elements, we can simply split them into half and check whether the selected element is a
peak element or not.
90
80
70
60
50
40
30
25 25
20
15
10
5 2
0
0 2 4 6 8 10 12
def find_peak(A):
if not A:
return -1
left, right = 0, len(A) - 1
5 . 3 1 F i n d i n g p e a k e l e m e nt o f a n a r r a y
D i vi de an d Co n qu er Al g or i th m s | 314
60
54
50
40
30
20
10
0
0 2 4 6 8 10 12
while left + 1 < right:
mid = left + (right - left) / 2
if A[mid] < A[mid - 1]:
right = mid
elif A[mid] < A[mid + 1]:
left = mid
else:
return mid
mid = left if A[left] > A[right] else right
return mid
A = [35, 5, 20, 2, 40, 25, 80, 25, 15, 40]
peak = find_peak(A)
print A, "\n", A[peak]
Note: Another important observation is that, the above algorithm gives us any one of the
possible peak elements. But, it cannot guarantee the highest peak element. If the input
has only one peak, the above algorithm works and gives the highest peak element. In fact,
it would be the maximum element.
5 . 3 2 F i n d i n g p e a k e l e m e nt i n t w o - d i m en s i o na l a r r a y
D i vi de an d Co n qu er Al g or i th m s | 315
Examples
Example-1: In the following two-dimensional matrix, there are multiple peaks (highlighted).
5 19 11 14
31 9 6 22
3 8 12 2
21 3 4 28
Example-2: In the following two-dimensional matrix, there is only one peak element and it
is 400.
51 19 11 14
61 9 60 22
73 81 120 42
21 3 400 208
Now, use (i, j) as a start point on row i to find 1D-peak on row i. Finding 1D peak in row i
takes O(logm) as row i has m elements and with divide and conquer algorithm for finding
the 1D peak it takes O(logm) complexity. So the overall time complexity is O(logn + logm).
20
24 23 22
25 15 19
26 27 28 29
5 . 3 2 F i n d i n g p e a k e l e m e nt i n t w o - d i m en s i o na l a r r a y
D i vi de an d Co n qu er Al g or i th m s | 317
It works on the principle, that it selects a particular element to start with. Then it begins
traversing across the array, by selecting the neighbour with a higher value. If there is no
neighbour with a higher value than the current element, it just returns the current element.
For the following 2D array, let us start with the first element (51). For 51, it has a neighbor
(61) which is greater than its value. So, move to element 61.
51 19 11 14
61 9 60 22
73 81 120 42
21 3 400 208
For element 61, it has a neighbor (73) greater than its value. So, move to element 73.
51 19 11 14
61 9 60 22
73 81 120 42
21 3 400 208
For element 73, it has a neighbor (81) greater than its value. So, move to element 81.
51 19 11 14
61 9 60 22
73 81 120 42
21 3 400 208
For element 81, it has a neighbor (120) greater than its value. So, move to element 120.
51 19 11 14
61 9 60 22
73 81 120 42
21 3 400 208
For element 120, it has a neighbor (400) greater than its value. So, move to element 400.
51 19 11 14
61 9 60 22
73 81 120 42
21 3 400 208
For element 400, it has got all neighbors whose values are lesser than its value. Hence,
400 is the peak element.
Logically, this algorithm is correct, as it returns the element - which has none of its
neighbours greater than the current element.
import random
import pprint
5 . 3 2 F i n d i n g p e a k e l e m e nt i n t w o - d i m en s i o na l a r r a y
D i vi de an d Co n qu er Al g or i th m s | 318
import operator
def greedy_ascent(matrix):
j = len(matrix[0]) // 2
i = len(matrix) // 2
while True:
left, right, up, bottom = 0, 0, 0, 0
if j > 0:
left = matrix[i][j - 1]
if j < len(matrix[0]) - 1:
right = matrix[i][j + 1]
if i > 0:
up = matrix[i-1][j]
if i < len(matrix) - 1:
bottom = matrix[i+1][j]
if (left <= matrix[i][j] >= right) and (up <= matrix[i][j] >= bottom):
return (i, j, matrix[i][j])
my_list = [left, up, right, bottom]
max_neighbor_index, max_neighbor_value = \
max(enumerate(my_list), key=operator.itemgetter(1))
if max_neighbor_index == 0:
j=j-1
elif max_neighbor_index == 1:
i=i-1
elif max_neighbor_index == 2:
j=j+1
else:
i=i+1
def generate_2d_array(n=7, m=7, lower=0, upper=9):
return [[random.randint(lower, upper) for _ in range(m)] for _ in range(n)]
if __name__ == '__main__':
matrix = generate_2d_array(upper=9)
pprint.pprint(matrix)
x = greedy_ascent(matrix)
pprint.pprint(x)
Observations
Greedy ascent algorithm might not give the highest peak in the 2D array. For example, in
the following matrix, the highest peak element is 519, but the algorithm would give us 400.
51 19 11 519
61 9 60 22
73 81 120 42
21 3 400 208
Time Complexity: In the worst case, we might need to traverse all elements of the 2D array.
In the following matrix, we would be traversing most of the elements in the order: 1, 2, 3,
4, 5, 6, 7, 8, 9, 10, 11, 12, 13, and 14.
1 2 3 4
0 0 0 5
13 14 1 6
12 1 0 7
11 10 9 8
5 . 3 2 F i n d i n g p e a k e l e m e nt i n t w o - d i m en s i o na l a r r a y
D i vi de an d Co n qu er Al g or i th m s | 319
Hence, its time complexity is Θ(mn). In the worst case, greedy ascent algorithm complexity
is equivalent to straight forward (brute-force) algorithm time complexity.
Space Complexity: O(1)
Algorithm
m: number of columns
n: number of rows
Pick middle column j = / 2
Find global maximum in the column j. Let us say the maximum element in column
j is at A[ ][j]. It means, the maximum element in column j is in row .
Compare A[ ][j-1], A[ ][j], A[rowmax][j+1]. Basically, we are comparing
the maximum value of j column with values of previous (j-1) and next columns
(j+1) in the same row .
Pick left columns if A[ ][j - 1] > A[ ][j]
If A[ ][j] >= A[ ][j-1] and A[ ][j] >= A[ ][j+1] then
A[ ][j] is a 2D peak
Solve the new problem with half of the number of columns
If you have a single column, find global maximum and be done (base case)
51 19 11 519
61 9 60 22
73 81 120 42
21 3 40 208
j column
51 19 11 519
61 9 60 22
73 81 120 42
21 3 40 208
j column
Now, compare A[]i[j] with its neighbors A[]i[j-1] and A[]i[j+1] in the row i. So, among A[3][1]
(73), A[3][2] (81), and A[3][3] (120) the maximum is 120. Since 120 is on the right side of
81, we need to select the right half. As a result, the resultant matrix would be:
5 . 3 2 F i n d i n g p e a k e l e m e nt i n t w o - d i m en s i o na l a r r a y
D i vi de an d Co n qu er Al g or i th m s | 320
11 519
60 22
120 42
40 208
Let us repeat the process for the new matrix. Here, j = = 1, and the maximum element in
column j is 120. The value of i for this is 3.
11 519
60 22
120 42
40 208
j column
Element 120, it has a right column only. Between 120 and 42, the maximum element is
120. Hence 120 is the peak element and it is the end of processing.
import random
import pprint
def peak_find_2d(matrix):
j = len(matrix[0]) // 2
# maxvalue is the global maximum in column j
# rowmax is the row index of the maxvalue
maxvalue, rowmax = -1, -1
for row in range(len(matrix)):
if maxvalue <= matrix[row][j]:
maxvalue = matrix[row][j]
rowmax = row
print(rowmax, j, maxvalue)
left, right = 0, 0
if j > 0:
left = matrix[rowmax][j - 1]
if j < len(matrix[0]) - 1:
right = matrix[rowmax][j + 1]
if left <= maxvalue >= right:
return (rowmax, j, maxvalue)
if left > maxvalue:
half = []
for row in matrix:
half.append(row[:j + 1])
return peak_find_2d(half)
if right > maxvalue:
half = []
for row in matrix:
half.append(row[j:])
return peak_find_2d(half)
def generate_2d_array(n=7, m=7, lower=0, upper=9):
return [[random.randint(lower, upper) for _ in range(m)] for _ in range(n)]
5 . 3 2 F i n d i n g p e a k e l e m e nt i n t w o - d i m en s i o na l a r r a y
D i vi de an d Co n qu er Al g or i th m s | 321
if __name__ == '__main__':
matrix = generate_2d_array(upper=9)
pprint.pprint(matrix)
peak_find_2(matrix)
Time Complexity: Let us analyze the efficiency of the above divide and conquer algorithm.
Let T( , ) denote the runtime of the algorithm when run on a 2D matrix with n rows and
columns. The number of elements in the middle column is , so the time complexity to
find a maximum in column is O( ).
Checking the two-dimensional neighbors of the maximum element requires O(1) time. The
recursive call reduces the number of columns to at the most , but does not change the
number of rows. Therefore, we may write the following recurrence relation for the runtime
of the algorithm:
( , ) = O(1) + O( ) + ( , )
2
Intuitively, the number of rows in the problem does not change over time, so the cost per
recursive call is always O(1) + O( ). The number of columns is halved at every step, so
the number of recursive calls is at the most O(1 + ). So we may guess a bound of O((1
+ )(1 + ))O( ).
In other terms, this algorithm needs O( ) iterations and O( ) to find the maximum in
the column per iteration. Thus, the complexity of this algorithm is O( ).
To show this bound more formally, we must first rewrite the recurrence relation using
constants , > 0, instead of big-O notation:
( , ) ≤ + + ( , )
2
Observation
This algorithm is not designed for finding multiple peaks in the matrix. It only returns the
first peak it finds. In the tracing, it gave the peak element as 120. But, in the matrix the
maximum peak is 208.
else:
if A[left] < X:
return left+1
else:
return left
A=[1, 3, 4, 6, 8, 10, 14, 18, 25, 27, 29, 45]
print find_left_boundary(A, 0, len(A), 26)
Time Complexity: O( ).
Space Complexity: O( ) for recursive stack and O(1) if we implement the iterative version
for the function _ _ .
5 . 3 F i n d i n g t h e s ma l l e s t i nt e ge r g r e a t er t h a n g iv e n e l e m en t
D i vi de an d Co n qu er Al g or i th m s | 323
Naïve approach
One simplest approach for solving this problem is, first, sort all the elements of the array
in the increasing order. We can use any efficient sorting algorithm like quicksort whose
time complexity is O( ). After the sorting, first elements will be the smallest
elements of the array.
Sort the array in the ascending order, and then pick first elements of the array.
The running time calculation of this approach is trivial. Sorting of numbers would take
O( ) and picking first elements is of O(1).
def k_smallest( A, k ):
if k>= len(A):
return None
# sort the elements in ascending order
A.sort()
return A[:k]
A = [10, 5, 1, 6, 20, 19, 22, 29, 32, 29]
print k_smallest(A, 3)
∴ The total complexity of this approach is: O( + 1) = O( ).
To sort elements of the array, we could scan through the elements times to have the
desired result. This method is analogous to the one used in the selection sort, every time
we find out the smallest element in the whole sequence by comparing every element. In
this method, the sequence has to be traversed times. So, the complexity is O( × ).
def k_smallest( A, k ):
if k>= len(A):
return None
for i in range( k ):
smallest = i
for j in range( i + 1 , len(A) ):
if A[j] < A[smallest]:
smallest = j
A[smallest], A[i] = A[i], A[smallest]
return A[:k]
5.35 Finding s m a l l e s t e l e m e n ts i n a n a r r a y
D i vi de an d Co n qu er Al g or i th m s | 324
root key
Left subtree keys < root key Right subtree keys > root key
Example: The left tree is a binary search tree and the right tree is not a binary search tree
(at node 3 it’s not satisfying the binary search tree property).
7 3
4 9 1 6
2 5 2 7
Algorithm
1. Insert all the elements in a binary search tree.
2. Do an in-order traversal and print elements which will be the smallest ones.
This is due to the fact that, in-order traversal on binary search tree produces a
sorted array.
'''Binary Search Tree Node'''
class BSTNode:
def __init__(self, data):
self.data = data #root node
self.left = None #left child
self.right = None #right child
def k_smallest(root, k):
result = []
def funct(root):
if(not root):
return
if len(result) >= k:
return
5.35 Finding s m a l l e s t e l e m e n ts i n a n a r r a y
D i vi de an d Co n qu er Al g or i th m s | 325
funct(root.left)
result.append(root.data)
funct(root.right)
funct(root)
return result
node1, node2, node3, node4, node5, node6 = \
BSTNode(6), BSTNode(3), BSTNode(8), BSTNode(1), BSTNode(4), BSTNode(7)
node1.left, node1.right = node2, node3
node2.left, node2.right = node4, node5
node3.left = node6
result = k_smallest(node1, 3)
print result
The cost of creation of a binary search tree with elements is O( ) and the traversal
up to elements is O( ). Hence the complexity is O( + ) = O( ).
If the numbers are sorted in the descending order, we will be getting a
tree which will be skewed towards the left. In that case, the construction
( )
of the tree will be 0 + 1 + 2 + . . . + ( − 1) = which is O( ). To
escape from this, we can keep the tree balanced, so that the cost of
constructing the tree will be only .
5.35 Finding s m a l l e s t e l e m e n ts i n a n a r r a y
D i vi de an d Co n qu er Al g or i th m s | 326
Swap [ ] and [ ]
3) Return first elements of A[].
def k_smallest( A, k ):
maxIndex = A.index(max(A[:k]))
for i in range(k, len(A)):
if A[i] < A[maxIndex]:
A[maxIndex], A[i] = A[i], A[maxIndex]
return A[:k]
A = [10, 5, 1, 6, 20, 19, 22, 29, 32, 29, 4]
print k_smallest(A, 3)
First step of the algorithm would take O( ). The second step of the algorithm would
consume O(( − ) × ) as we keep updating the maximum element in elements with the
lesser elements from − elements.
Time Complexity: O(( − ) × ).
Space Complexity: O(1).
For details on
chapter.
ℎ , refer ℎ section in ℎ
Algorithm
1. Build a min-heap with the given elements.
2. Extract first elements of the heap.
class MinHeap:
def __init__(self):
self.A = [0]
self.size = 0
def percolate_up(self,i):
while i // 2 > 0:
if self.A[i] < self.A[i // 2]:
tmp = self.A[i // 2]
self.A[i // 2] = self.A[i]
self.A[i] = tmp
i = i // 2
def insert(self,k):
self.A.append(k)
self.size = self.size + 1
self.percolate_up(self.size)
def percolate_down(self,i):
while (i * 2) <= self.size:
minChild = self.min_child(i)
if self.A[i] > self.A[minChild]:
tmp = self.A[i]
self.A[i] = self.A[minChild]
self.A[minChild] = tmp
i = minChild
5.35 Finding s m a l l e s t e l e m e n ts i n a n a r r a y
D i vi de an d Co n qu er Al g or i th m s | 327
def min_child(self,i):
if i * 2 + 1 > self.size:
return i * 2
else:
if self.A[i*2] < self.A[i*2+1]:
return i * 2
else:
return i * 2 + 1
def delete_min(self):
retval = self.A[1]
self.A[1] = self.A[self.size]
self.size = self.size - 1
self.A.pop()
self.percolate_down(1)
return retval
def build_heap(self, A):
i = len(A) // 2
self.size = len(A)
self.A = [0] + A[:]
while (i > 0):
self.percolate_down(i)
i=i-1
def k_smallest(self, k):
result = []
for i in range(k):
result.append(self.delete_min())
return result
h = MinHeap()
h.build_heap([10, 5, 1, 6, 20, 19, 22, 29, 32, 29, 4])
print h.k_smallest(3)
As discussed, time complexity of insert/delete operations with binary heaps is O( ). The
first step of the algorithm would take O( ). The second step of the algorithm takes
O( ) as we are performing deletions on a heap of size . So, the overall running time
of the algorithm is O( ).
Algorithm
1. Build a max-heap with first elements of the given array.
2. For each element X of the remaining − elements:
a. If X < maximum element in max-heap, delete that maximum element and
insert X into max-heap.
3. Return max-heap elements.
class MaxHeap:
def __init__(self):
self.A = [0]
self.size = 0
def percolate_up(self,i):
while i // 2 > 0:
if self.A[i] > self.A[i // 2]:
tmp = self.A[i // 2]
5.35 Finding s m a l l e s t e l e m e n ts i n a n a r r a y
D i vi de an d Co n qu er Al g or i th m s | 328
self.A[i // 2] = self.A[i]
self.A[i] = tmp
i = i // 2
def insert(self,k):
self.A.append(k)
self.size = self.size + 1
self.percolate_up(self.size)
def percolate_down(self,i):
while (i * 2) <= self.size:
maxChild = self.max_child(i)
if self.A[i] < self.A[maxChild]:
tmp = self.A[i]
self.A[i] = self.A[maxChild]
self.A[maxChild] = tmp
i = maxChild
def max_child(self,i):
if i * 2 + 1 > self.size:
return i * 2
else:
if self.A[i*2] < self.A[i*2+1]:
return i * 2 + 1
else:
return i * 2
def maximum (self):
if self.size >=1:
return self.A[1]
return None
def delete_max(self):
retval = self.A[1]
self.A[1] = self.A[self.size]
self.size = self.size - 1
self.A.pop()
self.percolate_down(1)
return retval
def build_heap(self, A):
i = len(A) // 2
self.size = len(A)
self.A = [0] + A[:]
while (i > 0):
self.percolate_down(i)
i=i-1
def k_smallest(self, A, k):
self.build_heap(A[:k])
result = []
for X in range(k, len(A)):
m = self.maximum()
if A[X] < m:
self.delete_max()
self.insert(A[X])
while self.size > 0:
result.append(self.delete_max())
return result
h = MaxHeap()
print h.k_smallest([10, 5, 1, 6, 20, 19, 22, 29, 32, 29, 4], 5)
5.35 Finding s m a l l e s t e l e m e n ts i n a n a r r a y
D i v i de an d Co n qu er Al g or i th m s | 329
First step of the algorithm would take O( ) as the size of the −ℎ is . The second
step of the algorithm would consume O(( − ) × ) as we keep updating the maximum
element in −ℎ with the lesser elements from − elements.
Time Complexity: O( +( − ) ).
Space Complexity: O(1).
Algorithm
1. Choose a random index, , of the array.
2. Partition the array so that:
[ ... − 1] <= [ ] <= [ + 1. . ℎ ℎ].
3. If < , then it must be on the left of the pivot, skip the elements to the
right of and work with the elements to the left of .
4. If = , then it must be the pivot and print all the elements from to
.
5. If > then it must be on the right of pivot, add the elements to the left
of to the , and work with the elements to the right of .
Also, adjust the by subtracting the number of elements in the left subarray added
to result array.
5.35 Finding s m a l l e s t e l e m e n ts i n a n a r r a y
D i vi de an d Co n qu er Al g or i th m s | 330
The randomization of pivots makes the algorithm perform consistently even with
unfavorable data orderings.
import random
def kth_smallest(A, k):
"Find the nth rank ordered element (the least value has rank 0)."
A = list(A)
if not 0 <= k < len(A):
raise ValueError('not enough elements for the given rank')
while True:
A[pivotIndex] = random.randrange(len(A))
pivotCount = 0
under, over = [], []
uappend, oappend = under.append, over.append
for elem in A:
if elem < A[pivotIndex]:
uappend(elem)
elif elem > A[pivotIndex]:
oappend(elem)
else:
pivotCount += 1
if k < len(under):
A = under
elif k < len(under) + pivotCount:
return A[pivotIndex]
else:
A = over
k -= len(under) + pivotCount
A= [2,1,5,234,3,44,7,6,4,9,11,12,14,13]
for i in range (len(A)):
print kth_smallest(A, i)
Time Complexity: O( ) in worst case is similar to quicksort. Although the worst case is
the same as that of quicksort, this performs much better on the average [O( ) − average
case].
The best complexity of this algorithm is linear, i.e., O(n). Best case would occur if we are
able to choose a pivot that causes exactly half of the array to be eliminated in each phase.
This means, in each iteration, we would consider only the remaining elements. This leads
to the following recurrence:
1, =1
( )=
+ , >1
2
Applying the master theorem would give the running time of this
algorithm as, ( ) = O( ). Since we eliminate a constant fraction of the array with each
phase, we get the convergent geometric series in the analysis. This shows that the total
running time is indeed linear in n. This lesson is well worth remembering. It is often
possible to achieve linear running times in ways that you would not expect.
Number 2 5 7 8 10 15 21 37 41
Position 1 2 3 4 5 6 7 8 9
For example, rank of 8 is 4, one plus the number of elements smaller than 8 which is 3.
The selection problem is stated as follows:
Given a set A of distinct numbers and an integer , 1 ≤ ≤ , output the element of A
of rank .
: Find the -smallest element in an array of elements in
the best possible way.
This problem is anologous to and
all the solutions discussed for that problem are valid for this problem as
well. The only difference is that instead of returning a list of elements,
we would consider only the element.
Naïve approach
One simplest approach for solving this problem is, first, sort all the elements of the array
in the increasing order. After the sorting, ( − 1) element (assuming array index starts
with 0) in the sorted array will be the −smallest element of the array.
Sort the array in the ascending order, and then pick ( − 1) element of the sorted array.
The running time calculation of this approach is trivial. Sorting of numbers would take
O( ) and picking ( − 1) element in the sorted array would take O(1).
def kth_smallest( A, k ):
if k >= len(A):
return None
# sort the elements in ascending order
A.sort()
return A[k-1]
A = [10, 5, 1, 6, 20, 19, 22, 29, 32, 29]
print kth_smallest(A, 3)
∴ The total complexity of this approach is: O( + 1) = O( ).
5.36 Finding ℎ - s m a l l e st e l e m e nt i n a n a r r a y
D i vi de an d Co n qu er Al g or i th m s | 332
tree which will be skewed towards the left. In that case, the construction
( )
of the tree will be 0 + 1 + 2 + . . . + ( − 1) = which is O( ). To
escape from this, we can keep the tree balanced, so that the cost of
constructing the tree will be only .
b. If the number is smaller than the largest element of the tree, remove the
largest element of the tree and add the new element. This step is to make
sure that a smaller element replaces a larger element from the tree. And
of course the cost of this operation is , since the tree is a balanced
binary search tree with elements.
Once Step 2 is over, the balanced tree with elements will have the smallest elements.
The only remaining task is to print out the largest element of the tree.
Time Complexity:
1. For the first elements, we make the balance binary search tree. Hence, the cost
of this operation is O( ).
2. For the rest − elements, the complexity is O(( − ) ).
Step 2 has a complexity of ( − ) . The total cost is +( − ) = which
is O( ). This bound is actually better than the ones provided earlier.
For details on
chapter.
ℎ , refer ℎ section in ℎ
Algorithm
1. Build a min-heap with the given elements.
2. Extract first elements of the heap, and return the last element extracted.
class MinHeap:
def __init__(self):
self.A = [0]
self.size = 0
def percolate_up(self,i):
while i // 2 > 0:
if self.A[i] < self.A[i // 2]:
tmp = self.A[i // 2]
self.A[i // 2] = self.A[i]
self.A[i] = tmp
i = i // 2
def insert(self,k):
self.A.append(k)
self.size = self.size + 1
self.percolate_up(self.size)
def percolate_down(self,i):
while (i * 2) <= self.size:
minChild = self.min_child(i)
if self.A[i] > self.A[minChild]:
tmp = self.A[i]
self.A[i] = self.A[minChild]
self.A[minChild] = tmp
i = minChild
def min_child(self,i):
if i * 2 + 1 > self.size:
return i * 2
5.36 Finding ℎ - s m a l l e st e l e m e nt i n a n a r r a y
D i vi de an d Co n qu er Al g or i th m s | 334
else:
if self.A[i*2] < self.A[i*2+1]:
return i * 2
else: return i * 2 + 1
def delete_min(self):
retval = self.A[1]
self.A[1] = self.A[self.size]
self.size = self.size - 1
self.A.pop()
self.percolate_down(1)
return retval
def build_heap(self, A):
i = len(A) // 2
self.size = len(A)
self.A = [0] + A[:]
while (i > 0):
self.percolate_down(i)
i=i-1
def kth_smallest(self, k):
for i in range(k-1):
self.delete_min()
return self.delete_min()
h = MinHeap()
h.build_heap([10, 5, 1, 6, 20, 19, 22, 29, 32, 29, 4])
print h.kth_smallest(3)
As discussed, time complexity of insert/delete operations with binary heaps is O( ). The
first step of the algorithm would take O( ). The second step of the algorithm takes
O( ) as we are performing deletions on a heap of size . So, the overall running time
of the algorithm is O( ).
Algorithm
1. Build a max-heap with the first elements of the given array.
2. For each element X of the remaining − elements:
a. If X < maximum element in max-heap, delete that maximum element and
insert X into max-heap.
3. Return the maximum element from max-heap.
class MaxHeap:
def __init__(self):
self.A = [0]
self.size = 0
# Refer the other functions in
def kth_smallest(self, A, k):
self.build_heap(A[:k])
for X in range(k, len(A)):
m = self.maximum()
if A[X] < m:
self.delete_max()
self.insert(A[X])
return self.maximum()
5.36 Finding ℎ - s m a l l e st e l e m e nt i n a n a r r a y
D i vi de an d Co n qu er Al g or i th m s | 335
h = MaxHeap()
print h.kth_smallest([10, 5, 1, 6, 20, 19, 22, 29, 32, 29, 4], 5)
First step of the algorithm would take O( ), as the size of the −ℎ is . The
second step of the algorithm would consume O(( − ) × ), as we need to keep updating
the maximum element in −ℎ with the lesser elements from − elements.
Time Complexity: O( +( − ) ).
Space Complexity: O(1).
Algorithm
1. Choose a random index, , of the array.
2. Partition the array so that:
[ ... − 1] <= [ ] <= [ + 1. . ℎ ℎ].
3. If < then it must be on the left of the pivot, skip the elements to the
right of and work with the elements to the left of .
4. If = then it must be the −smallest element of the array.
5. If > then it must be on the right of pivot. Skip the elements to the left
of and work with the elements to the right of . Also, adjust the
by subtracting the number of elements in the left subarray being discarded.
The randomization of pivots makes the algorithm perform consistently even with
unfavorable data orderings.
import random
def kth_smallest(A, k):
"Find the nth rank ordered element (the least value has rank 0)."
5.36 Finding ℎ - s m a l l e st e l e m e nt i n a n a r r a y
D i vi de an d Co n qu er Al g or i th m s | 336
What is a median?
A median is the middle number in a sorted list of numbers. To determine the median value
in a sequence of numbers, the numbers must first be arranged in value order from the
lowest to highest. If there is an odd amount of numbers, the median value is the number
that is in the middle, with the same amount of numbers below and above. If there is an
even amount of numbers in the list, the middle pair must be determined, added together
and divided by two to find the median value. The median can be used to determine an
approximate average.
5.36 Finding ℎ - s m a l l e st e l e m e nt i n a n a r r a y
D i vi de an d Co n qu er Al g or i th m s | 337
Let us assume that the number of elements in the array be n. So, if is odd then the
median is defined to be element of index (with 0 being the first index of the array). When
is even, there are two choices: and . In statistics, it is common to return the average
of the two elements.
Median of medians is a modified version of quick selection algorithm where we improve
pivot selection to guarantee reasonable good worst case split. The algorithm divides the
array to groups of size 5 (the last group can be of any size < 5), and then calculates the
median of each group by sorting and selecting the middle element (sorting complexity of 5
elements is constant). It finds the median of these medians by recursively calling itself, and
selects the median of medians as the pivot for partition. Then it continues similar to the
previous selection algorithm by recursively calling the left or right subarray depending on
the rank of the pivot after partitioning.
Select median of medians as pivot.
The algorithm (A, ) to find the smallest element from set of elements is as
follows:
Algorithm: (A, )
( )
1. Partition into groups, with each group having five items (the last
group may have fewer items).
2. Sort each group separately (e.g., insertion sort).
3. Find the median of each of the groups and store them in some array (let us say
).
4. Use recursively to find the median of (median of medians). Let us say
the median of medians is .
( )
= ( , )
5. Let = # elements of smaller than ;
6. If( == + 1)
return
# Partition with pivot
7. Else partition into and
= {items smaller than }
= {items larger than }
# Next,form a subproblem
8. If( < + 1)
return Selection(X, k)
9. Else
return Selection(Y, k – (q+1))
Before developing recurrence, let us consider the representation of the input below. In the
figure, each circle is an element and each column is grouped with 5 elements. The black
circles indicate the median in each group of 5 elements. As discussed, sort each column
using constant time insertion sort.
Medians
5.36 Finding ℎ - s m a l l e st e l e m e nt i n a n a r r a y
D i vi de an d Co n qu er Al g or i th m s | 338
Median of medians
Median of medians, m
After sorting, rearrange the medians so that all medians will be in the ascending order
Rearrange the medians so that all medians will be in the ascending order
Median of medians
Items>= Gray
In the figure above the gray circled element is the median of medians (let us call this ). It
can be seen that at least of 5 element group medians ≤ . Also, these of 5 element groups
contribute 3 elements that are ≤ except 2 groups [last group which may contain fewer
than 5 elements, and other group which contains ]. Similarly, at least of 5 element groups
contribute 3 elements that are ≥ as shown above.
5.36 Finding ℎ - s m a l l e st e l e m e nt i n a n a r r a y
D i vi de an d Co n qu er Al g or i th m s | 339
Components in recurrence:
In our selection algorithm, we choose , which is the median of medians, to be a
pivot, and partition A into two sets and . We need to select the set which gives
maximum size (to get the worst case).
The time in function when called from procedure . The number
of keys in the input to this call to is .
The number of comparisons required to partition the array. This number is
ℎ( ), let us say .
We have established the following recurrence: ( ) = + Θ( ) + { ( ), ( )}
The worst case complexity of this approach is O(n) because the median of medians chosen
as pivot is either greater than or less than at least 30% of the elements. So even in the
worst case we can eliminate constant proportion of the elements at each iteration, which
is what we want but cannot achieve with the previous approach. We can also write the
recurrence relation for worst case and verify that it’s linear. term comes from selecting
the median of medians as pivot, and is when the pivot produces the worst split.
From the above discussion we have seen that, if we select median of medians as pivot,
the partition sizes are: − 6 and + 6. If we select the maximum of these, then we get:
( ) = + Θ( ) + +6
( ) ≈ + Θ( ) + +O(1)
7
≤ + +Θ( ) + O(1)
10
Finally, ( ) = Θ( )
CHUNK_SIZE = 5
def kth_medianOfMedians(A, k):
if len(A) <= CHUNK_SIZE:
return get_kth(A, k)
chunks = splitIntoChunks(A, CHUNK_SIZE)
medians_list = []
for chunk in chunks:
median_chunk = get_median(chunk)
medians_list.append(median_chunk)
size = len(medians_list)
mom = kth_medianOfMedians(medians_list, size / 2 + (size % 2))
smaller, larger = split_listByPivot(A, mom)
valuesBeforeMom = len(smaller)
if valuesBeforeMom == (k - 1):
return mom
elif valuesBeforeMom > (k - 1):
return kth_medianOfMedians(smaller, k)
else:
return kth_medianOfMedians(larger, k - valuesBeforeMom - 1)
5.36 Finding ℎ - s m a l l e st e l e m e nt i n a n a r r a y
D i vi de an d Co n qu er Al g or i th m s | 340
1
2 −2 ≥ −4
2 3 3
Likewise, this is a lower bound. Thus, up to − ( − 4) = + 4 elements are fed into the
recursive call to . The recursive step that finds the median of medians runs on a
problem of size , and consequently the time recurrence is:
( )= + + 4 +Θ( )
Likewise this is a lower bound. Thus up to − ( − 8) = + 8 elements are fed into the
recursive call to Select. The recursive step that finds the median of medians runs on a
problem of size , and consequently the time recurrence is
( ) = + + 8 + O( )
( ) ≤ + + 8 + O( )
≤ + + 8 + ,
= − + + 9
7
= ( + ) − − 9
7
This is bounded above by ( + ) provided that − 9 ≥ 0. Therefore, we can select 7
as the group size.
Naïve approach
Let and be two sorted ascending array, with and as length respectively. We need
to find the smallest element from the union of that two array. One trivial way of solving
this problem is, merge both arrays into one sorted array and then return the smallest
element of merged array. This will require O( ) time, and in the worst case, can be the
last element in the union of two arrays. In that case, would be equal to + , where
and are sizes of two sorted arrays.
5.37 Finding ℎ s m a l l e s t e le m e n t i n tw o s o r te d a r r a y s
D i vi de an d Co n qu er Al g or i th m s | 341
5.37 Finding ℎ s m a l l e s t e le m e n t i n tw o s o r te d a r r a y s
D i vi de an d Co n qu er Al g or i th m s | 342
5.37 Finding ℎ s m a l l e s t e le m e n t i n tw o s o r te d a r r a y s
D i vi de an d Co n qu er Al g or i th m s | 343
( )
=
2
( )
=
Now, we have indexes and pointing to middle elements of the arrays and respectively.
Since the two arrays were already sorted, there would be − 1 elements less than [ ] and
− 1 elements less than [ ].
+ < or + ≥
If + < , then we have less number of elements on the left side of [ ] and [ ]. So, we
need to add more elements for finding the smallest element. Next question would be,
how to decide whether to add elements from array or array ? This has to be decided by
[ ] and [ ].
If [ ] > [ ], then we can skip all the elements of till [ ] element. Because, it guarantees
that smallest element would not be in the elements less than [ ] as [ ] is less than
[ ] and + < . So, we can reduce the size of the array to [ + 1: ] (starting from + 1
till end of array ). Since, we have discarded + 1 elements of , the smallest element
would now be reduced to ( − − 1) smallest element.
If [ ] ≤ [ ], then we can skip all the elements of till [ ] element. Because, it guarantees
that smallest element would not be in elements less than [ ] as [ ] is less than [ ]
and + < . So, we can reduce the size of the array to [ + 1: ] (starting from + 1 till
end of array ). Since, we have discarded + 1 elements of , the smallest element would
now be reduced to ( − − 1) smallest element.
On the similar lines, if + ≥ , then we have more number of elements on the left side of
[ ] and [ ]. So, we need to discard few elements for finding the smallest element. Next
question would be, how to decide whether to discard elements from array or array ?
This has to be again decided by [ ] and [ ].
If [ ] > [ ], then we can skip all the elements of starting from [ + 1] till the end of the
array. Because, it guarantees that smallest element would not be in elements greater
than [ ] as [ ] is greater than [ ] and + ≥ . So, we can reduce the size of the array
5.37 Finding ℎ s m a l l e s t e le m e n t i n tw o s o r te d a r r a y s
D i vi de an d Co n qu er Al g or i th m s | 344
to [: ] (starting from 0 element till [ ]). Since we have discarded elements of on the
right side of [ ], there would not be any change in .
If [ ] ≤ [ ], then we can skip all the elements of starting from [ + 1] till the end of the
array. Because, it guarantees that smallest element would not be in elements greater
than [ ] as [ ] is greater than [ ] and + ≥ . So, we can reduce the size of array to
[: ] (starting from 0 element till [ ]). Since we have discarded elements of on the right
side of [ ], there would not be any change in .
With these new arrays and value, recursively perform the same operations until we reach
the base case. Also, note that in each of the iterations, we are either discarding half of
either array or array .
Example
As an example, consider the following two arrays to find the 5th smallest element.
0 1 2 3 4 5 6 7
A 3 4 29 41 45 49 79 89
B 1 5 8 10 50
For these arrays, the and indexes can be calculated with as:
( ) ( )
= 5, = = = 4 and = = = 2
As a first step, compare + and : 2 + 4 > 5. Hence, compare A[4] and B[2].
0 1 2 3 4 5 6 7
A 3 4 29 41 45 49 79 89
B 1 5 8 10 50
Since [4] > [2], discard the elements of starting from [ ] till the end of array . Notice
that, there is no change in value as + ≥ .
0 1 2 3 4
A 3 4 29 41
B 1 5 8 10 50
Now, repeat the process on these resultant subarrays.
( ) ( )
= 5, = = = 2 and = = = 2
0 1 2 3 4
A 3 4 29 41
B 1 5 8 10 50
In this case, + < and [ ] > [ ]. Hence, discard the elements of starting from 0 till
[ ]. Notice that, there is a change to value with − − 1.
0 1 2 3
A 3 4 29 41
B 10 50
5−2−1 = 2
Next,
( ) ( )
= 2, = = = 2 and = = = 1
5.37 Finding ℎ s m a l l e s t e le m e n t i n tw o s o r te d a r r a y s
D i vi de an d Co n qu er Al g or i th m s | 345
0 1 2 3
A 3 4 29 41
B 10 50
5−2−1 = 2
In this case, + > and [ ] < [ ]. Hence, discard the elements of starting from till
the end of array . Notice that, there is no change in value as + ≥ .
0 1 2 3
A 3 4 29 41
B 10
Next,
( ) ( )
= 2, = = = 2 and = = =0
0 1 2 3
A 3 4 29 41
B 10
In this case, + ≥ and [ ] > [ ]. Hence, discard the elements of starting from till
the end of array . Notice that, there is no change in value as + ≥ .
0 1
A 3 4
B 10
Next,
( ) ( )
= 2, = = = 1 and = = =0
0 1
A 3 4
B 10
In this case, + < and [ ] < [ ]. Hence, discard the elements of starting from 0 till
[ ]. Notice that, there is a change in value with − − 1.
0
A
B 10
2−1−1 = 0
With = 0, we have come to base case and need to return [ ]. Hence, return [0] which
is 10.
def kthSmallest(A, B, k):
m = len(A); n= len(B)
if k >= m + n:
return -1
if m == 0:
return B[k]
elif n == 0:
return A[k]
i = m/2
j = n/2
if i+j<k:
if A[i]>B[j]:
return kthSmallest(A, B[j+1:], k-j-1)
else:
return kthSmallest(A[i+1:], B, k-i-1)
5.37 Finding ℎ s m a l l e s t e le m e n t i n tw o s o r te d a r r a y s
D i vi de an d Co n qu er Al g or i th m s | 346
else:
if A[i]>B[j]:
return kthSmallest(A[:i], B, k)
else:
return kthSmallest(A, B[:j], k)
A = [3, 4, 29, 41, 45, 49, 79, 89]
B = [1, 5, 8, 10, 50]
for i in range(14):
print kthSmallest(A, B, i)
Performance
In each iteration, one of the arrays would be reduced to half. Since the size of arrays and
are and respectively, the total reductions on would be (as it would be half the
size) and on , reductions would be . So, the total running time of the algorithm is
+ .
Time Complexity: O( + ).
Space Complexity: O(1).
.
.
.
There’s no other one egg solution. Sure, if we’d been feeling lucky we could have gone up
the floors in two’s. But imagine if the egg had broken on floor 20, we have no way of knowing
that it would have also broken on floor 19!
Time Complexity: O( ). In the worst case, we might need to try from all floors.
Space Complexity: O(1).
5 . 3 8 M a ny e gg s p r o b l e m
D i vi de an d Co n qu er Al g or i th m s | 347
DP solution
This problem can be solved using divide and conquer strategy. For example, in the following
board, the missing square is at location (6,1).
0 1 2 3 4 5 6 7
0
1
2
3
4
5
6
7
If the board is of size 2 x 2 (2 x 2 ) with one square missing, we can use a single title as
shown below.
What if the size of the board is 4 x 4 (2 x 2 ) with one missing square? How do we tile the
board?
0 1 2 3
0
1
2
3
Let us split the board into four equal sized (2 x 2) quadrants. Of the four quadrants, one of
the quadrants would have the missing square. Each of the quadrant has a different origin:
Quadrant 0 has origin (0, 0): (0, 0)
Quadrant 1 has origin (0, ): (0, 2)
Quadrant 2 has origin ( , 0): (2, 0)
Quadrant 3 has origin ( , ): (2, 2)
In this example, quadrant 3 has one missing square. Place a L shaped tromino tile at the
center such that it does not cover the quadrant that has a missing square. Instead of using
colors, we can use letters or numbers so that we can easily differentiate the titles.
0 1 2 3
0
1 9 9
2 9
3
Now, it can be observed that all four quadrants of size 2 x 2 have a missing square. For
each of these, solve them recursively. For quadrant 0, we can place the L shaped tromino
tile as shown below with the base case.
0 1 2 3
0 8 8
1 8 9 9
2 9
3
For quadrant 1, we can place the L shaped tromino tile as shown below with the base case.
0 1 2 3
0 8 8 7 7
1 8 9 9 7
2 9
3
Similarly, for quadrants 2 and 3, we can place the L shaped tromino tiles as shown below
with the base case.
0 1 2 3
0 8 8 7 7
1 8 9 9 7
2 6 9
3 6 6
Next, quadrant 3:
0 1 2 3
0 8 8 7 7
1 8 9 9 7
2 6 9 5
3 6 6 5 5
So, given a 2 x2 checkerboard with one missing square, we can recursively tile that square
with trominoes. Here's how we do it:
1. Split the board into four equal sized quadrants.
2. The missing square is in one of these four quadrants. Place an L shaped tile at the
center such that it does not cover the × quadrant that has a missing square.
Now all the four quadrants of size × have a missing square.
3. Recursively tile all the four quadrants.
empty_cell = -1
#This procedure is a wrapper for recursiveTile that does all the work
def tromino_tiling(n, rMiss, cMiss):
#This procedure prints a given tiled board using letters for tiles
def print_board(board):
for i in range(len(board)):
row = ''
for j in range(len(board[0])):
if board[i][j] != empty_cell:
row += chr((board[i][j] % 10) + ord('0'))
else:
row += ' '
print (row)
print_board(tromino_tiling(3, 4, 6))
Sample output:
0 0 1 1 5 5 6 6
0 4 4 1 5 9 9 6
2 4 3 3 7 7 9 8
2 2 3 0 0 7 8 8
0 0 1 0 5 5 6
0 4 1 1 5 9 6 6
2 4 4 3 7 9 9 8
2 2 3 3 7 7 8 8
Performance
For a given matrix of size × , there are four × subproblems. This algorithm,
recursively tiles all the four subproblems. Hence, the recurrence for this algorithm can be
written as:
( )= 4 +1
4
Using divide and conquer master theorem, we could derive the running time of this
algorithm as ( ) = O( ).
each column is in sorted order, we know that no element of the last column could possibly
be equal to the number we want to search for, and so we can discard the last column of
the matrix. Finally, if it's less than the value in question, then we know that since each row
is in sorted order, none of the values in the first row can equal the element in question,
since they're no bigger than the last element of that row, which is in turn smaller than the
element in question. This gives a very straightforward algorithm for finding the element -
we keep looking at the last element of the first row, then decide whether to discard the last
row or the last column.
As an example, consider the following 4 × 6 for searching the element 6.
0 1 2 3 4 5
0 1 2 2 2 3 4
1 1 2 3 3 4 5
2 3 4 4 4 4 6
3 4 5 7 7 8 9
Let us start at the bottom-left corner (3, 0) of the matrix, and matrix[3][0]<6. So, 6 cannot
be in that column. Hence, move to next column.
0 1 2 3 4 5
0 1 2 2 2 3 4
1 1 2 3 3 4 5
2 3 4 4 4 4 6
3 4 5 7 7 8 9
Next, matrix[3][1]<6. Element 6 cannot be in that column; move to the next column.
0 1 2 3 4 5
0 1 2 2 2 3 4
1 1 2 3 3 4 5
2 3 4 4 4 4 6
3 4 5 7 7 8 9
Next, matrix[3][2]>6. Element 6 cannot be in that row; move to the previous row.
0 1 2 3 4 5
0 1 2 2 2 3 4
1 1 2 3 3 4 5
2 3 4 4 4 4 6
3 4 5 7 7 8 9
Next, matrix[2][2]<6. Element 6 cannot be in that column; move to the next column.
0 1 2 3 4 5
0 1 2 2 2 3 4
1 1 2 3 3 4 5
2 3 4 4 4 4 6
3 4 5 7 7 8 9
Next, matrix[2][3]<6. Element 6 cannot be in that column; move to the next column.
0 1 2 3 4 5
0 1 2 2 2 3 4
1 1 2 3 3 4 5
2 3 4 4 4 4 6
3 4 5 7 7 8 9
Next, matrix[2][4]<6. Element 6 cannot be in that column; move to the next column.
0 1 2 3 4 5
0 1 2 2 2 3 4
1 1 2 3 3 4 5
2 3 4 4 4 4 6
3 4 5 7 7 8 9
Next, matrix[2][5]=6. Hence return the current location (2, 5).
def grid_search(matrix, value):
m = len(matrix)
if m == 0:
return None, None
n = len(matrix[0])
if n == 0:
return None, None
i=0
j=n-1
while i < m and j >= 0:
if matrix[i][j] == value:
return i, j
elif matrix[i][j] < value:
i=i+1
else:
j=j-1
return None, None
matrix = [[1, 2, 2, 2, 3, 4],
[1, 2, 3, 3, 4, 5],
[3, 4, 4, 4, 4, 6],
[4, 5, 6, 7, 8, 9]
]
print grid_search(matrix, 6)
Performance
As mentioned above, this will run in O( + ) time. This is fine when the matrix is
approximately square, but not optimal when the matrix is much wider than it is tall, or
vice versa.
For example, consider what happens when = 1: we just have a normal sorted list, and
can apply binary search to solve the problem in O( ) time, but line-at-a-time will take
O( ) time. Line-at-a-time is not optimal when the matrix is very tall or very wide.
column_mid + 1, max_column)
if row is not None and column is not None:
return row, column
return grid_search(matrix, value, row_mid + 1, max_row, min_column, max_column)
else:
row, column = grid_search(matrix, value, min_row, row_mid - 1, \
min_column, max_column)
if row is not None and column is not None:
return row, column
return grid_search(matrix, value, row_mid + 1, max_row, min_column, column_mid)
matrix = [[1, 2, 2, 2, 3, 4],
[1, 2, 3, 3, 4, 5],
[3, 4, 4, 4, 4, 6],
[4, 5, 6, 7, 8, 9]
]
print grid_search(matrix, 6, 0, len(matrix)-1, 0, len(matrix[0])-1)
Performance
This algorithm has an interesting recurrence relation for its running time, assuming that
the asymmetry of the search areas doesn’t affect the running time:
( )= 2 + +1 = 3 + 1
2 2 2
.
By using master theorem, the recurrence relation solves to ( ) = O( ) = O( ).
Algorithm: One approach that uses a row-by-row binary search looks like this:
1. Start with a rectangular array where < . Let us say is the number of rows
and is the number of columns in the matrix.
2. Do a binary search on the middle row for value. If we find it, we're done.
3. Otherwise we've found an adjacent pair of numbers and , where < < .
4. The rectangle of numbers above and to the left of is less than , so we can
eliminate it.
5. The rectangle below and to the right of is greater than value, so we can eliminate
it.
6. Go to step (2) for each of the two remaining rectangles.
For example, consider the following 4 × 6 for searching the element 6.
0 1 2 3 4 5
0 1 2 2 2 3 4
1 1 2 3 3 4 9
2 3 4 4 4 4 10
3 4 5 6 7 8 19
For this example, < and midpoint of row is:
+ 0 + 3
_ = = =1
2 2
Now, let us perform binary search on this middle row of matrix for the value 6. Element 6
cannot be found in middle row and it is between 4 and 9. Here, is 4 and is 9. The
rectangle of numbers above and to the left of is less than , so we can eliminate it.
0 1 2 3 4 5
0 1 2 2 2 3 4
1 1 2 3 3 4 9
2 3 4 4 4 4 10
3 4 5 6 7 8 19
The rectangle below and to the right of is greater than value, so we can eliminate it.
0 1 2 3 4 5
0 1 2 2 2 3 4
1 1 2 3 3 4 9
2 3 4 4 4 4 10
3 4 5 6 7 8 19
Now, we need to perform the operations recursively on the remaining two rectangles. The
top right rectangle has only one element and is not equal to 6. Hence, we can return (None,
None) from it.
0 1 2 3 4 5
0 1 2 2 2 3 4
1 1 2 3 3 4 9
2 3 4 4 4 4 10
3 4 5 6 7 8 19
For this rectangle, midpoint of row is:
+ 2 + 3
_ = = =2
2 2
Now, let us perform binary search on this middle row of matrix for the value 6. Element 6
cannot be found in middle row and it is between 4 and None (as there is no right element).
Here, is 4 and is None. The rectangle of numbers above and to the left of is less than
, so we can eliminate it. Since r is None, there is nothing to discard.
0 1 2 3 4 5
0 1 2 2 2 3 4
1 1 2 3 3 4 9
2 3 4 4 4 4 10
3 4 5 6 7 8 19
For this remaining rectangle, midpoint of row is:
+ 3 + 3
_ = = =3
2 2
Now, let us perform binary search on this middle row of matrix for the value 6. Element 6
can be found in the middle row and it is at (3, 2). Since we found the desired element, it is
the end of algorithm, and return the local (3, 2).
def binary_search(A, value):
low = 0
high = len(A)-1
while low <= high:
mid = (low+high)//2
if A[mid] > value: high = mid-1
elif A[mid] < value: low = mid+1
else: return mid, mid
return low, high
Performance
In terms of worst-case complexity, this algorithm does ( ) work to eliminate half of the
possible solutions, and then recursively calls itself twice on two smaller problems. We do
have to repeat a smaller version of that ( ) work for every row, but if the number of rows
is small compared to the number of columns, then being able to eliminate all of those
columns in logarithmic time starts to become worthwhile.
This gives the algorithm a complexity of ( , ) = ( ) + 2 ( , ), which shows to
be O( ( )).
( , )
This approach takes time O( ( )), where = ( , ) and = ( , )
. This is not
optimal when ≈ , (that is to say, ≈ 1). In that case the running time reduces to O(( +
) ( + )), which is less efficient than approach.
Notice that for <= , this problem has a lower bound of O( ( )). This bound make
sense, as it gives us linear performance when == and logarithmic performance when
== 1.
Actually, it turns out that this algorithm is not optimal even when >> or >> .
That’s too bad, since otherwise an optimal solution would be to just switch between multi-
binary-search and based on how tall/wide the matrix was.
Chapter
Dynamic
Programming 6
6.1 Introduction
In this chapter, we will try to solve few of the problems for which we failed to get the optimal
solutions using other techniques (say, and & approaches). Dynamic
Programming is a simple technique but it can be difficult to master. The ability to tackle
this type of problems would increase your skill greatly. Dynamic programming was
invented by ℎ .
One easy way to identify and master DP problems is by solving as many problems as
possible. The term DP is not related to coding, but it is from literature which means filling
tables.
6 . 1 I n t r o d u c t io n
D y n am i c Pro g r am mi n g | 359
If the given problem can be broken up into smaller subproblems and these smaller
subproblems are in turn divided into still-smaller ones, and in this process, if we observe
some over-lapping subproblems, then it's a big hint for DP. Also, the optimal solutions to
the subproblems contribute to the optimal solution of the given problem.
Greedy algorithms are one which find optimal solution at each and every stage with the
hope of finding global optimum at the end. The main difference between DP and greedy is
that, the choice made by a greedy algorithm may depend on choices made so far but not
on future choices or all the solutions to the subproblem. It iteratively makes one greedy
choice after the other, reducing each given problem into a smaller one.
In other words, a greedy algorithm never reconsiders its choices. This is the main difference
from dynamic programming, which is exhaustive and is guaranteed to find the solution.
After every stage, dynamic programming makes decisions based on all the decisions made
in the previous stage, and may reconsider the algorithmic path of the previous stage
solution.
The main difference between dynamic programming and divide and conquer is that in the
case of the latter, subproblems are independent, whereas in DP there can be an overlap of
subproblems.
6 . 3 P r o p e r t i e s o f d y na m i c p r og r a m m i ng s t r a t e gy
D y n am i c Pro g r am mi n g | 360
Calling (5) produces a call tree that calls the function on the same value many times:
(5)
(4) + (3)
( (3) + (2)) + ( (2) + (1))
(( (2) + (1)) + ( (1) + (0))) + (( (1) + (0)) + (1))
((( (1) + (0)) + (1)) + ( (1) + (0))) + (( (1) + (0)) + (1))
6 . 6 D y n a m i c p r o g ra m m i n g a p p r o a c h e s
D y n am i c Pro g r am mi n g | 361
fib(5)
fib(3) fib(4)
fib(1) fib(0)
In the above example, (2) was calculated three times (overlapping of subproblems). If is
big, then many more values of (subproblems) are recalculated, which leads to an
exponential time algorithm. Instead of solving the same subproblems again and again, we
can store the previous calculated values and reduce the complexity.
The recursive implementation can be given as:
def fib(n):
if n == 0:
return 0
elif n == 1:
return 1
else:
return fib(n-1)+fib(n-2)
print (fib(10))
Solving the above recurrence gives:
√
( )= ( − 1) + ( − 2) + 1 ≈ ≈ 2 = O(2 )
return table[m]
return func(n)
print(fib_memoization(10))
Alternative coding:
def fib_memoization_dictionary(n):
'''Using memoization and using a dictionary as a table.'''
table = {}
def func(m):
if m not in table:
if m <= 1:
table[m] = m
else:
table[m] = func(m-1) + func(m-2)
return table[m]
return func(n)
print(fib_memoization_dictionary(10))
Both versions of the Fibonacci series implementations clearly reduce the problem
complexity to O( ). This is because if a value is already computed then we do not call the
subproblems again. Instead, we directly take its value from the table.
Time Complexity: O( ).
Space Complexity: O( ), for table.
Further improving
One more observation from the Fibonacci series is: The current value is the sum of the
previous two calculations only. This indicates that we don’t have to store all the previous
values. Instead, if we store just the last two values, we can calculate the current value. The
implementation for this is given below:
def fibo(n):
a, b = 0, 1
for i in range(n):
a, b = b, a + b
return a
print(fibo(10))
Time Complexity: O( ).
Space Complexity: O(1).
Note: This method may not be applicable (available) for all problems.
Observations
While solving the problems using DP, try to figure out the following:
See how the problems are defined in terms of subproblems recursively.
See if we can use some table [memoization] to avoid the repeated calculations.
For simplicity, let us assume that we have already calculated ! and want to find !. For
finding !, we just need to see the table and use the existing entries if they are already
computed. If < , then we do not have to recalculate !. If > , then we can use !
and call the factorial on the remaining numbers only.
The above implementation clearly reduces the complexity to O( ( , )). This is because
if the (n) is already there, then we need not recalculate the value again. If we fill these
newly computed values, then the subsequent calls further reduce the complexity.
Time Complexity: O( ( , )).
Space Complexity: O( ( , )) for table.
subproblems but only some of them. In such cases the recursive implementation can be
much faster. Recursion with memoization is also better whenever the state space is
irregular, i.e., whenever it is hard to specify an order of evaluation iteratively. Recursion
with memoization is faster because only subproblems that are necessary in a given problem
instance are solved.
Tabulation methods are better whenever the state space is dense and regular. If we need
to compute the solutions to all the subproblems anyway, we may as well do it without all
the function calling overhead. An additional advantage of the iterative approach is that we
are often able to save memory by forgetting the solutions to subproblems that we won't
need in future. For example, if we only need row of the table to compute row + 1, there
is no need to remember row − 1 anymore. On the flip side, in tabulation method, we solve
all subproblems in spite of the fact that some subproblems may not be needed for a given
problem instance.
6 . 8 E x a m p l es o f D P a l g o r i th m s
D y n am i c Pro g r am mi n g | 366
and the remaining number of stairs would be n-1 (3-1=2). These two steps can be completed
with (2) for which we have already got the solution (2) = 2.
Similarly, if we make 2 step jump, it would lead us to second stair and the remaining
number of stairs would be − 2 (3-2=1). For the remaining 1 stair, we have to use 1 step
jump which is equal to F(1).
So, we get:
(2) = 2
Now, consider F(n) for n=3. If we have 3 stairs and by starting at the ground, we can make
either 1 step jump, 2 step jump or 3 step jump. If we make 1 step jump, it would lead us
to the first stair and the remaining number of stairs would be n-1 (3-1=2). These two steps
can be completed with (2) for which we have already got the solution (2) = 2.
Similarly, if we make 2 step jump, it would lead us to the second stair and the remaining
number of stairs would be n-2 (3-2=1). For the remaining 1 stair, we have to use 1 step
jump which is equal to (1).
Similarly, if we make 3 step jump, it would lead us to top of the stairs.
With 1 step jump, number of ways to complete
remaining 2 stairs
+
Number of ways to reach With 2 step jump, number of ways to complete
=
top of 3 stairs remaining 1 stairs.
+
With 3 step jump, we reach top of the 3 stairs.
ℎ1 , (2) + ℎ2 , (1)
(3) =
+ ℎ3 , (0)
(3) = F(2) + F(1) + F(0)
(3) = 2+1+0=3
So, we can reach the 3 stair from either the 2 stair, the 1 stair or directly from the
ground with 3 step jump. Similarly, we can reach the stair from either the − 1 stair,
− 2 stair or the − 3 stair.
( ) = ( − 1) + ( − 2) + ( − 3)
This is the Tribonacci recurrence. Calling (5) produces a call tree that calls the function
on the same value many times:
(5)
(4) + (3) + (2)
(3) + (2) + (1) + (2) + (1) + (0) + (2)
(( (2) + (1) + (0)) + (2) + (1) + (0) + (2)
In the above example, (2) is calculated three times (overlapping of subproblems). If is
big, then many more values of (subproblems) are recalculated, which leads to an
exponential time algorithm. Instead of solving the same subproblems again and again, we
can store the values calculated previously and reduce the complexity.
The recursive implementation can be given as:
def trib(n):
if n == 0: return 0
elif n == 1: return 1
elif n == 2: return 2
else: return trib(n-1)+ trib(n-2) + trib(n-3)
print (trib(10))
Solving the above recurrence gives:
( )= ( − 1) + ( − 2) + ( − 3) + 1 ≈ 3 = O(3 )
6 . 1 1 C l i m b i n g n s ta i r s w i t h ta k i n g o n ly 1 , 2 o r 3 s t e p s
D y n am i c Pro g r am mi n g | 368
Further improving
One more observation from the Tribonacci series is: The current value is the sum of the
previous three calculations only. This indicates that we don’t have to store all the previous
values. Instead, if we store just the last three values, we can calculate the current value.
The implementation for this is given below:
def trib(n):
a, b, c = 0, 1, 2
for i in range(n):
a, b, c = b, c, a + b + c
return a
print(trib(10))
Time Complexity: O( ).
Space Complexity: O(1).
Note: This method may not be applicable (available) for all problems.
6 . 1 1 C l i m b i n g n s ta i r s w i t h ta k i n g o n ly 1 , 2 o r 3 s t e p s
D y n am i c Pro g r am mi n g | 369
else:
table[n, r] = func(n-1, r-1) + func(n-1, r)
return table[n, r]
return func(m, p)
print func_memoization(10, 5)
def func_tabulation_dp(m, p):
'''Using DP and using a dictionary as a table.'''
table = {}
for n in range(m+1):
for r in range(min(n, p)+1):
if r == 0 or n == r:
table[n,r] = 1
else:
table[n,r] = table[n-1,r-1] + table[n-1,r]
return table[m,p]
print func_tabulation_dp(10, 5)
B D CA B A
From the above observation, we can see that the current characters of and may or may
not match. That means, suppose that the first two characters differ. Then it is not possible
for both of them to be part of a common subsequence - one or the other (or maybe both)
will have to be removed. Finally, observe that once we have decided what to do with the
first characters of the strings, the remaining subproblem is again an problem, on two
shorter strings. Therefore, we can solve it recursively.
6 . 1 2 L o n g e s t c o m m o n s u b s eq u en c e
D y n am i c Pro g r am mi n g | 370
The solution to should find two sequences in and and let us say the starting index
of sequence in is and the starting index of sequence in is . Also, assume that [ … ]
is a substring of starting at character and going until the end of , and that [ … ] is a
substring of starting at character and going until the end of .
Based on the above discussion, we get the possibilities as described below:
1) If [ ] == [ ] : 1 + ( + 1, + 1)
2) If [ ] ≠ [ ]: ( , + 1) // skipping character of
3) If [ ] ≠ [ ]: ( + 1, ) // skipping character of
In the first case, if [ ] is equal to [ ], we get a matching pair and can count it towards the
total length of the . Otherwise, we need to skip either character of or character
of and find the longest common subsequence. Now, ( , ) can be defined as:
0, = =
(, )= { ( , + 1), ( + 1, )}, X[i] ≠ Y[j]
1 + [ + 1, + 1], X[i] == Y[j]
LCS has many applications. In web searching, we find the smallest number of changes that
are needed to change one word into the other. A ℎ here is an insertion, deletion or
replacement of a single character.
def LCS(X, Y):
if not X or not Y:
return ""
x, m, y, n = X[0], X[1:], Y[0], Y[1:]
if x == y:
return x+LCS(m, n)
else:
# Use key=len to select the maximum string in a list efficiently
# Python has property len.
return max(LCS (X, n), LCS(m, Y), key=len)
print "Longest common subsequence: ", LCS('ABCBDAB', 'BDCABA')
# Finding length of longest common subsequence
def LCS_length(X, Y):
if not X or not Y:
return 0
x, m, y, n = X[0], X[1:], Y[0], Y[1:]
if x == y:
return 1+LCS_length(m, n)
else:
return max(LCS_length(X, n), LCS_length(m, Y))
print "Longest common subsequence length: ", LCS_length('ABCBDAB', 'BDCABA')
This is a correct solution but it is very time consuming. For example, if the two strings have
no matching characters, the last line always gets executed, which gives (if == ) close to
O(2 ).
DP solution
The problem with the above recursive solution is that the same subproblems get called
many different times. A subproblem consists of a call to _ ℎ, with the arguments
being two suffixes of and , so there are exactly ( + 1)( + 1) possible subproblems (a
relatively small number). If there are nearly 2 recursive calls, some of these subproblems
must be solved over and over.
6 . 1 2 L o n g e s t c o m m o n s u b s eq u en c e
D y n am i c Pro g r am mi n g | 371
6 . 1 2 L o n g e s t c o m m o n s u b s eq u en c e
D y n am i c Pro g r am mi n g | 372
maximum value. Then we move to that cell (it will be one of (1, 1), (0, 1) or (1, 0)) and repeat
this until we hit the boundary of the table. Every time we pass through a cell ( , ) where
[ ] == [ ], we have a matching pair and print [ ]. At the end, we will have printed the
longest common subsequence in O( ) time.
An alternative way of getting path is to keep a separate table for each cell. This will tell us
which direction we came from, when computing the value of that cell. At the end, we again
start at cell (0, 0) and follow these directions upto the opposite corner of the table.
From the above examples, hope the idea behind DP is understood. Now let us see more
problems which can be easily solved using the DP technique.
Alternative DP solution
In the above discussion, we have assumed that ( , ) is the length of the with [ … ]
and [ … ]. We can solve the problem by changing the definition as ( , ) is the length
of the with [1 … ] and [1 … ]. Assume that [ … ] is a substring of starting at
character 1 and going till index of , and [ … ] is a substring of starting at character
1 and going till index of .
Based on the above discussion, here we get the possibilities as described below:
1) If [ ] == [ ] : 1 + ( − 1, − 1)
2) If [ ] ≠ [ ]: ( , − 1) # skipping character of
3) If [ ] ≠ [ ]: ( − 1, ) # skipping character of
In the first case, if [ ] is equal to [ ], we get a matching pair and can count it towards the
total length of the . Otherwise, we need to skip either character of or character
of and find the longest common subsequence.
Now, ( , ) can be defined as:
0, = −1 = −1
(, )= { ( , − 1), ( − 1, )}, X[i] ≠ Y[j]
1 + [ − 1, − 1], X[i] == Y[j]
:
def LCS(X, Y, i, j):
if i == -1 or j == -1:
return 0
if X[i] == Y[j]:
return 1 + LCS(X, Y, i-1, j-1)
return max(LCS(X, Y, i-1, j), LCS(X, Y, i, j-1))
X = 'ABCBDAB'
Y = 'BDCABA'
print "Longest common subsequence: ", LCS(X, Y, len(X)-1, len(Y)-1)
:
def LCS_finder(X, Y):
LCS = [[0 for j in range(len(Y)+1)] for i in range(len(X)+1)]
# row 0 and column 0 are initialized to 0 already
for i, x in enumerate(X):
for j, y in enumerate(Y):
if x == y:
LCS[i+1][j+1] = LCS[i][j] + 1
else:
LCS[i+1][j+1] = max(LCS[i+1][j], LCS[i][j+1])
6 . 1 2 L o n g e s t c o m m o n s u b s eq u en c e
D y n am i c Pro g r am mi n g | 373
Computing binomial coefficients is non optimization problem but can be solved using
dynamic programming. Binomial coefficients are represented by ( , ) or and can be
used to represent the coefficients of a binomial:
( , 0) + ( , 1) + ( , 2) …+ ( , )
( + ) =
… + ( , − 1) ( )
+ ( , )
( , 0) + ( , 1) + ( , 2) …+ ( , ) …+
=
( , − 1) + ( , )
For example, the number of unique 5-card hands from a standard 52-card deck is (52,
5). One problem with using the above binomial coefficient formula directly in most
languages is that ! grows very fast and overflows an integer representation before we can
do the division to bring the value back to a value that can be represented. When calculating
the number of unique 5-card hands from a standard 52-card deck (e.g., (52, 5)) for
example, the value of 52! Is:
52! = 80, 658, 175, 170, 943, 878, 571, 660, 636, 856, 403, 766, 975, 289, 505, 440,
883, 277, 824, 000, 000, 000, 000
This is too bigger value and cannot fit into a 64-bit integer representation.
6 . 1 3 C o m p u t i ng a b i no m i a l co e f f i c i e nt : n c h oo s e k
D y n am i c Pro g r am mi n g | 374
1, =0
1, =
=
−1 −1
+ , ℎ
−1
If = 0, then there is exactly one zero-element set of our n-element set—it's the empty set.
If = , then there is exactly one n-element set—it's the full n-element set. If > , then
there are no k-element subsets. The proof of this identity is combinatorial, which means
that we will construct an explicit bijection between a set counted by the left-hand side and
a set counted by the right-hand side. This is often one of the best ways of understanding
simple binomial coefficient identities.
On the left-hand side, we count all the -element subsets of an -element set . On the
right hand side, we count two different collections of sets: the ( − 1)-element and -element
subsets of an ( − 1)-element set. The trick is to recognize that we get an ( − 1)-element set
′ from our original set by removing one of the elements . When we do this, we affect the
subsets in one of the two ways:
1, =0
( , ) = 1, =
( − 1 , ) + ( − 1 , − 1), ℎ
This formulation does not require the computation of factorials. In fact, the only
computation needed is addition.
# Recursive Call
return n_choose_k(n-1 , k-1) + n_choose_k(n-1 , k)
print(n_choose_k(5,2)) # 10
This will work for non-negative integer inputs and with . However, this ends up
repeating many instances of recursive calls, and ends up being very slow.
The problem here with efficiency is the same as with Fibonacci. Many recursive calls to the
function get recomputed many times. To calculate (5,2) recursively we call (4,1) + (4,2)
6 . 1 3 C o m p u t i ng a b i no m i a l co e f f i c i e nt : n c h oo s e k
D y n am i c Pro g r am mi n g | 375
which calls (3,0) + (3,1) and (3,1) + (3,2). This continues and in the end we make the
following calls a number of times.
C(5,2)
C(4,1) C(4,2)
Performance
With recursive implementation, the recurrence for the running time can be given as:
O(1), =0
T( , ) = O(1), =
T( − 1, ) + T( − 1, − 1), ℎ
This is very similar to the above recursive definition. In fact, we can show that T( , ) =
O( ) which is not a very good running time at all. Again the problem with the direct
recursive implementation is that it does far more work than is needed because it solves the
same subproblems many times.
6 . 1 3 C o m p u t i ng a b i no m i a l co e f f i c i e nt : n c h oo s e k
Abstractly, Pascal’s triangle relates to the binomial coefficient as in:
6 . 1 3 C o m p u t i ng a b i no m i a l co e f f i c i e nt : n c h oo s e k
Row #
C(0, 0) 0
C(1, 0) C(1, 1) 1
. .
D y n am i c Pro g r am mi n g | 376
. .
C(n, 0) C(n, 1) C(n, 2) C(n, 3) C(n, 4) C(n, 5) … +C(n, k) … C(n, n-1) C(n, n) n
D y n am i c Pro g r am mi n g | 377
For example, (5,2) would get the following values with the recursive formula:
( , ) = ( − 1 , ) + ( − 1 , − 1)
Row #
1 0
1 1 1
1 2 1 2
1 3 3 1 3
1 4 6 4 1 4
1 5 10 10 5 1 5
From the above table, it is pretty clear that Pascal’s triangle relates to the binomial
coefficient. So, using Pascal’s triangle is the easiest way of calculating binomial coefficient.
To calculate the binomial coefficient ( , ), we use the row of the Pascal’s triangle.
The following code calculates the binomial coefficient with the help of dynamic
programming (Pascal's algorithm). Elements are saved in a table [ ][ ] where they are
initially set to 0. The table is then reused between calls where the results of previous calls
to calculate ( , ), and its recursive calls ( − 1, ) and ( − 1, − 1), have been saved in
the table [ ][ ].
def n_choose_k(n, k):
C = [[0 for i in range(k+1)] for j in range(n+1)]
# Calculate value of binomial coefficient in bottom up manner
for i in range(n+1):
for j in range(min(i, k)+1):
# base cases
if j == 0 or j == i:
C[i][j] = 1
else:
C[i][j] = C[i-1][j-1] + C[i-1][j]
return C[n][k]
print(n_choose_k(5,2)) # 10
Performance
Time wise, the running time of the algorithm above is O( ), where is the value of the first
parameter.
Improving DP solution
There are certainly a number of ways to optimize the above dynamic programming code. At
any given time, no more than two of the rows of the table are needed. Furthermore, based
on the value of , we could stop computing the values in a row once we get to the
element in that row. Time wise, the running time of the algorithm above is O( ), where
is the value of the first parameter.
6 . 1 3 C o m p u t i ng a b i no m i a l co e f f i c i e nt : n c h oo s e k
D y n am i c Pro g r am mi n g | 378
Performance
With the improvements, the amount of running space is reduced to O( ) and the running
time is improved to O( ). The time is only an improvement for small values of .
Time Complexity: O( ).
Space Complexity: O( ).
This is a classic success of dynamic programming over recursion.
The code for the given recursive formula can be given as:
def f(n) :
sum = 0
if(n==0 or n==1):
return 2
# recursive case
for i in range(1, n):
sum += 2 * f(i) * f(i-1)
return sum
DP solution
Before finding a solution, let us see how the values are calculated.
(0) = (1) = 2
(2) = 2 × (1) × (0)
(3) = 2 × (1) × (0) + 2 × (2) × (1)
(4) = 2 × (1) × (0) + 2 × (2) × (1) + 2 × (3) × (2)
From the above calculations, it is clear that there are lots of repeated calculations with the
same input values. Let us use a table for avoiding these repeated calculations, and the
implementation can be given as:
6 . 1 4 S o l v i n g r e c u r r e n ce r e l a t i o n s w it h D P
D y n am i c Pro g r am mi n g | 379
def f2(n) :
T = [0] * (n+1)
T[0] = T[1] = 2
for i in range(2, n+1):
T[i] = 0
for j in range(1, i):
T[i] += 2 * T[j] * T[j-1]
return T[n]
print f2(4)
Time Complexity: O( ), two loops.
Space Complexity: O( ), for table.
Improving DP solution
Since all subproblem calculations are dependent only on previous calculations, code can
be modified as:
def f(n):
T = [0] * (n+1)
T[0] = T[1] = 2
T[2] = 2 * T[0] * T[1]
for i in range(3, n+1):
T[i]=T[i-1] + 2 * T[i-1] * T[i-2]
return T[n]
print f(4)
Time Complexity: O( ), since only one loop.
Space Complexity: O( ).
Examples
Example-1
For A = [2, −6, 3, −2, 4, 1], here are some contiguous subsequences:
[2],
[3, −2, 4], and
[−6, 2]
The sequence [2, 3, 4] is not a , even though it is a subsequence.
Among the , we need to find the maximum .
For the above example, the is [3, -2, 4, 1] with sum 7.
6 . 1 5 M a x i m u m v a l ue c on t i gu o us s u b s eq u e nc e
D y n am i c Pro g r am mi n g | 380
Example-2
For A = [-2, 11, -4, 13, -5, 2], here are some contiguous subsequences:
[-2],
[11, −4, 13, -5],
[−5, 2], and
[11, -4, 13]
The sequence [11, 13, 2] is not a , even though it is a subsequence.
Among the , the is [11, -4, 13] with
sum 20.
Example-3
For A = [1, -3, 4, -2, -1, 6], here are some contiguous subsequences:
[-3, 4],
[1, −3, 4, -2],
[−2, -1, 6], and
[1, -3, 4, -2]
The sequence [1, 4, 6] is not a , even though it is a subsequence.
Among the , the is [4, -2, -1, 6]
with sum 7.
6 . 1 5 M a x i m u m v a l ue c on t i gu o us s u b s eq u e nc e
D y n am i c Pro g r am mi n g | 381
6 . 1 5 M a x i m u m v a l ue c on t i gu o us s u b s eq u e nc e
D y n am i c Pro g r am mi n g | 382
6 . 1 5 M a x i m u m v a l ue c on t i gu o us s u b s eq u e nc e
D y n am i c Pro g r am mi n g | 383
DP solution
As we have seen in the previous sections, similar to DC technique, DP is also to synthesize
the solution for big problems with the solutions of smaller problems. The core idea
underlying DP is to develop a recursion function to transfer from one state to the other.
Suppose we have known the maximum subsequence sum for the first i elements (A[0]...A[ -
1]). For sequence A[0]...A[i], we need to determine whether the maximum subsequence
includes element [ ] or not. If it is, the maximum subsequence for the first + 1 elements
is a subsequence ended with element [ ]. Otherwise, the maximum subsequence for the
first + 1 elements is the same as that of the first elements.
6 . 1 5 M a x i m u m v a l ue c on t i gu o us s u b s eq u e nc e
D y n am i c Pro g r am mi n g | 384
For simplicity, let us say, ( ) indicates the maximum value subsequence ending at .
….. ?
[]
[i -1]
[i]
To find the maximum sum we have to do one of the following and select the maximum
among them.
Either extend the old sum by adding [ ] or
Start new window starting with one element [ ]
The recursive function is defined as follows:
[] =0
()= ( − 1) + [ ]
[]
Where, ( − 1) + [ ] indicates the case of extending the previous sum by adding [ ] and
[ ] indicates the new window starting at [ ].
With each element of , you also keep the starting element of the sum (the same as for (i
- 1) or [i] if you restart). At the end, you scan for the maximum value and return it and
the starting and ending indexes. Alternatively, you could keep track of the maximum value
as you create .
Time Complexity: A is of size and evaluating each element of takes O(n). Scanning
also takes O(n) time for a total time of O(n).
def max_contiguous_sum_dp(A):
maxSum = 0
n = len(A)
M = [0] * (n+1)
M[0] = A[0]
for i in range(1, n):
M[i] = max(A[i], M[i-1] + A[i])
for i in range(0, n):
if (M[i] > maxSum):
maxSum = M[i]
return maxSum
A = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
print max_contiguous_sum_dp(A)
Time Complexity: O( ).
Space Complexity: O( ), for table.
Further improving
We can solve this problem without DP too (without auxiliary memory) with a little trick.
One simple way is to look for all positive contiguous segments of the array ( )
and keep track of the maximum sum contiguous segment among all positive segments
( ). Each time we get a positive sum, compare it ( ) with
and update if it is greater than . Let us consider the following code for
the above observation.
6 . 1 5 M a x i m u m v a l ue c on t i gu o us s u b s eq u e nc e
D y n am i c Pro g r am mi n g | 385
def max_contiguous_sum(A):
sumSoFar = sumEndingHere = 0
n = len(A)
for i in range(0, n) :
sumEndingHere = sumEndingHere + A[i]
if(sumEndingHere < 0):
sumEndingHere = 0
continue
if(sumSoFar < sumEndingHere):
sumSoFar = sumEndingHere
return sumSoFar
A = [-2, 3, -16, 100, -4, 5]
print max_contiguous_sum(A)
Note: The algorithm doesn't work if the input contains all the negative numbers. It returns
0 if all numbers are negative. To overcome this, we can add an extra check before the actual
implementation. The phase will look if all numbers are negative, and if they are, it will
return the maximum of them (or the smallest in terms of absolute value).
Time Complexity: O( ), for one scan of input elements.
Space Complexity: O(1), for table.
Alternative DP solution
In the previous section DP solution, we have assumed that ( ) indicates maximum sum
over all windows ending at . Let us see whether we can assume that ( ) indicates
maximum sum over all windows starting at and ending at ?
Let us say, ( ) indicates maximum sum over all windows starting at .
? …..
[]
[i+1]
[i]
To find maximum window we have to do one of the following and select the maximum
among them.
Either extend the old sum by adding [ ] or
Start new window starting with one element [ ]
[] = −1
()= ( + 1) + [ ]
[]
Where, ( − 1) + [ ] indicates the case of extending the previous sum by adding [ ], and
[ ] indicates the new window starting at [ ].
def max_contiguous_sum_dp(A):
maxSum = 0
6 . 1 5 M a x i m u m v a l ue c on t i gu o us s u b s eq u e nc e
D y n am i c Pro g r am mi n g | 386
n = len(A)
M = [0] * (n+1)
M[n] = A[n-1]
for i in range(n-2, 0, -1):
M[i] = max(A[i], M[i+1] + A[i])
for i in range(0, n):
if (M[i] > maxSum):
maxSum = M[i]
return maxSum
A = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
print max_contiguous_sum_dp(A)
Time Complexity: O( ).
Space Complexity: O( ), for table.
….. ….. ?
6 . 1 6 M a x i m u m s u m s u ba r r a y w it h c ons t r a i n t - 1
D y n am i c Pro g r am mi n g | 387
def max_sum_with_no_two_contiguous_numbers(A):
n = len(A)
M = [0] * (n+1)
M[0] = A[0]
M[1] = max(A[0], A[1])
for i in range(2, n):
if( M[i-1]>M[i-2]+A[i]):
M[i] = M[i-1]
else:
M[i] = M[i-2]+A[i]
return M[n-1]
A = [-2, 3, -16, 100, -4, 5]
print max_sum_with_no_two_contiguous_numbers(A)
Time Complexity: O( ).
Space Complexity: O( ).
….. ….. ?
M = [0] * (n)
M[0] = A[0]
M[1] = max(A[0], A[1])
for i in range(2, n):
if( M[i-1] > M[i-2] +A[i]):
M[i] = M[i-1]
else:
M[i] = M[i-2] + A[i]
return M[n-1]
A = [-2, 3, -16, 100, -4, 5]
print house_robber(A)
Assume that ( ) represents the maximum sum from 1 to numbers without selecting
three contiguous numbers. While computing ( ), the decision we have to make is, whether
to select element or not. This gives us the following possibilities:
[ ] + [ − 1] + ( − 3)
( )= [ ] + ( − 2)
( − 1)
In the given problem, the restriction is not to select three continuous numbers, but
we can select two elements continuously and skip the third one. That is what the
first case says in the above recursive formula. i.e., skipping [ − 2].
Other possibility is, selecting element and skipping second − 1 element. This
is the second case (skipping [ − 1]).
The third term defines the case of not selecting element and as a result we
should solve the problem with − 1 elements.
def max_sum_with_no_three_contiguous_numbers(A):
n = len(A)
M = [0] * (n)
M[0] = A[0]
M[1] = max(A[0], A[1], A[0] + A[1])
M[2] = max(M[1], A[2] + M[0], A[2] + A[1])
for i in range(3, n):
M[i] = max(M[i-1], A[i] + M[i-2], A[i] + A[i-1] + M[i-3])
return M[n-1]
A = [2, 13, 16, 100, 4, 5]
print max_sum_with_no_three_contiguous_numbers(A)
Time Complexity: O( ).
Space Complexity: O( ).
6 . 1 8 M a x i m u m s u m s u ba r r a y w it h c ons t r a i n t - 2
D y n am i c Pro g r am mi n g | 389
Recursive formulation
First, we define ( ) to be the total profit from the best valid configuration using only
locations from within 1, 2, . . . , .
The first step in designing a recursive algorithm is determining the base case. Eventually,
all recursive steps must reduce to the base case.
General case?
First we ask, “What is the maximum profit SSS can get?” And later we can extend the
algorithm to give us the actual locations that lead to that maximum profit. As stated above,
let ( ) be the maximum profit SSS can get with the first locations.
If > 0, we have two options:
1. Do not open a restaurant at location , or
2. Open a restaurant at location .
If no restaurant is opened at location , then the optimal value will be optimal profit from
valid configuration using only location 1, 2, . . . , − 1. This is just ( − 1).
6 . 1 9 S S S r e s t a u r a n ts
D y n am i c Pro g r am mi n g | 390
Opening a restaurant at location gives the expected profit ( ). After building at location
, we cannot open another restaurant within a distance of from . So, the location to the
left where a restaurant can be built is:
{, ℎ ≤ ≤ − }
To obtain a maximum profit, we need to obtain the maximum profits from the remaining
locations 1, 2, . . . , . This is just ( ) + { ( )}.
For these two possibilities, we can view the problem recursively as follows:
( − 1)
()=
( ) + ( ), ℎ ≤ ≤ −
This formula immediately translates into a recursive algorithm.
def sss_restaurants(distances, profit, i, k):
def funct(i):
if i == 0:
return 0
else:
M = funct(i-1)
for j in range(i-1, -1, -1):
if( distances[j] < distances[i]-k):
M = max(M, profit[i]+funct(j))
return M
return funct(i)
distances = [0, 2, 4, 5, 6]
profits = [0, 10, 20, 40, 80]
print sss_restaurants(distances, profits, len(distances)-1, 2)
Performance
Considering the above implementation, following is the recursion tree for an array of size
4. If value is less than the distance between the locations, then the recursion tree would
be heavy. In this case, for every , we would consider all values less than .
3 2 1 0
2 1 0 1 0 0
1 0 0 0
6 . 1 9 S S S r e s t a u r a n ts
D y n am i c Pro g r am mi n g | 391
( ) = ( − 1) + ( − 2) + ( − 3) + . . . + (1)
where recursively ( − 1) = ( − 2) + ( − 3)+ . . . + (1)
Clearly, we can observe that:
( ) = 2 ∗ ( − 1)
So our compact recurrence relation is: ( ) = 2 ∗ ( − 1)
( ) = 2 ∗ ( − 1)
( − 1) = 2 ∗ ( − 2)
. .. … …
(2) = 2 ∗ (1)
(1) = 2 ∗ (0)
Among the above equations, multiply the equation by 2 and then add all the
equations. We then clearly have:
( ) = (2 ) ∗ (0) = O(2 )
Time Complexity: O(2 ).
Space Complexity: O(1).
DP solution
We can see that there are many subproblems which are being solved again and again. So
this problem has overlapping substructure property and recomputation of the same
subproblems can be avoided by either using memoization or tabulation.
The bottom-up (tabulation) approach solves all the required subproblems first, then the
larger ones. However if we can store the solutions to the smaller problems in a bottom-up
manner rather than recompute them, the runtime can be drastically improved (at the cost
of additional memory usage). To implement this approach, we simply solve the problems
starting for smaller lengths and store these optimal revenues in a table (of size + 1).
Then when evaluating longer lengths, we simply look-up these values to determine the
optimal revenue for the larger piece. The answer will once again be stored in [ ].
def sss_restaurants(distances, profit, k):
n = len(distances)-1
M = (n + 1) * [0]
for i in range(1, n+1):
q = M[i-1]
for j in range(0, i+1):
if( distances[j] <= distances[i]-k):
q = max(q, profit[i] + M[j])
M[i] = q
return M[n]
distances = [0, 2, 4, 5, 6, 6]
profits = [0, 10, 20, 40, 80, 100]
print sss_restaurants(distances, profits, 2)
Often the bottom up approach is simpler to write, and has less overhead, because we don’t
have to keep a recursive call stack. Most of the people write the bottom up procedure when
they implement a dynamic programming algorithm.
Performance
The running time of this implementation is simply (assuming that the value is less than
the distance between the locations):
6 . 1 9 S S S r e s t a u r a n ts
D y n am i c Pro g r am mi n g | 392
( ) =
( + 1)
=
2
Time Complexity: O( ), because of the nested loops. Thus, we have reduced the
runtime from exponential to polynomial. This immediately gives a O( ) algorithm if we use
brute force to find every in O( ) time. A little more thought shows that we could find =
{ ∶ ≤ − } in O( ) time using binary search. This would give us an O( )
algorithm.
Space Complexity: O(n).
Example
For example, consider a route with eight gas stations having 15, 8, 2, 6, 18, 9, 21, and 30
gallons of gas; from each of those gas stations to the next requires 8, 6, 30, 9, 15, 21, 2,
and 18 gallons of gas.
Gas 15 8 2 6 18 9 21 30
Cost 8 6 30 9 15 21 2 18
18 15 8
30 8
2 6
21 2
21 30
9 6
15 18 9
Obviously, we can’t start our trip at the third gas station, with 2 gallons of gas, because
getting to the next gas station requires 30 gallons of gas and we would run out of gas
reaching it.
18 15 8
30 8
2 6
21 2
21 30
9 6
15 18 9
It is obvious that there isn’t any solution (should return -1) if the sum of gas[] is smaller
than the sum of cost[] array.
6 . 2 0 G a s s ta t io n s
D y n am i c Pro g r am mi n g | 393
DP solution
The catch in brute force algorithm is that, if we start at station , reach station and cannot
proceed to station + 1; instead of starting from + 1 station, we can actually start from
+ 1 station because of the following reason: If we start at any index between + 1 and ,
we cannot reach + 1 since at each step in the previous iteration, we had zero or more gas
but still we are unable to reach + 1.
In other words, if we run out of gas at station then starting anywhere from 0 to – 1 would
be pointless. That is because: the accumulated sum from station 0 to station − 1 is
positive, i.e. removing a positive number will not make the sum bigger at station .
If at any time, we find that the + [ ]– ( ) is lesser
than zero, we find an impossible route. In this case, we set the start position at next station,
and re-initialize the fuel amount of the car to zero.
def can_complete_tour(gas, cost):
if(len(gas) < 2):
return -1
pre = len(gas)-1
end = 0
gas_balance = gas[end] - cost[end]
while(pre != end): # extand the path [pre+1, end] to full path
if(gas_balance < 0): # if gas_balance is not enough to go to next station
gas_balance += gas[pre] - cost[pre]
pre = (pre + len(gas) - 1) % len(gas) # extend to previous station
else:
end = (end + 1) % len(gas) # extend to next station
gas_balance += gas[end] - cost[end]
if gas_balance < 0:
6 . 2 0 G a s s ta t io n s
D y n am i c Pro g r am mi n g | 394
return -1
else:
return (pre+1) % len(gas)
gas = [15, 8, 2, 6, 18, 9, 21, 30]
cost = [8, 6, 30, 9, 15, 21, 2, 18]
print "We can start the tour from", can_complete_tour(gas, cost)
Time Complexity: O( ).
Space Complexity: O(1).
Examples
Given array of integers = [-2, 1, 6, -5, 9, -1, 19]
Start Index Ending Index Range Sum
0 3 0
2 6 28
5 5 -1
DP solution
Idea behind this solution is very much similar to algorithm. Imagine that we
pre-compute the cumulative sum from index 0 to .
[ ] >0
_ [ ]=
[0] =0
To compute the sum of elements of given range, we simply need to subtract the cumulative
sum of elements till starting index of the given range (sum of elements from the beginning
till starting index of given range) from the ending index cumulative sum. Now, we can
calculate _ as following:
_ [ _ + 1]
_ ( _ , _ ) = −
_ [ _ ]
6 . 2 1 R a n ge s u m q u e r y
D y n am i c Pro g r am mi n g | 395
cumulative_sum = []
def cumulative_sum_pre_process(A):
global cumulative_sum
cumulative_sum.append(A[0])
for i in range(0, len(A)):
cumulative_sum.append(cumulative_sum[i] + A[i])
def range_sum_dp(A, start_index, end_index):
return cumulative_sum[end_index+1] - cumulative_sum[start_index]
A= [-2, 1, 6, -5, 9, -1, 19]
cumulative_sum_pre_process(A)
print range_sum_dp(A, 0, 3)
Time Complexity: O(1) per query. But, the _ pre-computation would take O( )
time. Since the cumulative sum is cached, each _ query can be calculated in O(1)
time.
Space Complexity: O( ).
Examples
As an example, consider the following matrix:
0 1 2 3 4
0 3 0 1 4 2
1 5 6 3 2 1
2 1 2 4 1 5
3 4 1 9 1 7
4 1 0 3 0 5
In this matrix, range sum between the locations (1, 2) and (3, 4) is: 33
0 1 2 3 4
0 3 0 1 4 2
1 5 6 3 2 1
2 1 2 4 1 5
3 4 1 9 1 7
4 1 0 3 0 5
And, range sum between the locations (0,2) and (4, 3) is: 28
0 1 2 3 4
0 3 0 1 4 2
1 5 6 3 2 1
2 1 2 4 1 5
3 4 1 9 1 7
4 1 0 3 0 5
toal_sum = 0
for i in range(row1, row2 + 1):
for j in range(col1, col2 + 1):
toal_sum += matrix[i][j]
return toal_sum
matrix = [
[3, 0, 1, 4, 2],
[4, 6, 3, 2, 1],
[1, 2, 9, 1, 5],
[4, 1, 2, 1, 7],
[1, 0, 3, 0, 5]
]
print range_sum(matrix, 1, 2, 3, 4)
Performance
Time Complexity: O( ) time per query. Assume that and represents the number of
rows and columns respectively, each _ query can go through at the most ×
elements.
Space Complexity: O(1).
DP solution
Idea behind this solution is is pre-processing. Since _ could be called many times,
we definitely need to do some pre-processing very much similar to 1D range query. We used
a cumulative sum array in the 1D version. We notice that the cumulative sum is computed
with respect to the origin at index 0. Extending this analogy to the 2D case, we could pre-
compute a cumulative region sum with respect to the origin at (0, 0).
,
⎧
[ ][ ] >0 >0
_ [ ][ ] =
⎨ ,
⎩ [0][0] =0
0 1 2 3 4
0 3 3 4 8 10
1 7 13 17 23 26
2 8 16 29 36 44
3 12 21 36 44 59
4 13 22 40 48 68
To compute the sum of elements of given range, we simply need to subtract the cumulative
sum of elements which are not a part of the given range. So, we need to subtract the
cumulative sums of the following two regions:
(0, 0) to (0, 4):
0 1 2 3 4
0 3 3 4 8 10
1 7 13 17 23 26
2 8 16 29 36 44
3 12 21 36 44 59
4 13 22 40 48 68
and (0, 0) to (3, 1):
0 1 2 3 4
0 3 3 4 8 10
1 7 13 17 23 26
2 8 16 29 36 44
3 12 21 36 44 59
4 13 22 40 48 68
Notice that, cumulative sums of range (0, 0) to (0, 1) are covered twice. Hence, we would
need to add the cumulative sum of range (0, 0) to (0, 1) to the result.
0 1 2 3 4
0 3 3 4 8 10
1 7 13 17 23 26
2 8 16 29 36 44
3 12 21 36 44 59
4 13 22 40 48 68
Hence, to compute the range sum of elements between (1, 2) and (3, 4), we would the
following:
Get cumulative sum between (0, 0) and (3, 4),
Subtract cumulative sum between (0, 0) and (0, 4),
Subtract cumulative sum between (0, 0) and (3, 1), and
Add cumulative sum between (0, 0) and (0, 1)
Now, let us generalize the discussion. For a given matrix, the cumulative sum between the
range (row1, col1) to (row, col2) can be determined as:
col1 col2
row1
row2
_ ( 2, 2)
−
_ ( 1, 1, 2, 2) _ ( 1 − 1, 2)
= −
_ ( 2, 1 − 1)
+
_ ( 1 − 1, 1 − 1)
class RangeSum(object):
def __init__(self, matrix):
self.m = len(matrix)
self.n = len(matrix[0]) if self.m else 0
self.cumulative_sums = [[0] * (self.n + 1) for i in range(self.m + 1)]
self.__pre_compute(matrix)
def __pre_compute(self, matrix):
for i in range(self.m):
for j in range(self.n):
self.cumulative_sums[i+1][j+1] = self.cumulative_sums[i+1][j] + \
self.cumulative_sums[i][j+1] + matrix[i][j] - self.cumulative_sums[i][j]
def range_sum(self, row1, col1, row2, col2):
return self.cumulative_sums[row2+1][col2+1] + self.cumulative_sums[row1][col1] \
- self.cumulative_sums[row1][col2 + 1] - self.cumulative_sums[row2 + 1][col1]
matrix = [
[3, 0, 1, 4, 2],
[4, 6, 3, 2, 1],
[1, 2, 9, 1, 5],
[4, 1, 2, 1, 7],
[1, 0, 3, 0, 5]
]
mtx = RangeSum (matrix)
print mtx.range_sum(1, 2, 3, 4)
Performance
Time Complexity: Each _ query takes O(1) time and the pre-computation takes
O( ) time.
Space Complexity: O( ). The algorithm uses O( ) space to store the cumulative region
sum.
.
.
.
There’s no other one egg solution. Sure, if we’d been feeling lucky we could have gone up
the floors in two’s but imagine if the egg broke on floor 20; we have no way of knowing if it
would have also broken on floor 19!
Time Complexity: O( ). In the worst case, we might need to try from all floors.
Space Complexity: O(1).
Many eggs
If we have an infinite number of eggs (Or at least as many eggs as we need), what would
our strategy be here?
In this case, we would use one of a divide and conquer strategy, the binary search. For
example, assume we have a total of 100 floors. First we’d go to floor 50 and drop an egg. It
either breaks, or it does not. The outcome of this drop instantly cuts our problem in half.
If it breaks, we know the solution lies in the bottom half of the building (floor 1 – floor 49).
If it survives, we know the solution is in the top half of the building (floor 51 – 100). On
each drop, we keep dividing the problem in half and half again until we get to our solution.
We can quickly see that the number of drops required for this solution is , where is
the number of floors of the building.
Because this building does not have a number of floors equal to a round number power of
two, we need to round up to number of drops to get seven ( = 6.644 ≈ 7).
Using seven drops, we could solve this problem for any building up to 128 floors. Eight
drops would allow us to solve the puzzle for a building with twice the height at 256 floors.
Depending on the final answer, the actual number of eggs broken using a binary search
will vary.
Time Complexity: O( ).
Space Complexity: O(1).
floor 50. If it broke, we’d be instantly reduced to a one egg problem. So then we will have
to start with our last egg from floor 1 and keep going up one floor at a time until that
breaks. As a worst case, it will take us 50 drops.
What happens if we started off with our first egg going up by floors ten at a time? We can
start dropping our egg from floor 10; if it survives, we try floor 20, then floor 30 … we carry
on until the first egg breaks. Once we’ve broken our first egg, we know that the solution
must lay somewhere in the nine floors just below, so we back off nine floors and step
through these floors one at a time until we find a solution.
This is certainly an improvement, but what is our worst case with this strategy? Well,
imagine we dropped eggs on every 10 floor all the way up, and our first egg broke on floor
100. This has taken us ten drops so far. Now we know the solution must lay somewhere
between floor 91 and floor 99 and we have to go through these in ones, starting at floor 91.
The worst case would be - if the solution was on floor 99, and this would take us nine more
drops to determine. The worst case, therefore, if we go by tens, is 19 drops.
Thinking about the 10 floor strategy again we can see that, while our worst case is 19
drops, some other possible solutions will take less than this (for instance, if the first egg
broke on floor 10 then, at worst, from here we only have to make nine more drops to find
the solution). Knowing that, what if we drop our first egg from floor 11 instead? If the egg
breaks on this floor, it will still only take ten more drops to find the solution (and if it
doesn’t break, great, we’ve eliminated more floors than before! win-win?) Let's follow this
idea, and see where it leads to.
Well, how about if we drop our first egg from floor 12 then? A similar argument to the
above; if it breaks, we only have to try eleven floors with the second egg. If it doesn’t break,
we can step up another dozen floors, and so on … But hold on a minute! … First if we try
floor 12, then 24, then 36, then 48, 60, 72, 84, 96 then it finally breaks, we’ve wasted eight
drops already, and we still have to check (up to) eleven more floors with our second egg, so
we’re back at a worst case of 19 drops.
Problems where the solution lays lower down the building are taking less drops than when
the solution lays higher up. We need to come up with a strategy that makes things more
uniform.
What we need is a solution that minimizes our maximum regret. The above examples hint
towards what we need is a strategy that tries to make solutions to all possible answers the
same depth (same number of drops). The way to reduce the worst case is to attempt to
make all cases take the same number of drops.
As I hope you can see by now, if the solution lays somewhere in a floor low down, then we
have extra-headroom when we need to step by singles, but, as we get higher up the
building, we’ve already used drop chances to get there, so we have less drops left when we
have to switch to going floor-by-floor.
Let’s break out some algebra. Imagine that we drop our first egg from floor . If it breaks,
we can step through the previous ( − 1) floors one-by-one. If it doesn’t break, rather than
jumping up another floors, we should step up just ( − 1) floors (because we have one
less drop available if we have to switch to one-by-one floors), so the next floor we should
try is floor + ( − 1).
Similarly, if this drop does not break, we need to jump up to floor + ( − 1) + ( − 2), then
floor + ( − 1) + ( − 2) + ( − 3)…
We keep reducing the step by one each time we jump up, until that step-up is just one
floor, and get the following equation for an floor building:
+ ( − 1) + ( − 2) + ( − 3) + ( − 4) + … + 1 =
So, if the answer is , then we are trying the intervals at , − 1, − 2 … .1. Given that the
number of floors is , we have to relate these two. Since the maximum floor from which we
can try is , the total skips should be less than . This summation, as many will recognize,
is the formula for triangular numbers (which kind of makes sense, since we’re reducing the
step by one each drop we make) and can be simplified to:
+ ( − 1) + ( − 2) + ⋯ + 1 =
( + 1)
=
2
= √
Complexity of this process is O √ and for a 100 floor building, would be √100 = 13.651 ≈
14.
As an alternative, we can determine the value of by deriving the value from quadratic
equation.
+ ( − 1) + ( − 2) + ⋯ + 1 =
+ = 2
+ −2 = 0
From mathematics, if the quadratic equation is this:
+ + = 0
We could derive the value of using the formula:
− ±√ −4
=
2
For a 100 storey building, we can derive the value of from quadratic expression by using
the formula:
+ − 200 = 0
−1 ± √1 − 4 × 1 × 200
= = 13.651
2×1
We now have all the information to compute the optimal solution for the two egg problem.
Our first drop should be from floor 14. If egg survives we step up 13 floors to floor 27, then
step up 12 floors to 39 …
import math
def two_eggs(floors):
t1 = math.sqrt((1^2) + (4*1*200))
t3 = (-1 + t1)/2
t4 = (-1 - t1)/2
return math.ceil(max(t3,t4))
floors = 100
eggs = 2
print "Minimum attempts with", eggs, "eggs and", floors, "floors is", two_eggs(floors)
The optimal strategy is to work our way through the table until the first egg breaks, then
back up to one floor higher than the line above and then proceed floor-by-floor until we
find the exact solution. The maximum of 14 drops is a combination of first egg drops (made
in steps), and second egg drops (made in ones). For every drop we take hopping up the
tower, we reduce the worst-case number of single drops we have to take so that no solution
is an outlier.
Drop Number Floor Number
#1 14
#2 27
#3 39
#4 50
#5 60
#6 69
#7 77
#8 84
#9 90
#10 95
#11 99
#12 100
6 . 2 4 E e g g s a n d F f l oo r s p r o b l e m
D y n am i c Pro g r am mi n g | 403
DP solution
Let us assume that [ ] indicates the number of ways to paint the fence with boards and
k colors. If the number of boards are 0,
(0) = 0
If the number of boards are 1; we can paint the board with any of the k available colors.
k possibilities for
coloring this board
(1) =
If the number of boards are 2, we can paint both the two boards with any of the k available
colors. We are allowed to use same colors for two adjacent boards of the fence. But, we
6 . 2 5 P a i n t i n g g ra n d m o th e r ’ s ho u se f e nc e
D y n am i c Pro g r am mi n g | 404
cannot use the same color for 3 boards of the fence, as the maximum allowed adjacent
fence boards with same color is 2.
k possibilities for
coloring this board
The first and second together has options. If the first and the second do not have the
same color, the total is × ( − 1), then the third one has options.
6 . 2 5 P a i n t i n g g ra n d m o th e r ’ s ho u se f e nc e
D y n am i c Pro g r am mi n g | 405
Example
Given the following matrix, the minimum cost for painting houses is 11. House 0 is
blue, house 1 is green, and house 2 is blue, 2 + 5 + 4 = 11.
Red Blue Green
House-0 13 2 10
House-1 10 13 5
House-2 13 4 9
DP solution
We can paint the house with blue, red or green. If we paint house with red, we cannot
paint − 1 house with red. We have to select either blue or green. Let us assume that
C[i][j] represents the minimum paint cost from house 0 to house when house uses color
. To get the minimum cost for painting house, we have the following cases:
− 1: Painting house with red and painting − 1 house with blue or green
C[i][0] = min( C[i-1][1], C[i-1][2] ) + cost of painting house with red.
6 . 2 6 P a i n t i n g co l o ny h o us e s w i th r e d , b l u e a n d g r ee n
D y n am i c Pro g r am mi n g | 406
Also, to get the minimum cost for painting − 1 house, we have to select the minimum
cost painting which might come either with blue or green.
− 2: Painting house with blue and painting − 1 house with red or green
C[i][1] = min( C[i-1][0], C[i-1][2] ) + cost of painting house with blue.
Also, to get the minimum cost for painting − 1 house, we have to select the minimum
cost painting which might come either with red or green.
− 3: Painting house with green and painting − 1 house with red or blue
C[i][2] = min( C[i-1][1], C[i-1][0] ) + cost of painting house with green.
Also, to get the minimum cost for painting − 1 house, we have to select the minimum
cost painting which might come either with red or blue.
Once we populate the table, the final minimum cost for painting all houses would be the
minimum of all in the last row of table.
Minimum cost for painting all houses = min(C[n-1][0], C[n-1][1], C[n-1][2])
def paint_house_min_cost_dp(costs):
n = len(costs)
if n < 1:
return None
elif n == 1:
return min(costs[0])
C = [[0 for j in range(3)] for i in range(n)]
C[0] = costs[0]
# calculate mininum cost to paint n houses
for i in range(1, n):
# painting ith house with red: hence we have select blue or green for i-1 house
C[i][0] = min(C[i - 1][1] + costs[i][0], C[i - 1][2] + costs[i][0])
# painting ith house with blue: hence we have select red or green for i-1 house
C[i][1] = min(C[i - 1][0] + costs[i][1], C[i - 1][2] + costs[i][1])
# painting ith house with green: hence we have select red or blue for i-1 house
C[i][2] = min(C[i - 1][0] + costs[i][2], C[i - 1][1] + costs[i][2])
return min(C[n - 1])
print paint_house_min_cost_dp([[13, 2, 10], [10, 13, 5], [13, 4, 9]])
Time Complexity: O(3 ) ≈ O( ).
Space Complexity: O(3 ).
Also, painting the first house with red color costs different from painting second house with
red color. The costs are different for each house and each color. The cost of painting each
house with a certain color is represented by an cost matrix. For example, costs[0][0]
is the cost of painting house 0 with color red; costs[1][2] is the cost of painting house 1
with color green, and so on... You have to paint the houses with minimum cost. How would
you do it?
6 . 2 7 P a i n t i n g co l o ny h o us e s w i th k c o lo r s
D y n am i c Pro g r am mi n g | 407
DP solution
The idea is the same as the previous one, but can't simplify it to three variants. We can
paint the house with one of the colors. If we paint house with color, we cannot
paint − 1 house with color. We have to select one of the remaining − 1 colors. Let
us assume that C[i][j] represents the minimum paint cost from house 0 to house when
house uses color . The formula will be:
[ ][ ] = { [ − 1][ ] + [ ][ ] ≠ }
def paint_house_k_colors_dp(costs):
n = len(costs)
k = len(costs[0])
if n < 1:
return None
elif n == 1:
return min(costs[0])
C = [[float("inf") for j in range(k)] for i in range(n)]
C[0] = costs[0]
# calculate mininum cost to paint n houses
for i in range(1, n):
for j in range(0, k):
for m in range(0, k):
if m != j:
C[i][j] = min(C[i-1][m] + costs[i][j], C[i][j])
return min(C[n - 1])
print paint_house_k_colors_dp([[13, 2, 10], [10, 13, 5], [13, 4, 9]])
Time Complexity: O( ).
Space Complexity: O( ).
6 . 2 7 P a i n t i n g co l o ny h o us e s w i th k c o lo r s
D y n am i c Pro g r am mi n g | 408
[ ] = [Number of possibilities for first digit × Number of lucky numbers with − 1 digits]
− Number of unlucky numbers
[ ] = 10 × [ − 1] − 1 × 1 × [ − 2]
The first term (10 × [ − 1]) says that, pick any of the 10 values for the first digit and make
sure the rest of the number is lucky. This counts all the lucky numbers with up to digits,
but we get a few unlucky ones there, too. Namely, we also count the numbers that start
with 13 and have no other 13s appearing anywhere. The second term subtracts all the
"almost lucky" numbers we have erroneously counted. There are exactly [ Ǧ2] of them
because they all look like 13xxx, where xxx is an ( Ǧ2)digit lucky number.
def lucky(n):
'''lucky implementation with recursion'''
if n == 0:
return 0
elif n == 1:
return 1
else:
return 10*lucky(n-1) - lucky(n-2)
print lucky(3)
Time complexity: O(2 ).
Space Complexity: O(1).
Since [ ] depends on both [ Ǧ1] and [ Ǧ2], we need two base cases here.
[0] = 1 and [1] = 10
Here is a simple recursive implementation that uses a memoization table to store the
values of [ ].
def lucky_memoization(m):
'''Using memoization and using a dictionary as a table.'''
table = {}
def lucky(n):
if n not in table:
if n == 0:
table[n] = 0
elif n == 1:
table[n] = 1
else:
table[n] = 10*lucky(n-1) - lucky(n-2)
return table[n]
return lucky(m)
print lucky_memoization(3)
Time complexity: O(n).
Space Complexity: O(n).
def lucky_tabulation_dp(n):
'''Using DP and using a dictionary as a table.'''
table = {}
for i in range(n+1):
if i == 0:
table[i] = 0
elif i == 1:
table[i] = 1
else:
table[i] = 10*table[i-1] - table[i-2]
return table[n]
print lucky_tabulation_dp(3)
Time complexity: O( ).
Space Complexity: O( ).
Note that we have to actually use recursive calls to ( Ǧ1) and ( Ǧ2) in an order for
this to work. We cannot simply use [ Ǧ1] and [ Ǧ2]. This time, the function works in linear
time, computing [ ] for all the values between 2 and the target number, n. Each value
takes constant time to compute and is computed exactly once. As we have seen above,
without memoization, the () function would require exponential time – 2 recursive
calls. The difference in running time is huge.
6.28 Unlucky numbers
D y n am i c Pro g r am mi n g | 410
Examples
Given = 2, return 91. The answer should be the total numbers in the range of 0 = <
10 = 100, excluding [11, 22, 33, 44, 55, 66, 77, 88, 99].
6 . 2 9 C o u nt n u m b e r s w i t h u n i q ue d i g i ts
D y n am i c Pro g r am mi n g | 411
0 =0
⎧
⎪10 =1
ℎ =
⎨10 + 9× (10 − + 1) ≥2
⎪
⎩
def unique_digits(n):
if n == 0: return 0
if n == 1: return 10
total = 10
count = 9
for i in xrange(2, n+1):
count = count * (10 - i + 1)
total += count
return total
print unique_digits(2)
print unique_digits(3)
Time complexity: O( ), where is the number of digits.
Space Complexity: O(1).
DP solution
Above brute force algorithm gives us the hint that, number of unique numbers with
depends on the number of unique numbers with −1 .
ℎ = ℎ −1 × (10 − i + 1)
def unique_digits(n):
if n==0:
return 1
if n==1:
return 10
table = [0 for i in range(n+1)]
table[0]=0
table[1]=10
table[2]=81
total = table[0]+table[1]+table[2]
for i in range(3,n+1):
table[i] = table[i-1]*(10-i+1)
total += table[i]
return total
print unique_digits(2)
print unique_digits(3)
Time complexity: O( ), where is the number of digits.
Space Complexity: O( ).
Other alternative way of implementing the solution could be: maintain the table with
number of unique numbers with exact n digits. At the end, return the sum of all unique
numbers with exact digits for ranging from 1 .
def unique_digits(n):
if n==0:
return 1
if n==1:
return 10
table = [0 for i in range(n+1)]
6 . 2 9 C o u nt n u m b e r s w i t h u n i q ue d i g i ts
D y n am i c Pro g r am mi n g | 412
table[0]=0
table[1]=10
table[2]=81
for i in range(3,n+1):
table[i] = table[i-1]*(10-i+1)
return sum(table)
print unique_digits(2)
print unique_digits(3)
Time complexity: O( ), where is the number of digits.
Space Complexity: O( ).
= 1, = , ≥0
1 1
2 1
2
1 2
6 . 3 0 C a ta l a n n u m b e r s
D y n am i c Pro g r am mi n g | 413
3 1 3 1 2
3 2 3 1
2 1 3
2
1 2
3
Let us assume that the nodes of the tree are numbered from 1 to . Among the nodes, we
have to select some node as the root, and then divide the nodes which are less than the
root node into left subtree, and elements greater than root node into right subtree. Since
we have already numbered the vertices, let us assume that the root element we selected is
element.
If we select element as root, then we get − 1 elements on the left subtree and −
elements on the right subtree. Since is the Catalan number for elements,
represents the Catalan number for left subtree elements ( − 1 elements) and
represents the Catalan number for right subtree elements. The two subtrees are
independent of each other, so we simply multiply the two numbers. That is, the Catalan
number for a fixed value is × .
Since there are nodes, for we will get choices. The total Catalan number with nodes
can be given as:
= ×
The base case would be , and obviously, the number of ways to arrange this single node
is 1.
We can convert this simple recursive definition to code as shown below.
def Catalan(n):
if n == 0: return 1
else: count = 0
for i in range(n):
count += Catalan(i) * Catalan(n - i - 1)
return count
print Catalan(3)
Time Complexity: O .
√
6 . 3 1 B i n a r y se a r c h t r e e s w i th n v e r t i ce s
D y n am i c Pro g r am mi n g | 414
In other words, the maximum depth or height of the recursive function is n. Hence, the
space complexity of the algorithm is O(n) and it is for the runtime stack of the recursive
algorithm.
DP solution
The recursive call depends only on the numbers to and for any value of , there
are a lot of recalculations. We will keep a table of previously computed values of . If the
function () is called with parameter i, and if it has already been computed before,
then we can simply avoid recalculating the same subproblem.
def Catalan(n):
'''Using UP and using a dictionary as a table.'''
catalan=[1,1]+[0]*n
for i in range(2,n+1):
for j in range(n):
catalan[i]+=catalan[j]*catalan[i-j-1]
return catalan[n]
print Catalan(3)
To compute ( ), we need to compute all of the ( ) values between 0 and − 1,
and each one will be computed exactly once, in linear time. Hence, the time complexity of
this implementation O( ).
( )!
As seen above, the Catalan number can be represented by direct equation as: !(
.
)!
So, we can precompute the Catalan numbers and return the Catalan number from the
precomputed table.
catalan=[]
# first Catalan number is 1
catalan.append(1)
for i in range (1,1001):
x=catalan[i-1]*(4*i-2)/(i+1)
catalan.append(x)
def catalanNumber(n):
return catalan[n]
print catalanNumber(3)
[, ]= [ , ] × [ , ], ℎ 1≤ ≤ 1≤ ≤
6 . 3 1 B i n a r y se a r c h t r e e s w i th n v e r t i ce s
D y n am i c Pro g r am mi n g | 415
p p
q r
r
This corresponds to the (hopefully familiar) rule that the [ , ] entry of C is the dot product
of the (horizontal) row of A and the (vertical) column of B. Observe that there are ×
total entries in C and each takes O(q) time to compute, thus the total time to multiply these
two matrices is proportional to the product of the dimensions, .
Complexity of × : since × has × entries and each of these entries takes to
compute. The complexity is Theta( × × ).
( )= ( )× ( − )
The base case would be (1), and obviously, the number of ways to parenthesize the two
matrices is 1.
(1) = 1
6 . 3 1 B i n a r y se a r c h t r e e s w i th n v e r t i ce s
D y n am i c Pro g r am mi n g | 416
This is related to the Catalan numbers (which in turn is related to the number of different
binary trees on n nodes). As said above, applying Stirling’s formula, we find that C(n) is
/
O . Since 4 is exponential and is just polynomial, the exponential will dominate,
√
implying that function grows very fast. Thus, this will not be practical except for very small
n. In summary, brute force is not an option.
DP solution
Now let us use DP to improve the time complexity. This problem, like other dynamic
programming problems involves determining a structure (in this case, a parenthesization).
We want to break the problem into subproblems, whose solutions can be combined to solve
the global problem. As is common to any DP solution, we need to find some way to break
the problem into smaller subproblems, and we need to determine a recursive formulation,
which represents the optimum solution to each problem in terms of solutions to the
subproblems. Let us think of how we can do this. Since matrices cannot be reordered, it
makes sense to think about sequences of matrices.
How might we do this recursively? One way is that for each possible split for the final
multiplication, recursively solve for the optimal parenthesization of the left and right sides,
and calculate the total cost (the sum of the costs returned by the two recursive calls plus
the cost of the final multiplication, where “ ” depends on the location of that split).
Then take the overall best top-level split.
For dynamic programming, the key question now is: In the above procedure, as you go
through the recursion, what do the subproblems look like and how many are there?
Answer: Each subproblem looks like “what is the best way to multiply some sub-interval
of the matrices × . . .× ?” So, there are only O( ) different subproblems.
The second question is now: How long does it take to solve a given subproblem assuming
that you’ve already solved all the smaller subproblems (i.e., how much time is spent inside
any given recursive call)?
The answer is to figure out how to multiply × . . .× best, we just consider all the possible
middle points k and select the one that minimizes:
Minimum cost to multiply ... ? already computed
+
Minimum cost to multiply ... ? already computed
+
Cost to multiply the results? get this from the dimensions
This just takes O(1) work for any given k, and there are at the most n different values k to
consider, so overall we just spend O(n) time per subproblem. So, if we use dynamic
programming to save our results in a lookup table, then (since there are only O( )
subproblems) we will spend only O( ) time overall.
6 . 3 1 B i n a r y se a r c h t r e e s w i th n v e r t i ce s
D y n am i c Pro g r am mi n g | 417
The subproblems can be solved by recursively applying the same scheme. The former
problem can be solved by just considering all the possible values of k. Notice that this
problem satisfies the principle of optimality, because if we want to find the optimal
sequence for multiplying × . . .× we must use the optimal sequences for ... and
... . In other words, the subproblems must be solved optimally for the global problem
to be solved optimally.
Assume that, [ , ] represents the least number of multiplications needed to multiply ·
· · . As a basis observe that if = then the sequence contains only one matrix, and so
the cost is 0. (There is nothing to multiply.)
0 , =
[ , ] =
[ , ]+ [ + 1, ] + , <
The above recursive formula says that we have to find split point such that it produces
the minimum number of multiplications. After computing all the possible values for , we
have to select the value which gives minimum value. We can use one more table
(say, [ , ]) to reconstruct the optimal parenthesizations. Compute the [ , ] and [ , ] in a
bottom-up fashion. We will store the solutions to the subproblems in a table, and build the
table in a bottom-up manner.
First solve for all subproblems with − = 1, then solve for all with − = 2, and so on,
storing your results in an × matrix.
def matrixChainOrder(P):
""" Input: P[] = {40, 20, 30, 10, 30}, Output: 26000
There are 4 matrices of dimensions 40x20, 20x30, 30x10 and 10x30.
Let the input 4 matrices be A, B, C and D. The minimum number of
multiplications are obtained by putting parenthesis in the following way
(A(BC))D --> 20*30*10 + 40*20*10 + 40*10*30 """
n = len(P)
M = [[0 for j in range(n)] for i in range(n)]
for i in range(1, n):
M[i][i] = 0
for sublen in range(2, n + 1):
for i in range(1, n - sublen + 1):
j = i + sublen - 1
M[i][j] = float("inf")
for k in range(i, j):
M[i][j] = min(M[i][j], M[i][k] + M[k+1][j] + P[i - 1] * P[k] * P[j])
return M[1][-1]
print matrixChainOrder([40, 20, 30, 10, 30])
Performance
How many subproblems are there? In the above formula, can range from 1 and
can range from 1 . So there are a total of subproblems, and also we are doing − 1
such operations [since the total number of operations we need for A × A × A × . . . ×
is − 1]. So, the running time of this algorithm is O( ).
Space Complexity: O( ).
6 . 3 2 R o d c u tt i n g p r o b l e m
D y n am i c Pro g r am mi n g | 418
This rod cutting problem is very much related to any real-world problem we face. We have
a rod of some size and we want to cut it into parts and sell in such a way that we get the
maximum revenue out of it. Now, here is the catch, prices of different size of the pieces are
different and it is a possibility that cutting into smaller pieces can fetch more revenue than
selling a bigger piece, so a different strategy is needed.
Example
As an example, consider a rod of length 4 inches. This rod can be cut into pieces with sizes
of 1, 2, 3, or 4 inches (no cut). Assume that the prices for these rods are:
Rod length 1 2 3 4
Price 2 8 10 12
The 4 inch rod can be cut in 8 different ways as shown below.
4“
1” 3“
2” 2“
3“ 1”
1” 1” 2”
1” 2” 1”
2” 1” 1”
6 . 3 2 R o d c u tt i n g p r o b l e m
D y n am i c Pro g r am mi n g | 419
1” 1” 1” 1”
Recursive solution
As stated in problem statement, let ( ) be the maximum amount of revenue you can get
with a piece of length .
The first step in designing a recursive algorithm is determining the base case. Eventually,
all recursive steps must be reduced to the base case.
General case?
First we ask, “What is the maximum amount of revenue we can get?” And later we can
extend the algorithm to give us the actual rod decomposition that leads to that maximum
revenue. We can view the problem recursively as follows:
First, cut a piece of the left end of the rod, and sell it.
Then, find the optimal way to cut the remainder of the rod.
Now, we don’t know how large a piece we should cut off. So we try all possible cases. First
we try cutting a piece of length 1, and combining it with the optimal way to cut a rod of
length − 1. Then we try cutting a piece of length 2, and combining it with the optimal way
to cut a rod of length − 2. We try all the possible lengths and then pick the best one.
()= { ( ) + ( − ), ≥1
Alternatively, we can write it as:
6 . 3 2 R o d c u tt i n g p r o b l e m
D y n am i c Pro g r am mi n g | 420
Performance
Considering the above implementation, following is a recursion tree for an array of size 4.
_ ( , − 1) gives us the maximum revenue for a rod with length . However, the
computation time is ridiculous, because there are so many subproblems. If you draw the
recursion tree, you will see that we are actually doing a lot of extra work, because we are
computing the same things over and over again. For example, in the computation of = 4,
we compute the optimal solution for = 2 four times! It is much better to compute it once,
and then refer to it in the future recursive calls.
3 2 1 0
2 1 0 1 0 0
1 0 0 0
For an input array of size , the function _ ( , ) would make recursive calls and
_ ( , − 1) would make − 1 recursive calls and goes on...
Let ( ) be the total number of calls to _ ( , − 1), for any . As per the recursive
algorithm, since we don’t know how large a piece we should cut off, we try all possible
cases. First, we try cutting a piece of length 1, and combining it with the optimal way to
cut a rod of length − 1. Then, we try cutting a piece of length 2, and combining it with
the optimal way to cut a rod of length − 2. We try all the possible lengths and then pick
the best one.
Now, observe the recurrence (1 in the recurrence indicates the recursive call for ( )):
6 . 3 2 R o d c u tt i n g p r o b l e m
D y n am i c Pro g r am mi n g | 421
( ) = 1+ ( − )
= 1+ ( )
= 2
Time Complexity: O(2 ).
Space Complexity: O(1).
We can see that there are many subproblems which are being solved again and again. So
this problem has overlapping substructure property and recomputation of the same
subproblems can be avoided by either using memoization or tabulation.
The top-down (memoization) approach recurse from larger problems to smaller
subproblems until it reaches a pre-calculated subproblem. After that, it returns,
then combines the solutions of subproblems to solve the larger ones.
The bottom-up (tabulation) approach, as you can tell from its name, solves all the
required subproblems first, and then the larger ones.
Both methods are O( ) but the bottom-up way has better constants.
Memoization solution
One way we can do this is by writing the recursion as normal, but store the result of the
recursive calls, and if we need the result in a future recursive call, we can use the
precomputed value. The answer will be stored in the [ ].
def cutRod(prices, rodSize):
r = (rodSize + 1) * [None]
def func(p, n):
# use known answer, if one exists
if r[n] is not None:
return r[n]
# otherwise use original recursive formulation
if n == 0:
q=0
else:
q = float('-inf')
# Consider a first cut of length i, for i from 1 to n inclusive
for i in range(1, n+1):
q = max(q, p[i] + func(p, n-i)) # recur on length n-i
# memoize answer in table before returning
r[n] = q
return q
return func(prices, rodSize)
p = [0, 2, 8, 10, 12]
print cutRod(p, len(p)-1)
Performance
Each subproblem is solved exactly once, and to solve a subproblem of size , we run through
iterations of the loop. So, the total number of iterations of the loop, over all
recursive calls, forms an arithmetic series, which produces O( ) iterations in total.
Time Complexity: O( ).
Space Complexity: O(n), for the runtime stack.
6 . 3 2 R o d c u tt i n g p r o b l e m
D y n am i c Pro g r am mi n g | 422
DP solution
However if we can store the solutions to the smaller problems in a bottom-up manner
rather than recompute them, the runtime can be drastically improved (at the cost of
additional memory usage). To implement this approach, we simply solve the problems
starting for smaller lengths and store these optimal revenues in a table (of size + 1).
Then, when evaluating longer lengths, we simply look-up these values to determine the
optimal revenue for the larger piece. The answer will once again be stored in [ ].
def cutRod(p, n):
r = (n + 1) * [0]
for j in range(1, n+1):
q = float('-inf')
for i in range(1, j+1):
q = max(q, p[i] + r[j-i])
r[j] = q
return r[n]
p = [0, 2, 8, 10, 12]
print cutRod(p, len(p)-1)
Often the bottom up approach is simpler to write, and has less overhead, because you don’t
have to keep a recursive call stack. Most of the people write the bottom up procedure when
they implement a dynamic programming algorithm.
Performance
The running time of this implementation is simply:
( ) =
( + 1)
=
2
Time Complexity: O( ), because of the nested loops. Thus, we have reduced the
runtime from exponential to polynomial.
Space Complexity: O(n).
6 . 3 2 R o d c u tt i n g p r o b l e m
D y n am i c Pro g r am mi n g | 423
revenue = r[n]
pieces = []
while n > 0:
pieces.append(cuts[n])
n = n - cuts[n] # continue on what remains
return revenue, pieces
p = [0, 2, 8, 10, 12]
print cutRod(p, len(p)-1)
Time Complexity: O( ), no change in running time.
Space Complexity: O(n).
Example
Consider the following price table along with two additional arrays r and cuts. The array r
maintains the revenue of the cuts. These two arrays would get zeros as part of initialization.
Piece length, 0 1 2 3 4
Price, ( ) 0 2 8 10 12
Revenue, ( ) 0 0 0 0 0
Cuts, () 0 0 0 0 0
We don’t know how large a piece we should cut off. So we try all possible cases. First, we
try cutting a piece of length 1, and combining it with the optimal way to cut a rod of length
− 1. Then we try cutting a piece of length 2, and combining it with the optimal way to cut
a rod of length − 2. We try all the possible lengths and then pick the best one.
For this example, is 4, and the possible cut positions are: 1, 2, 3, or 4. So, let us try all
these possibilities one by one.
For the first step, = 1 (piece length is 1). For this piece, the only possible cut location is 1
(possible values for : 1 to ).
= −∞
For = 1: (1) + (1 − 1) is greater than , so update with (1) + (1 − 1), and [1] with
1.
→ = (1) + (1 − 1) = 2 + (0) = 2 + 0 = 2
→ [1] = 1
This completes the processing of piece with length 1. Hence, update (1) with .
Piece length, 0 1 2 3 4
Price, ( ) 0 2 8 10 12
Revenue, ( ) 0 2 0 0 0
Cuts, () 0 1 0 0 0
Next, = 2 (piece length is 2). For this piece, the possible cut locations are 1, and 2 (1 to ).
= −∞
For = 1: (1) + (2 − 1) is greater than , so update with (1) + (2 − 1), and [2] with
1.
→ = (1) + (2 − 1) = 2 + (1) = 2 + 2 = 4
→ [2] = 1
For = 2: (2) + (2 − 2) is greater than , so update with (2) + (2 − 2), and [2] with
2.
→ = (2) + (2 − 1) = 8 + (0) = 8 + 0 = 8
→ [2] = 2
This completes the processing of piece with length 2. Hence, update (2) with .
6 . 3 2 R o d c u tt i n g p r o b l e m
D y n am i c Pro g r am mi n g | 424
Piece length, 0 1 2 3 4
Price, ( ) 0 2 8 10 12
Revenue, ( ) 0 2 8 0 0
Cuts, () 0 1 2 0 0
Next, = 3 (piece length is 3). For this piece, the possible cut locations are 1, 2, and 3 (1 to
).
= −∞
For = 1: (1) + (3 − 1) is greater than , so update with (1) + (3 − 1), and [3] with
1.
→ = (1) + (3 − 1) = 2 + (2) = 2 + 8 = 10
→ [3] = 1
For = 2: (2) + (3 − 2) is not greater than (10), so skip it.
For = 3: (3) + (3 − 3) is not greater than (10), so skip it.
This completes the processing of piece with length 3. Hence, update (3) with .
Piece length, 0 1 2 3 4
Price, ( ) 0 2 8 10 12
Revenue, ( ) 0 2 8 10 0
Cuts, () 0 1 2 1 0
Next, = 4 (piece length is 4). For this piece, the possible cut locations are 1, 2, 3, and 4 (1
to ).
= −∞
For = 1: (1) + (4 − 1) is greater than , so update with (1) + (4 − 1), and [4] with
1.
→ = (1) + (4 − 1) = 2 + (3) = 2 + 10 = 12
→ [4] = 1
For = 2: (2) + (4 − 2) is greater than , so update with (2) + (4 − 2), and [4] with
2.
→ = (2) + (4 − 2) = 8 + (2) = 8 + 8 = 16
→ [4] = 2
For = 3: (3) + (4 − 3) is not greater than (16), so skip it.
For = 4: (4) + (4 − 4) is not greater than (16), so skip it.
This completes the processing of piece with length 4. Hence, update (3) with .
Piece length, 0 1 2 3 4
Price, ( ) 0 2 8 10 12
Revenue, ( ) 0 2 8 10 16
Cuts, () 0 1 2 1 2
Now, we have completed the processing of all possible cut locations. Hence, the maximum
revenue is 16 which is [ ].
To retrieve the actual cut locations for this revenue, let us use the array and trace it
back. The maximum revenue is at [ ] and corresponding cut is [ ] which is 2.
Hence, the rod is cut once at location 2. For the remaining piece of length 2, [2] is 2.
Which means no need of another cut. So, the final result would be [2, 2]. It means, cutting
the rod at location 2 would give us the maximum revenue (16).
6 . 3 2 R o d c u tt i n g p r o b l e m
D y n am i c Pro g r am mi n g | 425
The constraint in the 0/1 knapsack problem is that, each item must be put entirely in the
knapsack or not included at all. Objects cannot be broken up (i.e. included fractionally).
For example, suppose we are going by flight, and assume there is a limitation on the
luggage weight. Also, the items which we are carrying can be of different types (like laptops,
etc.). In this case, our objective is to select the items with maximum value. That means, we
need to tell the customs officer to select the items which have more weight and less value
(profit).
It is this 0/1 property that makes the knapsack problem hard; a simple greedy algorithm
finds the optimal selection whenever we are allowed to subdivide objects arbitrarily. For
each item, we could compute its “value per unit cost”, and take as much of the most
expensive item until we have it all or the knapsack is full. Repeat with the next most
expensive item, until the knapsack is full. Unfortunately, this 0/1 constraint is usually
inherent in most applications.
This problem is of interest in its own right because it formalizes the natural problem of
selecting items so that a given budget is exceeded, but profit is as large as possible.
Questions like that often arise as subproblems of other problems.
Many real-world problems relate to the knapsack problem:
Cutting stock,
Cargo loading,
Production scheduling,
Project selection,
Capital budgeting,
Portfolio management, and many more.
Since it is NP-hard, the knapsack problem is the basis for a public key encryption system.
: In the , we are given a knapsack with capacity and
a set of items. Each item of comes along with a value (profit) and a weight . We
are asked to choose a subset of the items as to maximize total profit but the total weight
not exceeding the capacity .
6 . 3 3 0 - 1 K n a p sa c k p r o b l e m
D y n am i c Pro g r am mi n g | 426
… …
Algorithm:
for every subset of n items:
if the subset fits in the knapsack, record its value
Select the subset that fits with the largest value
For each of the possible selection of items, we check the total value of all selected items
and their total weight. If the weight is less than the capacity and if the sums of the values
of the selected items is greater than the previous maximum value seen so far, then update
the best value with the current value.
from random import randint
def binary(i, digits):
"""Return i as a binary string with given number of digits,
padding with leading 0's as needed.
>>> binary(0, 4)
6 . 3 3 0 - 1 K n a p sa c k p r o b l e m
D y n am i c Pro g r am mi n g | 427
'0000'
"""
result = bin(i)[2:] # Python's built-in gives e.g. '0b0'
return '0'*(digits-len(result)) + result
def knapsack(weights, values, capacity):
n = len(weights)
count = 2**n
(best_value, best_items, best_weight) = (0, [], 0)
for i in xrange(count):
i_base2 = binary(i, n)
print ('i_base2 = {}'.format(i_base2))
item_numbers = filter(lambda item: i_base2[item]=='1', range(n))
print ('item numbers = {}'.format(item_numbers))
weight = reduce(lambda w,item: w + weights[item], item_numbers, 0)
print ('weight = {}'.format(weight))
if weight <= capacity:
value = reduce(lambda w,item: w + values[item], item_numbers, 0)
print ('value = {}'.format(value))
if value > best_value:
(best_value, best_items, best_weight) = (value, item_numbers, weight)
print ('best_value = {}'.format(best_value))
print 'The answer is item numbers={}, value={}.'.format(best_items, best_value)
def main():
n=6
# Generate a sample problem.
# 1. Random weights and values
(min_weight, max_weight, min_value, max_value) = (2,20, 10,50)
weights = map(lambda x: randint(min_weight, max_weight), range(n))
values = map(lambda x: randint(min_weight, max_weight), range(n))
# 2. An arbitrary but consisten capacity.
capacity = sum(weights)/2 #
template = ' {:6} {:6} {:6}'
print template.format('item', 'weight', 'value')
print template.format('-'*8, '-'*8, '-'*8)
for i in range(n):
print template.format(i, weights[i], values[i])
knapsack(weights, values, capacity)
if __name__ == '__main__':
main()
Performance
In the above algorithm, for each of the selections, we have to check how many set bits
(one’1) are present in the current binary number, and for those items which has 1, we have
to sum the values and weights. This would cost O( ). Since there have been 2 possibilities
for selecting the items, the overall complexity would be O( × 2 ). Therefore, the
complexity of the brute force algorithm is O( 2 ). Since the complexity of this algorithm
grows exponentially, it can only be used for small instances of the inputs.
6 . 3 3 0 - 1 K n a p sa c k p r o b l e m
D y n am i c Pro g r am mi n g | 428
0/1 knapsack, each item is either fully chosen or not chosen at all. Recall how we found
an optimal solution for fractional knapsack problem when our greedy choice function
picked as much as possible the next item with the highest value-to-weight ratio.
If we apply the greedy technique for this 0/1 knapsack, it might not give the optimal
solution. For example, the optimal solution is to choose items 1 and 3, with total value of
9 and total capacity 4 (which fills the knapsack). But, the greedy algorithm produces, 1
and a fraction of item 2 which is not optimal.
DP solution
A dynamic programming solution can be designed in a way that produces the optimal
answer. To do this, we must:
1. Identify a recursive definition of how a larger solution is built from optimal results
for smaller subproblems.
2. Create a table that we can build bottom-up to calculate results for subproblems
and eventually solve the entire problem.
How can we break an entire problem down into subproblems in a way that uses optimal
results of subproblems? First we need to make sure we have a clear idea of what a
subproblem solution might look like.
6 . 3 3 0 - 1 K n a p sa c k p r o b l e m
D y n am i c Pro g r am mi n g | 429
(0, ) = 0, 0 ≤ ≤
2. If you have a knapsack with zero capacity, you can’t add an item to it. If weight
capacity is 0, then no benefit can be obtained and hence we return 0 as value with
an empty collection.
( , 0) = 0, 0 ≤ ≤
General case
Assume that for some given values of and we already had a correct solution to a
subproblem stored in [ − 1, ]. We want to extend this subproblem, and the question at
this point is now:
Can I add item to the knapsack, and if I can, will this improve the total? I might be able
to add this item but only if I remove one or more items that reduce the overall value, I
don’t want that!
There are really three cases to consider when calculating [ , ] for some given values of
and :
(, )= { ( − 1, ), ( − 1, − )+ }
1. Let’s assume for the moment that we are able to add this item to the knapsack
that has capacity . In this case, the total value for this subproblem will be plus
the best solution that exists for the − 1 items that came before for the smaller
knapsack weight − . If we’re going to add the item, we have less room for the
previous − 1 items. In this case, the value for ( , ) will thus be ( − 1, − w ) +
.
2. But adding this item may not be a good idea. Perhaps the best solution at this
point does not include this item. In that case, the value [ , ] would be what we
had for the previous − 1 items, or simply ( − 1, ).
3. It might not be possible to add this item to the knapsack – there may not be room
for it! This would be the case if − < 0 (that is, < ). In this case, we can’t add
the item, so the value to store would be the best we had for the − 1 items that
came before it, ( − 1, ) (same as case 2 above).
Now, we have all the data required to solve this 0/1 knapsack problem recursively. Code
for this recursive algorithm would look like the following:
from random import randint
def knapsack(weights, values, capacity, i):
# Base cases
if i == 0 or capacity == 0 :
return 0
# return the maximum of two cases:
# 1. ith item selected
# 2. ith item not selected
else:
return max(values[i-1] + knapsack(weights, values, capacity - weights[i-1], i-1), \
knapsack(weights, values, capacity, i-1))
def main():
n=6
# Generate a sample problem.
6 . 3 3 0 - 1 K n a p sa c k p r o b l e m
D y n am i c Pro g r am mi n g | 430
Performance
The above recursive version would have the same running time as that of brute force
algorithm. The number of recursive calls are equal to the number of possible binary
representations with bits. Notice that, in the recursive version, we are not required to
sum the values and weights. The maximum of two recursive calls would be taken and
return to its parent function. Therefore, the complexity of the recursive algorithm is O(2 ).
V(1,C)
V(2,C) V(2, C- )
Clearly, this code requires a recursive stack, hence the space complexity is equal to the
depth of the stack. In our demonstration above, you can see that the recursion stack is
level deep where is the number of items or the length of the items array. We can say that
the space complexity is O( ).
6 . 3 3 0 - 1 K n a p sa c k p r o b l e m
D y n am i c Pro g r am mi n g | 431
We can build the table as shown below with the weight bounds as the columns and the
number of items as the rows. Also, it computes the table in bottom-up fashion by working
in row-major fashion, i.e. calculating an entire row for increasing the values of , then
moving to the next row, etc.
[, ] =0 1 2 …
=0 0 0 0 0 0
1 0
2 0
… 0
0
At this point, we know how to calculate the value for ( , ) for given values of and . The
pseudo-code looks like this:
if ( no room for item i):
V(i, j) = V(i-1, j) # best result for i-1 items
elif ( best to add item i ):
V(i, j) = + V(i-1, j- ) # Case 1 above
else: # not best to add item i
V(i, j) = V(i-1, j) # best result for i-1 items
How do we know if it is the best to add item ? We simply compare the values that would
be assigned to [ , ] in the last two cases of the if/else sequence above. Thus this code
fragment would become:
if (j- < 0): # no room for item i
V(i, j) = V(i-1, j) # best result for i-1 items
else:
val_with_ith_item = + V(i-1, j- ) # Case 1 above
val_without_ith_item = V(i-1, j) # best result for i-1 items
V[i, j] = max(val_with_ith_item, val_without_ith_item )
6 . 3 3 0 - 1 K n a p sa c k p r o b l e m
D y n am i c Pro g r am mi n g | 432
# If this next item (i-1) won't fit into the V[i][j] solution,
# then the best we have is the previous solution, without it.
V[i][j] = V[i-1][j]
else:
# 1) the previous solution without it, or
# 2) the best solution without the weight of (i-1).
V[i][ j] = max(V[i-1][ j], V[i-1][ j - weights[i-1]] + values[i-1])
# At this point we have the matrix of all smaller solutions
# and the value of the total solution.
(value, j, items) = (V[n][C], C, [])
for i in range(n, 0, -1):
print('i={}, value={}, j={}, items={}'.format(i, value, j, items))
if weights[i-1] <= j:
if V[i-1][ j] < V[i-1][ j-weights[i-1]] + values[i-1]:
(value, j, items) = (value - values[i-1], j - weights[i-1], items + [i-1])
items.reverse()
print 'The answer is: items={}, value={}.'.format(items, V[n][C])
def main():
n=6
# Generate a sample problem.
# 1. Random weights and values
(min_weight, max_weight, min_value, max_value) = (2,20, 10,50)
weights = map(lambda x: randint(min_weight, max_weight), range(n))
values = map(lambda x: randint(min_weight, max_weight), range(n))
# 2. An arbitrary but consisten C.
C = sum(weights)/2 #
template = ' {:6} {:6} {:6}'
print template.format('item', 'weight', 'value')
print template.format('-'*8, '-'*8, '-'*8)
for i in range(n):
print template.format(i, weights[i], values[i])
knapsack(weights, values, C)
if __name__ == '__main__':
main()
Performance
Note on runtime: Clearly the running time of this algorithm is O( × ), based on the nested
loop structure and the simple operation inside both the loops. Comparing this with the
brute force algorithm O( 2 ), we find that depending on , either the dynamic programming
algorithm is more efficient or the brute force algorithm could be more efficient. For example,
for = 6, = 100000, brute force is preferable, but for = 40 and = 1000, the dynamic
programming solution is preferable.
Since the time to calculate each entry in the table [ ][ ] is constant, the time complexity
is O( ).
This is not an in-place algorithm, since the table requires O( × ) cells and this is not
linear in . But dynamic programming algorithms save time by storing results, so we
wouldn't expect any dynamic programming solution to be in-place.
Important: Is the time-complexity of this solution polynomial in terms of its input sizes? It
might appear to be so, but technically this is an exponential solution. Why?
6 . 3 3 0 - 1 K n a p sa c k p r o b l e m
D y n am i c Pro g r am mi n g | 433
Consider the size of the input values that make up the values and weights. There are two
values for each of the input items. But, consider the value , the knapsack capacity. This
is one value, and it's always one input item no matter what value it takes on. But if this
value changes, the size of the table and the time it takes to create it changes. For example,
if the capacity doubles, then the execution time and the space used also doubles.
But is this different than, say, sequential search? In that problem, if is the number of
items in the array, if doubles, then the time of execution also doubles (since sequential
search has linear time-complexity). But is a count of the input items in this problem,
whereas in the knapsack problem, the capacity W is a value that is processed (not a count
of input items). The complexity depends on the size of this single value.
Most often we think of integer values as being encoded in binary notation. The size of a
value is the number of bits required to store it. Thus if the size of an input storing a value
like C increases by one (i.e., one bit), then that input could represent twice as many values.
For this reason, the complexity of the dynamic programming solution for the knapsack
problem (and many other problems) grows exponentially. For this problem, if the size of
increases by one bit, the amount of work doubles. Compare this to a problem where the
amount of work is proportional to 2 : if the input size increases to + 1, then the amount
of work doubles.
However, as is true for many algorithms with exponential time-complexity, this solution
will run in a reasonable amount of time for many values of and .
Greedy solution
The algorithm for solving this real-life problem is exactly what we do in real life, which is
to use always the greatest value coins for the existing amount and to use as many of those
coins as possible without exceeding the existing amount. After deducting this sum from
the existing amount, we use the remainder as the new existing amount and repeat the
process.
This is a greedy algorithm. We apply the best solution for the current step without regard
for the optimal solution. This is usually easy to understand and simple to implement. With
some coin combinations (say, American currency), the greedy algorithm provides the best
solution because locally optimal solutions lead to globally optimal solutions as well.
6 . 3 4 M a k i n g c h a ng e p r o b l e m
D y n am i c Pro g r am mi n g | 434
The greedy algorithm might not work for all coin combinations. For example, the greedy
algorithm is not optimal for the problem of making change with the minimum number of
coins when the denominations are 1, 5, 10, 20, 25, and 50. In order to make 40 rupees, the
greedy algorithm would use three coins of 25, 10, and 5 rupees. The optimal solution is to
use two 20 rupee coins. This solution is not optimal, however, as we can produce 40 with
two 20s.
def make_change(C):
denominations = [10, 5, 1] # must be sorted
coins_count = 0
for coin in denominations:
# Update coins_count with the number of denominations 'are held' in the C.
coins_count += C // coin
# Put remainder to the residuary C.
C %= coin
return coins_count
print make_change(40)
The greedy algorithm always provides a solution but doesn’t guarantee the smallest
number of coins used. Assuming that one of the coins is a one rupee coin, the greedy
algorithm takes O( ) for any kind of coin set denomination, where n is the number of
different coins in a particular set.
Naïve algorithm
The first naive solution (brute force algorithm) we could think of would be to try all coin
combinations that sum up to and keep the one using the least coins. Assuming that we
have n different coin values this solution could end up being as slow as O( ), in the case
where we have to try all possible combinations. Even though the final result would be
correct (provided the implementation is correct) this solution is very slow and would almost
definitely not be accepted in a programming competition.
Recursive solution
Let us simplify the problem as follows:
Given a positive integer , how many ways can we make change for rupees using the
coins with denominations 1, 5, 10, 20, 25, and 50 rupees?
Recursively, we could break down the problem as follows. To make change for rupees we
could:
1) Give the customer a 50 rupee coin, and make change for − 50 rupees
2) Give the customer a 25 rupee coin, and make change for − 25 rupees
3) Give the customer a 20 rupee coin, and make change for − 20 rupees
4) Give the customer a 10 rupee coin, and make change for − 10 rupees
5) Give the customer a 5 rupee coin, and make change for − 5 rupees
6) Give the customer a 1 rupee coin, and make change for − 1 rupees
Among all the recursive sub calls, we would consider the recursive call which gives us the
minimum coins.
6 . 3 4 M a k i n g c h a ng e p r o b l e m
D y n am i c Pro g r am mi n g | 435
= 79
50 25 20 10 5 1
= 29 = 54 = 59 = 69 = 74 = 78
Let us formulate the recurrence. Let ( ) indicate the minimum number of coins required
to make change for the amount of money equal to .
( )= { ( − )} + 1
That is, if the coin denomination is the last coin denomination added to the solution,
then the optimal way to finish the solution with that one is to optimally make change for
the amount of money j − d and then add one extra coin with denomination . In the above
recursive definition, +1 indicates that, we are adding a coin as part of the current iteration
to make change for the amount j.
The objective is to find the value ( ) as it indicates the minimum number of
coins required to make change for the amount of money equal to and given
amount is C.
6 . 3 4 M a k i n g c h a ng e p r o b l e m
D y n am i c Pro g r am mi n g | 436
Performance
There's a whole bunch of stuff going on here, but one of the things you'll notice is that the
larger n gets, the slower and slower this will run, or maybe your computer will run out of
stack space. Further analysis will show that many, many method calls get repeated in the
course of a single initial method call.
Time Complexity: In the worst case, complexity is exponential in the number of the coins
n. The reason is that every coin denomination could have at the most values. Therefore,
the number of possible combinations is:
× × …× = × × …×
= O( )
Space Complexity: In the worst case, the maximum depth of recursion is n. Therefore, we
need O(n) space for the system recursive runtime stack.
6 . 3 4 M a k i n g c h a ng e p r o b l e m
D y n am i c Pro g r am mi n g | 437
Performance
Time Complexity: O( ), where is the amount, n is the denomination count. In the worst
case the recursive tree of the algorithm has height of and the algorithm solves only
subproblems because it caches pre-calculated solutions in a table. Each subproblem is
computed with n iterations, one by coin denomination. Therefore there is O( ) time
complexity.
Space Complexity: O( ), where is the amount to change. We use extra space for the
memoization table.
Example
As an example, let us assume that we have the coins with denominations 1, 5, 10, 20, 25,
and 50. To make change for = 40, we would start with getting change for 0. That is, the
minimum number of coins to make change for zero rupees is 0.
[0] = 0
To make change for 1 rupee, we would consider all the denominations and select the one
which gives the minimum number of coins.
1 + [1 − 1] = 1 + [0] = 1
⎧ ⎫
⎪ 1 + [1 − 5] = ∞ ⎪
⎪ ⎪
1 + [1 − 10] = ∞
[1] = =1
⎨ 1 + [1 − 20] = ∞ ⎬
⎪
⎪ 1 + [1 − 25] = ∞ ⎪
⎪
⎩ 1 + [1 − 50] = ∞ ⎭
Now, to make change for 2 rupees, we would consider all the denominations and select the
one which gives the minimum number of coins.
1 + [2 − 1] = 1 + [1] = 2
⎧ ⎫
⎪ 1 + [2 − 5] = ∞ ⎪
⎪ ⎪
1 + [2 − 10] = ∞
[2] = =2
⎨ 1 + [2 − 20] = ∞ ⎬
⎪
⎪ 1 + [2 − 25] = ∞ ⎪
⎪
⎩ 1 + [2 − 50] = ∞ ⎭
Similarly, [3] = 3, [4] = 4, and [5] = 1.
6 . 3 4 M a k i n g c h a ng e p r o b l e m
D y n am i c Pro g r am mi n g | 438
Now, to make change for 6 rupees, we would consider all the denominations and select the
one which gives the minimum number of coins.
1 + [6 − 1] = 1 + [5] = 2
⎧ ⎫
⎪ 1 + [6 − 5] = 1 + [1] = 2 ⎪
⎪ ⎪
1 + [6 − 10] = ∞
[6] = =2
⎨ 1 + [6 − 20] = ∞ ⎬
⎪
⎪ 1 + [6 − 25] = ∞ ⎪
⎪
⎩ 1 + [6 − 50] = ∞ ⎭
This process would continue in the bottom up fashion for the value [ ] which indicates
the minimum number of coins to make change for the amount rupees, and that is the
result.
def print_coins(min_coins, denominations):
start = len(min_coins) - 1
if min_coins[start] == -1:
print "No Solution Possible."
return
print "Coins:",
while start != 0:
coin = denominations[min_coins[start]]
print "%d " % coin,
start = start - coin
def make_change(denominations, C):
cols = C + 1
table =[0 if idx == 0 else float("inf") for idx in range(cols)]
min_coins = [-1 for _ in range(C + 1)]
for j in range(len(denominations)):
for i in range(1, cols):
coin = denominations[j]
if i >= denominations[j]:
if table[i] > 1 + table[i - coin]:
table[i] = 1 + table[i - coin]
min_coins[i] = j
print_coins(min_coins, denominations)
return table[cols - 1]
print make_change([1, 5, 10, 20, 25, 50], 40)
Performance
Time Complexity: O( ). Since we are solving C subproblems and each of them requires
minimization of terms.
Space Complexity: O( ).
Example
Goal of this problem is to find a subsequence that is just a subset of elements and does
not happen to be contiguous. But the elements in the subsequence should form a strictly
increasing sequence and at the same time the subsequence should contain as many
elements as possible.
6 . 3 5 L o n g e s t i nc r e a s i ng s u b se q u e n ce [ L I S ]
D y n am i c Pro g r am mi n g | 439
2
5 8 9 20 14 7 19
For example, if the sequence is (5,6,2,3,4,1,9,9,8,9,5), then (5,6), (3,5), (1,8,9) are all increasing
subsequences. The longest one of them is (2, 3, 4, 8, 9). Following are few other examples:
Array Few possible LIS Length of LIS
[] [] 0
[14] [14] 1
[14, 21, 7, 10, 5, 8, 9, 20] [5, 8, 9, 20], [7, 8, 9, 20] 4
[5, 8, 9, 20, 14, 2, 7, 19] [5, 8, 9, 14, 19] 5
[5, 8, 9, 20, 0, 2, 7, 19] [5, 8, 9, 20], [5, 8, 9, 19], [0, 2, 7, 19] 4
[14, 2, 7, 9, 15, 8, 17, 20] [2, 7, 9, 15, 17, 20] 6
Like we did for other problems, let's concentrate on computing the length of the longest
increasing subsequence. Once we have that, we will figure out how to rebuild the
subsequence itself. The first step is to come up with a recursive solution.
Recursive solution
The first step in designing a recursive algorithm is determining the base case. Eventually,
all recursive steps must reduce to the base case.
General case?
Now, let us generalize the discussion and decide about element. For element, there
are two possibilities:
1. Include current element in LIS if it is greater than the previous element in LIS, and
recurse for remaining items.
2. Exclude current element from LIS, and recurse for remaining items.
Finally, return the maximum value either by including or excluding current element.
Let ( ) represent the longest increasing subsequence starting at index 0, and ending at
. The optimal way to obtain a strictly increasing subsequence ending at position is to
extend some subsequence starting at some earlier position . For this the recursive formula
can be written as:
1, =0
()=
{ [ ] [ ]} { ( ) + 1}, ≥1
The above recurrence says that we have to select some earlier position which gives the
maximum subsequence. The 1 in the recursive formula indicates the addition of
element.
6 . 3 5 L o n g e s t i nc r e a s i ng s u b se q u e n ce [ L I S ]
D y n am i c Pro g r am mi n g | 440
0 … … …
After finding the maximum subsequences for all positions, we have to select the one among
all which gives the maximum subsequence and it is defined as:
{ ( )}
global max_lis_length
def lis(A, i):
# Declaration to allow modification of the global copy of max_lis_length
global max_lis_length
if i == 0: # Base case
return 1
max_lis_with_ith_element = 1
for j in xrange(0, i):
if A[j] < A[i]:
max_lis_with_ith_element = max(max_lis_with_ith_element, 1 + lis(A, j) )
# Check if currently calculated LIS ending at A[i] is longer than
# the previously calculated LIS and update max_lis_length accordingly
if (max_lis_length < max_lis_with_ith_element):
max_lis_length = max_lis_with_ith_element
return max_lis_with_ith_element
def main(): # test code
# Following declaration is needed to modify the global max_lis_length in lis()
global max_lis_length
max_lis_length = 1
A = [5, 8, 9, 20, 14, 2, 7, 19]
print "Length of LIS is", lis(A, len(A)-1)
if __name__=="__main__":
main()
Performance
Considering the above implementation, following is recursion tree for an array of size 4.
( , − 1) gives us the length of for the array of elements .
lis(A, 4)
lis(A, 1) lis(A, 0)
lis(A, 0)
For an input array of size , the function ( , ) would make recursive calls and ( , −
1) would make − 1 recursive calls and goes on... Let ( ) be the number of calls to ( , ),
for any A. Now, observe the recurrence:
6 . 3 5 L o n g e s t i nc r e a s i ng s u b se q u e n ce [ L I S ]
D y n am i c Pro g r am mi n g | 441
( ) = ( − 1) + ( − 2) + ( − 3) + . . . + (1)
where recursively ( − 1) = ( − 2) + ( − 3)+ . . . + (1)
Clearly, we can observe that:
( ) = 2 ∗ ( − 1)
So our compact recurrence relation is: ( ) = 2 ∗ ( − 1)
( ) = 2 ∗ ( − 1)
( − 1) = 2 ∗ ( − 2)
. .. … …
(2) = 2 ∗ (1)
(1) = 2 ∗ (0)
Among the above equations, multiply the equation by 2 and then add all the
equations. We then clearly have:
( ) = (2 ) ∗ (0) = O(2 )
Time Complexity: O(2 ).
Space Complexity: O(1).
DP solution
We can see that there are many subproblems which are solved again and again. So this
problem has overlapping substructure property and recomputation of same subproblems
can be avoided by either using memoization or tabulation.
The problem has an optimal substructure. That means, the problem can be broken down
into smaller, simple “subproblems”, which can further be divided into yet simpler, smaller
subproblems until the solution becomes trivial. The above solution also exhibits
overlapping subproblems. With the recursion tree of the solution, we could see that the
same subproblems are getting computed again and again. We know that problems having
optimal substructure and overlapping subproblems can be solved by using dynamic
programming, in which subproblem solutions are memoized rather than computed again
and again. The memoized version follows the top-down approach, since we first break the
problem into subproblems and then calculate and store values.
We can also solve this problem in a bottom-up manner. In the bottom-up approach, we
solve smaller subproblems first, then solve larger subproblems from them. The following
bottom-up approach computes [ ], for each 0 ≤ < , which stores the length of the
longest increasing subsequence of subarray [0.. ] that ends with [ ]. To calculate [ ],
we consider of all smaller values of (say ) already computed and pick the maximum
[ ] where [ ] is less than the current element [ ].
Following is a tabulated implementation of the LIS problem.
def lis(A):
LIS = [1 for _ in range(len(A))]
for i in range(len(A)):
for j in range(i):
if A[j] <= A[i]:
LIS[i] = max(LIS[i], LIS[j] + 1)
return max(LIS)
A = [5, 8, 9, 20, 14, 2, 7, 19]
print lis(A)
Example
This idea is relatively easy. For this problem, we create another array where the element
contains the longest increasing subsequence found from the first element to the
6 . 3 5 L o n g e s t i nc r e a s i ng s u b se q u e n ce [ L I S ]
D y n am i c Pro g r am mi n g | 442
element, and which includes the element. Let the array be LIS[]. LIS[ ] will contain the
increasing subsequence length which includes the element.
If we take any element from the sequence, it’s a valid increasing subsequence, but it may
not be the longest. So, initially we say that all the values in LIS[] are 1.
Index→ 0 1 2 3 4 5 6 7
Input array, 5 8 9 20 14 2 7 19
LIS 1 1 1 1 1 1 1 1
Now, we start from the leftmost element which is 5 and since there are no elements on left
side of 5, no further processing is required for the element 5.
Index→ 0 1 2 3 4 5 6 7
Input array, 5 8 9 20 14 2 7 19
LIS 1 2 1 1 1 1 1 1
Now, the next element is 8, and we try to find all the numbers which are less than 8 and
which lie before this 8. The only element which is in the left and less than 8 is 5. So,
definitely we can make the sequence [5, 8]. So, LIS[] value for the element 8 will be 2.
Index→ 0 1 2 3 4 5 6 7
Input array, 5 8 9 20 14 2 7 19
LIS 1 2 1 1 1 1 1 1
Now, the next element is 9 and the LIS[] value is 1. The elements which are on the left and
less than 9 are 5, and 8. Observe that since the LIS[] value of 5 is 1, that means if we take
5 we could somehow make a subsequence which contains 5 and whose length is 1. And
now we have an element (9) which is greater than 5. So, obviously their LIS[] value will be
2. Next, the LIS[] value of 8 is already 2, but if we add 9 after 8, then the LIS[] value of 9
will be 3. So, we will update LIS[] value for 9 as 3.
Index→ 0 1 2 3 4 5 6 7
Input array, 5 8 9 20 14 2 7 19
LIS 1 2 3 1 1 1 1 1
The next element is 20 and the LIS[] value is 1. The elements which are less than 20 and
to the left are 5, 8, and 9. Observe that since the LIS[] value of 5 is 1, and if we add the
element (20) which is greater than 5; the LIS[] value of 20 will be 2. Next, the LIS[] value of
8 is already 2, and if we add 20 after 8, then the LIS[] value of 20 will be 3. Similarly, the
LIS[] value of 9 is already 3, and if we add 20 after 9, then the LIS[] value of 20 will be 4.
Index→ 0 1 2 3 4 5 6 7
Input array, 5 8 9 20 14 2 7 19
LIS 1 2 3 4 1 1 1 1
Next, there is 14 whose LIS[] value is 1. The elements which are less than 14 and on the
left are 5, 8, and 9. Observe that since the LIS[] value of 5 is 1, and if we add the element
(14) which is greater than 5; the LIS[] value of 14 will be 2. Next, the LIS[] value of 8 is
already 2, and if we add 14 after 8, then the LIS[] value of 14 will be 3. Similarly, the LIS[]
value of 9 is already 3, and if we add 14 after 9, then the LIS[] value of 14 will be 4.
Index→ 0 1 2 3 4 5 6 7
Input array, 5 8 9 20 14 2 7 19
LIS 1 2 3 4 4 1 1 1
The next element is 2 but there is no element on the left which is less than 2.
Index→ 0 1 2 3 4 5 6 7
Input array, 5 8 9 20 14 2 7 19
LIS 1 2 3 4 4 1 1 1
6 . 3 5 L o n g e s t i nc r e a s i ng s u b se q u e n ce [ L I S ]
D y n am i c Pro g r am mi n g | 443
The next element is 7 whose LIS[] value is 1. The only element which is less than 7 and to
the left is 5. Observe that since the LIS[] value of 5 is 1, and if we add the element (7) which
is greater than 5; the LIS[] value of 7 will be 2.
Index→ 0 1 2 3 4 5 6 7
Input array, 5 8 9 20 14 2 7 19
LIS 1 2 3 4 4 1 2 1
Next, there is 19 whose LIS[] value is 1. The elements which is less than 19 and to the left
are 5, 8, 9, and 14. Observe that since the LIS[] value of 5 is 1, and if we add the element
(19) which is greater than 5; the LIS[] value of 19 will be 2. Next, the LIS[] value of 8 is
already 2, and if we add 19 after 8, then the LIS[] value of 19 will be 3. Next, the LIS[] value
of 9 is already 3, and if we add 19 after 9, then the LIS[] value of 19 will be 4. Similarly, the
LIS[] value of 14 is already 4, and if we add 19 after 14, then the LIS[] value of 19 will be 5.
Index→ 0 1 2 3 4 5 6 7
Input array, 5 8 9 20 14 2 7 19
LIS 1 2 3 4 4 1 2 5
Now we iterate through the [] and find the maximum element, which is the result. So,
for the given sequence the result is 5.
Index→ 0 1 2 3 4 5 6 7
Input array, 5 8 9 20 14 2 7 19
LIS 1 2 3 4 4 1 2 5
Reconstructing an LIS
Now you may want to reconstruct one of the longest increasing subsequences. The
technique is quite easy.
Find an item whose LIS[] value is maximum. The element 19 has the maximum LIS value
(5).
Index→ 0 1 2 3 4 5 6 7
Input array, 5 8 9 20 14 2 7 19
LIS 1 2 3 4 4 1 2 5
Now iterate to the left and find an element which is less than 19 and whose LIS[] value is
one less than 5. So, we have 14 whose LIS[] value is 4 and 14 < 19.
Index→ 0 1 2 3 4 5 6 7
Input array, 5 8 9 20 14 2 7 19
LIS 1 2 3 4 4 1 2 5
Again iterate to the left for the next item which is less than 14 and whose LIS[] value is 3.
We have 9.
Index→ 0 1 2 3 4 5 6 7
Input array, 5 8 9 20 14 2 7 19
LIS 1 2 3 4 4 1 2 5
Iterate left for an element which is less than 9 and whose LIS[] value is 2, which is 8.
Index→ 0 1 2 3 4 5 6 7
Input array, 5 8 9 20 14 2 7 19
LIS 1 2 3 4 4 1 2 5
Next, iterate to the left for an element which is less than 8 and whose LIS[] value is 1, which
is 5.
Index→ 0 1 2 3 4 5 6 7
Input array, 5 8 9 20 14 2 7 19
LIS 1 2 3 4 4 1 2 5
6 . 3 5 L o n g e s t i nc r e a s i ng s u b se q u e n ce [ L I S ]
D y n am i c Pro g r am mi n g | 444
Since we have got an LIS[] value with 1, we can stop or we may continue but we will get
none. So, the longest increasing subsequence is [5, 8, 9, 14, 19].
def lis(A):
LIS = [1 for _ in range(len(A))]
for i in range(len(A)):
for j in range(i):
if A[j] <= A[i]:
LIS[i] = max(LIS[i], LIS[j] + 1)
## trace subsequence back to output
result = []
element = LIS[len(LIS)-1]
for i in range(len(LIS)-1, -1, -1):
if LIS[i] == element:
result.append(A[i])
element -= 1
return list(result.__reversed__())
A = [5, 8, 9, 20, 14, 2, 7, 19]
print lis(A)
Performance
Time Complexity: O( ), because of the nested loops.
Space Complexity: O( ), for table.
There is another method for solving the LIS problem. Other method is to
sort the given sequence, save it in another array, and then take out the
“Longest Common Subsequence” (LCS) of the two arrays (given input
array and the new array which has sorted sequence of input array). This
method has a time complexity of O( ).
Recursive solution
The first step in designing a recursive algorithm is determining the base case. Eventually,
all recursive steps must reduce to the base case.
6 . 3 6 L o n g e s t i nc r e a s i ng s u b se q u e n ce [ L I S ] w i t h c o n st r a i n t
D y n am i c Pro g r am mi n g | 445
General case?
Now, let us generalize the discussion and decide about element. For element, there
are two possibilities:
1. Include current element in LIS if it is greater than the previous element with a
difference of ( [ ] + ≤ [ ]) in LIS, and recurse for the remaining items.
2. Exclude current element from LIS, and recurse for the remaining items.
Finally, we return the maximum value either by including or excluding current element.
Let ( ) represent the longest increasing subsequence starting at index 0, and ending at
. The optimal way to obtain a strictly increasing subsequence ending at position is to
extend some subsequence starting at some earlier position . For this the recursive formula
can be written as:
1, =0
()=
{ [ ] [ ]} { ( ) + 1}, ≥1
The above recurrence says that we have to select some earlier position which gives the
maximum subsequence. The 1 in the recursive formula indicates the addition of
element.
0 … … …
After finding the maximum subsequences for all the positions, we have to select the one
among all which gives the maximum subsequence and it is defined as:
{ ( )}
global max_lis_length
def lis(A, i, d):
# Declaration to allow modification of the global copy of max_lis_length
global max_lis_length
# Base case
if i == 0:
return 1
max_lis_with_ith_element = 1
for j in xrange(0, i):
if A[j] + d < A[i]:
max_lis_with_ith_element = max(max_lis_with_ith_element, 1 + lis(A, j, d) )
# Check if currently calculated LIS ending at
# A[i] is longer than the previously calculated
# LIS and update max_lis_length accordingly
if (max_lis_length < max_lis_with_ith_element):
max_lis_length = max_lis_with_ith_element
return max_lis_with_ith_element
# Test code
def main():
# Following declaration is needed to allow modification
# of the global copy of max_lis_length in lis()
global max_lis_length
max_lis_length = 1
A = [5, 8, 9, 20, 14, 2, 7, 19]
print "Length of LIS is", lis(A, len(A)-1, 2)
if __name__=="__main__":
main()
6 . 3 6 L o n g e s t i nc r e a s i ng s u b se q u e n ce [ L I S ] w i t h c o n st r a i n t
D y n am i c Pro g r am mi n g | 446
Performance
Considering the above implementation, following is the recursion tree for an array of size
4. ( , − 1, ) gives us the length of for .
lis(A, 4, 2)
lis(A, 1, 2) lis(A, 0, 2)
lis(A, 0, 2)
For an input array of size , the function ( , − 1, ) would make recursive calls and
( , − 1, ) would make − 1 recursive calls and goes on... So, the total number of
recursive calls equal to:
× − 1 × − 2 … . .× 2 × 1 = ! ≈ O( )
Time Complexity: O( ).
Space Complexity: O(1).
DP solution
We can see that there are many subproblems which are solved again and again. So this
problem has overlapping substructure property and recomputation of the same
subproblems can be avoided by either using memoization or tabulation.
In line with LIS dynamic programming solution, we can solve this problem in a bottom-up
manner. In the bottom-up approach, we solve smaller subproblems first, then solve larger
subproblems from them. The following bottom-up approach computes [ ], for each 0 ≤
< , which stores the length of the longest increasing subsequence of subarray [0.. ]
that ends with [ ]. To calculate [ ], we consider of all the smaller values of (say )
already computed and pick the maximum [ ] where [ ] + is lesser than the current
element [ ].
Following is a tabulated implementation for the LIS problem.
def lis(A, d):
LIS = [1 for _ in range(len(A))]
for i in range(len(A)):
for j in range(i):
if A[j]+d <= A[i]:
LIS[i] = max(LIS[i], LIS[j] + 1)
return max(LIS)
A = [5, 8, 9, 20, 14, 2, 7, 19]
print lis(A, 2)
Example
6 . 3 6 L o n g e s t i nc r e a s i ng s u b se q u e n ce [ L I S ] w i t h c o n st r a i n t
D y n am i c Pro g r am mi n g | 447
Let the array be LIS[]. LIS[ ] will contain the increasing subsequence length which includes
the element.
If we take any element from the sequence, it’s a valid increasing subsequence, but it may
not be the longest. So, initially we say that all the values in LIS[] are 1.
Index→ 0 1 2 3 4 5 6 7
Input array, 5 8 9 20 14 2 7 19
LIS 1 1 1 1 1 1 1 1
Now, we start from the leftmost element which is 5 and since there are no elements on left
side of 5, no further processing is required for the element 5.
Index→ 0 1 2 3 4 5 6 7
Input array, 5 8 9 20 14 2 7 19
LIS 1 2 1 1 1 1 1 1
Now, the next element is 8, and we try to find all the numbers which are less than 8-2 (8-
d) and which lie before this 8. The only element which is on the left and lesser than 6 is 5.
So, definitely we can make the sequence [5, 8]. So, LIS[] value for the element 8 will be 2.
Index→ 0 1 2 3 4 5 6 7
Input array, 5 8 9 20 14 2 7 19
LIS 1 2 1 1 1 1 1 1
Now, the next element is 9 and the LIS[] value is 1. The only element which is on the left
and less than 9− is 5. Observe that since the LIS[] value of 5 is 1, that means if we take
5 we could somehow make a subsequence which contains 5 and whose length is 1. And
now we have an element (9) which is ≥ 5 + (5 + 2 = 7). So, obviously their LIS[] value will
be 2.
Index→ 0 1 2 3 4 5 6 7
Input array, 5 8 9 20 14 2 7 19
LIS 1 2 2 1 1 1 1 1
The next element is 20 and the LIS[] value is 1. The elements which are lesser than 20−
and in left are 5, 8, and 9. Observe that since the LIS[] value of 5 is 1, and if we add the
element (20) which is greater than 5 + ; the LIS[] value of 20 will be 2. Next, the LIS[] value
of 8 is already 2, and if we add 20 after 8, then the LIS[] value of 20 will be 3. Similarly, the
LIS[] value of 9 is already 2, and if we add 20 after 9, then the LIS[] value of 20 will be 3.
Hence, no increase in LIS value for 20.
Index→ 0 1 2 3 4 5 6 7
Input array, 5 8 9 20 14 2 7 19
LIS 1 2 2 3 1 1 1 1
Next, there is 14 whose LIS[] value is 1. The elements which are less than 14 − and on
the left are 5, 8, and 9. Observe that since the LIS[] value of 5 is 1, and if we add the element
(14) which is greater than 5; the LIS[] value of 14 will be 2. Next, the LIS[] value of 8 is
already 2, and if we add 14 after 8, then the LIS[] value of 14 will be 3. Similarly, the LIS[]
value of 9 is already 2, and if we add 14 after 9, then the LIS[] value of 14 will be 3. Hence,
no increase in LIS value for 14.
Index→ 0 1 2 3 4 5 6 7
Input array, 5 8 9 20 14 2 7 19
LIS 1 2 2 3 3 1 1 1
The next element is 2 but there is no element on the left which is lesser than 2.
Index→ 0 1 2 3 4 5 6 7
Input array, 5 8 9 20 14 2 7 19
LIS 1 2 2 3 3 1 1 1
6 . 3 6 L o n g e s t i nc r e a s i ng s u b se q u e n ce [ L I S ] w i t h c o n st r a i n t
D y n am i c Pro g r am mi n g | 448
The next element is 7 whose LIS[] value is 1. The only element which is lesser than or equal
to 7 − and to the left is 5. Observe that since the LIS[] value of 5 is 1, and if we add the
element (7) which is greater than 5; the LIS[] value of 7 will be 2.
Index→ 0 1 2 3 4 5 6 7
Input array, 5 8 9 20 14 2 7 19
LIS 1 2 2 3 3 1 2 1
Next, there is 19 whose LIS[] value is 1. The elements which are less than 19 − and to the
left are 5, 8, 9, and 14. Observe that since the LIS[] value of 5 is 1, and if we add the element
(19) which is greater than 5; the LIS[] value of 19 will be 2. Next, the LIS[] value of 8 is
already 2, and if we add 19 after 8, then the LIS[] value of 19 will be 3. Next, the LIS[] value
of 9 is already 2, and if we add 19 after 9, then the LIS[] value of 19 will be 3. Hence, no
increase in LIS value for 19. Similarly, the LIS[] value of 14 is already 3, and if we add 19
after 14, then the LIS[] value of 19 will be 4.
Index→ 0 1 2 3 4 5 6 7
Input array, 5 8 9 20 14 2 7 19
LIS 1 2 2 3 3 1 2 4
Now we iterate through the [] and find the maximum element, which is the result. So,
for the given sequence, the result is 4.
Index→ 0 1 2 3 4 5 6 7
Input array, 5 8 9 20 14 2 7 19
LIS 1 2 2 3 3 1 2 4
Reconstructing a LIS
Now, you may want to reconstruct one of the longest increasing subsequences. We can use
the same LIS reconstruction process for this problem as well.
Find an item whose LIS[] value is the maximum. The element 19 has the maximum LIS
value (4).
Index→ 0 1 2 3 4 5 6 7
Input array, 5 8 9 20 14 2 7 19
LIS 1 2 2 3 3 1 2 4
Now iterate to left and find an element which is lesser than 19 and whose LIS[] value is one
lesser than 4. So, we have 14 whose LIS[] value is 3 and 14 < 19.
Index→ 0 1 2 3 4 5 6 7
Input array, 5 8 9 20 14 2 7 19
LIS 1 2 2 3 3 1 2 4
Again iterate left for the next item which is lesser than 14 and whose LIS[] value is 2. We
have 9.
Index→ 0 1 2 3 4 5 6 7
Input array, 5 8 9 20 14 2 7 19
LIS 1 2 2 3 3 1 2 4
Next, iterate to the left for an element which is lesser than 8 and whose LIS[] value is 1,
which is 5.
Index→ 0 1 2 3 4 5 6 7
Input array, 5 8 9 20 14 2 7 19
LIS 1 2 2 3 3 1 2 4
6 . 3 6 L o n g e s t i nc r e a s i ng s u b se q u e n ce [ L I S ] w i t h c o n st r a i n t
D y n am i c Pro g r am mi n g | 449
Since we have got an LIS[] value with 1, we can stop or we may continue, but we will get
none. So, the longest increasing subsequence with difference of 2 between the elements in
the sequence is [5, 9, 14, 19].
def lis(A, d):
LIS = [1 for _ in range(len(A))]
for i in range(len(A)):
for j in range(i):
if A[j] + d <= A[i]:
LIS[i] = max(LIS[i], LIS[j] + 1)
## trace subsequence back to output
result = []
element = LIS[len(LIS)-1]
for i in range(len(LIS)-1, -1, -1):
if LIS[i] == element:
result.append(A[i])
element -= 1
return list(result.__reversed__())
A = [5, 8, 9, 20, 14, 2, 7, 19]
print lis(A, 2)
Performance
Time Complexity: O( ), because of the nested loops.
Space Complexity: O( ), for the auxiliary table.
Box rotations
For all boxes we have to consider all the orientations with respect to rotation. For a 3 −
box (with three dimensions- height, depth and width), the number of combinations of the
6 . 3 7 B o x s ta c k i n g
D y n am i c Pro g r am mi n g | 450
dimensions is three. We can place the box on any of the three different types of bases
available. This will give us three different heights. This is sufficient because the heights
that we get after rotation are the only heights possible. No other heights can be present.
height=h
depth=d
width=w
Width as height:
width=h
height=w
depth=d
Depth as height:
height=d depth=w
width=h
6 . 3 7 B o x s ta c k i n g
D y n am i c Pro g r am mi n g | 451
So, for each given box we will generate all the possibilities. Since we have limited boxes, we
will use all the boxes to make the stack of maximum height. This simplification allows us
to forget about the rotations of the boxes and just focus on the stacking of n boxes with
each height as ℎ and a base area of ( × ).
From the above figure, we can see that we can stack only a box on top of another box if the
dimensions of the 2-D base of the lower box are each strictly larger than those of the 2-D
base of the higher box.
2 …….. ……..
1
To create a stack with maximum height we need to place the box with the max base area
at the bottom and, on top of that, box with 2 best base area and so on. We allow a box
on top of box only if box is smaller than box in both the dimensions ( < && <
).
Sort the boxes in the descending order based on their base area.
A rotation is an order wherein the depth is greater than or equal to the width ( ≤ ).
Having this constraint avoids the repetition of the same order, but with the width and depth
switched. Now what we do is, make a stack of boxes that is as tall as possible and has the
maximum height.
DP solution
Now let us solve this using DP. First, select the boxes in the order of decreasing base area.
Now, let us say ( ) represents the tallest stack of boxes with box on top. This is very
similar to the LIS problem because the stack of boxes with ending box is equal to finding
a subsequence with the first boxes due to the sorting by decreasing base area. The order
of the boxes on the stack is going to be equal to the order of the sequence.
Now we can write ( ) recursively. In order to form a stack which ends on box , we need
to extend a previous stack ending at . That means, we need to put box at the top of the
stack [ box is the current top of the stack]. To put box at the top of the stack we should
satisfy the condition > > [this ensures that the lower level box has more base
than the boxes above it]. Based on this logic, we can write the recursive formula as:
()= { ( )} + ℎ
Box stacking problem can be reduced to the longest common subsequence (LIS). Similar to
the LIS problem, at the end we have to select the best over all the potential values. This is
because we are not sure which box might end up on top.
{ ( )}
from collections import namedtuple
from itertools import permutations
box = namedtuple("Box", "height depth width")
6 . 3 7 B o x s ta c k i n g
D y n am i c Pro g r am mi n g | 452
def create_rotation(given_boxs):
for current_box in given_boxs:
for (height, depth, width) in permutations((current_box.height,\
current_box.depth, current_box.width)):
if depth >= width:
yield box(height, depth, width)
def sort_by_decreasing_base_area(rotations):
return sorted(rotations, key=lambda box: box.depth * box.width, reverse=True)
def can_stack(box1, box2):
return box1.depth < box2.depth and box1.width < box2.width
def tallest_box_stack(boxs):
#create boxes and sort them with base area in decreasing order
boxes = sort_by_decreasing_base_area([rotation for rotation in create_rotation(boxs)])
num_boxes = len(boxes)
T = [rotation.height for rotation in boxes]
R = [i for i in range(num_boxes)]
for i in range(1, num_boxes):
for j in range(0, i):
if can_stack(boxes[i], boxes[j]):
stacked_height = T[j] + boxes[i].height
if stacked_height > T[i]:
T[i] = stacked_height
R[i] = j
max_height = max(T)
start_index = T.index(max_height)
# Prints the boxes which were stored in R list.
while True:
print boxes[start_index]
next_index = R[start_index]
if next_index == start_index:
break
start_index = next_index
return max_height
b1 = box(3, 2, 5)
b2 = box(1, 2, 4)
print tallest_box_stack([b1, b2])
Performance
Time Complexity: O( ). This solution needs O( ) to sort boxes and O( ) to apply DP.
Space Complexity: O( ).
Example
The figure shows an example problem with eight cities on each side of the river. Our goal
is to construct as many bridges as possible without any crosses between the left side cities
to the right side cities of the river.
6 5
2 1
5 8
8 R 4
i
v
7 e 2
r
4 6
3 7
For this example, the civil engineers could build four bridges, connecting 5 with 5, 1 with
1, 8 with 8, and 4 with 4.
6 5
2 1
5 8
8 R 4
i
v
7 e 2
r
4 6
3 7
One more possibility is, connecting 1 with 1, 3 with 3, 4 with 4, 6 with 6, and 7 with 7.
1 5
2 1
3 8
5 R 4
i
v
6 e 2
r
7 6
8 7
Among these two, the second arrangement allows us to build 5 bridges against 4 bridges
with the first arrangement.
DP solution
Let us consider the figure shown below. It can be seen that there are cities on the left
side of the river and cities on the right side of the river. Also, note that we are connecting
the cities which have the same number [a requirement in the problem]. Our goal is to
connect the maximum cities on the left side of the river with the cities on the right side of
the river, without any cross edges.
1 R 3
i
v
2 e
Left side cities r Right side cities
…
….
We can reduce this problem to the longest increasing subsequence problem. To build up
to how you'd use the longest increasing subsequence algorithm to solve this problem, let's
start off with some intuition and then build up to a solution. Since you can only build
bridges between the cities at matching indices, you can think of the set of bridges that you
end up building as the largest set of pairs you can find that don't contain any crossing. So
under what circumstances would you have a crossing?
Let's see when this can happen. Suppose that we sort all of the bridges built by their first
city. If two bridges cross, then we must have that there is some bridge ( , ) such that for
some other bridge ( , ) one of the following holds:
… R …
i
v
e
r
Left side cities Right side cities
…. ….
… …
… R …
i
v
e
r
Left side cities Right side cities
…. ….
… …
Given that this property needs to hold, we need to ensure that for every set of bridges, we
have that exactly one of the two following properties holds for any pair of bridges ( , ),
( , ): either
≤ and ≤
or
≥ and ≥
If ≤ and ≤ :
… R …
i
v
e
r
Left side cities Right side cities
…. ….
… …
If ≥ and ≥ :
… R …
i
v
e
r
Left side cities Right side cities
…. ….
… …
In other words, if we were to sort the cities on the left side of the river, the cities on the
right side would always be increasing. Similarly, if we were to sort the cities on the right
side of the river the cities on the left side would always be increasing.
So, let us sort the cities on the left side of the river.
Now, we have the elements sorted by their left cities, so we can check if two pairs are in
correct order by looking at their positions in the right cities. With the left side cities sorted,
to get the maximum number of bridges, we have to maximize the cities on the right side
with the increasing order. This turns out to be finding the longest increasing subsequence
of the right side cities with the sorted left side cities.
def lis(A):
LIS = [1 for _ in range(len(A))]
for i in range(len(A)):
for j in range(i):
if A[j] <= A[i]:
LIS[i] = max(LIS[i], LIS[j] + 1)
## trace subsequence back to output
result = []
element = LIS[len(LIS)-1]
for i in range(len(LIS)-1, -1, -1):
if LIS[i] == element:
result.append(A[i])
element -= 1
return list(result.__reversed__())
l_cities = [6, 2, 5, 1, 8, 7, 4, 3]
r_cities = [5, 1, 8, 3, 4, 2, 6, 7]
l_cities.sort()
bridges = lis(r_cities)
for i in range(len(bridges)):
print "Adding bridge:", bridges[i], "-->", bridges[i]
Performance
Since we can sort the pairs by their left side cities in O( ) and find the longest
increasing subsequence on the right side cities in O( ), this is an O( ) solution to the
problem.
Time Complexity: O( ), because of the nested loops and is the same as LIS algorithm
running time.
Space Complexity: O( ), for table.
Example
For example, if A = {1, 5, 11, 5}, the array can be partitioned as {1, 5, 5} and {11}. Similarly,
if A = {1, 5, 3}, the set cannot be partitioned into equal sum subsets.
Notice that, there could be multiple partition solutions for a given set of elements. For
example, the set A = {1, 2, 3, 4, 5, 6, 7, 8} can be partitioned in two ways as:
{1, 3, 6, 8}, and {2, 4, 5, 7 }
{1, 4, 5, 8}, and {2, 3, 6, 7 }
This problem is an optimization version of the partition problem. Following are the two
main steps to solve this problem:
1. Calculate the sum of the array. If the sum is odd, there cannot be two subsets with
an equal sum, so return false.
2. If the sum of the array elements is even, calculate and find a subset of the
array with a sum equal to .
The first step is simple. The second step is crucial, and it can be solved either using
recursion or dynamic programming.
Recursive solution
Following is the recursive property of the second step mentioned above. The idea is to
consider each item in the given set A one by one and for each item, there are two
possibilities.
Let _ (A, n, sum/2) be the function that returns true if there is a subset of A[0..n-
1] with sum equal to . The _ problem can be divided into two subproblems:
6 . 3 9 P a r t i t i o n i n g e l e m e nt s i n to t w o e q u a l s u b se t s
D y n am i c Pro g r am mi n g | 458
_ ( , , ) = _ ( , − 1, ) _ ( , − 1, − [ − 1])
2 2 2
# A utility function that returns True if there is a subset of A[] with sum equal to given sum
def subset_sum (A, n, sum):
if (sum == 0):
return True
if (n == 0 and sum != 0):
return False
# If last element is greater than sum, then ignore it
if (A[n-1] > sum):
return subset_sum(A, n-1, sum)
return subset_sum(A, n-1, sum) or subset_sum(A, n-1, sum-A[n-1])
# Returns True if A[] can be partitioned in two subsets of equal sum, otherwise False
def find_partition(A):
# calculate sum of all elements
sum = 0
n = len(A)
for i in range(0,n):
sum += A[i]
# If sum is odd, there cannot be two subsets with equal sum
if (sum%2 != 0):
return False
# Find if there is subset with sum equal to half of total sum
return subset_sum(A, n, sum/2)
Performance
Time Complexity: O(2 ). In the worst case, this recursive solution tries two possibilities
(whether to include or exclude) for every element.
Space Complexity: In the worst case, the maximum depth of recursion is n. Therefore, we
need O(n) space for the system recursive runtime stack.
DP solution
The problem has an optimal substructure. That means the problem can be broken down
into smaller, simple subproblems, which can further be divided into yet simpler, smaller
subproblems until the solution becomes trivial. The above solution also exhibits
overlapping subproblems. If we draw the recursion tree of the solution, we can see that the
same subproblems are getting computed again and again. We know that problems having
optimal substructure and overlapping subproblems can be solved by using dynamic
programming.
We create a 2D array [][] of size ( )×( + 1) and construct the solution in a bottom-up
manner such that every filled entry has the following property
6 . 3 9 P a r t i t i o n i n g e l e m e nt s i n to t w o e q u a l s u b se t s
D y n am i c Pro g r am mi n g | 459
def find_partition(A):
# calculate sum of all elements
sum = 0
n = len(A)
for i in range(0,n):
sum += A[i]
# If sum is odd, there cannot be two subsets with equal sum
if (sum%2 != 0):
return False
T = [[False for x in range(n+1)] for x in range(sum//2 + 1)]
# initialize top row as true
for i in range(0,n):
T[0][i] = True
# initialize leftmost column, except T[0][0], as 0
for i in range(1,sum//2+1):
T[i][0] = False
# Fill the partition table in bottom up manner
for i in range(1,sum//2+1):
for j in range(0,n+1):
T[i][j] = T[i][j-1]
if (i >= A[j-1]):
T[i][j] = T[i][j] or T[i - A[j-1]][j-1]
return T[sum/2][n]
Performance
Time Complexity: O( × ).
Space Complexity: O( × ). Notice that this solution will not be feasible for arrays with
a big sum.
DP Solution
Assume that the numbers are . . . . Let us use DP to solve this problem. We will create
a boolean array with size equal to + 1. Assume that [ ] is 1 if there exists a subset of
given elements whose sum is . That means, after the algorithm finishes, [ ] will be 1,
if and only if there is a subset of the numbers that has the sum . Once we have that value
then we just need to return [ /2]. If it is 1, then there is a subset that adds up to half the
total sum.
Initially we set all values of to 0. Then we set [0] to 1. This is because we can always
build 0 by taking an empty set. If we have no numbers in , then we are done! Otherwise,
we pick the first number, [0]. We can either throw it away or take it into our subset. This
means that the new [] should have [0] and [ [0]] set to 1. This creates the base case.
We continue by taking the next element of .
Suppose that we have already taken care of the first − 1 elements of A. Now we take [ ]
and look at our table []. After processing − 1 elements, the array has a 1 in every
location that corresponds to a sum that we can make from the numbers we have already
processed. Now we add the new number, [ ]. What should the table look like?
First of all, we can simply ignore [ ]. That means, no one should disappear from [] – we
can still make all those sums. Now consider some location of [ ] that has a 1 in it. It
corresponds to some subset of the previous numbers that add up to j. If we add [ ] to that
subset, we will get a new subset with a total sum + [ ]. So we should set [ + [ ]] to 1
as well. That's all. Based on the above discussion, we can write the algorithm as:
def subset_sum(A):
n = len(A)
K=0
for i in range(0, n):
K += A[i]
T = [0] * (K+1)
T[0] = 1
for i in range(1, K+1):
T[i] = 0
# process the numbers one by one
for i in range(0, n):
for j in range(K - A[i], 0, -1):
if( T[j] ):
T[j + A[i]] = 1
return T[K//2]
A = [3,2,4,19,3,7,13,10,6,11]
print subset_sum(A)
Performance
In the above code, loop moves from right to left. This reduces the double counting problem.
That means, if we move from left to right, then we may do the repeated calculations.
Time Complexity: O( ), for the two loops.
Space Complexity: O( ), for the auxiliary boolean table .
Performance
After the improvements, the time complexity is still O( ), but we have removed some
useless steps.
Example
The parenthesization or counting boolean parenthesization problem is somewhat similar
to optimal binary search tree finding. Let the number of symbols be and between the
symbols there are boolean operators like &, |, ^, etc.
For example, if = 4, 1 | 0 & 1 ^ 0 . Our goal is to count the number of ways to parenthesize
the expression with boolean operators so that it evaluates to . In the above case, if we
use 1 | ( (0 & 1 ) ^ 0 ) then it evaluates to .
1 |( (0 & 1) ^ 0) =
Recursive solution
Let’s understand the concept first and then we will see how we can implement it. If we start
with an expression with only one boolean value 1 or 0, how many way we can parenthesized
it? Well, there are two answers to it. For 1, there is one way to parenthesize, (1), whereas
for 0, there is no way we can parenthesize which evaluates to true.
First take away from this insight is that, we got the base case for recursion, if our solution
is recursive. Second take away is that, there can be two different outputs an expression or
subexpression can evaluate and we have to store both of them.
Now let us see how the recursion solves this problem for general case. In order to
parenthesize the boolean expression, we iterate through its operators, parenthesizing what
is to their left and right. For example, '0|1&0' can be parenthesized as either '0|(1&0)' or
'(0|1)&0'. Now, for each of these inner expressions, the number of ways in which we can
obtain the desired boolean result depends on the operator: '&' (and), '|' (or) or '^' (xor).
Therefore, we must break down each inner expression, taking into account the
combinations that yield the result we are after. For example, if the operator is '&' and
'result' is true, the only valid ways of parenthesizing the two expressions are those for which
they both evaluate to true.
6 . 4 1 C o u nt i n g b oo l ea n p a r e nt h e s i za t ion s
D y n am i c Pro g r am mi n g | 462
Let ( , ) represent the number of ways to parenthesize the sub expression with symbols
… [symbols/operands means only 1 and 0 and not the operators] with boolean operators
so that it evaluates to . Also, and take the values from 1 to . For example, in the
above case, (2, 4) = 0 because there is no way to parenthesize the expression 0 & 1 ^ 0 to
make it .
Similarly, let ( , ) represent the number of ways to parenthesize the sub expression with
symbols … with boolean operators so that it evaluates to . The base cases are ( , )
and ( , ).
1 ℎ 1
(, )=
0 ℎ 0
0 ℎ 1
(, )=
1 ℎ 0
Now we are going to compute ( , + 1) and ( , + 1) for all values of . Similarly, ( , + 2)
and ( , + 2) for all values of and so on. Now let’s generalize the solution.
( , ) ( + 1, ), " "
(, )= (, ) ( + 1, ) − ( , ) ( + 1, ), ""
( , ) ( + 1, ) + ( , ) ( + 1, ), " "
Where, ( , ) = ( , ) + ( , ).
1 2 … … +1 … …
, ,
In the above recursive formula, ( , ) indicates the number of ways to parenthesize the
expression. Let us assume that we have some subproblems which are ending at . Then
the total number of ways to parenthesize from is the sum of counts of parenthesizing
from and from + 1 . To parenthesize between and + 1 there are three ways:
“ ”, “ ” and “ ”.
If we use “ ” between and + 1, then the final expression becomes only
when both are . If both are then we can include them to get the final count.
If we use “ ”, and at least one of them is , then the result becomes . Instead
of including all the three possibilities for “ ”, we are giving one alternative where
we are subtracting the “false” cases from total possibilities.
The same is the case with “ ”. The conversation is as in the above two cases.
#! /usr/bin/env python
import collections
import re
def get_operator_indexes(str_):
""" Return a generator of the indexes where operators can be found.
The returned generator yields, one by one, the indexes of 'str_' where
there is an operator: either the character '&', '|', or '^'. For example,
'1^0&0' yields 1 first and then 3, as '^' is the second character of the
bool_expr and '&' the fourth.
"""
pattern = "&|\||\^"
6 . 4 1 C o u nt i n g b oo l ea n p a r e nt h e s i za t ion s
D y n am i c Pro g r am mi n g | 463
DP solution
Now let us see how the DP improves the efficiency of this problem. The problem can be
solved using dynamic programming with the same approach used to solve matrix
multiplication problem. First we will solve for all the subexpressions of one symbol, then
for subexpressions of two symbols and progressively we will find the result for the original
problem. We would use the Python programming language utility functions to cache the
results and avoid recalculating the functions with the same arguments again and again.
import collections
import functools
import re
def memoize(f):
cache = {}
@functools.wraps(f)
def memf(*args, **kwargs):
fkwargs = frozenset(kwargs.items())
if (args, fkwargs) not in cache:
cache[args, fkwargs] = f(*args, **kwargs)
return cache[args, fkwargs]
return memf
def get_operator_indexes(str_):
pattern = "&|\||\^"
for match in re.finditer(pattern, str_):
yield match.start()
LOGICAL_OPS = collections.defaultdict(dict)
6 . 4 1 C o u nt i n g b oo l ea n p a r e nt h e s i za t ion s
D y n am i c Pro g r am mi n g | 464
6 . 4 1 C o u nt i n g b oo l ea n p a r e nt h e s i za t ion s
D y n am i c Pro g r am mi n g | 465
if (operators[k] == '^'):
T[i][j] += F[i][k]*T[k+1][j] + T[i][k]*F[k+1][j]
F[i][j] += T[i][k]*T[k+1][j] + F[i][k]*F[k+1][j]
i=i+1
return T[0][n-1]
print count_parenthesize("1101", "|&^", 4)
Performance
How many subproblems are there? In the above formula, the index can range from
1 to , and the index can range from to . So, there are a total of subproblems. Also,
we are calculating summation for all such values. So, the total running time of the
algorithm is O( ).
Space Complexity: O( ).
< < ℎ
Example
In the following BSTs, the left binary tree is a binary search tree and the right binary tree
is not a binary search tree (at node 5 it’s not satisfying the binary search tree property).
Element 2 is less than 5 but on the right subtree of 5.
6 5
3 8 3 6
1 4 7 9 1 4 2 8
6 . 4 2 O p t i m a l b i n a r y s ea r c h t r e e s
D y n am i c Pro g r am mi n g | 466
If we are searching for an element and if the left subtree root data is less than the
element we want to search, then skip it. The same is the case with the right subtree.
Because of this, binary search trees take lesser time for searching an element than
regular binary trees. In other words, the binary search trees consider either the left
or right subtrees for searching an element but not both.
The basic operations that can be performed on binary search tree (BST) are
insertion of element, deletion of element, and searching for an element. While
performing these operations on BST, the height of the tree gets changed each time.
Hence there exists variations in time complexities of best case, average case, and
worst case.
The basic operations on a binary search tree take time proportional to the height of
the tree. For a complete binary tree with n nodes, such operations runs in O( )
worst-case time. If the tree is a linear chain of nodes (skew-tree), however, the
same operations take O( ) worst-case time.
For detailed discussion on binary search trees (BSTs), refer “Finding the
smallest element in BST” section in " chapter.
Before solving the problem let us understand the problem with an example. Assume that
we are given an array A = [3, 12, 21, 32, 35]. There are many ways to represent these
elements, two of which are listed below.
12 35
3 32 12
21 35 3 21
32
Of the two, which representation is better? The search time for an element depends on
the depth of the node. The average number of comparisons for the first tree is: =
and for the second tree, the average number of comparisons is: = . Of the two,
the first tree gives better results.
For example consider a three node BST A = [3, 12, 21]. These three nodes can be represented
in the following three different ways.
6 . 4 2 O p t i m a l b i n a r y s ea r c h t r e e s
D y n am i c Pro g r am mi n g | 467
32 12 3
12 3 32 12
3 32
For these BSTs, values of keys are not important; the only requirement is that they should
be arranged in an order). Let us assume that the probabilities of the elements are 0.7, 0.2
and 0.1 for the elements 3, 12, and 13 respectively. The average search time for the above
trees is:
Recursive solution
An alternative would be a recursive algorithm. Consider the characteristics of any optimal
tree. Of course it has a root and two subtrees. Both subtrees must themselves be optimal
binary search trees with respect to their keys and frequencies. First, any subtree of any
binary search tree must be a binary search tree. Second, the subtrees must also be optimal.
If a tree is optimal, then so are its subtrees (since otherwise we would get a
better tree by substituting a subtree for an optimal one). This gives the
algorithm idea to systematically build bigger optimal subtrees.
For simplicity, let us assume that the given array is and the corresponding frequencies
are in array . [ ] indicates the frequency of element [ ].
Since there are “n” possible keys as candidates for the root of the optimal tree, the recursive
solution must try them all. For each candidate key as root, all keys lesser than that key
must appear in its left subtree while all keys greater than it must appear in its right subtree.
The idea is, one of the keys in A[1], …,A[n] , say A[k], where 1 ≤ k ≤ n, must be the root.
Then, as per binary search rule, left subtree of A[k] contains A[1],...,A[k-1] and right subtree
6 . 4 2 O p t i m a l b i n a r y s ea r c h t r e e s
D y n am i c Pro g r am mi n g | 468
of A[k] contains A[k+1], ...,A[n]. So, the idea is to examine all candidate roots A[k] , for 1 ≤
k ≤ n and determining all optimal BSTs containing A[1],...,A[k-1] and A[k+1], ...,A[n].
A[k]
A[1],...,A[k-1] A[ +1],...,A[n]
With this, the total search time S(r) of the tree with root r can be defined as:
( )= ( ℎ( , ) + 1) × [ ])
( )= ( ℎ( , ) + 1) × [ ] + [ ]+ ( ℎ( , ) + 1) × [ ]
( )= ( . )+ ( . ℎ )+ []
In the given n elements, any node can be a root. Since can vary from 1 to , we need to
minimize the total search time for the given keys 1 to considering all possible values for
. Let (1, ) be the optimal binary search tree with keys 1 to .
(1, ) = { ( )}
(, ) = []
On the similar lines, the optimal binary search tree with keys from to can be given as:
( , )= { ( )}
The above discussion can be converted to code as follows:
def get_OBST(A, F, i, j, level):
if i > j:
return 0
min_value = float("inf")
for index in range(i, j + 1):
val = (get_OBST(A, F, i, index - 1, level + 1) # left tree
+ level * F[index] # value at level
+ get_OBST(A, F, index + 1, j, level + 1)) # right tree
min_value = min(val, min_value)
return min_value
def OBST(A, F):
return get_OBST(A, F, 0, len(A) - 1, 1)
6 . 4 2 O p t i m a l b i n a r y s ea r c h t r e e s
D y n am i c Pro g r am mi n g | 469
Performance
Even though, recursive solution is easy to code, the number of recursive calls is exhaustive.
With recursion also we can still try all the possible solutions. This is no better than brute
force approach but gives us a simpler way of constructing all possible binary search trees.
Number of different binary trees with n nodes is , which is exponential in n. This is
far too large to try all possibilities, so we need to look for a more efficient way to construct
an optimum tree.
DP solution
As seen in the previous sections, problems having optimal substructure and overlapping
subproblems can be solved with the dynamic programming, in which subproblem solutions
are memoized rather than computed again and again.
So, we use dynamic programming to give a more efficient algorithm: maintain a table to
store solutions to subproblems already solved; and solve all the subproblems one by one,
using the recursive formula we found earlier, in an appropriate order.
The idea is to create a table as shown below:
def OBST(A, F, get_bst=False):
n = len(A)
table = [[None] * n for _ in xrange(n)]
for i in xrange(n):
table[i][i] = (F[i], i)
# let optimal BST for subproblem A[i..j] be T
# if table[i][j] = (cost, keyidx)
# then cost is the cost of T, keyidx is index of the root key of T
for s in xrange(1, n):
for i in xrange(n-s):
# compute cost for A[i..i+s]
minimal, root = float('inf'), -1
# search root with minimal cost
freq_sum = sum(F[x] for x in xrange(i, i+s+1))
for r in xrange(i, i+s+1):
left = 0 if r == i else table[i][r-1][0]
right = 0 if r == i+s else table[r+1][i+s][0]
cost = left + right + freq_sum
if cost < minimal:
minimal = cost
root = r
table[i][i+s] = (minimal, root)
if get_bst:
tree = {}
stack = [(0, n-1)]
while stack:
i, j = stack.pop()
root = table[i][j][1]
left, right = None, None
if root != i:
stack.append((i, root-1))
left = table[i][root-1][1]
if root != j:
6 . 4 2 O p t i m a l b i n a r y s ea r c h t r e e s
D y n am i c Pro g r am mi n g | 470
stack.append((root+1, j))
right = table[root+1][j][1]
if left is None and right is None:
tree[root] = None
else:
tree[root] = (left, right)
return (table[0][n-1][0], tree)
return table[0][n-1][0]
if __name__ == '__main__':
assert OBST([0, 1], [30, 40]) == 100
assert OBST(['a', 'b', 'c'], [30, 10, 40]) == 130
assert OBST([0, 1], [0.6, 0.4]) == 1.4
assert OBST(range(1, 8), [0.05, 0.4, 0.08, 0.04, 0.1, 0.1, 0.23]) == 2.18
Example
Let us find the optimal binary search tree for n = 5, having the keys A=[10, 12, 20, 35, 46]
and frequencies F=[34, 8, 50, 21, 16]. The following is the table as they would appear after
the initialization.
0 1 2 3 4
0 None None None None None
1 None None None None None
2 None None None None None
3 None None None None None
4 None None None None None
Next, initialize the table with base conditions (table[i][i] = (F[i], i)). This determines the
optimal binary search trees with a single element.
0 1 2 3 4
0 34,0 None None None None
1 None 8,1 None None None
2 None None 50,2 None None
3 None None None 21,3 None
4 None None None None 16,4
Now, for each of the element (key), treat it as a root and determine the optimal binary
search tree for the elements lesser than that selected element and another optimal binary
search tree for the elements greater than that selected element. If that is lesser than the
previous value, update its total search time as minimal value seen so far. During these
calculations, ignore the already solved subproblems, and instead, pick the value from the
table.
The final table values would be:
0 1 2 3 4
0 34,0 50,0 142,2 184,2 232,2
1 None 8,1 66,2 108,2 156,2
2 None None 50,2 92,2 140,2
3 None None None 21,3 53,3
4 None None None None 16,4
Performance
The main part of the algorithm consists of three nested loops each iterating through at
most of the n values. The running time is therefore in O( ).
Space Complexity: O( ) where n is the number of elements in the optimal binary search
tree. Therefore, as ‘n’ increases it will run out of storage even before it runs out of time. The
6 . 4 2 O p t i m a l b i n a r y s ea r c h t r e e s
D y n am i c Pro g r am mi n g | 471
storage needed can be reduced to almost half by implementing the two-dimensional arrays
as one-dimensional arrays.
Examples
For example, the edit distance between “Hello” and “Fello” is 1. The edit distance between
“good” and “goodbye” is 3. The edit distance between any string and itself is 0. The edit
distance between "horizon" and "horzon" is 1. The edit distance between "horizon" and
"horizontal" is 3.
At any given point we are looking at the last letter of a substring of "pea", the source, and
comparing it against the last letter of a substring of "ate", the target. We can characterize
the difference as either an insertion of a character onto the target, a deletion of a character
on the source, or a replacement of the character on the source with the character on the
target. We then look at new substrings that differ from the old ones by the designated edit
operation. We keep track of the total number of edits until we have finally achieved the net
result of transforming "pea" into "ate".
Here's one path to transform "pea" into "ate".
STEP COMPARISON EDIT NEEDED TOTAL EDITING
1. "p" to "" delete: +1 edit "p" to "" in 1 edit
2. "e" to "" delete: +1 edit "pe" to "" in 2 edits
3. "a" to "a" no edits needed! "pea" to "a" in 2 edits
4. "a" to "t" add: +1 edit "pea" to "at" in 3 edits
5. "a" to "e" add: +1 edit "pea" to "ate" in 4 edits
The edit path is not unique, as here is another path, with a shorter edit distance:
6 . 4 3 E d i t d i s t a nc e
D y n am i c Pro g r am mi n g | 472
Recursive algorithm
So, how does one go about attacking this large problem? Well, it's just like "How do you eat
a shark? One bite at a time!" That is, we need to break the problem down into smaller
pieces, that we can solve. The final solution thus becomes the result of putting all the sub-
solutions together.
Suppose, for example, that we wanted to compute the edit distance between “Ceil” and
“trials”. Starting with “Ceil”, we consider what has to be done to get “trials” if the last step
taken were “add”, “delete”, or “replace”, respectively:
If we knew how to convert “Ceil” to “trial”, we could add “s” to get the desired word.
If we knew how to convert “Cei” to “trials”, then we would actually have “trialsl”
and we could delete that last character to get the desired word.
If we knew how to convert “Cei” to “trial”, then we would actually have “triall” and
we could replace the final “l” with “s” to get the desired word.
Notice that what we have done was to reduce the original problem to 3 “smaller” problems:
convert “Ceil” to “trial”, or “Cei” to “trials”, or “Cei” to “trial”.
We continue, recursively, to break down these problems:
1. Convert “Ceil” to “trial”, then add “s” to get the desired word.
To convert “Ceil” to “trial”,
Add Convert “Ceil” to “tria”, then add “l”
Delete Convert “Cei” to “trial”, giving “triall”, then delete.
Replace Convert “Cei” to “tria”, giving “trial”, and no replace is actually needed.
2. Convert “Cei” to “trials”, giving “trialsl”, then remove the last character.
To convert “Cei” to “trials”,
Add Convert “Cei” to “trial”, then add “s”
Delete Convert “Ce” to “trials”, giving “trialsi”, then delete.
Replace Convert “Ce” to “trial”, giving “triali”, and replace the final character.
3. Convert “Cei” to “trial”, giving “triall”, then replace the final “l” with “s”.
To convert “Cei” to “trial”,
Add Convert “Cei” to “tria”, then add “l”
Delete Convert “Ce” to “trial”, giving “triali”, then remove.
Replace Convert “Ce” to “tria”, giving “triai”, and replace the final character.
Now we have nine subproblems to solve, but note that the strings involved are getting
shorter. Eventually we will get down to subproblems involving an empty string, such as
`Convert “” to “xyz”,' which can be trivially solved by a series of “Adds”.
6 . 4 3 E d i t d i s t a nc e
D y n am i c Pro g r am mi n g | 473
Before attempting the code, let us concentrate on the recursive formulation of the problem.
Let ( , ) represent the minimum cost required to transform the first characters of to
the first characters of . That means, [1 … ] to [1 … ].
1 + ( − 1, )
⎧
( , − 1) + 1
( , )=
⎨ ( − 1, − 1), [ ] == [ ]
⎩ ( − 1, − 1) + 1 [ ]≠ [ ]
Based on the above discussion, we have the following cases:
If we delete character from , then we have to convert the remaining −
1 characters of to characters of
If we insert character in , then convert these characters of to − 1
characters of
If [ ] == [ ], then we have to convert the remaining − 1 characters of to − 1
characters of
If [ ] ≠ [ ], then we have to replace character of with character of B and
convert the remaining − 1 characters of to − 1 characters of
After calculating all the possibilities, we have to select the one which gives the lowest
conversion cost. Here you see the recursive implementation of the edit distance calculation.
def edit_distance (A, B):
if (A == ""):
return len(B) # base case
elif (B == ""):
return len(A) # base case
else:
add_distance = edit_distance(A, B[:-1]) + 1
delete_distance = edit_distance(A[:-1], B) + 1
change_distance = edit_distance(A[:-1], B[:-1]) + int(A[len(A)-1]!=B[len(B)-1])
return min(min(add_distance, delete_distance), change_distance)
print edit_distance("Ceil", "trials")
In the main portion, we don't know, off hand, whether the cheapest way to convert one
string into another involves a final add, delete, or replace, so we evaluate all the three
possibilities and return the minimum distance from among the three.
In each case, we recursively compute the distance (number of adds, deletes, and replaces)
required to “set up” a final add, delete, or replace. We add one to the add distance and the
remove distance to account for the final add or remove. For the replace distance, we add
one only if the final characters in the strings are different (if not, no final change is
required).
DP solution
We can solve the problem with dynamic programming by reversing the direction again, so
that we work the smaller subproblems first, keeping the answers in a table.
For example, in converting “Ceil” to “trials”, we start by forming a table of the cost (edit
distance) to convert “” to “”, “t”, “tr”, “tri”, etc.:
“” t r i a l s
“” 0 1 2 3 4 5 6
In other words, we need 0 steps to convert “” to “”, 1 step to convert “” to “t”, 2 steps to
convert “” to “tr”, and so on.
Next, we add a row to describe the cost of converting “C” to “”, “t”, “tr”, …, “trials”:
6 . 4 3 E d i t d i s t a nc e
D y n am i c Pro g r am mi n g | 474
“” t r i a l s
“” 0 1 2 3 4 5 6
C 1 1 2 3 4 5 6
OK, clearly we need 1 step to convert “C” to “”. How are the other entries in this row
computed?
Let's back up just a bit:
“” t r i a l s
“” 0 1 2 3 4 5 6
C 1 ?
What's the minimum cost to convert “C” to “t”? It's the smallest of the three values
computed as
1 plus the cost of converting “C” to “” (we get this cost by looking one
Add
position to the left).
1 plus the cost of converting “” to “t”, giving “tC” (we get this cost by looking
Delete
up one position).
1 (because “C” and “t” are different characters) plus the cost of converting “”
Replace
to “” (we get this cost by looking diagonally up and one position to the left).
The last of these yields the minimal distance: 1.
“” t r i a l s
“” 0 1 2 3 4 5 6
C 1 1 ?
What's the minimum cost to convert “C” to “tr”? It's the smallest of the three values
computed as
1 plus the cost of converting “C” to “t” (we get this cost by looking one position
Add
to the left).
1 plus the cost of converting “” to “tr”, giving “trZ” (we get this cost by looking
Delete
up one position).
1 (because “C” and “t” are different characters) plus the cost of converting “”
Replace
to “t” (we get this cost by looking diagonally up and one position to the left).
The last of these yields the minimal distance: 2.
Got the idea? Try filling in the rest of the row before reading further.
“” t r i a l s
“” 0 1 2 3 4 5 6
C 1 1 2 3 4 5 6
Add the next row, using the same technique:
t r i a l s
0 1 2 3 4 5 6
C 1 1 2 3 4 5 6
e 2 2 2 3 4 5 6
The row after that becomes a bit more interesting. When we get this far:
“” t r i a l s
“” 0 1 2 3 4 5 6
C 1 1 2 3 4 5 6
e 2 2 2 3 4 5 6
i 3 3 3 ?
6 . 4 3 E d i t d i s t a nc e
D y n am i c Pro g r am mi n g | 475
we are looking at the cost of converting “Zei” to “tri”. It's the smallest of the three values
computed as
1 plus the cost of converting “Cei” to “tr” (we get this cost by looking to the
Add
left one position).
1 plus the cost of converting “Ce” to “tri”, giving “trii” (we get this cost by
Delete
looking up one position).
Zero (because “i” and “i” are the same character) plus the cost of converting
Replace “Ce” to “tr” (we get this cost by looking diagonally up and to the left one
position).
The last of these yields the minimal cost of 2.
“” t r i a l s
“” 0 1 2 3 4 5 6
C 1 1 2 3 4 5 6
e 2 2 2 3 4 5 6
i 3 3 3 2 ?
Then we can fill out the rest of the row:
“” t r i a l s
“” 0 1 2 3 4 5 6
C 1 1 2 3 4 5 6
e 2 2 2 3 4 5 6
i 3 3 3 2 3 4 5
Finally, the last row of the table:
“” t r i a l s
“” 0 1 2 3 4 5 6
C 1 1 2 3 4 5 6
e 2 2 2 3 4 5 6
i 3 3 3 2 3 4 5
l 4 4 4 3 3 3 4
Notice that this last row, again, has a situation where the cost of a change is zero plus the
subproblem cost, because the two characters involved are the same (“l”).
From the lower right hand corner, then, we read out the edit distance between “Ceil” and
“trials” as 4.
Let's try solving all the subproblems first, i.e. a bottom-up approach. But we need to keep
track of what we've solved so that we can use them in subsequent calculations. Since the
subproblems involve all possible sub-strings in all possible configurations, we need to find
some way to easily represent all those possible combinations. So, matrices (two-
dimensional arrays) fit our design bill very well.
So, one way to represent all the possible subproblems is to create a matrix, "T", that holds
the results (minimum edit distances) for all the subproblems where [ ][ ] is the edit
distance between the substring of A of length to the substring of B of length . String A is
across rows and string B is across the columns, starting at position #1 (not 0!).
Technically, each row or column position in the matrix represents the entire sub-string
where the last character in the substring is the one currently being compared.
The first column and row, [ ][0] and [0][ ] represents the distance of the substrings to
the empty string.
One way to think of the first column/row as of "padding" and empty string in front of the
original strings.
6 . 4 3 E d i t d i s t a nc e
D y n am i c Pro g r am mi n g | 476
Let's see how the recurrence relationships manifest themselves in this implementation. The
base case edit distance values are trivial to calculate: They are just the lengths of the sub-
strings. We can just fill our matrix with those values using simple loops.
Notice that, the subproblem values that any spot in the matrix depends on are only to the
left and above that point in the matrix (see the diagram below).
[ ][ -1] [ ][ ]
That is, the edit distance value at any given point in the matrix does depend on any
values to its right or below it. This means that if we iterate through the matrix from the left
to left right and from top to bottom, we will always be iterating into positions in the matrix
whose values only depend on values that we've already calculated!
The last element in the matrix, at the lower right corner, holds the edit distance for the
entire source string being transformed into the entire target string, and thus, is our final
answer to the whole problem.
def edit_distance(A, B):
m=len(A)+1
n=len(B)+1
table = {}
for i in range(m):
table[i,0]=i
for j in range(n):
table[0,j]=j
for i in range(1, m):
for j in range(1, n):
cost = 0 if A[i-1] == B[j-1] else 1
table[i,j] = min(table[i, j-1]+1, table[i-1, j]+1, table[i-1, j-1]+cost)
return table[i,j]
print edit_distance("Ceil", "trials")
Performance
How many subproblems are there? In the above code, can range from 1 and can
range from 1 . This gives subproblems and each one takes O(1). Hence, the time
complexity is O( ).
Space Complexity: O( ), where is number of rows and is number of columns in the
matrix.
6 . 4 4 A l l p a i r s s h o r t es t pa t h p r o b l e m : F l o y d ' s a l go r i t h m
D y n am i c Pro g r am mi n g | 477
problem of finding the shortest path between all pairs of vertices on a graph is similar to
making a table of all the distances between all pairs of cities on a road map.
A weighted graph is a collection of points (vertices) connected by lines (edges), where each
edge has a weight (some real number) associated with it. One of the most common examples
of a graph in the real world is a road map. Each location is a vertex and each road
connecting locations is an edge. We can think of the distance traveled on a road from one
location to another as the weight of that edge.
Given a weighted graph, it is often of interest to know the shortest path from one vertex in
the graph to another.
What if we have an unweighted graph and are simply interested in the question, "Is there
chapter.
a path from to ?" We can use FloydWarshall to solve this question easily.
Floyd-Warshall algorithm
The Floyd-Warshall algorithm determines the shortest path between all pairs of vertices in
a graph. It uses a dynamic programming methodology to solve the all-pairs-shortest-path
problem. It follows recursive approach to find the minimum distances between all nodes in
a graph. The striking feature of this algorithm is its usage of dynamic programming to avoid
redundancy and thus solving the all-pairs-shortest-path problem in O( ).
Assume that the vertices in a graph are numbered from 1 to n. Consider the subset {1,2...,k}
of these n vertices. Imagine finding the shortest path from vertex to vertex that uses
vertices in the set {1,2, ..., } only. There are two situations:
1) is an intermediate vertex on the shortest path.
2) is not an intermediate vertex on the shortest path.
is an intermediate vertex
6 . 4 4 A l l p a i r s s h o r t es t pa t h p r o b l e m : F l o y d ' s a l go r i t h m
D y n am i c Pro g r am mi n g | 478
In the first situation, we can break down our shortest path into two paths: to and then
to . Notice that all the intermediate vertices from to are from the set {1,2,..., − 1} and
that all the intermediate vertices from to are from the set {1,2,..., − 1} also. In the second
situation, simply have all the intermediate vertices from the set {1,2,..., − 1}.
Now, define the function D for a weighted graph with the vertices {1,2,...n} as follows:
( , , ) = the shortest distance from vertex to vertex using the intermediate vertices in
the set {1,2,..., }.
Now, using the ideas from above, we can actually recursively define the function :
[ ][ ], =0
(, , )=
( , , − 1), ( , , − 1) + ( , , − 1) , > 0
( , , − 1)
( , , − 1)
( , , − 1)
The first line says that if we do not allow intermediate vertices, then the shortest path
between two vertices is the weight of the edge that connects them. If no such weight exists,
we usually define this shortest path to be of length infinity.
The second line pertains to allowing intermediate vertices. It says that the minimum path
from to through vertices {1,2,..., } is either the minimum path from to through vertices
{1,2,..., − 1} OR the sum of the minimum path from vertex to through {1,2,..., − 1} plus
the minimum path from vertex to through {1,2,... − 1}. Since this is the case, we
compute both and choose the smaller of these.
The Floyd’s algorithm for all pairs shortest path problem uses two-dimensional matrix that
stores all the weights between one vertex and another. From the definition, [ ][ ] = ∞ if
there is no path from to . The algorithm makes passes over A. Let D , D , . . . , D be the
values of two-dimensional matrix over the passes, with D being the initial state. Here,
we use one matrix and overwrite it for each iteration of .
After the k iteration, each value of matrix (D [ ][ ]) indicates the smallest length of any
path from vertex to vertex that does not pass through the vertices { + 1, + 2, … . }. That
means, it passes through the vertices possibly through {1, 2, 3, … , }.
The pass explores whether the vertex lies on an optimal path from to , for all , . The
same is shown in the diagram below.
6 . 4 4 A l l p a i r s s h o r t es t pa t h p r o b l e m : F l o y d ' s a l go r i t h m
D y n am i c Pro g r am mi n g | 479
D [ , ]
D [, ]
D [, ]
def adj(graph):
vertices = graph.keys()
D = {}
for i in vertices:
D[i] = {}
for j in vertices:
try:
D[i][j] = graph[i][j]
except KeyError:
# the distance from a node to itself is 0
if i == j:
D[i][j] = 0
# the distance from a node to an unconnected node is infinity
else:
D[i][j] = float('inf')
return D
def floyd_warshall(graph):
vertices = graph.keys()
d = dict(graph) # copy graph
for k in vertices:
for i in vertices:
for j in vertices:
d[i][j] = min(d[i][j], d[i][k] + d[k][j])
return d
if __name__ == "__main__":
graph = {'c': {'a':5, 'b':2, 'd':1, 'h':2},
'a': {'b':3, 'c':5, 'e':8, 'f':1},
'b': {'a':3, 'c':2, 'g':1},
'd': {'c':1, 'e':4},
'e': {'a':8, 'd':4, 'f':6, 'h':1},
'f': {'a':1, 'e':6, 'g':5},
'g': {'b':1, 'f':5, 'h':1},
'h': {'c':2, 'e':1, 'g':1}}
matrix=adj(graph)
print matrix
print floyd_warshall(matrix)
Performance
Time Complexity: The Floyd-Warshall all-pairs shortest path runs in O( ) time, which is
asymptotically no better than n calls to Dijkstra's algorithm. However, the loops are so tight
and the program is so short that it runs better in practice.
Space Complexity: O( ).
6 . 4 4 A l l p a i r s s h o r t es t pa t h p r o b l e m : F l o y d ' s a l go r i t h m
D y n am i c Pro g r am mi n g | 480
Johnson's algorithm
Johnson algorithm works well for sparse graphs. Johnson's algorithm is interesting
because it uses two other shortest path algorithms as subroutines. It uses Bellman-Ford
in order to reweight the input graph to eliminate negative edges and detect negative cycles.
With this new, altered graph, it then uses Dijkstra's shortest path algorithm to calculate
the shortest path between all pairs of vertices. The output of the algorithm is then the set
of the shortest paths in the original graph.
Johnson's algorithm runs in O(VE + V2log(V)) time for sparse graphs. The key idea in this
algorithm is to reweigh all edges so that they are all positive, then run Dijkstra from each
vertex and finally undo the reweighing.
It turns out, however, that to find the function for reweighing all edges, a set of difference
constraints need to be satisfied. It makes us first run Bellman-Ford to solve these
constraints.
Reweighing takes O(EV) time, running Dijkstra on each vertex takes O(VE + V2logV) and
undoing reweighing takes O(V2) time. Of these terms O(VE + V2logV) dominates and defines
algorithm's running time (for dense it's still O(V3).
Solution strategy
Find the sum of coins at even and odd positions say E and O respectively. Now if E>O,
pick coins only at even positions else pick coins only at odd positions.
The above strategy says that:
Find the sum of all coins that are at even-numbered positions (call this sum E).
Find the sum of all coins that are at odd-numbered positions (call this sum O).
If E > O, take the right-most coin first. Choose all the even-numbered coins in
subsequent moves.
If E < O, take the left-most coin first. Choose all the odd-numbered coins in
subsequent moves.
If E == O, you will guarantee to get a tie if you stick with taking only even-numbered
or odd-numbered coins.
6 . 4 5 O p t i m a l s t r a t e gy f o r a ga m e
D y n am i c Pro g r am mi n g | 481
You might be wondering how you can always choose coins at odd or even positions. Let us
illustrate this using an example. Assume that we have 10 coins numbered from 1 to 10.
If you take the left-most coin (numbered 1), your opponent can only have the choice of
taking coin numbered 2 or 10 (which are both even-numbered coins). On the other hand,
if you choose to take the coin numbered 10 (the right-most coin), your opponent can take
the coin numbered 1 or 9 (which are odd-numbered coins).
Notice that the total number of coins change from even to odd and vice-versa when a player
takes turn each time. Therefore, by going first and depending on the coin you choose, you
are essentially forcing your opponent to take either only even-numbered or odd-numbered
coins.
Example
Assume that n = 6 and values are {4, 3, 3, 4, 2, 3}. Then the sum at even positions is 10 (3
+ 4 + 3) and at odd positions is 9 (4 + 3 + 2).
4 3 3 4 2 3
1 2 3 4 5 6
So you will always pick the coins at even positions to ensure wining. How to achieve it?
You first pick the coin at position 6 (value 3).
4 3 3 4 2 3
1 2 3 4 5 6
Now the opponent is forced to choose a coin either at position 1 or 5. If he pick a coin at
position 1, you choose a coin at position 2. Else if he picks coin at position 5, you will
choose a coin at position 4. Following this strategy, you will always ensure picking coins
at even positions (same for odd positions).
Recursive solution
6 . 4 5 O p t i m a l s t r a t e gy f o r a ga m e
D y n am i c Pro g r am mi n g | 482
Since the opponent is as smart as you, we need to take all the possible combinations in
consideration before our move. First, we would need some observations to establish a
recurrence relation, which is essential as our first step in solving DP problems.
For each turn either we or our opponent selects the coin only from the ends of the row. Let
us define the subproblems as:
( , ): denotes the maximum possible value. We can definitely win, if it is our turn and the
only coins remaining are ... .
1 2 … … … …
… …
1 2 … +1 −1 …
… v … …
Selecting the coin: Here also the argument is the same as above. If we select the
coin, then the remaining range is from i to j − 1. Since we selected the coin,
we get the value for that. From the remaining range i to j − 1, the opponent can
select either the coin or the − 1 coin. But the opponent’s selection should be
minimized as much as possible [the term].
1 2 … +1 −1 …
… … …
6 . 4 5 O p t i m a l s t r a t e gy f o r a ga m e
D y n am i c Pro g r am mi n g | 483
Now we have the recursive function in hand and the above recurrence relation could be
implemented in few lines of code. But if we follow simple recursion, its complexity is
exponential. The reason is that each recursive call branches into a total of four separate
recursive calls, and it could be n levels deep from the very first call).
def game(coins, i, j):
#exit condition, i > j (not i == j)
if i > j:
return 0
else:
#Each player leaves the minimum value
path1 = coins[i] + min(game(coins, i+2, j), game(coins, i+1, j-1))
path2 = coins[j] + min(game(coins, i+1, j-1), game(coins, i, j-2))
return max(path1,path2)
# row of n coins
coins = [4, 3, 3, 4, 2, 3]
print game(coins, 0, len(coins)-1)
DP solution
Calculating all those moves will include repetitive calculations. So, this is the place where
we can use dynamic programming to show its magic. Let us solve the problem using the
dynamic programming technique.
To reduce the time complexity, we need to store the intermediate values in a table and use
them, when required. Dynamic programming provides an efficient way by avoiding re-
computations using intermediate results stored in a table.
def game(coins):
n = len(coins)
V = [[0 for i in range(n)] for j in range(n)]
#Initialize for length=1
for i in range(n):
V[i][i] = coins[i]
#Generate coins for length=2, 3 ....
#size+1 to include all coins!
for l in range(2, n+1):
#start value range 0 & 1
for i in range(0, n):
#end coins changes based on i and length(l)
#size =2 -> [0, 1] [1, 2],[2, 3]
#size =3 -> [0, 2], [1, 3]
j=i+l-1
#IMPORTANT: break when i or j index is not valid
if i >= n or i+1 >= n or i+2 >= n or j >= n:
break
#option 1: pick i
path1 = coins[i] + min(V[i+2][j],V[i+1][j-1])
#option 2: pick j
path2 = coins[j] + min(V[i+1][j-1],V[i][j-2])
V[i][j] = max(path1, path2)
#print(V)
return V[0][n-1]
# row of n coins
6 . 4 5 O p t i m a l s t r a t e gy f o r a ga m e
D y n am i c Pro g r am mi n g | 484
coins = [4, 3, 3, 4, 2, 3]
print game(coins)
Performance
How many subproblems are there? In the above formula, can range from 1 to and
can range from 1 to . There are a total of subproblems and each takes O(1) and the total
time complexity is O( ).
Space Complexity: O( ).
6.46 Tiling
: Assume that we use dominoes measuring 2 × 1 to tile an infinite strip
of height 2. How many ways can one tile a 2 × strip of square cells with 1 × 2 dominoes?
Solution:
Notice that we can place tiles either vertically or horizontally. For placing vertical tiles, we
need a gap of at least 2 × 2. For placing horizontal tiles, we need a gap of 2 × 1. In this
manner, the problem is reduced to finding the number of ways to partition using the
numbers 1 and 2 in an order that considered relevant [1]. For example: 11 = 1 + 2 + 2 +
1 + 2 + 2 + 1.
If we have to find such arrangements for 12, we can either place a 1 at the end or we can
add 2 in the arrangements possible with 10. Similarly, let us say we have possible
arrangements for n. Then for ( + 1), we can either place just 1 at the end we can find
possible arrangements for ( − 1) and put a 2 at the end. Going by the above theory:
= +
A string
and =
is called a palindrome if we form a string ′ by reversing the string
′
6.46 Tiling
D y n am i c Pro g r am mi n g | 485
The basic difference between the longest palindrome substring and the longest palindrome
subsequence is that, in the case of the longest palindrome substring, the output string
should have the contiguous characters, which gives the maximum palindrome; and in the
case of the longest palindrome subsequence, the output is the sequence of characters
where the characters might not be contiguous but they should be in an increasing sequence
with respect to their positions in the given string.
Therefore, we could scan each substring, check for a palindrome, and update the length of
the longest palindrome substring discovered so far.
def LPS(S):
lps = ""
def is_palindrome(str):
if str == str[::-1] :
return True
for idX, item in enumerate(S):
for idY, item in enumerate(S):
subStr = S[idX:idY+1]
if is_palindrome(subStr) and (len(subStr) > len(lps)):
lps = subStr
return lps
lps = LPS("abaccddccefeg")
print lps, len(lps)
Since the palindrome test takes linear time, this idea takes O( ) running time.
Space Complexity: O(1).
DP solution
The problem definition allows us to formulate an optimal substructure for the problem. To
improve over the brute force solution, first think, how we can avoid unnecessary re-
computation in validating palindromes. Consider the case “ ”. If we already know that
“ ” is a palindrome, it is obvious that “ ” must be a palindrome since the two left
and right end letters are the same.
6 . 4 7 L o n g e s t pa l i n d r o m e s u bs t r i n g
D y n am i c Pro g r am mi n g | 486
Let S be the input string, i and j are two indices of the string. We define a 2-dimensional
array " " and let [ ][ ] denote whether a substring from to is a palindrome or not. So,
we maintain a boolean table [ ][ ] that is filled in bottom up manner.
The value of [ ][ ] is true, if the substring is a palindrome, otherwise false. To calculate
[ ][ ], we check the value of [ + 1][ − 1], if the value is true and [ ] is same as [ ], then
we make [ ][ ] true. Otherwise, the value of [ ][ ] is made false.
, [] []
[ ][ ] =
, ℎ
In other words,
, []= [] [ + 1][ − 1]
[ ][ ] =
, ℎ
The base cases are:
, =
[ ][ ] =
, +1= [ ]= [ ]( ℎ ℎ ℎ )
This gives a straight forward DP solution, we first initialize the one and two letter
palindromes, and work our way up finding all three letter palindromes, and so on…
Algorithm
1. Create a two dimensional boolean array, i.e. [ ][ ], where [ ][ ] is true if the string
from i index to j index is a palindrome.
2. All the substrings of length 1 are palindromes, so make [ ][ ] true.
3. For all the substrings of length 2, if the first and second character are same then it
is a palindrome i.e., if [ ] = [ + 1] make [ ][ + 1] = .
4. Now, check for substrings having length more than 2.
a. The condition is, [ ][ ] is true if the value of [ + 1][ − 1] is true and [ ]
is same as [ ].
def LPS(S):
n = len(S)
T = [[False]*n for x in range(n)]
n = len(S)
maxLen = 1
maxStart = 0
# T[i][i] = True
for i in range(n):
T[i][i] = True
# T[i][i+1] = True if S[i] == S[i+1]
for i in range(n-1):
if S[i] == S[i+1]:
T[i][i+1] = True
maxLen = 2
maxStart = i
# T[i][j] = True if S[i]==S[j] and T[i+1][j-1] == True
for length in range(3, n+1):
for i in range(n- length + 1):
j = i + length - 1
if S[i] == S[j] and T[i+1][j-1] == True:
T[i][j] = True
maxLen = length
maxStart = i
return S[maxStart:maxStart+maxLen]
6 . 4 7 L o n g e s t pa l i n d r o m e s u bs t r i n g
D y n am i c Pro g r am mi n g | 487
S = "abaccddccefeg"
lps = LPS(S)
print lps, len(lps)
Example
For example, let us consider the string =“ ”. For this example, the initialization
of the table would be:
0 1 2 3 4 5 6 7 8
0 False False False False False False False False False
1 False False False False False False False False False
2 False False False False False False False False False
3 False False False False False False False False False
4 False False False False False False False False False
5 False False False False False False False False False
6 False False False False False False False False False
7 False False False False False False False False False
8 False False False False False False False False False
Next, we would need to set the diagonal to ( [ ][ ] = ). That is to indicate a
substring with a single character is a palindrome.
0 1 2 3 4 5 6 7 8
0 True False False False False False False False False
1 False True False False False False False False False
2 False False True False False False False False False
3 False False False True False False False False False
4 False False False False True False False False False
5 False False False False False True False False False
6 False False False False False False True False False
7 False False False False False False False True False
8 False False False False False False False False True
Now, we need to process the second base case: substrings with length 2. If both the
characters are same, we would need to set them to . But, in this example, no two
characters are same. Hence, no change in the entries of the table.
Next, we need to process substrings of length 3, 4, and so on. As a result, the final entries
of the table would look like:
0 1 2 3 4 5 6 7 8
0 True False False False False False False False True
1 False True False False False False False True False
2 False False True False False False True False False
3 False False False True False True False False False
4 False False False False True False False False False
5 False False False False False True False False False
6 False False False False False False True False False
7 False False False False False False False True False
8 False False False False False False False False True
Performance
The first loop takes O( ) time while the second loop takes O( − ) which is also
O( ). Therefore, the total running time of the algorithm is O( ).
The algorithm uses O( ) auxiliary space to store the intermediate results in the table.
6 . 4 7 L o n g e s t pa l i n d r o m e s u bs t r i n g
D y n am i c Pro g r am mi n g | 488
For example, if the current center is between the two ‘p’s in “unflappable”, we’d see the
gap, ‘pp’, ‘appa’ and then we would note that the next two neighbors: ‘l’ and ‘b’, are not the
same and stop. If we find a palindrome of length two or more, and it’s longer than the
longest we have found so far, we make a note of it and move to the next step. If the string
is of length n, there are − 2 character centers (there’s no need to examine the first and
last characters since they don’t have neighbors on one side and so can’t be possible
palindrome centers), and there are − 1 inter-character gaps. This makes 2 − 3 possible
centers.
def expand(S, left, right):
n = len(S)
while (left >= 0 and right < n and S[left] == S[right]):
left -= 1
right += 1
6 . 4 7 L o n g e s t pa l i n d r o m e s u bs t r i n g
D y n am i c Pro g r am mi n g | 489
return S[left+1:right]
def LPS(S) :
n = len(S)
if (n == 0):
return ""
if (n == 1):
return S
longest = ""
for i in range(n):
# get longest palindrome with center of i
p1 = expand(S, i, i)
if (len(p1) > len(longest)):
longest = p1
# get longest palindrome with center of i, i+1
p2 = expand(S, i, i+1)
if (len(p2) > len(longest)):
longest = p2
return longest
S = "abaccddccefeg"
lps = LPS(S)
print lps, len(lps)
Performance
We examine all 2 − 3 possible centers and find the longest palindrome for that center,
keeping track of the overall longest palindrome. For each center, we would have to check
up to m characters either side, where <= . All in all then, this algorithm is O( ); that
is, it will execute in time that is proportional to the square of the number of characters.
Space Complexity: O(1).
Manacher’s algorithm
How do you improve over the simpler O( ) approach?
Consider the worst case scenarios. The worst case scenarios are the inputs with multiple
palindromes overlapping each other.
For example, the inputs: “aaaaaaaaa” and “cabcbabcbabcba”. In fact, we could take
advantage of the palindrome’s symmetric property and avoid some of the unnecessary
computations.
There is, however, yet another algorithm, Manacher’s algorithm, that proves to be linear.
We’ll sketch out the details here.
As seen above, a palindrome’s center can either be at the position of a character (for
palindromes of odd length) or between two characters (for palindromes of even length).
a b b a
or
a b a
It is troublesome when writing code to differentiate an odd-length palindrome substring
from an even-length one. By inserting some special character, e.g., ’#’ which does not
6 . 4 7 L o n g e s t pa l i n d r o m e s u bs t r i n g
D y n am i c Pro g r am mi n g | 490
appear in the original string in between every two characters, the problem can be
equivalently converted over to finding an odd-length palindrome substring. Also, @ and $
signs are sentinels appended to each end to avoid bounds checking.
For instance, the original string "abaaba", can be changed to "@a#b#a#a#b#a$".
abaaba
@a#b#a#a#b#a$
We know that the longest palindrome substring is the string itself "abaaba" of even length.
In the transformed domain, the longest palindrome substring is centered on the 3rd ’#’ with
@(a#b#a)#(a#b#a)$ of odd-length. Another example, the original string is "abcba", the
longest substring in the transformed domain is @(a#b#)c(#b#a)$ of odd-length again.
In conclusion, in the transformed domain, odd-length palindrome’s center will land at ’#’
and even-length palindrome will center around a character in the original string. Moreover,
since we doubled the string length, the original length of the palindrome substring is just
a single sided substring starting from the center to one end, e.g, @(a#b#a)#(a#b#a)$, the
length is calculated from center ’#’ to ’a’ on the right end, which is 6.
Let’s take “abbaca” as our example string in which we want to discover the longest
palindrome.
Let’s insert a special character in between each of the letters to make another string (and
then we don’t have to worry about the gaps): @a#b#b#a#c#a$. We’ll assume that the
palindromes we look for cannot contain the special character (otherwise, we’ll be saying
that #a# is a palindrome).
abbaca
@a#b#b#a#c#a$
To find the longest palindromic substring, we need to expand around each [ ] such that
[ − ] … [ + ] forms a palindrome. We should immediately see that is the length of
the palindrome itself centered at [ ].
To store this data, in addition to , we need to create an auxiliary integer array , where
[ ] equals to the length of the palindrome centers at [ ]. The longest palindromic
substring would then be the maximum element in .
This [] array is calculated from the left end, position 1, to the right end. [0] is always
initialized to be 0.
For this example, let’s create (from scratch) the array for each character position in this
longer string that defines the longest palindrome at that centre. This gets us [0, 1, 0, 1, 4,
1, 0, 1, 0, 3, 0, 1, 0] –4 is for the gap between the two ‘b’s, the 3 is positioned at ‘c’. Notice
something interesting about this array? The numbers of form palindromes as well.
index 0 1 2 3 4 5 6 7 8 9 10 11 12
a b b a c a
@ a # b # b # a # c # a $
0 1 0 1 4 1 0 1 0 3 0 1 0
Like the K-M-P string search algorithm, we need to avoid any unnecessary comparisons
based on the information from the previous comparisons. Using this insight, we can fill in
this array ahead of the current centre as we read through the input string. Instead of
reading through the string character by character, we shall read through it centre by
centre, recording the current centre’s palindrome length, and estimating the palindrome
lengths that are in front of us.
Assume that we reached a state where table is partially completed. Assume that we have
arrived at index = 5, and we need to calculate [5] (indicated by the question mark ?)
index 0 1 2 3 4 5 6 7 8 9 10 11 12
6 . 4 7 L o n g e s t pa l i n d r o m e s u bs t r i n g
D y n am i c Pro g r am mi n g | 491
a b b a c a
@ a # b # b # a # c # a $
0 1 0 1 4 ?
When we get to the gap between the ‘b’s, we’re at this situation: @a#b#|b#a#c#a$ (where
the vertical mark indicates where we’ve reached), and the lengths array is [0, 1, 0, 1, 4, ?]
since we’ve worked out that “abba” is a palindrome centred there. We could fill in the
lengths array to the right with guestimates from what’s happened on the left: [0, 1, 0, 1,
4, 1, 0, 1, 0, ?] (guestimates are in italics). The second ‘b’ can’t be a centre (it’s in the middle
of the palindrome), but that second ‘a’ certainly could be, so we jump there and make it
the next centre we consider for a palindrome.
This is the essence of Manacher’s algorithm. There are some special cases where a
palindrome substring is the prefix to a longer palindrome, but in essence, using this
technique, we can jump ahead in the string and not have to consider every character as a
centre. For this reason Manacher’s algorithm turns out to be linear in time.
Manacher’s algorithm
1. Take advantage of the palindrome’s symmetric property and avoid some of the
unnecessary computations.
Create a new string ( ) by adding # on the left and right side of each character
in the string. Also, @ and $ signs are sentinels appended to each end to avoid
bounds checking.
New string ( ) length is odd.
2. Consider each character in to be center of a palindrome and expand. Find new
center ( ), end of palindrome ( ) & at each index.
3. If current index ( ) < , see if we can take advantage of the already calculated value.
4. Iterate and find the length of palindrome and reconstruct the characters.
6 . 4 7 L o n g e s t pa l i n d r o m e s u bs t r i n g
D y n am i c Pro g r am mi n g | 492
Example
Let us trace the algorithm with the following example:
index 0 1 2 3 4 5 6 7 8 9 10 11 12
a b b a c a
@ a # b # b # a # c # a $
0 0 0 0 0 0 0 0 0 0 0 0 0
Now, we need to find the length of the longest palindrome for each index of , starting with
index = 1. Also, the initial values of and are 0.
Center index of longest palindrome calculated so far
Right index of palindrome calculated so far
For the first iteration, the values , , and are:
1
0
0
We need to expand with i as the center on both sides with the comparison:
[ + [ ]] == [ − [ ]]
This condition is true as [1] == [1] is always true. Hence, increase the value at [1].
index 0 1 2 3 4 5 6 7 8 9 10 11 12
a b b a c a
@ a # b # b # a # c # a $
0 1 0 0 0 0 0 0 0 0 0 0 0
6 . 4 7 L o n g e s t pa l i n d r o m e s u bs t r i n g
D y n am i c Pro g r am mi n g | 493
Next, the condition would fail as the value [2] and [0] are not the same.
Now, we need to update , and with new values as [ ] + (1 + 1 = 2) is greater than
current (0). As a result, the new values are:
1
1
2
Next, we need to process a character of with index = 2. The condition [2] == [2] is
true. Also, we cannot expand further as [3] and [1] are not same.
index 0 1 2 3 4 5 6 7 8 9 10 11 12
a b b a c a
@ a # b # b # a # c # a $
0 1 1 0 0 0 0 0 0 0 0 0 0
We need to update , and with new values as [ ] + (1 + 2 = 3) is greater than current
(2). As a result, the new values are:
2
2
3
Next, we need to process a character of with index = 3 and the condition [3] == [3]
is true.
index 0 1 2 3 4 5 6 7 8 9 10 11 12
a b b a c a
@ a # b # b # a # c # a $
0 1 1 1 0 0 0 0 0 0 0 0 0
We can expand as [4] and [2] are same.
index 0 1 2 3 4 5 6 7 8 9 10 11 12
a b b a c a
@ a # b # b # a # c # a $
0 1 1 2 0 0 0 0 0 0 0 0 0
But, we stop after this as [5] and [0] are not same. Now, we need to update , and with
new values as [ ] + (2 + 3 = 5) is greater than current (3). As a result, the new values
are:
3
3
5
Next, we need to process a character of with index = 4. Now, we have reached an
important case: < . This allows us to skip some updates to array . The size of the
can be obtained by using the values of and i.
= 2× − = 2×3−4=2
For index = 4, we can determine the minimum length of palindrome [5] by taking the
minimum of and − (5 − 4).
index 0 1 2 3 4 5 6 7 8 9 10 11 12
a b b a c a
@ a # b # b # a # c # a $
0 1 1 2 1 0 0 0 0 0 0 0 0
We can further expand as the pairs (T[5], T[3]), (T[6], T[2]), and (T[7], T[1]) are same.
index 0 1 2 3 4 5 6 7 8 9 10 11 12
a b b a c a
@ a # b # b # a # c # a $
6 . 4 7 L o n g e s t pa l i n d r o m e s u bs t r i n g
D y n am i c Pro g r am mi n g | 494
0 1 1 2 4 0 0 0 0 0 0 0 0
We need to update , and with new values as [ ] + (4 + 4 = 8) is greater than current
(5). As a result, the new values are:
4
4
8
Next, process a character of with index = 5, and we have the case < . The size of the
can be obtained by using the values of and i.
= 2× − = 2×4−5=3
For index = 5, we can determine the minimum length of palindrome [5] by taking the
minimum of and − .
index 0 1 2 3 4 5 6 7 8 9 10 11 12
a b b a c a
@ a # b # b # a # c # a $
0 1 1 2 4 2 0 0 0 0 0 0 0
There is no change in values and as [ ] + (2 + 5 = 7) is less than current (8).
Similarly, this process would continue for each index of T. The final values of array are:
index 0 1 2 3 4 5 6 7 8 9 10 11 12
a b b a c a
@ a # b # b # a # c # a $
0 1 1 2 4 2 1 2 1 3 1 1 0
Now, with the help of array , we can determine the longest palindromic substring. To
obtain that, just scan through the array and find the maximum value in it.
index 0 1 2 3 4 5 6 7 8 9 10 11 12
0 1 1 2 4 2 1 2 1 3 1 1 0
In the above array, the index 4 has the maximum value 4. It means with index 4 as center
we have the longest palindromic substring. To get the final string, just pick 4 elements on
both sides of with index 4 as a center (as we have extra “#” characters in ); and then
remove these extra padded characters which we added at the beginning.
Example
For example, the string “AGCTCBMAACTGGAM” has many palindromes as subsequences,
but " " is the palindrome with maximum length 10.
6 . 4 8 L o n g e s t pa l i n d r o m e s u bs e q u e n ce
D y n am i c Pro g r am mi n g | 495
subsequence. However, the time complexity for that is exponential time O(2 ), where n is
the number of characters in the string.
Example
For the string " ", we have following subsequences:
< > , , , , , ,
Total number of subsequences = 2 = 2 = 8.
There are no more than O( ) substrings in a string of length (while there are
exactly 2 subsequences). For the string , the possible substrings are:
< >, , , , , ,
Notice that, is a subsequence but not a substring.
A subset describes a selection of objects, where the order among them does not matter.
Many of the algorithmic problems in this catalog seek the best subset of a group of things:
vertex cover seeks the smallest subset of vertices to touch each edge in a graph; knapsack
seeks the most profitable subset of items of bounded total size; and set packing seeks the
smallest subset of subsets that together cover each item exactly once.
There are 2 distinct subsets of an n-element set, including the empty set as well as the
set itself. This grows exponentially, but at a considerably smaller rate than the !
permutations of n items. Indeed, since 2 = 1,048,576, a brute-force search through all
subsets of 20 elements is easily manageable, although by n=30, 2 = 1,073,741,824, so
you will certainly be pushing things.
def longest_palindrome_subsequence(S):
lps = ""
def is_palindrome(str):
if str == str[::-1] :
return True
# generates all subsets (powerset)
def powerset(s):
n = len(s)
masks = [1<<j for j in xrange(n)]
for i in xrange(2**n):
yield [s[j] for j in range(n) if (masks[j] & i)]
for subseq in powerset(S):
if is_palindrome(subseq) and (len(subseq) > len(lps)):
lps = subseq
return lps
lps = longest_palindrome_subsequence("abaccddccefeg")
print lps, len(lps)
Recursive solution
This definition allows us to formulate an optimal substructure for the problem. If we look
at the substring [ , . . , ] of the string S, then we can find a palindrome subsequence of
length at least 2 if [ ] = [ ]. If they are not same, then we seek the maximum length
palindrome subsequences in substrings [ + 1, . . . , ] and [ , . . . , − 1]. Also every character
[ ] is a palindrome in itself of length 1. Therefore base cases are given by [ , ] = 1. Let
us define the maximum length palindrome for the substring [ , . . . , ] as ( , ).
6 . 4 8 L o n g e s t pa l i n d r o m e s u bs e q u e n ce
D y n am i c Pro g r am mi n g | 496
This yields the below recursive relation to find the longest palindromic subsequence of a
sequence S.
1, =
( , ) = 2 + ( + 1, − 1), []= []
( ( , − 1), ( + 1, ), []≠ []
Based on the recursive definition we present the following code:
# Returns the length of the longest palindromic subsequence in S
def LPS(S, i, j):
# Base case: If there is only 1 character
if (i == j):
return 1
# Base case: If there are only 2 characters and both are same
if (S[i] == S[j] and i + 1 == j):
return 2
# If the first and last characters are same
if (S[i] == S[j]):
return LPS(S, i+1, j-1) + 2
# If the first and last characters are not same
return max( LPS(S, i, j-1), LPS(S, i+1, j) )
S = "abaccddccefeg"
lps = LPS(S, 0, len(S)-1)
print lps, len(lps)
Performance
Let us consider recursion tree for sequence of length 6 having all distinct characters (say
"abcdef") whose LPS length is 1.
LPS(0,5)
LPS(1,5) LPS(0,4)
The worst case time complexity of the above solution is exponential O(2 ) and auxiliary
space used by the program is O(n) (for the runtime stack). The worst case happens when
there is no repeated character present in S (i.e. LPS length is 1) and each recursive call will
end up in two recursive calls.
DP solution
Let us use DP to solve this problem. The LPS problem has an optimal substructure and
also exhibits overlapping subproblems. As we can see, the same subproblems are getting
computed again and again. We know that problems having optimal substructure and
overlapping subproblems can be solved by dynamic programming, in which subproblem
6 . 4 8 L o n g e s t pa l i n d r o m e s u bs e q u e n ce
D y n am i c Pro g r am mi n g | 497
solutions are stored in a table rather than computed again and again. This method is
illustrated below.
If we look at the substring S[i, . . , j] of the string , then we can find a palindrome
subsequence of length at least 2 if S[i] == S[j]. If they are not the same, then we have to
find the maximum length palindrome in subsequences S[i + 1, . . . , j] and S[i, . . . , j − 1].
Also, every character [ ] is a palindrome of length 1. Therefore, the base cases are given
by [ , ] = 1. Let us define the maximum length palindrome for the substring S[i, . . . , j] as
L[i][j]. That is, the value [i][j] for each interval [i,j] is the length of the longest palindromic
subsequence within the corresponding substring interval.
1, =
[i][j] = 2 + [ + 1][ − 1], []= []
( [ ][ − 1], [ + 1][ ]), [ ]≠ [ ]
def longest_palindrome_subsequence(S):
n = len(S)
L =[[0 for x in range(n)] for x in range(n)]
# palindromes with length 1
for i in range(0,n-1):
L[i][i] = 1
# palindromes with length up to j+1
for k in range(2,n+1):
for i in range(0,n-k+1):
j = i+k-1
if S[i] == S[j] and k == 2:
L[i][j] = 2
if S[i] == S[j]:
L[i][j] = 2 + L[i+1][j-1]
else:
L[i][j] = max( L[i+1][j] , L[i][j-1] )
#print L
return L[0][n-1]
print longest_palindrome_subsequence("Career Monk Publications")
Time Complexity: First ‘ ’ loop takes O( ) time while the second ‘for’ loop takes O( − )
which is also O( ). Therefore, the total running time of the algorithm is given by O( ).
Space Complexity: O( ) where is number of characters in the given string S.
Recursive solution
First of all, notice the difference between a subsequence and a substring. The main
difference between a substring and a subsequence is, a substring must include continuous
characters of the original string, whereas subsequence does not need to be, just maintain
the relative order of the selected characters. For example, " ℎ" is a subsequence of
" ℎ" while " ℎ " is not.
6 . 4 9 C o u nt i n g t he s u b se q ue n c es i n a st r i n g
D y n am i c Pro g r am mi n g | 498
Alternatively, a subsequence of a string is a new string which is formed from the original
string by deleting some (can be none) of the characters without disturbing the relative
positions of the remaining characters. (i.e., " " is a subsequence of " " while " " is
not).
This problem can be solved in a recursive way. We define the recursive computation
structure to be ( , ) indicating the count of distinct subsequences of characters of
appear in characters of . It's easier if we include 0 in the recursive definition to
accommodate the case when there are no characters (empty string) to be considered.
1, =0
⎧ 0, =0
( , )=
⎨ ( − 1, − 1) + ( − 1, ), [ ] == [ ]
⎩ ( − 1, ), []≠ []
Let us concentrate on the components of the above recursive formula:
If = 0, then we can treat empty string also a valid subsequence in and return
the count as 1.
If = 0, the count becomes 0 since is empty.
If S[i] == T[j], it means character of and character of are the same. In this
case, we have to check the subproblems with − 1 characters of and − 1
characters of , and also we have to count the result of − 1 characters of with
characters of . This is because even all characters of might be appearing in
− 1 characters of .
If S[i] ≠ T[j], then we have to get the result of subproblem with − 1 characters of
and characters of . No matter what current char of S is, we simply don't use it.
We will only use chars [0,... − 2] from S no matter how many solutions are there
to cover [0... − 1].
After computing all the values, we have to select the one which gives the maximum count.
This recursive definition can be converted to code as shown below.
def distinct_subsequences(S, T):
m = len(S)
n = len(T)
# Base conditions
if (m==0):
return 0
if (n==0):
return 1
if (m==1 and n==1):
if (S[0]==T[0]):
return 1
else:
return 0
if (S[m-1]==T[n-1]):
return distinct_subsequences(S[:-1], T[:-1]) + distinct_subsequences(S[:-1], T)
else:
return distinct_subsequences(S[:-1], T)
print distinct_subsequences("abadcb", "ab")
However, due to duplicated work, this algorithm is not efficient enough to pass large data
set. Similar to the hint from the other solutions, a bottom-up built table can be derived.
DP solution
Notice that in the prior recursive solution, there are lots of duplicate computations. To
optimize it, we can sacrifice little space for time, using dynamic programming. This time,
6 . 4 9 C o u nt i n g t he s u b se q ue n c es i n a st r i n g
D y n am i c Pro g r am mi n g | 499
we store all previous computation results in a table of size × , where and are the
lengths of strings S and T, respectively.
def distinct_subsequences(S, T):
m = len(S)
n = len(T)
C = [[0 for _ in xrange(n+1)] for _ in xrange(m+1)]
for j in xrange(n+1):
C[0][j] = 0
for i in xrange(m+1):
C[i][0] = 1
for i in xrange(1, m+1):
for j in xrange(1, n+1):
if S[i-1]==T[j-1]:
C[i][j] = C[i-1][j]+C[i-1][j-1]
else:
C[i][j] = C[i-1][j]
return C[m][n]
print distinct_subsequences("abadcb", "ab")
Example
As an example, consider the strings = ” ” and = “ ”. For these strings, create a
table of size + 1 × + 1 with 7 rows and 3 columns. The columns indicate the string
characters and rows are for indicating the string characters. As per the algorithm, the
initialization of the table would be:
0 1 2
0 0 0 0
1 0 0 0
2 0 0 0
3 0 0 0
4 0 0 0
5 0 0 0
6 0 0 0
Now, let us fill the table with base conditions:
0, =0
1, =0
0 1 2
0 1 0 0
1 1 0 0
2 1 0 0
3 1 0 0
4 1 0 0
5 1 0 0
6 1 0 0
For the general case, third nested for loop processes the characters of and , one by one,
and uses the recursive definition to determine the value of [ ][ ].
The values of i and j for the first iteration are: = 1, = 1. For these values, the condition
[ − 1] == [ − 1] is true. Hence,
[ ][ ] = [ − 1][ ] + [ − 1][ − 1] → [1][1] = [0][1] + [0][0] = 0 + 1 = 1
6 . 4 9 C o u nt i n g t he s u b se q ue n c es i n a st r i n g
D y n am i c Pro g r am mi n g | 500
0 1 2
0 1 0 0
1 1 1 0
2 1 0 0
3 1 0 0
4 1 0 0
5 1 0 0
6 1 0 0
Next, the values of i and j for the second iteration are: = 1, = 2. For these values, the
condition [ − 1] == [ − 1] is false. Hence,
[ ][ ] = [ − 1][ ] → [1][2] = [0][2] = 0
0 1 2
0 1 0 0
1 1 1 0
2 1 0 0
3 1 0 0
4 1 0 0
5 1 0 0
6 1 0 0
Next, the values of i and j for the third iteration are: = 2, = 1. For these values, the
condition [ − 1] == [ − 1] is false. Hence,
[ ][ ] = [ − 1][ ] → [2][1] = [1][1] = 1
0 1 2
0 1 0 0
1 1 1 0
2 1 1 0
3 1 0 0
4 1 0 0
5 1 0 0
6 1 0 0
This process would be continued until all the characters of and are processed. The final
values of the table would be:
0 1 2
0 1 0 0
1 1 1 0
2 1 1 1
3 1 2 1
4 1 2 1
5 1 2 1
6 1 2 3
As the last statement of the algorithm, the value at [ ][ ] ( [6][2] = 3]) would be returned.
Performance
How many subproblems are there? In the above formula, can range from 1 to and
can range from 1 to . There are a total of subproblems and each one takes O(1).
Hence, the time complexity is O( ).
Space Complexity: O( ) where is the number of rows and is the number of columns
in the table.
6 . 4 9 C o u nt i n g t he s u b se q ue n c es i n a st r i n g
D y n am i c Pro g r am mi n g | 501
Space optimization
If you try to solve this by hand, you'll quickly realize that what you do is to simply fill out
a table. The rows are chars from , and columns are chars from . You update in row-wise
fashion each time (the inner loop). For each cell you first drag what's directly above the
current cell ( [ ][ ]= [ − 1][ ]). And if chars are same, you increment it by its top-left
neighbor ( [ ][ ] += [ − 1][ − 1]). So all computation can be done with the storage of a
vector instead of a table.
There is one problem however, which is in second case, we need [ − 1] (when reduced to
1-d vector, ignored) but [ − 1] might be updated before reaching [ ]. The workaround
is to use a temporary variable to hold its value.
[ − 1][ − 1] [ − 1][ ]
[ ][ ]
Performance
Time Complexity: There is no change in the running time and it is O( ).
Space Complexity: O( ) where is the number of characters in the string .
[i-1][j]
S[i][j-1] [ ][ ]
To find the best solution for that cell, we should have already found the best solutions for
all of the cells from which we can arrive to the current cell. Let us assume that ( , )
indicates the number of apples that we can collect by reaching the position ( , ) in the table
. From above, a recurrent relation can be easily obtained as:
( , − 1), >0
(, )= [ ][ ] +
( − 1, ), >0
( , ) must be calculated by going first from the left to right in each row and process the
rows from top to bottom, or by going first from the top to bottom in each column and
process the columns from the left to right.
Implementation
For the position [0][0], there won’t be previous row and column. In other words, the
number of apples we can collect is equal to [0][0] for the position [0][0].
S[0][0] = Apples[0][0]
For the first row, there won’t be previous row. Hence, we can only move from left to right
by collecting the apples from the previous column position. This is a kind of initialization
for the table . The complete first row can be initialized as:
for j in range(1, m):
S[0][j] = Apples[0][j] + S[0][j-1]
On similar lines, for the first column, there won’t be a previous column. We can only move
from the top to bottom by collecting the apples from the previous row position. The complete
first column can be initialized as:
for i in range(1, n):
S[i][0] = Apples[i][0] + S[i-1][0]
def find_maximum_apples_count(Apples, n, m):
S =[[0 for x in range(m)] for x in range(n)]
# Initialize position S[0][0].
# We cannot collect any apples other than Apples[0][0]
S[0][0] = Apples[0][0]
# Initialize the first row
for j in range(1, m):
S[0][j] = Apples[0][j] + S[0][j-1]
# Initialize the first column
for i in range(1, n):
S[i][0] = Apples[i][0] + S[i-1][0]
for i in range(1, n):
for j in range(1, m):
previous_column = S[i][j-1]
previous_row = S[i-1][j]
if (previous_column > previous_row):
S[i][j] = Apples[i][j] + previous_column
else:
S[i][j] = Apples[i][j ]+ previous_row
return S[n-1][m-1]
Apples = [ [1, 2, 4, 7], [2, 1, 6, 1], [12, 5, 9, 19], [4, 29, 50, 60] ]
print find_maximum_apples_count(Apples, len(Apples), len(Apples[0]))
Performance
How many such subproblems are there? In the above formula, can range from 1
and can range from 1 . There are a total of subproblems and each one takes O(1).
Time Complexity is O( ).
Space Complexity: O( ), where is the number of rows and is the number of columns
in the given matrix.
1. From the left (if it's not situated on the first column),
2. From the top (if it's not situated on the first row) or
3. From the diagonal
To find the best solution for a cell, we should have already found the best solutions for all
of the cells from which we can arrive to the current cell. Let us assume that ( , ) indicates
the number of apples that we can collect by reaching the position ( , ) in the table S.
( , − 1), >0
(, )= [ ][ ] + ( − 1, ), >0
( − 1, − 1), >0 >0
( , ) must be calculated by going first from the left to right in each row and process the
rows from top to bottom, or by going first from top to bottom in each column and process
the columns from left to right.
[ -1][ ]
[ -1][ -1]
S[ ][ -1] [ ][ ]
6 . 5 1 A p p l e c o u n t v a r i a n t w i t h 3 wa y s o f r e a ch i n g a l o ca t io n
D y n am i c Pro g r am mi n g | 504
Performance
How many such subproblems are there? In the above formula, can range from 1 to
and can range from 1 . There are a total of subproblems and and each one
takes O(1).
Time Complexity is O( ).
Space Complexity: O( ) where is the number of rows and is the number of columns
in the given matrix.
Naïve approach
The naïve approach consists of trying to find out every possible square of 1’s that can be
formed from within the matrix. The question now is – how to go for it?
6 . 5 2 L a r g e s t sq ua r e s u b- m a t r i x w i t h a l l 1 ’ s
D y n am i c Pro g r am mi n g | 505
We use a variable to contain the size of the largest square found so far and another variable
to store the size of the current, both initialized to 0. Starting from the top left cell in the
matrix, we search for a 1. No operation is needed for a 0.
0 1 1 0 1
1 1 0 1 0
0 1 1 1 0
1 1 1 1 0
1 1 1 1 1
0 0 0 0 0
Whenever a 1 is found, try to find out the largest square that can be formed including that
1. For this, move diagonally (right and downwards), i.e. increment the row index and
column index temporarily and then check whether all the elements of that row and column
are 1 or not. If all the elements happen to be 1, move diagonally further as previously. If
even one element turns out to be 0, we stop this diagonal movement and update the size
of the largest square. Now, continue the traversal of the matrix from the element next to
the initial 1 found, till all the elements of the matrix have been traversed.
def maximalSquare(matrix):
rows = len(matrix)
columns = len(matrix[0])
maxsqlen = 0
for i in range(rows):
for j in range(columns):
if (matrix[i][j] == 1) :
sqlen = 1
flag = True
while (sqlen + i < rows and sqlen + j < columns and flag) :
for k in range(j, sqlen + j + 1):
if (matrix[i + sqlen][k] == '0') :
flag = False
break
for k in range(i, sqlen + i + 1):
if (matrix[k][j + sqlen] == 0) :
flag = False
break
if (flag):
sqlen += 1
if (maxsqlen < sqlen):
maxsqlen = sqlen
return maxsqlen
matrix=[[0, 1, 1, 0, 1],
[1, 1, 0, 1, 0],
[0, 1, 1, 1, 0],
[1, 1, 1, 1, 0],
[1, 1, 1, 1, 1],
[0, 0, 0, 0, 0]]
print maximalSquare(matrix)
Performance
We scan through all the elements of the matrix, and for each of the 1, check whether it
forms a bigger square with all 1’s. This would take O( × ), where m is the number of rows
and n is the number of columns in the matrix. In worst case, we need to traverse the
complete matrix for every 1. So, the overall running time of the algorithm is O(( × ) ).
6 . 5 2 L a r g e s t sq ua r e s u b- m a t r i x w i t h a l l 1 ’ s
D y n am i c Pro g r am mi n g | 506
This naïve approach does not need any extra space. Hence, the space complexity of the
algorithm is O(1).
Recursive solution
To formulate the recursive solution, let us break down the problem into smaller
subproblems to get started. Consider a 1×1 matrix. For this matrix, if the element is 1, we
definitely have a solution with answer as 1. Obviously, if the element is 0, the solution
would be 0.
Now, let us consider any 2×2 matrix. In order to be a 2×2 matrix with all 1’s, each of top,
left, and top-left neighbor of its bottom-right corner has to be a 1×1 square matrix with 1
in their respective cells.
1 1 1 1
1 ? 1 2
If any of the top, left, and top-left neighbor 1×1 square matrix has a zero, we cannot make
a 2×2 matrix with all 1’s. The three possibilities are:
0 1 0 1
1 ? 1 1
1 0 1 0
1 ? 1 1
1 1 1 1
0 ? 0 1
In general, for any × square matrix, each of its neighbors at the top, left, and top-left
corner should at-least have a size of − 1 × − 1. The reverse of this statement is also
true. If the size of the square sub-matrix ending at the top, left, and top-left neighbors of
any cell in the given matrix is at-least ( − 1), then we can get × sub-matrix from that
cell. That is the reason behind picking up the smallest neighboring square and adding 1 to
it.
if (matrix[m][n] == 0):
return matrix[m][n]
# find largest square matrix ending at matrix[m][n-1]
left = maximalSquare(matrix, m, n - 1)
# find largest square matrix ending at matrix[m-1][n]
top = maximalSquare(matrix, m - 1, n)
# find largest square matrix ending at matrix[m-1][n-1]
diagonal = maximalSquare(matrix, m - 1, n - 1)
# minimum of top, left, and diagonal
size = 1 + min (min(top, left), diagonal)
# update maximum size found so far
maxsqlen = max(maxsqlen, size)
# return the size of largest square matrix ending at matrix[m][n]
return size
matrix=[ [0, 1, 1, 0, 1],
[1, 1, 0, 1, 0],
[0, 1, 1, 1, 0],
[1, 1, 1, 1, 0],
[1, 1, 1, 1, 1],
[0, 0, 0, 0, 1]]
rows = len(matrix)
columns = len(matrix[0])
maximalSquare(matrix, rows-1, columns-1)
print maxsqlen
Performance
Starting with ( , , ), the function would make 3
recursive calls at each level. Also, the maximum depth of the recursion tree is n. So, the
total number of recursive calls would be O(3 ).
( , , )
( , − 1, ) ( , , − 1)
( , − 1, − 1)
( , − 2, ) ( , , − 2)
( , − 2, − 2)
6 . 5 2 L a r g e s t sq ua r e s u b- m a t r i x w i t h a l l 1 ’ s
D y n am i c Pro g r am mi n g | 508
recursive call is , the runtime stack size is O(n). Hence, the space complexity of this
recursive algorithm is O(n).
DP solution
The above solution exhibits overlapping subproblems. If we draw the recursion tree of the
solution, we can see that the same subproblems are getting computed again and again. We
know that problems having optimal substructure and overlapping subproblems can be
solved by using dynamic programming. So, let us try solving this problem using DP. Let
the given binary matrix be [ ][ ]. The idea of the algorithm is to construct a
temporary matrix [ ][ ] in which each entry [ ][ ] represents the size of the square sub-
matrix with all 1’ including [ ][ ] and [ ][ ] is the rightmost and bottom-most
entry in the sub-matrix.
The size of the largest square sub-matrix ending at a cell [ ][ ] will be 1 plus minimum
among the largest square sub-matrix ending at [ ][ − 1], [ − 1][ ] and [ − 1][ − 1]. The
result will be the maximum of all square sub-matrix ending at [ ][ ] for all possible values
of and .
[ − 1][ − 1] L[i-1][j]
[ ][ − 1] ?
Algorithm
1) Construct an auxiliary matrix [ ][ ] for the given matrix [ ][ ] and initialize
with 0.
2) For other entries, use the following expressions to construct L[ ][ ]:
if( [ ][ ] 1 )
[ ][ ] = ( [ ][ − 1], [ − 1][ ], [ − 1][ − 1]) + 1
else
[ ][ ] = 0
3) While updating the [ ][ ], keep track of the maximum value seen so far (say,
).
4) Return the value of .
def maximalSquare(matrix):
if len(matrix) == 0:
return 0
L = [[0]*len(matrix[0]) for i in xrange(0, len(matrix))]
maxsqlen = 0
for i in xrange(0, len(matrix)):
for j in xrange(0, len(matrix[0])):
if matrix[i][j] is 1:
if i == 0:
L[i][j] = 1
elif j == 0:
L[i][j] = 1
else:
L[i][j] = min(L[i - 1][j], L[i][j - 1], L[i - 1][j - 1]) + 1
maxsqlen = max(maxsqlen, L[i][j])
return maxsqlen
matrix=[ [0, 1, 1, 0, 1],
6 . 5 2 L a r g e s t sq ua r e s u b- m a t r i x w i t h a l l 1 ’ s
D y n am i c Pro g r am mi n g | 509
[1, 1, 0, 1, 0],
[0, 1, 1, 1, 0],
[1, 1, 1, 1, 0],
[1, 1, 1, 1, 1],
[0, 0, 0, 0, 1]]
print maximalSquare(matrix)
Performance
ℎ ? In the above code, can range from 1 to and can range
from 1 to . There are a total of subproblems and each one takes O(1).
Hence, the running time of the algorithm is O( ).
Space complexity is O( ), where is the number of rows and is the number of columns
in the given matrix.
[ − 1][ − 1] [ − 1][ ]
[ ][ − 1] ?
[]
[ − 1] ?
def maximalSquare(matrix):
if len(matrix) == 0:
return 0
L = [0] * (len(matrix[0])+1)
maxsqlen = 0
previous = 0
for i in xrange(0, len(matrix)):
for j in xrange(0, len(matrix[0])):
temp = L[j]
if matrix[i][j] == 1:
if i == 0 or j == 0:
L[j] = 1
else:
6 . 5 2 L a r g e s t sq ua r e s u b- m a t r i x w i t h a l l 1 ’ s
D y n am i c Pro g r am mi n g | 510
Performance
There is no change in the running time of the algorithm. Hence, the time complexity of the
algorithm is O( ).
Space complexity is O( ), where is the number of columns in the given matrix.
6 . 5 3 M a x i m u m s i z e s u b- m a t r i x w i t h a ll 1 ’ s
D y n am i c Pro g r am mi n g | 511
( )( )
Similarly for an × 1, we have + ( − 1) + ( − 2). . . + 1 = rectangles.
If we add another column to × 1, first we have as many rectangles in the second column
( )( )
as the first, and then we have that same number of 2 × rectangles. So × 2 = 3
( ) ( )( )
If we add another column to × 2, we add another in that column, another
( )( ) ( )( )
for new × 2 section and another for the 3-wides, so we have 6
( )( )
( )( ) ( )( )( )
So for × , we'll have =
def maximalRectangle(matrix):
m = len(matrix)
if(m == 0):
return 0
maxArea = 0
n = len(matrix[0])
for mFrom in range(m):
for mTo in range(mFrom, m):
for nFrom in range(n):
for nTo in range(nFrom, n):
if(isValid(matrix, nFrom, nTo, mFrom, mTo)):
maxArea = max(maxArea, getArea(nFrom, nTo, mFrom, mTo))
return maxArea
def getArea( nFrom, nTo, mFrom, mTo):
return (nTo - nFrom + 1) * (mTo - mFrom + 1)
def isValid(matrix, nFrom, nTo, mFrom, mTo):
for i in range(mFrom, mTo+1):
for j in range(nFrom, nTo+1):
if(matrix[i][j] != 1):
return False
return True
matrix=[ [1, 1, 0, 0, 1, 0],
[0, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 0],
[0, 0, 1, 1, 0, 0]]
print maximalRectangle (matrix)
6 . 5 3 M a x i m u m s i z e s u b- m a t r i x w i t h a ll 1 ’ s
D y n am i c Pro g r am mi n g | 512
1 1 0 0 1 0
The maximum rectangle in this histogram has 2 cells.
1 1 0 0 1 0
Now, let us add the second row. The histogram for the first two rows is:
1 1 0 0 1 0
0 1 1 1 1 1
The maximum rectangle in this histogram has 4 cells.
1 1 0 0 1 0
0 1 1 1 1 1
The histogram for the first three rows is:
1 1 0 0 1 0
0 1 1 1 1 1
1 1 1 1 1 0
The maximum rectangle in this histogram has 8 cells.
1 1 0 0 1 0
0 1 1 1 1 1
1 1 1 1 1 0
The histogram for all the four rows is:
1 1 0 0 1 0
0 1 1 1 1 1
1 1 1 1 1 0
0 0 1 1 0 0
The maximum rectangle in this histogram has 6 cells.
1 1 0 0 1 0
0 1 1 1 1 1
1 1 1 1 1 0
0 0 1 1 0 0
With this process, we can determine the maximum rectangle for each of the row. By the
time we complete the processing of the last row, we can get the maximum rectangle with
all 1’s by maintaining a simple variable to keep track of the maximum area seen so far..
So, if we calculate this maximum area for all the rows, maximum area among all of them
would be our answer. We can extend our solution very easily to find start and end co-
ordinates.
The next question would be how do we get the histogram for a particular row?
For this, we need to generate an auxiliary matrix ℎ ℎ [][] where each element represents
the number of 1s above and including it, up until the first 0. ℎ ℎ [][] for the above matrix
will be as shown below:
110010
021121
132230
003300
Now we can simply call our maximum rectangle in histogram on every row in ℎ ℎ [][]
and update the maximum area every time.
6 . 5 3 M a x i m u m s i z e s u b- m a t r i x w i t h a ll 1 ’ s
D y n am i c Pro g r am mi n g | 513
For the
refer ℎ
ℎ algorithm and discussion,
chapter.
def maxRectangleAreaOneRow(height):
stack=[]; i=0; maxArea=0
while i<len(height):
if stack==[] or height[i]>height[stack[-1]]:
stack.append(i)
else:
curr=stack.pop()
width=i if stack==[] else i-stack[-1]-1
maxArea=max(maxArea,width*height[curr])
i-=1
i+=1
while stack!=[]:
curr=stack.pop()
width=i if stack==[] else len(height)-stack[-1]-1
maxArea=max(maxArea,width*height[curr])
return maxArea
def maximalRectangle(matrix):
m=len(matrix)
n=len(matrix[0])
heights = [[0 for j in range(n+1)] for i in range(m+1)]
for i in range(m):
for j in range(n):
if matrix[i][j] is 1:
heights[i][j]=1+heights[i-1][j]
maxArea = 0
currArea=-1
for i in range(m):
currArea = maxRectangleAreaOneRow(heights[i])
maxArea = max(currArea, maxArea)
return maxArea
matrix=[ [1,1,0,0,1,0],
[0,1,1,1,1,1],
[1,1,1,1,1,0],
[0,0,1,1,0,0]
]
print maximalRectangle(matrix)
Performance
Time complexity
To implement − , an auxiliary matrix will be prepared for a quick retrieval
of bar height in each maximal histogram, so the ℎ ℎ () query takes only O(1) time.
Sweeping through a histogram of length takes O( ) amortized time considering the stack
operation. The total length of every possible maximal histogram will not exceed × ,
which is the size of a matrix. So the total time complexity is O( ).
Space complexity
6 . 5 3 M a x i m u m s i z e s u b- m a t r i x w i t h a ll 1 ’ s
D y n am i c Pro g r am mi n g | 514
Performance
Time complexity
There is no change in time complexity of this approach. So, the total time complexity is
O( ).
Space complexity
In the implementation of the algorithm, an auxiliary matrix of size will be created.
Sweeping through a histogram requires a stack of size n, which could be reused for each
histogram. So, the total space complexity is O( + ) ≅ O( ) as the maximum stack size
would be equal to the number of columns in a row.
6 . 5 3 M a x i m u m s i z e s u b- m a t r i x w i t h a ll 1 ’ s
D y n am i c Pro g r am mi n g | 515
Performance
Time complexity
There is no change in time complexity of this approach. So, the total time complexity is
O( ).
Space complexity
Sweeping through a histogram requires a stack of size n, which could be reused for each
histogram. So, the total space complexity is O( ) as the maximum stack size would be
equal to the number of columns in a row.
6 . 5 4 M a x i m u m s u m s u b- m a t r i x
D y n am i c Pro g r am mi n g | 516
Since there are such possibilities, we can compute them in O( ) time. After we have
pre-processed the matrix to create the cumulative sum matrix, we consider every sub-
matrix formed by rows 1 to 2, and columns 1 to 2; and calculate the sub-matrix
sum in constant time using below relation:
[ 2 + 1][ 2 + 1]
−
[ 2 + 1][ 1]
− = −
[ 1][ 2 + 1]
+
[ 1][ 1]
So, with pre-computed cumulative sums, the sum of any rectangular subarray of
can be computed in constant time.
If the sub-matrix sum is more than maximum found so far, we update the maximum sum.
We can also store the sub-matrix coordinates to print the maximum sum sub-matrix.
def preComputeCumulativeSums(matrix):
n = len(matrix)
global cumulativeSums
for i in range (0, n+1):
for j in range (0, n+1):
if(i==0 or j==0):
cumulativeSums[i][j] = 0
else:
cumulativeSums[i][j] = cumulativeSums[i-1][j] + \
cumulativeSums[i][j-1] - \
cumulativeSums[i-1][j-1] + \
matrix[i-1][j-1]
def maximumSumRectangle(matrix):
n = len(matrix)
maxSum = float("-inf")
for row1 in range (0, n):
for row2 in range (row1, n):
for col1 in range (0, n):
for col2 in range (col1, n):
currentSum = cumulativeSums[row2+1][col2+1] – \
cumulativeSums[row2+1][col1] – \
cumulativeSums[row1][col2+1] + \
cumulativeSums[row1][col1]
maxSum = max(maxSum, currentSum)
print row1, col1, row2, col2, maxSum
return maxSum
matrix = [ [0, -2, -7, 0],
[-4, 1, -4, 1],
[9, 2, -6, 2 ],
6 . 5 4 M a x i m u m s u m s u b- m a t r i x
D y n am i c Pro g r am mi n g | 517
[-1, 8, 0, -2]]
n = len(matrix)
cumulativeSums =[[0 for x in range(n+1)] for x in range(n+1)]
preComputeCumulativeSums(matrix)
print maximumSumRectangle(matrix)
Performance
Time complexity
This gives an O( ) algorithm: For each of the possible rectangle (the upper-left and the
lower-right corner of the rectangular subarray), use the table to compute
its sum.
Space complexity
In implementation of the algorithm, an auxiliary matrix of size will be created for the
cumulative sums. So the space complexity of the algorithm is O( ).
Refer to
solution before reading this.
problem and ’
As we have seen, the maximum sum array of a 1 − D array algorithm scans the array one
entry at a time and keeps a running total of the entries. At any point, if this total becomes
negative, then set it to 0. This algorithm is called ’ algorithm. We use this as an
auxiliary function to solve a two-dimensional problem in the following way.
We want to have a way to compute the sum along a row, for any start point to any endpoint.
To compute that sum in O(1) time rather than just adding, it takes O(n) time where n is the
number of elements in a row. With some precomputing, this can be achieved. Here's how.
Suppose we have a matrix:
a d g
b e h
c f i
We can precompute the following matrix:
a a+d a+d+g
b b+e b+e+h
c c+f c+f+i
With this precomputed matrix, we can get the sum running along any row from any start
to endpoint in the row just by subtracting two values.
For example, consider the following matrix.
0 1 2 3
0 1 2 3 4
1 5 6 7 8
6 . 5 4 M a x i m u m s u m s u b- m a t r i x
D y n am i c Pro g r am mi n g | 518
2 9 10 11 12
3 13 14 15 16
For this matrix, the precomputed matrix is:
0 1 2 3
0 1 3 6 10
1 6 11 18 26
2 9 19 30 42
3 13 27 42 58
Now, the sum of elements in the second row of the matrix from second column (index 1) to
fourth column can be calculated using the precomputed matrix as: subtract 5 from 26=
21.
0 1 2 3 0 1 2 3
0 1 2 3 4 0 1 3 6 10
1 5 6 7 8 1 5 11 18 26
2 9 10 11 12 2 9 19 30 42
3 13 14 15 16 3 13 27 42 58
Now, let us use the ’ algorithm and precomputed matrix to find the maximum
sum rectangle in a given matrix.
Now what about actually figuring out the top and bottom row? Just try all possibilities. Try
putting the top anywhere you can and putting the bottom anywhere you can, and run the
’ algorithm described previously for every possibility. When you find a max, you
keep track of the top and bottom position.
ifrom collections import namedtuple
Result = namedtuple("Result","maxSum topLeftRow \
topLeftColumn bottomRightRow bottomRightColumn")
KadanesResult = namedtuple("KadanesResult","maxSum start end")
def kadanes(A):
max = 0
maxStart = -1
maxEnd = -1
currentStart = 0
maxSoFar = 0
for i in range(0, len(A)):
maxSoFar += A[i]
if maxSoFar < 0:
maxSoFar = 0
currentStart = i + 1
if maxSoFar > max:
maxStart = currentStart
maxEnd = i
max = maxSoFar
return KadanesResult(max, maxStart, maxEnd)
def maximalRectangle(matrix):
rows = len(matrix)
cols = len(matrix[0])
result = Result(float("-inf"), -1, -1, -1, -1)
for left in range(cols):
A = [0 for _ in range(rows)]
for right in range(left, cols):
6 . 5 4 M a x i m u m s u m s u b- m a t r i x
D y n am i c Pro g r am mi n g | 519
for i in range(rows):
A[i] += matrix[i][right]
kadanes_result = kadanes(A)
if kadanes_result.maxSum > result.maxSum:
result = Result(kadanes_result.maxSum, \
kadanes_result.start, left, kadanes_result.end, right)
return result
if __name__ == '__main__':
matrix=[ [0, -2, -7, 0],
[-4, 1, -4, 1],
[9, 2, -6, 2 ],
[-1, 8, 0, -2]]
result = maximalRectangle(matrix)
assert 18 == result.maxSum
print result)
Example
As an example, consider the following matrix:
0 1 2 3
0 0 -2 -7 0
1 -4 1 -4 1
2 9 2 -6 2
3 -1 8 0 -2
In the first iteration, the values of variables are:
left 0
right 0
A 0, -4, 9, 1
0 1 2 3
0 0 -2 -7 0
1 -4 1 -4 1
2 9 2 -6 2
3 -1 8 0 -2
The ’ algorithm on A would return (9, 2, 2) as the maximum sum is 9 with the
starting and ending indexes 2.
In the second iteration, the values of variables are:
left 0
right 1
A -2, -3, 11, 7
0 1 2 3
0 0 -2 -7 0
1 -4 1 -4 1
2 9 2 -6 2
3 -1 8 0 -2
The ’ algorithm on A would return (18, 2, 3) as the maximum sum is 18 with the
starting and ending indexes 2, and 3 respectively.
In the third iteration, the values of variables were:
left 0
6 . 5 4 M a x i m u m s u m s u b- m a t r i x
D y n am i c Pro g r am mi n g | 520
right 3
A -9, -7, 5, 7
0 1 2 3
0 0 -2 -7 0
1 -4 1 -4 1
2 9 2 -6 2
3 -1 8 0 -2
The ’ algorithm on A would return (12, 2, 3), as the maximum sum is 12 with the
starting and ending indexes 2, and 3 respectively.
In the fourth iteration, the values of variables are:
left 0
right 4
A -9, -6, 7, 5
0 1 2 3
0 0 -2 -7 0
1 -4 1 -4 1
2 9 2 -6 2
3 -1 8 0 -2
The ’ algorithm on A would return (12, 2, 3), as the maximum sum is 12 with the
starting and ending indexes 2, and 3 respectively.
In the next iteration, the values of variables are:
left 1
right 4
A -2, 1, 2, 8
Notice that, left (column) would start from 1.
0 1 2 3
0 0 -2 -7 0
1 -4 1 -4 1
2 9 2 -6 2
3 -1 8 0 -2
The ’ algorithm on A would return (11, 1, 3), as the maximum sum is 11 with the
starting and ending indexes as 1, and 3 respectively.
This process would continue for each of the possible column combinations. While
processing, it maintains a running sum and compares it with maximum sum seen so far.
If it is greater, it updates the maximum sum.
Performance
Time complexity
The column combinations would take O( ) where n is the number of columns. ’
algorithm on a row takes O(n) time where n is the number of columns. So, the total running
time of the algorithm is O( × ). If = , the time required is O( ).
Space complexity
In implementation of the algorithm, an auxiliary array of size will be created for the
precomputed sums. So, the space complexity of the algorithm is O( ).
6 . 5 4 M a x i m u m s u m s u b- m a t r i x
D y n am i c Pro g r am mi n g | 521
Example
As an example, consider the following array which indicates the maximum length of the
jump from the given location. Our goal is to reach the last index with the minimum number
of jumps.
Array indicating jumps → 2 3 1 1 4
Index → 0 1 2 3 4
Two possible ways to reach the last index 4 (index list) are:
0→2→3→4 (jump 2 to index 2, and then jump 1 to index 3, and then jump 1 to
index 4)
0→1→4 (jump 1 to index 1 (maximum jump length is 2 but selected 1), and then
jump 3 to index 4)
Array indicating jumps → 2 3 1 1 4
Index → 0 1 2 3 4
Since the second solution has only 2 jumps; it is the optimal result.
Currently, we are at = 1 and [ ] = 4. So, we can make a jump of size 4, but instead we
consider all possible jumps we can make from the current location and attain the maximum
distance which is within the bounds of the array.
6 . 5 5 F i n d i n g m i n i m u m n u m b e r o f s q u a r e s t o su m
D y n am i c Pro g r am mi n g | 522
Out of these 4 possible jumps, for each of them, follow the same process until we reach the
last index.
If we are at position , we know which positions to the right are reachable with one
additional jump. Same rule applies to those right positions as well.
While jumping, keep track of the number of jumps. At the end of processing, return the
minimum number of jumps to reach the last element.
def jumps(A, index):
if (index >= len(A) - 1) : return 0
minimum = float("infinity")
i=1
while ( i <= A[index]):
minimum = min(minimum, 1 + jumps(A, index + i))
i = i +1
return minimum
A = [2, 3, 1, 1, 4]
print jumps(A, 0)
A = [1, 4, 2, 1, 9, 3, 4, 5, 2, 7, 9]
print jumps(A, 0)
Example
To understand the recursive algorithm, consider the same array we have used in the above
example. The array elements indicate the length of the jump from the given location. Our
goal is to reach the last index with the minimum number of jumps.
jumps(A, 0)
First, we would be checking the possible jumps from position 0 (as the index being passed
is 0). For this function call, the minimum jumps is being initialized to ∞, and this value
would get updated during the recursive calls.
jumps(A, 0)
minimum =∞
Now, for each of the possible jumps, recursively call the function from position 0.
First it starts with + 1, and then + 2 as there are 2 maximum jumps from
position 0.
jumps(A, 0)
minimum =∞
jumps(A, 2)
6 . 5 6 F i n d i n g o p t i m minimum
a l n u m b=∞
er of jumps
D y n am i c Pro g r am mi n g | 523
Now from position 1, for each of the possible jumps, recursively call the function.
jumps(A, 0)
minimum =∞
jumps(A, 1)
minimum =∞
jumps(A, 0)
minimum =∞
jumps(A, 1)
minimum =∞
jumps(A, 3)
minimum =∞
Next, from position 3, for each of the possible jumps, recursively call the function.
jumps(A, 0)
minimum =∞
jumps(A, 1)
minimum =∞
jumps(A, 2)
minimum =∞ jumps(A, 3) jumps(A, 4)
minimum =∞ minimum =∞
jumps(A, 3)
minimum =∞
jumps(A, 4)
For jumps(A, 4) call, the index is equal to the last index of the array: ( <= ( ) − 1).
Hence, no further processing is required. 0 would be returned to its calling
function, ( , 3). As a result, minimum in jumps(A, 3) would be updated with 1 +
( , 4) = 1. This indicates that to reach 4, we would need 1 jump from the current
position 3.
jumps(A, 0)
minimum =∞
jumps(A, 1)
minimum =∞
jumps(A, 2)
minimum =∞ jumps(A, 3) jumps(A, 4)
minimum =∞ minimum =∞
jumps(A, 3)
minimum =1
0
For index 3, we are done with the processing as the maximum jumps from index 3 is 1
( [3] = 1).
For index 2 too, we are done with the processing as the maximum jumps from index 2 is 1
( [1] = 1). As a result, minimum in jumps(A, 2) would be updated with 1 + ( , 3) =
2. This indicates that to reach 4, we would need 2 jumps from the current position 2.
jumps(A, 0)
minimum =∞
jumps(A, 1)
minimum =∞
Next, for index 1, we are done with the processing jumps(A, 2) and we need to look for
jumps(A, 3). From jumps(A, 3), there would be a call for jumps(A, 4). As we have seen
jumps(A, 4) would return 0. As a result, return value from jumps(A, 3) would be 1.
Similarly, jumps(A, 4) would return 0 to jumps(A, 1).
jumps(A, 0)
minimum
=∞
jumps(A, 2)
jumps(A, 1)
minimum =∞
minimum
=∞
2 0
1
Next, for index 0, we are done with the processing jumps(A, 1) and we need to look for
jumps(A, 2). From jumps(A, 2), there would be a call for jumps(A, 3) which in turn would
call jumps(A, 4). As we have seen, jumps(A, 4) would return 0. As a result, return value
jumps(A, 0)
minimum =∞
2
jumps(A, 1)
minimum =∞
2 0
1
jumps(A, 0)
minimum =∞
2
1
Finally, jumps(A 0) would return 1 + {1, 2}. Hence the final minimum value is
2. This indicates that from position 0, we would need 2 minimum jumps to reach the last
index of the array.
Performance
In the above recursive algorithm, starting from index zero, we are checking all possible
jumps for each of the position. An input array like [6, 5, 4, 3, 2, 1] would make:
DP solution
Have you observed the problem with the above recursive algorithm? The function
( , ) is being called repeatedly with the same arguments. For example,
( , 1), and ( , 2) were repeated many times. To fix this issue, caching would help
us. As usual, the only change needed is to have a cache (table) and use that cache instead
of recalculating the information already known.
This problem is a classic example of dynamic programming. Initialization for this would be
marking all positions as ∞, indicating the number of jumps to reach any location is ∞.
n = len(A)
table = [float("infinity") for i in range (n)]
For first index ( = 0), the optimum number of jumps will be zero. Notice that, if value at
first index is zero, we can’t jump to any element and return infinite.
table[0] = 0
In the table, the element, table[ − 1], is ∞. This indicates that the number of jumps to
reach the last index is ∞. Now, go through all the indexes from 0 to − 2 (skipping − 1
index as our goal is to reach − 1 index), and at every index , check if we are able to jump
to the farthest right with minimum jumps. That is, check if the number of jumps ( [] +
1) are less than [ + ], then update the [ + ] with [ ] + 1, else just continue
to next index.
def jumps(A):
n = len(A)
table = [float("infinity") for i in range (n)]
table[0] = 0
for i in range(n-1):
for j in range(1, A[i]+1):
if (i + j < n):
table[i+j] = min(table[i+j], 1 + table[i])
return table[n-1]
A = [2, 3, 1, 1, 4]
print jumps(A)
Example
To understand the dynamic programming algorithm, consider the same array we have used
for tracing the recursive algorithm.
Array indicating jumps → 2 3 1 1 4
Index → 0 1 2 3 4
The initialization of the cache would be:
table → 0 ∞ ∞ ∞ ∞
Index → 0 1 2 3 4
As per the algorithm, starting with index 0 ( = 0), based on its maximum jump length ( [ ])
keep checking whether we can reach [ + ] with minimum jumps. Here, indicates the
jump length, which would range from 1 to [ ].
For = 0, the maximum jump length is 2. So, starting at location 0, we can make a single
jump with length 1 or 2. This gives the possible values for (1, 2). Hence, update A[0+1]
and A[0+2] with 1+A[0] as the previous values for these two locations were ∞.
table → 0 1 1 ∞ ∞
Index → 0 1 2 3 4
Next, for = 1, the maximum jump length is 3. So, starting at location 1, we can make a
single jump with length 1, 2 or 3. This gives the possible values of (1, 2, 3). Hence, update
A[1+1], A[1+2], and A[1+3] with min(previous value, 1+ [ ]).
table → 0 1 1 2 2
Index → 0 1 2 3 4
Next, for = 2, the maximum jump length is 1. So, starting at location 2, we can make a
single jump with length 1. This gives the one possible value of (1). Hence, update A[2+1]
with min(previous value, 1+ [ ]).
table → 0 1 1 2 2
Index → 0 1 2 3 4
Next, for = 3, the maximum jump length is 1. So, starting at location 3, we can make a
single jump with length 1. This gives the one possible value of (1). Hence, update A[3+1]
with min(previous value, 1+ [ ]).
table → 0 1 1 2 2
Index → 0 1 2 3 4
This completes the processing of algorithm and return the last index of the table. [ − 1]
= A[4] = 2, indicates that we need minimum 2 jumps to reach the last element from the
first element.
Performance
The above dynamic programming solution has two nested loops. The outer loop runs in
O( ) time and the number of iterations of the inner loop is equal to the value at [ ]. The
maximum value at any index of the input array cannot be greater than the length of the
array. Hence, the inner loop will run for maximum of O( ). Hence, the overall running time
of the algorithm is O( × )= O( ).
Example
This problem is pretty similar to: “ ℎ ℎ ”.
In this case, at every step, we are allowed to use any of the distances (jump lengths)
mentioned in the array.
As an example, consider the following array which indicates different jump lengths a frog
can use to jump from the given location. Our goal is to reach the location 6 ( ) with the
minimum number of jumps.
Array indicating jump lengths → 1 4 5 2
Index → 0 2 3 4
Two possible ways to reach the location 6 are:
0→1→4→2 (jump 1 to index 1, jump 3 to index 4, and then jump 2 to location 6)
6 . 5 7 F r o g r i v e r c r o s s i ng
D y n am i c Pro g r am mi n g | 528
Recursion is a good starting point for brute force solution. To understand the basic
intuition of the problem, consider the following input array: A = [1, 4, 5, 2] and target
location = 13.
Now, we start from the initial location, i.e. = 0. For this, we can make use of any of the
distances mentioned in the array.
So, what are the possible choices?
The possible jump sizes are 1, 4, 5, and 2. We can make a single jump of 1 step, 4 steps,
5 steps, or 2 steps.
Array indicating jumps →
Index → 0 1 2 3 4 5 6 … 12 13
For each of these 4 possible jumps, follow the same process until the location is reached.
If we are at location , we know which locations to the right are reachable with one
additional jump. Same rule applies to those right locations as well.
While jumping, keep track of the number of jumps. At the end of processing, return the
minimum number of jumps for reaching the location .
minimum = float("infinity")
i=0
while (i < len(A)):
if index + A[i] <= P:
minimum = min(minimum, 1 + jumps(A, index + A[i], P))
i=i+1
return minimum
A = [1, 4, 5, 2]
print jumps(A, 0, 13)
Example
To understand the recursive algorithm, consider the same array we have used in the above
example. The array elements indicate the jump lengths the frog can use. Our goal is to
reach the location (say, 13) with the minimum number of jumps.
6 . 5 7 F r o g r i v e r c r o s s i ng
D y n am i c Pro g r am mi n g | 529
First, we would be checking the possible jumps from position 0 (as the index being passed
is 0). For this function call, the minimum jumps is being initialized to ∞, and this value
would get updated during the recursive calls.
jumps(A, 0, 13)
minimum =∞
Now, for each of the possible jumps, recursively call the function from position 0.
First it starts with + [0], then + [1], + [2], and then + [3] as
there were 4 possible jumps.
jumps(A, 0, 13)
minimum =∞
jumps(A, 1, 13)
minimum =∞
Now from position 1, for each of the possible jumps, recursively call the function.
jumps(A, 0, 13)
minimum =∞
jumps(A, 1, 13)
minimum =∞
jumps(A, 2, 13)
minimum =∞
Next, from position 2, for each of the possible jumps, recursively call the function.
jumps(A, 0, 13)
minimum =∞
jumps(A, 1, 13)
minimum =∞
jumps(A, 2, 13)
minimum =∞
jumps(A, 3, 13)
minimum =∞
6 . 5 7 F r o g r i v e r c r o s s i ng
D y n am i c Pro g r am mi n g | 530
This would continue for a recursive depth of , that is, till ( , 13, 13), and it indicates
that for reaching the location , one possibility is making a jump 1 at every step. So, we
can reach (13) with 13 jumps from the first location.
jumps(A, 0, 13)
minimum =∞
jumps(A, 1, 13)
minimum =∞
jumps(A, 2, 13)
minimum =∞
Next, it would start with ( , 0 + [1], 13) and follows the same recursive pattern.
Performance
In the above recursive algorithm, starting from index zero, we are checking all possible
jumps for each of the location. With = 13, input array like this [1, 2, 3, 4, 5, 6] would
make:
6 recursive calls from location 1
…
…
DP solution
Have you observed the problem with the above recursive algorithm? The function
( , , ) is being called repeatedly with the same arguments. To fix this issue,
we could use caching. As usual, the only change needed is to have a cache (table) and use
that cache instead of recalculating the information already known.
6 . 5 7 F r o g r i v e r c r o s s i ng
D y n am i c Pro g r am mi n g | 531
This problem is a classic example of dynamic programming. Initialization for this would be
marking all locations as ∞, indicating that the number of jumps to reach any location is ∞.
Notice that, the size of the table is (not ) as we need to reach the location P with the
distances given in array with size .
table = [float("infinity") for i in range (P)]
For first index ( = 0), the optimum number of jumps will be zero. If value at the first location
of table is non zero, we can’t jump to any location and return infinite.
table[0] = 0
In the table, the index, table[ − 1], is ∞. This indicates that the number of jumps to
reach is ∞. Now, go through all indexes from 0 to − 2 (skipping − 1 index as our goal
is to reach − 1 index), and at every index , check if we are able to jump to the right location
+ [ ] with minimum jumps. That is, check if the number of jumps ( [ ] + 1) is less
than [ + [ ]], then update the [ + [ ]] with [ ] + 1, else just continue to
next index.
def jumps(A, P):
n = len(A)
table = [float("infinity") for i in range (P)]
table[0] = 0
for i in range(0, P):
for j in range(n):
if (i + A[j] < P):
table[i+A[j]] = min(table[i+A[j]], 1 + table[i])
return table[P-1]
A = [1, 4, 5, 2]
print jumps(A, 18)
Performance
The above dynamic programming solution has two nested loops. The outer loop runs in
O( ) time and the number of iterations of the inner loop is equal to the array size . So,
the inner loop will run for a maximum of O( ). Hence, the overall running time of the
algorithm is O( × )= O( ).
DP solution
This problem is pretty similar to: “ ”. In this case, instead of finding the
optimal jumps to cross the river, our goal is to determine the number of different ways the
frog can reach the position .
The problem can be solved by using dynamic programming. Let’s create a one dimensional
array, , consisting of elements, such that [ ] will be the number of ways in which
the frog can jump to position .
We update consecutive cells of . There is exactly one way for the frog to jump to
position 0, so [0] = 1.
1
Index → 0 1 2 3 4 … −2 −1
Next, consider some position > 0. The number of ways in which the frog can jump to
position with a final jump of is [ − ]. Thus, the number of ways in which the
frog can get to position is increased by the number of ways of getting to position − ,
for every jump . More precisely, [ ] is increased by the value of [ − ] (for all
≤ ).
[]= []+ [ − ] (for all ≤ )
Index → 0 … - … - … … −2 −1
Example
To understand the DP solution, consider the following array with value 3. The array
elements indicate the jump lengths the frog can use.
Array indicating jump lengths → 2 3 1 5 4
Index → 0 1 2 3 4
The initial call to the recursive function is _ ( , 3).
Our goal is to count the number of ways to reach the location 3 by using the jump sizes
given in the array. Since the value of is 3, we would need to create an array ( ) with
size + 1 and initialize the first value of array, [0], with 1.
1
Index → 0 1 2 3
Now, for each of the locations ≤ , starting with 1, we would try finding the number of
ways to reach location .
The next location to be processed is = 1. For this location, try checking whether we can
reach this location from any of the previous locations with a single jump by using the
jump sizes given in the array . The possible jump sizes are [2, 3, 1, 5, 4]. Hence, first
check whether 2 ≤ or not. This check makes sure that we do not cross the given location
.
2 ≤ → 2 ≤ 1 is false
Next jump size to be considered is 3, [1]:
3 ≤ → 3 ≤ 1 is false
6.58 Number of ways a frog can cross a river
D y n am i c Pro g r am mi n g | 533
4 ≤ → 4 ≤ 1 is false
This completes the processing of location = 2 and the value [2] indicates the number
of different ways reaching the location 2.
The next location to be processed is = 3. For this location, try checking whether we can
reach this location from any of the previous locations with a single jump by using the
jump sizes given in the array . The possible jump sizes are [2, 3, 1, 5, 4]. Hence, first
check whether 2 ≤ or not. This check makes sure that we do not cross the given location
.
2 ≤ → 2 ≤ 3 is true
This indicates that, we are able to reach location = 3 with a single jump by using the jump
size of 2, i.e. [0]. Notice that, we have used the jump size of [0] from the location − [0],
to reach the location . Hence, increase the number of ways of reaching location :
[] = [] + [ − [ ]]
[3] = [3] + 3 − [0] = [3] + [1] = 0 + 1 = 1
1 1 2 1
Index → 0 1 2 3
Next jump size to be considered is 3, [1]:
3 ≤ → 3 ≤ 3 is true
So, we are able to reach location = 3 with a single jump by using the jump size of 3, i.e.
[1]. Notice that, we have used the jump size of [1] from the location − [1], to reach the
location . Hence, increase the number of ways of reaching location :
[] = [] + [ − [ ]]
[3] = [3] + 3 − [1] = [3] + [0] = 1 + 1 = 2
1 1 2 2
Index → 0 1 2 3
Next jump size to be considered is 1, [2]:
1 ≤ → 1 ≤ 3 is true
So, we are able to reach location = 3 with a single jump by using the jump size of 1, i.e.
[2]. Notice that, we have used the jump size of [2] from the location − [2], to reach the
location . Hence, increase the number of ways of reaching location :
[] = [] + [ − [ ]]
[3] = [3] + 3 − [2] = [3] + [2] = 2 + 2 = 4
1 1 2 4
Index → 0 1 2 3
Next jump size to be considered is 5, [3]:
5 ≤ → 5 ≤ 3 is false
Next jump size is 4, [4]:
4 ≤ → 4 ≤ 3 is false
This completes the processing of location = 3 and the value [3] indicates the number
of different ways reaching the location 3. In this case, the current location is equal to .
Hence, it is the end of algorithm and return the value [3].
Performance
The above dynamic programming solution has two nested loops. The outer loop runs in
O( ) time and the number of iterations of the inner loop is equal to the array size . So,
the inner loop will run for a maximum of O( ). Hence, the overall running time of the
algorithm is O( × )= O( ).
Space Complexity: O( ).
6 . 5 9 F i n d i n g a s u b s eq ue n c e w i th a t o ta l
D y n am i c Pro g r am mi n g | 536
subSum[0] = True
p=0
while not subSum[X] and p < len( A ):
a = A[p]
q=X
while not subSum[X] and q >= a:
if not subSum[q] and subSum[q - a]:
subSum[q] = True
q -= 1
p += 1
return subSum[X]
Solution: This problem can be converted into making one set as close to as possible. We
consider an equivalent problem of making one set as close to W= as possible. Define
FD( , ) to be the minimal gap between the weight of the bag and W when using the first
gifts only. WLOG, we can assume the weight of the bag is always less than or equal to W.
Then fill the DP table for 0≤i≤ and 0≤ ≤W in which F(0, ) = W for all , and
( , )= { ( − 1, − )− , ( − 1, )} { ( − 1, − )≥
= ( − 1, ) otherwise
This takes O( ) time. ( , ) is the minimum gap. Finally, to reconstruct the answer, we
backtrack from ( , ). During backtracking, if ( , )= ( − 1, ) then is not selected in
the bag and we move to ( − 1, ). Otherwise, is selected and we move to ( − 1, − ).
Example
For the following grid, we should return 3. Placing a bomb at (1, 1) kills 3 enemies.
0 1 2 3 0 1 2 3
0 0 E 0 0 0 0 E 0 0
1 E 0 W E 1 E 0 W E
2 0 E 0 0 2 0 E 0 0
break
k = k-1
if max_killed_count < count:
max_killed_count = count
return max_killed_count
print max_enemy_killed_count_with_bomb([['0','E','0','0'],['E','0','W','E'],[0,'E','0','0']])
: The above code traverses through each element of the grid. Also, for each
of the location, it tries to find the enemy kill counts in all four directions. Hence, for each
of the locations, it tries to traverse:
1. Left to right would traverse maximum of column size ( )
2. Top to bottom would traverse maximum of row size ( )
3. Right to left would traverse maximum of column size ( )
4. Bottom to top would traverse maximum of row size ( )
So, the overall time complexity is: O( (2 + 2 )) = O(2 ( + )).
Space Complexity: O(1).
DP solution
How do we keep track of all kill counts for each of the location in 2D matrix? Let us try to
improve the brute force algorithm using DP solution. Let us maintain four cumulative
arrays:
1. _ _ ℎ : is the cumulative array with horizontal direction from the left to right
2. _ _ : is the cumulative array with vertical direction from the top to bottom
3. ℎ_ _ : is the cumulative array with horizontal direction from the right to left
4. _ _ : is the cumulative array with vertical direction from the bottom to
top
We set up these cumulative arrays for all positions [i][j] of the 2D matrix. For any
location[i][j], the kill count would be the sum of all four cumulative array positions from
the same location.
_ _ ℎ [ ][ ]
+
_ _ [ ][ ]
Kill count of position [i][j] = +
ℎ_ _ [ ][ ]
+
_ _ [ ][ ]
Finally, we compare the cumulative sum of each position and return the maximum.
_ _ [ ][ ] + ℎ_ _ [ ][ ] + _ _ [ ][ ]
def max_enemy_killed_count_with_bomb(matrix2d):
if not matrix2d or not matrix2d[0]:
return 0
wall = 'W'
enemy = 'E'
empty = '0'
n = len(matrix2d)
m = len(matrix2d[0])
6 . 6 2 B o m b i n g e ne m i e s
D y n am i c Pro g r am mi n g | 539
max_killed_count = 0
# left_to_right: is the cumulative array with horizontal direction from left to right
left_to_right = [ [ 0 for j in range(m) ] for i in range(n) ]
# top_to_bottom: is the cumulative array with vertical direction from top to bottom
top_to_bottom = [ [ 0 for j in range(m) ] for i in range(n) ]
# right_to_left: is cumulative array with horizontal direction from right to left
right_to_left = [ [ 0 for j in range(m) ] for i in range(n) ]
# bottom_to_top: is the cumulative array with vertical direction from bottom to top
bottom_to_top = [ [ 0 for j in range(m) ] for i in range(n) ]
for i in range(n):
for j in range(m):
if matrix2d[i][j] == empty:
count = 0
#count all possible kills in its upward direction
k = i-1
while (k >= 0):
if matrix2d[k][j] == enemy:
bottom_to_top[i][j] = bottom_to_top[i][j] + 1
elif matrix2d[k][j] == wall:
break
k = k-1
#count all possible kills in its downward direction
k = i+1
while (k < n):
if matrix2d[k][j] == enemy:
top_to_bottom[i][j] = top_to_bottom[i][j] + 1
elif matrix2d[k][j] == wall:
break
k = k+1
#count all possible kills in its right direction
k = j+1
while (k < m):
if matrix2d[i][k] == enemy:
left_to_right[i][j] = left_to_right[i][j] + 1
elif matrix2d[i][k] == wall:
break
k = k+1
#count all possible kills in its left direction
k = j-1
while (k >= 0):
if matrix2d[i][k] == enemy:
right_to_left[i][j] = right_to_left[i][j] + 1
elif matrix2d[i][k] == wall:
break
k = k-1
max_killed_count = 0
for i in range(n):
for j in range(m):
if (matrix2d[i][j] == '0'):
max_killed_count = max(max_killed_count, bottom_to_top[i][j] +\
top_to_bottom[i][j] + left_to_right[i][j] + right_to_left[i][j])
return max_killed_count
6 . 6 2 B o m b i n g e ne m i e s
D y n am i c Pro g r am mi n g | 540
print max_enemy_killed_count_with_bomb([['0','E','0','0'],['E','0','W','E'],[0,'E','0','0']])
One simple observation is that, for any location in the 2D matrix, the kill counts of the row
and column can be calculated in one shot instead of maintaining them separately. So, we
can combine _ _ and _ _ 2D arrays to one, _ _ . Similarly,
we can combine _ _ ℎ and ℎ _ _ 2D arrays to one, _ _ .
def max_enemy_killed_count_with_bomb(matrix2d):
if not matrix2d or not matrix2d[0]:
return 0
wall = 'W'
enemy = 'E'
empty = '0'
n = len(matrix2d)
m = len(matrix2d[0])
# for each empty cell, how many enemies in the same row
# will be killed if bomb there
row_kill_counts = [ [ 0 for j in range(m) ] for i in range(n) ]
# for each empty cell, how many enemies in the same col
# will be killed if bomb there
col_kill_counts = [ [ 0 for j in range(m) ] for i in range(n) ]
# calculate row_kill_counts
for i in range(n):
empty_cols = []
kill_count = 0
for j in range(m+1):
if j==m or matrix2d[i][j] == wall:
for emptyCol in empty_cols:
row_kill_counts[i][emptyCol] = kill_count
kill_count = 0
empty_cols = []
elif matrix2d[i][j] == enemy:
kill_count += 1
elif matrix2d[i][j] == empty:
empty_cols.append( j )
# calculate col_kill_counts
for j in range(m):
empty_rows = []
kill_count = 0
for i in range(n + 1):
if i == n or matrix2d[i][j] == wall:
for emptyRow in empty_rows:
col_kill_counts[emptyRow][j] = kill_count
kill_count = 0
empty_rows = []
elif matrix2d[i][j] == enemy:
kill_count += 1
elif matrix2d[i][j] == empty:
empty_rows.append(i)
# find max of row_kill_counts and col_kill_counts
ret = 0
for i in range(n):
for j in range(m):
ret = max( ret, row_kill_counts[i][j] + col_kill_counts[i][j] )
return ret
6 . 6 2 B o m b i n g e ne m i e s
D y n am i c Pro g r am mi n g | 541
print max_enemy_killed_count_with_bomb([['0','E','0','0'],['E','0','W','E'],[0,'E','0','0']])
: The above code traverses through each element of the grid. Also, for each
of the location, it tries to find the enemy kill counts in rows and columns. So, the overall
time complexity is: O( + ) ≈ O( ).
Space Complexity: O( ).
6 . 6 2 B o m b i n g e ne m i e s
Py th on Pr o gr am Ex ecu tio n | 542
Appendix
Python
Program
Execution I
We generally write a computer program using a high-level language. A high-level language
is one which is understandable by humans. It contains words and phrases from the English
(or other) language. But a computer does not understand high-level language. It only
understands program written in 0's and 1's in binary, called the ℎ (also called
or ). A program written in high-level language is called a .
We need to convert the source code into machine code and this is accomplished by
compilers and interpreters. Hence, a or an is a program that converts
program written in high-level language into machine code understood by the computer.
ℎ :
Source file Machine Output
Compiler Operating
code
System
Compilers Interpreters
Scans the entire program and translates Translates program one statement at a
it as a whole into machine code. time.
I . 1 C o m p i l e r s v e rs u s I nt e r p r e t e r s
Py th on Pr o gr am Ex ecu tio n | 543
It takes a large amount of time to analyze It takes less amount of time to analyze
the source code but the overall execution the source code but the overall execution
time is comparatively faster. time is slower.
Generates intermediate object code
No intermediate object code is generated,
which further requires linking, hence
hence are memory efficient.
requires more memory.
It generates the error message only after Continues translating the program until
scanning the whole program. Hence the first error is met, in which case it
debugging is comparatively hard. stops. Hence debugging is easy.
Compiled languages are efficient but Interpreted languages are less efficient
difficult to debug. but easier to debug.
Programming language like C, C++, Programming language like Perl, Python,
COBOL use compilers. PHP, Ruby use interpreters.
This file contains two Python print statements, which simply print a string (the text in
quotes) and a numeric expression result (2 ℎ 3) to the output stream.
You can create such a file of statements with any text editor you like. By convention, Python
program files are given names that end in .py; technically, this naming scheme is required
only for files that are “imported” but most Python files have .py names for consistency.
After you’ve typed these statements into a text file, you must tell Python to execute the
file—which simply means to run all the statements in the file from the top to bottom, one
after another. For example, here’s what happened when I ran this script from a command
prompt Linux’s command line:
# python hellodeveloper.py
Hello Developer!
8
I . 2 P y t h o n p ro g r a m s
Py th on Pr o gr am Ex ecu tio n | 544
Figure illustrates the runtime structure described here. Keep in mind that all of this
complexity is deliberately hidden from Python programmers. Byte code compilation is
automatic, and the PVM is just part of the Python system that you have installed on your
machine. Again, programmers simply code and run files of statements, and Python handles
the logistics of running them.
Runtime
I . 4 P y t h o n by t e c o d e co m p i l a t i o n
Co m pl exi ty Cl asses | 545
Appendix
II
Complexity
Classes
II.1 Introduction
In the previous chapters we have solved problems of different complexities. Some
algorithms have lower rates of growth while others have higher rates of growth. The
problems with lower rates of growth are called problems (or ) and
the problems with higher rates of growth are called ℎ problems (or ℎ ).
This classification is done based on the running time (or memory) that an algorithm takes
for solving the problem.
Time
Name Example Problems
Complexity
Adding an element to the front of a
O(1) Constant
linked list
Finding an element in a binary
O( ) Logarithmic
search tree
Finding an element in an unsorted
O( ) Linear Easy solved
array
problems
Linear
O( ) Merge sort
Logarithmic
Shortest path between two nodes in
O( ) Quadratic
a graph
O( ) Cubic Matrix Multiplication
O(2 ) Exponential The Towers of Hanoi problem Hard solved
O( !) Factorial Permutations of a string problems
There are a lot of problems for which we do not know the solutions. All the problems we
have seen so far are the ones which can be solved by computer in a deterministic time.
Before starting our discussion, let us look at the basic terminology we use in this chapter.
I I . 1 I n t r o d u ct i o n
Co m pl exi ty Cl asses | 546
algorithm to solve a problem, and we don't try every possibility. Mathematically, we can
represent these as:
Polynomial time is O( ), for some .
Exponential time is O( ), for some .
Yes
Input Algorithm
No
NP class
The complexity class ( stands for non-deterministic polynomial time) is the set of
decision problems that can be solved by a non-deterministic machine in polynomial time.
class problems refer to a set of problems whose solutions are hard to find, but easy to
verify.
For better understanding let us consider a college which has 500 students on its roll. Also,
assume that there are 100 rooms available for students. A selection of 100 students must
be paired together in rooms, but the dean of students has a list of pairings of certain
students who cannot room together for some reason.
I I . 3 W h a t i s a d e c is i o n p r o b l e m?
Co m pl exi ty Cl asses | 547
The total possible number of pairings is too large. But the solutions (the list of pairings)
provided to the dean, are easy to check for errors. If one of the prohibited pairs is on the
list, that's an error. In this problem, we can see that checking every possibility is very
difficult, but the result is easy to validate.
That means, if someone gives us a solution to the problem, we can tell them whether it is
right or not in polynomial time. Based on the above discussion, for class problems if
the answer is , then there is a proof of this fact, which can be verified in polynomial
time.
Co-NP class
− is the opposite of (complement of ). If the answer to a problem in − is
, then there is a proof of this fact that can be checked in polynomial time.
Solvable in polynomial time
answers can be checked in polynomial time
− answers can be checked in polynomial time
Co-NP NP
One of the important open questions in theoretical computer science is whether or not =
. Nobody knows. Intuitively, it should be obvious that ≠ , but nobody knows how
to prove it.
Another open question is whether and − are different. Even if we can verify every
YES answer quickly, there’s no reason to think that we can also verify NO answers quickly.
It is generally believed that ≠ − , but again nobody knows how to prove it.
NP-hard class
It is a class of problems such that every problem in reduces to it. All -hard problems
are not in , so it takes a long time to even check them. That means, if someone gives us
a solution for -hard problem, it takes a long time for us to check whether it is right or
not.
A problem is -hard indicates that if a polynomial-time algorithm (solution) exists for
then a polynomial-time algorithm for every problem is . Thus:
is -hard implies that if can be solved in polynomial time, then =
NP-Hard
NP
I I . 6 T y p e s o f c o m p l e x it y c la s se s
Co m pl exi ty Cl asses | 548
NP-complete class
Finally, a problem is -complete if it is a part of both -hard and . -complete
problems are the hardest problems in . If anyone finds a polynomial-time algorithm for
one -complete problem, then we can find polynomial-time algorithm for every -
complete problem. This means that we can check an answer fast and every problem in
reduces to it.
NP-Hard
NP
NP-Complete
NP-Hard
Co-NP
NP
P
NP-Complete
The set of problems that are -hard is a strict superset of the problems that are -
complete. Some problems (like the halting problem) are -hard, but not in . -hard
problems might be impossible to solve in general. We can tell the difference in difficulty
between -hard and -complete problems because the class includes everything
easier than its "toughest" problems – if a problem is not in , it is harder than all the
problems in .
Does P==NP?
If = , it means that every problem that can be checked quickly can be solved quickly
(remember the difference between checking if an answer is right and actually solving a
problem).
This is a big question (and nobody knows the answer), because right now there are lots of
-complete problems that can't be solved quickly. If = , that means there is a way
to solve them fast. Remember that "quickly" means not trial-and-error. It could take a
billion years, but as long as we didn't use trial and error, it was quick. In future, a computer
will be able to change those billion years into a few minutes.
II.7 Reductions
Before discussing reductions, let us consider the following scenario. Assume that we want
to solve problem but feel it’s very complicated. In this case what do we do?
I I . 7 R e d u ct i o n s
Co m pl exi ty Cl asses | 549
The first thing that comes to mind is, if we have a similar problem to that of (let us say
), then we try to map to and use ’ solution to solve also. This process is called
reduction.
Instance
of Input Solution to I
(for ) Algorithm for
Algorithm for
In order to map problem to problem , we need some algorithm and that may take linear
time or more. Based on this discussion the cost of solving problem can be given as:
= +
Now, let us consider the other scenario. For solving problem , sometimes we may need to
use ’ algorithm (solution) multiple times. In that case,
= ∗ +
The main thing in -Complete is reducibility. That means, we reduce (or transform) given
-Complete problems to other known -Complete problem. Since the -Complete
problems are hard to solve and in order to prove that given -Complete problem is hard,
we take one existing hard problem (which we can prove is hard) and try to map given
problem to that and finally we prove that the given problem is hard.
Note: It’s not compulsory to reduce the given problem to known hard problem to prove its
hardness. Sometimes, we reduce the known hard problem to given problem.
I I . 7 R e d u ct i o n s
Co m pl exi ty Cl asses | 550
3-CNF-SAT Clique
Shortest Path Problem (Shortest-Path): Given a directed graph and two vertices s and t,
check whether there is the shortest simple path from to .
Graph Coloring: A -coloring of a graph is to map one of ‘colors’ to each vertex, so that
every edge has two different colors at its endpoints. The graph coloring problem is to find
the smallest possible number of colors in a legal coloring.
3-Color problem: Given a graph, is it possible to color the graph with 3 colors in such a way
that every edge has two different colors?
Clique (also called complete graph): Given a graph, the problem is to compute
the number of nodes in its largest complete subgraph. That means, we need to find the
maximum subgraph which is also a complete graph.
Independent Set Problem (Ind_Set): Let be an arbitrary graph. An independent set in
is a subset of the vertices of with no edges between them. The maximum independent
set problem is the size of the largest independent set in a given graph.
Vertex Cover Problem (Vertex-Cover): A vertex cover of a graph is a set of vertices that
touches every edge in the graph. The vertex cover problem is to find the smallest vertex
cover in a given graph.
Subset Sum Problem (Subset-Sum): Given a set of integers and an integer , determine
whether has a subset whose elements sum to .
Integer Programming: Given integers , a find 0/1 variables x that satisfy a linear system
of equations.
a x = 1≤ ≤
I I . 7 R e d u ct i o n s
Co m pl exi ty Cl asses | 551
x ∈ {0,1} 1 ≤ ≤
In the figure, arrows indicate the reductions. For example, Ham-Cycle (Hamiltonian Cycle
Problem) can be reduced to CNF-SAT. Same is the case with any pair of problems. For our
discussion, we can ignore the reduction process for each of the problems. There is a
theorem called ’ ℎ which proves that Circuit satisfiability problem is NP-hard.
That means, Circuit satisfiability is a known -hard problem.
Note: Since the problems below are -Complete, they are and -hard too. For
simplicity we can ignore the proofs for these reductions.
I I . 8 C o m p l ex i t y c l a ss e s : P r o b l e m s & S ol u t i o n s
Co m pl exi ty Cl asses | 552
Problem-9 Consider the problem of determining. For a given boolean formula, check
whether every assignment to the variables satisfies it. Which of the following is
applicable?
(a) (b) (c) (d) -Hard
(e) CoNP-Hard (f) -Complete (g) -Complete
Solution: Tautology is the complimentary problem to Satisfiability, which is NP-complete,
so Tautology is -complete. So it is , -hard, and -complete.
Problem-10 Let be an -complete problem and and be two other problems not
known to be in . is polynomial time reducible to and is polynomial-time
reducible to . Which one of the following statements is true?
(a) is -complete (b) is -hard (c) is -complete (d) is -hard.
Solution: is -hard (b).
Problem-11 Let be the problem of finding a Hamiltonian cycle in a graph = ( , ), with
| | divisible by 3 and the problem of determining if Hamiltonian cycle exists in such
graphs. Which one of the following is true?
(a) Both and are -hard (b) is -hard, but is not
(c) is -hard, but is not (d) Neither nor is -hard
Solution: Both and are -hard (a).
Problem-12 Let be a problem that belongs to the class . State which of the following
is true?
(a) There is no polynomial time algorithm for .
(b) If can be solved deterministically in polynomial time, then = .
(c) If is -hard, then it is -complete.
(d) may be undecidable.
Solution: If is -hard, then it is -complete (c).
Problem-13 Suppose we assume − is known to be -complete. Based on our
reduction, can we say − is -complete?
Solution: Yes. This follows from the two conditions necessary to be -complete:
Independent Set is in , as stated in the problem.
A reduction from a known -complete problem.
Problem-14 Suppose is known to be -complete. Based on our reduction,
is -complete?
Solution: No. By reduction from Vertex-Cover to Independent-Set, we do not know the
difficulty of solving Independent-Set. This is because Independent-Set could still be a much
harder problem than Vertex-Cover. We have not proved that.
Problem-15 The class of NP is the class of languages that cannot be accepted in polynomial
time. Is it true? Explain.
Solution:
The class of is the class of languages that can be in .
The class of P is the class of languages that can be in .
The class of is the class of languages that can be in .
⊆ and “languages in P can be accepted in polynomial time”, the description
“languages in NP cannot be accepted in polynomial time” is wrong.
The term NP comes from nondeterministic polynomial time and is derived from an
alternative characterization by using nondeterministic polynomial time Turing machines.
It has nothing to do with “cannot be accepted in polynomial time”.
I I . 8 C o m p l ex i t y c l a ss e s : P r o b l e m s & S ol u t i o n s
Co m pl exi ty Cl asses | 553
Problem-16 Different encodings would cause different time complexity for the same
algorithm. Is it true?
Solution: True. The time complexity of the same algorithm is different between unary
encoding and binary encoding. But if the two encodings are polynomial related (e.g. base 2
& base 3 encodings), then changing between them will not cause the time complexity to
change.
Problem-17 If P = NP, then NPC (NP Complete) ⊆ P. Is it true?
Solution: True. If P = NP, then for any language L ∈ NP C (1) L ∈ NPC (2) L is NP-hard. By
the first condition, L ∈ NPC ⊆ NP = P ⇒ NPC ⊆ P.
Problem-18 If NPC ⊆ P, then P = NP. Is it true?
Solution: True. All the NP problems can be reduced to arbitrary NPC problems in
polynomial time, and NPC problems can be solved in polynomial time because NPC ⊆ P. ⇒
NP problem solvable in polynomial time ⇒ NP ⊆ P and trivially P ⊆ NP implies NP = P.
I I . 8 C o m p l ex i t y c l a ss e s : P r o b l e m s & S ol u t i o n s
Bibliography
[1] Donald E. Knuth. Fundamental Algorithms, volume 1 of The Art of Computer
Programming. Addison-Wesley, 1968. Second edition, 1973.
[2] Donald E. Knuth. Seminumerical Algorithms, volume 2 of The Art of Computer
Programming. Addison-Wesley, 1969. Second edition, 1981.
[3] Donald E. Knuth. Sorting and Searching, volume 3 of The Art of Computer
Programming. Addison-Wesley, 1973.
[4] Donald E. Knuth. Big omicron and big omega and big theta. ACM SIGACT News,
8(2):18-23, 1976.
[5] Donald E. Knuth, James H. Morris, Jr., and Vaughan R. Pratt. Fast pattern
matching in strings. SIAM Journal on Computing, 6(2):323-350, 1977.
[6] Robert W. Floyd. Algorithm 97 (SHORTEST PATH). Communications of the ACM,
5(6):345, 1962.
[7] Alfred V. Aho, John E. Hopcroft, and Jeffrey D. Ullman. The Design and Analysis of
Computer Algorithms. Addison-Wesley, 1974.
[8] Richard Bellman. Dynamic Programming. Princeton University Press, 1957.
[9] Jon L. Bentley. Programming Pearls. Addison-Wesley, 1986.
[10] J. A. Bondy and U. S. R. Murty. Graph Theory with Applications. American Elsevier,
1976.
[11] Stephen Cook. The complexity of theorem proving procedures. In Proceedings of the
Third Annual ACM Symposium on Theory of Computing, pages 151-158, 1971.
[12] E. W. Dijkstra. A note on two problems in connexion with graphs. Numerische
Mathematik, 1:269-271, 1959.
[13] Herbert Edelsbrunner. Algorithms in Combinatorial Geometry, volume 10 of EATCS
Monographs on Theoretical Computer Science. Springer-Verlag, 1987.
[14] Robert W. Floyd and Ronald L. Rivest. Expected time bounds for selection.
Communications of the ACM, 18(3):165-172, 1975.
[15] Paul W. Purdom, Jr., and Cynthia A. Brown. The Analysis of Algorithms. Holt,
Rinehart, and Winston, 1985.
[16] S. Sahni and T. Gonzalez. P-complete approximation problems. Journal of the ACM,
23:555-565, 1976.
[17] Herbert S. Wilf. Algorithms and Complexity. Prentice-Hall, 1986.
[18] Improving Saddleback Search: A Lesson in Algorithm Design
[19] A. Karatsuba: The Complexity of Computations. Proceedings of the Steklov Institute
of Mathematics, Vol. 211, 1995, pages 169 - 183, available at
http://www.ccas.ru/personal/karatsuba/divcen.pdf. A. A. Karatsuba reports
about the history of his invention and describes it in his own words.
[20] A. K. Dewdney: The (New) Turing Omnibus. Computer Science Press, Freeman, 2nd
ed., 1993; reprint (paperback) 2001. These “66 excursions in computer science”
include a visit to the multiplication algorithms of Karatsuba (for long numbers) and
Strassen (a similar idea for matrices).
[21] Wolfram Koepf: Computer algebra. Springer, 2006. A gentle introduction to
computer algebra. Unfortunately, only available in German.
[22] Joachim von zur Gathen, Jurgen Gerhard: ¨ Modern Computer Algebra. Cambridge
University Press, 2nd ed., 2003.