Lecture Notes
Lecture Notes
• NameError can occur in Python code, if the variable/function cannot be found in the namespace,
when it is used.
Week2
• https://www.youtube.com/watch?v=zS1dLE66smA
• Example of calculating time complexity follows:
• Complexity of the above function can be represented as f(n) = 2n^2 + 2n + 3. To calculate the
asymptotic complexity of f(n), we will use another function g(n) that is above f(n), beyond a specific
value of n (n0) and when multiplied by a constant factor c.
• This is represented graphically as follows:
This is because, each iteration in the loop, s is halved and iterates only 5 times.
• If f1(n) is O(g1(n)) and f2(n) is O(g2(n)), then f1(n) + f2(n) is O(max(g1(n), g2(n)))
• How to find time complexity of recursive algorithms by unwinding?
For example, to find the time complexity of a recursive function f with the following complexity for
the initial call.
f(n) = 2*f(n-1) + 1
Since f(1) = 1,
Search algorithms
Naïve search - O(1), O(n), O(n)
say, we’re searching for v in l.
for each item in the list {
If the item equals v, then you found what you’re searching for.
}
You didn’t find it anywhere!
Binary search
Implementation 1 - O(1), O(logn), O(logn)
say, you’re searching for v in l, starting at s and ending at e.
binary_search(v,l,s,e) {
if e <= s then you’ve just 1 element in the list, return True if it matches v, else False
divide the list at the midpoint //m
If v is equal to element at m, then you’ve found it (Best case)
else {
if v is more than element at m, then make a recursive call, with s = m + 1.
if v is less than element at m, then make a recursive cal, with e = m - 1.
}
}
Sorting algorithms
Selection sort - O(n2), O(n2), O(n2)
say, you’re sorting l (assume l has n elements)
if there’s no element in l, return empty list
else {
loop over all elements in l {
find the least among remaining elements in the list //“least”
if “least” is less than current element, then swap them.
}
}
•
For the above problem, the time complexity is O(n2). It’s obtained as follows
(n - 1) + (n - 2) + (n - 3) + …. + 2 + 1 = n * (n - 1) /2 = O(n2)
Week3
Quick sort - O(nlogn), O(nlogn), O(n2)
Linked Lists
say, you’re inserting a new node to the start of a linked list
add_head(L, e)
newest = Node(e)
newest.next = L.head
L.head = newest
However, deleting the tail will take O(n) time, since in order to set the previous node’s next to
None, previous must be located first, which takes O(n) time. In the case of a doubly-linked list, this
operation will take O(1), since previous is readily available for each node.
Week4 (Graphs)
BFS (Breadth-First Search)
Given an adjacency list and a node, search breadth first.
Initialize the "visited" dictionary for all keys in the list to False.
Consider the first key, add it to the queue. Mark it "visited"
Repeat as long as there are elements in the queue {
Take next element from the queue.
Get the list of values for the key from the adjacency list. These represent the incident nodes.
Append them in the same order into the queue, only if the they’re not already “visited”
}
After the queue is emptied, return the visited dictionary.
Topological sort
Given adjacency list, find the topological sort
Initialize indegree of all keys in adjacency list to 0.
Calculate indegree of all nodes (incident on keys) in the adjacency list //indegree
Add all nodes whose indegree is 0 to the queue //zerodegreeq
Repeat as long as there are items in zerodegreeq {
Take next element from the queue, and add to “toposortlist”
Reduce indegree for the node
Reduce indegree for all its neighbors
If the indegree is 0, then add to queue //zerodegreeq
}
return “toposortlist”
• Number of vertices in a graph is termed Order of the graph; Number of edges in a graph is termed
Size of the graph
• In a complete graph, every pair of distinct vertices is connected by an edge, and the degrees of all
vertices are equal.
• In a connected graph, there is a path between every pair of vertices.
• The maximum number of edges in an undirected graph with n vertices is n(n - 1)/2.
• The maximum number of edges in directed graph with n vertices is n(n - 1)
• Maximum number of edges in a directed acyclic graph (DAG) is n(n - 1)/2.
• Maximum path length in a DAG is (n - 1).
• The maximum number of edges in a connected graph with n vertices is (n - 1)(n - 2)/2.
• The minimum number of edges in a connected graph with n vertices is (n - 1). This happens when
the graph is a tree.
• It is possible but not necessary that a complete graph also be a connected graph. For example, a
complete graph with only one vertex is a connected graph, but a complete graph with two or more
disconnected components is not a connected graph.
• In any graph (both directed and undirected), the sum of the degrees of all the vertices is equal to
twice the number of edges. Thus, 2m = ∑deg(v), This is known as the handshaking lemma.
• In an undirected connected graph
o Sum of degrees of all vertices is even.
o Number of vertices with an odd degree is even.
• The minimum number of colors needed to color a planar graph with n vertices is 4.
• Complexity of DFS is O(n2) using adjacency matrix, and O(m + n) using adjacency list.
• Given adjacency list, the time complexity for finding the indegrees of all n vertices in a graph with m
edges, is O(m + n)
• BFS/DFS can be used to identify the count of connected components.
• In a depth-first traversal (DFS) of a graph G with n vertices, k edges are marked as tree edges. The
number of connected components is (n - k). See https://youtu.be/iPd5Q_MRmgM?&t=6553
• DFS can be used to detect cycles in a graph using pre/post numbering during traversal.
• While performing pre/post numbering using DFS algorithm in a graph, if (u, v) is an edge of the
graph such that [pre(v), post(v)] contains [pre(u), post(u)], then the graph has cycles. v->u is a back-
edge.
• DAG will have at least one vertex without incoming edges, but might have more than one too
(though not necessary)
• DAG will have at least one topological sort sequence, but might have more than one too (though not
necessary)
• It is not possible to topologically sort a graph with cycles. Thus, topological sort can be used as a
mechanism to identify if the graph has cycles.
• DFS will always produce same number of tree edges, irrespective of the order in which vertices are
considered. If the graph is connected, it’ll always produce (n-1) edges, if there are n vertices.
• Time complexity of topological sort using adjacency list, given that m=#edges and n = #vertices, is
O(m + n). Use of adjacency matrix will increase this to O(n2).
• Time complexity of finding the longest path in DAG using adjacency list, given that m=#edges and n
= #vertices, is O(m + n). Use of adjacency matrix will increase this to O(n2).
• Time complexity of an algorithm to compute incoming edges for each vertex, given that m=#edges
and n = #vertices, is O(m + n)
• If (u, v) is an edge of G that is not in the tree T generated as part of a BFS, and d is the shortest
distance of the vertex from the starting point, then the possible values of d(u) - d(v) are -1, 0, or 1. If
(u, v) is not an edge in G, then u is a leaf in T.
• Problem:
A connected undirected graph has 1081 edges. What is possible limits of #vertices?
• Problem:
In a graph with 4 nodes and 6 edges (with weights 1-6), what’s maximum possible weight that a
minimum weight spanning tree can have.
The most important thing to remember in this question is that the graph layout is not
known – it’s only known that the graph has 4 nodes and 6 edges. Here’re two possibilities
for MST for such a graph, and the second layout will generate the maximum possible
weight. Thus, the answer is 7.
Week5
• Dijkstra’s algorithm to find shortest path will not always work, when the graph has negative
weights. Use Bellman-Ford algorithm in this case.
• Bellman-Ford algorithm can work with negative weight edges, but not with negative weight
cycles.
• Bellman-Ford iterates through all edges in a set, each time relaxing the edges.
• Bellman-Ford is run for (n - 1) iterations, each time identifying paths that could reach each
vertex through one additional hop. Thus, the first iteration will check for all paths to each vertex
in one hop. Second iteration will check for all paths to each vertex in two hop. Until, we’ve
covered (n - 1) iterations.
• Bellman-Ford algorithm can detect negative weight cycles, by running it nth time. If the weights
reduce even after (n - 1) iterations, there’s a cycle.
• Dijkstra and Bellman-Ford can work on directed or undirected graphs.
• In a graph with all unique edge weights, it’s possible to have multiple shortest paths between
any two vertices. This is because, the number of edges might differ in both paths.
• Shortest path in a graph with n vertices can have 1 to (n - 1) edges.
• While finding the all-pairs shortest path using Floyd-Warshall algorithm on a directed weighted
graph, if any of the diagonal elements contain a negative weight at the end of the process, then
there exists a negative weighted cycle.
• Dijkstra has a time complexity of O(V2) using simple array, and O((E+V)logV) using a binary heap
for priority queue implementation.
• Bellman-Ford has a time complexity of O(VE). Normally, this is larger than Dijkstra’s since E > V
• All-pairs shortest paths obtained using Floyd-Warshall algorithm has a time complexity of O(n3)
• Formula to compute shortest path from vertex i to j (with k representing an intermediate vertex)
Spanning trees
• In a graph with n vertices, a spanning tree is a subset of the graph with the n vertices, and (n - 1)
edges.
• Number of spanning trees that can be constructed from a graph with n vertices and m edges is
mCn-1 - #cycles
• Maximum number of spanning trees that can be constructed from a graph is n(n - 2). This
happens when the graph is complete.
• Adding an edge to a spanning tree will create a cycle.
• In a spanning tree, every pair of nodes is connected by a unique path.
• For a weighted graph, multiple spanning trees could be constructed, but only one of them will
be minimum cost. Use Prim’s or Kruskal’s algorithm might yield different MST, but the cost of
both trees will be same.
• Both Kruskal and Prim can be used with arbitrary (including negative) weights and negative
weight cycles. Only shortest path algorithm is affected by negative weight cycles.
• It only makes sense to find minimal spanning trees for undirected graphs.
• Prim’s algorithm:
o Selected the edge with least weight/cost
o Select an edge connected to the last edge, with the least cost.
o Repeat this, until (n - 1) edges have been selected.
o Works very similar to Dijkstra, except the relaxation step, which assigns distances[v]
with min(d, distances[v]) instead of min(d + distances[u], distances[v])
• In a disconnected graph with multiple components, spanning trees doesn’t exist.
• If there are multiple components in the graph, Kruskal’s algorithm might be able to find the
minimum cost spanning tree for one of the components. Note that this isn’t possible using
Prim’s.
• Kruskals’s algorithm:
o Sort edges in the increasing order of weights, and select the one with least weight/cost
o Repeat this until (n - 1) edges have been selected, each time selecting the least
weight/cost. It’s not necessary that the selected edges are connected.
o Make sure, at each selection, that no cycles are formed at any point.
• Time complexity for Kruskal/Prim’s algorithm is O(n2). Using minheap, this can be reduced to
O(nlogn).
• Kruskal’s could find the “missing” edge costs. https://youtu.be/4ZlRH0eK-qQ?t=1001
• Kruskal’s might be able to find multiple “minimum cost spanning trees”, but the costs of all such
will be equal.
• In general, when the edge weights are unique and the graph is connected, both Prim's and
Kruskal's algorithms will produce the same minimum spanning tree. However, in certain cases
where the edge weights are not unique or the graph is not connected, the algorithms may
produce different trees.
Week6
• In Kruskal,
o Cost of union (of components) operation per pair of components is n. Considering that
this has to be repeated (n - 1) times, the total cost is n2 ; we can improve this to nlogn.
o This improvement can be achieved by maintaining a members and sizes dictionary.
o Sorting of edges is mlogm time, using merge sort.
o Total time complexity can be improved to (m + n) logn time.
• In Prim,
o The major bottleneck is to find the minimum cost edge from the graph, which is O(n).
Considering that this has to be repeated (n - 1) times, the total cost is n2 ;
o This can be reduced by keeping the costs in a 2-dimensional queue priority structure.
Each row in the matrix is kept sorted. An additional column keeps track of the number
of items in each row.
o Time complexity to insert into the matrix, while keeping a sorted row is O(logn)
o Time complexity to remove the maximum from the matrix is O(logn)
o Performing these operation n times, time complexity is O(nlogn)
o We can store the edge weights in a binary tree (heap), in order to improve the time
complexity.
o Using heap in Prim is advantageous over sorted arrays, since the distances are
recalculated each time an edge is added to the MCST. Since this step is missing in
Kruskal, using heap in Kruskal isn’t advantageous.
• Heaps are complete binary trees, with following constraints,
o Structural constraint, wherein the tree has to be filled up level by level, starting with
level0. Within each level, the nodes must be filled up from left to right. In other words
only the lowest level (and in the RHS) can be incomplete.
o Value constraint, wherein the nodes in the previous levels must be at least as high as
the lower levels.
• In the max-heap, value of each node (except the leaves) >= its children.
• In the min-heap, value of each node (except the leaves) <= its children.
• While inserting a node into the heap, every node must be reconciled with its parent for its
priority. If the node’s priority is more than the parent, it must be swapped with the parent.
• The number of nodes that fill up k levels is 20 + 21 + 22 + 23 + … + 2k = 2k+1 - 1
• If we have n nodes in the binary tree, the number of levels in the tree cannot exceed log(n + 1).
This also means that this is the maximum height you’ll have to navigate and perform swap
operations and hence the time complexity for inserting a node into a binary tree is O(logn), if n
represents the number of nodes.
• To “get” the highest priority node (delete_max) from a max_heap, follow these steps:
o Remove the root node and return.
o Swap the last node in the tree (right-most at the last level) into the root’s position.
o Find the largest among root’s children. Swap root with the highest among them.
o Repeat this until you reach the leaf node.
• The time complexity for the delete_max operation is also O(logn)
• If the nodes in a heap are stored in a list from left to right, then (2i + 1) and (2i + 2) are the
indices to the children of the ith node. Similarly, parent of ith node has an index (i - 1)//2
• Since insert and delete_max operation are done N times in the Prim’s, the total complexity is
nlog(n)
• To build a heap from an unsorted array, repeatedly “insert” each element into the list of nodes.
This can be done in O(n) time, although technically “heapify” gets called repeatedly (and
“heapify” has O(logn) time complexity.
• A binary tree (not binary search tree) is a structure that has at most two child per parent. At the
extreme case, this can be a linear structure.
• A binary tree is said to be complete, when each level is complete and can hold no more children.
• A binary tree with n leaves will have (n - 1) internal nodes include root.
• A heap is to be almost complete binary tree, since, except for the bottom-most level (leaves), all
other levels are complete in a heap. In a heap, it’s mandatory that at every level, nodes must be
inserted from left to right and don’t allow holes in between.
• Searching through a heap for an item will take O(n) time, because there’s no order to storing the
elements in a heap other than what parent-child relationship requires. Note that searching
through a “balanced” binary search tree is O(logn).
• Extracting the largest element in a BST is O(n), since elements are not necessarily arranged in
sorted order.
• Heap and Binary search tree are two different data structures, and it’s not necessary that a heap
is a BST or a BST is a heap.
• Binary search trees are best implemented using recursion. There are 3 traversal mechanisms:
in-order, pre-order and post-order, as depicted in the picture below.
• In-order is left-root-right, Pre-order is root-left-right, Post-order is left-right-root.
• In the case of BST, we can reconstruct the original tree, in the following cases.
o if only pre-order traversal is known, first node of which is always the root.
o if only post-order traversal is known, last node of which is always the root.
• Note that it’s not possible to reconstruct the tree, if only in-order traversal is known
• In-order traversal of a BST is always sorted.
• However, in the case of general binary tree, it’s not possible to reconstruct original tree, from
pre-order or post-order traversals, unless also supplied with in-order traversal order.
• Note that in the case of a traversal (pre-order, in-order or post-order), the time complexity
remains O(n).
• Every node in the binary search tree has a value, a left tree and a right tree. All leaves of the
tree have an empty node below it, in order to make it simpler for the recursion to work (serves
as a base case).
• Inserting a value into the binary search tree tries to locate the value to be inserted in the tree. If
the value is found, then it returns without doing anything (No duplicates are allowed in BST). If
not found, it sets its value and creates a new tree (node) on the left or the right of the current
node.
• It’s possible to have an unbalanced tree , for the which the complexity is O(n). If the tree is
balanced, all operations (including insert and delete) are O(logn)
• In a strict binary tree (where every node has 0 or more children), following property holds -
#leaves = (#nodes + 1)/2. Thus, if there are 21 leaves in a strict binary tree, it has 41 nodes.
https://discourse.onlinedegree.iitm.ac.in/t/no-of-nodes/56693/2
• A strict binary tree (each node has two children) with n leaves has (n - 1) internal nodes.
Similarly, a binary tree (strict or otherwise) with n leaves has (n - 1) internal nodes each with 2
children. Both of these are derivable from the previous note.
Week7
• Minimum number of nodes in AVL tree of height h is S(h) = S(h-2) + S(h-1) + 1 where S(0) = 0 and
S(1) = 1. In order to obtain this easily, use the formula S(h) = fib[h+2] - 1. For example, the
minimum number of nodes required to construct an AVL tree with height 12, S(12) = fib(14) - 1.
Since, fib(14) = 377. S(12) = 376.
• Similarly, maximum number of nodes in an AVL tree is 2h - 1. For example, the maximum
number of nodes in an AVL tree with height 12 is 214 - 1 = 16383
• Huffman encoding uses variable length encoding of letters.
• In this scheme, no letter will have other letters as prefixes. Otherwise, decoding isn’t possible.
• Following is the calculation involved.
• Binary trees can be used to represent encoding, where letters are leaves and path to leaf
describes encoding – 0 is left and 1 is right.
• In the above representation, every node in the tree has 0 or 2 children (no node with a single
child), thus constructing it as a full tree. In other words, none or two letters can be represented
with a certain number of bits.
Week8
• Given two sets of symbols, inversion is the set of all pairs (i , j) such that i < j, but j occurs before
i in either set. The maximum number of possible inversions is n(n - 1)/2, given n is the number of
elements in either set.
• In order to find all inversions, it’ll take O(n2) time using the naïve method. With the divide and
conquer approach (like merge-sort), it’ll take O(nlogn) time.
• Naïve algorithm used for Integer multiplication has a recurrence relation of 4T(n/2) + n = O(n2)
• Karatsuba’s algorithm used for integer multiplication has a recurrence relation of 3T(n/2) + n =
O(nlog3)
• Quick-select uses “wall index” returned by partitioning algorithm. This is the kth least in the
array.
• The time complexity of Quick-select algorithm is O(n) on average and O(n^2) in the worst case,
and is dependent on the partitioning strategy.
• The worst case time complexity of Fast-select algorithm is O(n).
• Time complexity of the brute force closest pair problem is O(n2), and using the divide-conquer
approach is O(nlogn)
• Time complexity of recurrence based algorithms at each level can be generally represented as ri
* f(n/ci), where r is the number of recurrence calls at the level i, c is the division factor and f(n)
represents the time spent on non-recursive work.
• For a recurrence relation T(n) = 2T(n/8) + O(n), the time complexity is O(n). This is the decreasing
case above.
• For a recurrence relation T(n) = 4T(n/4) + O(n), the time complexity is O(nlogn). This is the equal
case above. Example: 4-way merge sort.
• For a recurrence relation T(n) = T(n - 1) + O(n), the time complexity is O(n2). Example: Worst case of
quicksort.
• For a recurrence relation T(n) = T(n/2) + O(1), the time complexity is O(logn). Example: binary
search.
• For a recurrence relation T(n) = T(n/2) + O(n), the time complexity is O(n). This is the decreasing
case above. Example: Find kth smallest (largest) element using fastselect. In this case, algorithm
uses divide and conquer, and uses MoM to find the middle element.
Week9
• Two requirements for a problem to be solvable using dynamic programming are:
o Optimal substructure
o Overlapping subproblems
• Inductive structure for Grid Paths problem
• Assuming that the grid has m x n size, time complexity of a memorized solution is O(m + n), and that
of a dynamic programming solution for Grid Paths problem is O(mn)
• Inductive structure Longest Common Sub-word (LCW) problem
• Assuming that the words have has m and n characters respectively, time complexity of a
memorized/dynamic programming solution for LCW is O(mn)
• Inductive structure for Longest Common Subsequence (LCS) problem
• Assuming that the words have has m and n characters respectively, time complexity of a
memorized/dynamic programming solution for LCS is O(mn)
• Inductive structure for Edit distance problem
• Assuming that the documents have has m and n characters respectively, time complexity of a
memorized/dynamic programming solution for Edit Distance problem is O(mn)
• In order to multiply two matrices whose sizes are m x n and n x p respectively, it takes O(mnp) time.
• Associativity of multiplication of a sequence of matrices affects its time complexity.
Week10
• Boyer-Moore
• If the text has m characters and pattern has n characters, number of comparisons in the worst-case
of Boyer-Moore is (m - n + 1) * n. For example, see this post
https://discourse.onlinedegree.iitm.ac.in/t/practice-q-6-isnt-the-worst-case-complexity-of-boyer-
moore-m-n/84668
• In Rabin-Karp, the problem of matching text with pattern is reduced to numeric match, by replacing
each alphabet with a number. This means, comparison of text and pattern doesn’t have to done by
each character, but using simple arithmetic. Thus, the complexity can be reduced from O(m + n) to
O(n).
• Algorithm converts the text into blocks, before doing the arithmetic.
• However, the issue is that this depends on the size of alphabet. if there were only 10 characters in
the alphabet, you could use numbers 0-9, but with 80+ characters (ASCII) the gain in time
complexity will be offset with that of doing the earlier mentioned calculation with large numbers.
So, the algorithm resorts to using modulo arithmetic on the text block and pattern – modulo with a
specific prime (say, 13). If the modulo on the text block doesn’t match that from the pattern, the
pattern is obviously not found on the text block. Repeat with the next text block. In the best case,
the time complexity of Rabin-Karp is O(m +n), but in the worst case it can be O(mn)
• To draw the graph that represents finite automata machine of a text comparison based on a given
pattern, refer to https://www.youtube.com/watch?v=kuMuFu9IRtw. The process elaborated in this
video will help create a table that can be converted into a graph. Pattern matching a given text is
done by tracing through this graph. Input text is deemed to have matched the pattern, only if the
last node in the graph can be reached, while using the input text to trace through the states
depicted in the graph.
• To produce the LPS array (used in the KMP algorithm), part of process depicted in the above video
needed to be followed.
Week11
• Ford-Fulkerson algorithm - https://www.youtube.com/watch?v=GiN3jRdgxU4
• Time complexity for the Ford-Fulkerson algorithm and finds an augmented path in the algorithm is
O(E * max_flow)
• A subset S of V is called an independent set of G if no two vertices in S are adjacent.
• minimum vertex cover size + maximum independent set size = total number of vertices
• In linear programming, variables can be multiplied by some constant, we can not multiply variable
by variable in objective function or constraints.
• P includes those problems that are solvable in polynomial time. NP includes those that are solvable
in non-polynomial time. NP problems can be verified in polynomial time and hence the verifiability
of NP belong to P. Thus, it’s known that P is a subset of NP. However, it has not yet been proved
whether P is a proper subset of NP, or P equals NP. The prevailing assumption, however, is that P !=
NP.
• If problem A is reducible to problem B, and B is solvable, then A is solvable too.
• If problem A is reducible to problem B, it implies that problem B is at least as hard as problem A. This
is because if we can solve problem B, then we can also solve problem A by transforming it into
problem B and then solving it. So if problem B is easy to solve, then so is problem A. Conversely, if
problem A is hard to solve, then so is problem B.
• In A -> B (A reduced to B), it’s not possible to infer anything about the time complexity of A, unless
more details on the transformation/reduction is known. It’s only possible to infer that B has a
greater time complexity than A.
• NP-hard problems are the class of problems that are harder than all NP problems. It’s possible that
the verification of these too could be non-polynomial time. Now, problems that are NP-hard, but
whose verification can be done in polynomial time are called NP-complete problems.
• NP-complete problems lie in the intersection between NP and NP-hard problems. They’re at least as
hard as all NP problems.
Appendix
Check out this spreadsheet for the time complexities of some common algorithms learnt in the course.