Divide and Conquer Algorithms Explained
Divide and Conquer Algorithms Explained
Combine: Use the Solutions of Smaller Problems to find the overall result.
Examples of Divide and Conquer are Merge Sort, Quick Sort, Binary Search and Closest Pair of Points.
Let us understand the divide and conquer algorithm with the help of merge sort and Fibonacci series:
1. Merge sort:
Suppose we wish to arrange the letters of the word ‘TECHVIDVAV’ in ascending order. Then,
2. Write a function that generates the Fibonacci series using the divide and conquer approach.
fibonacci(n){
If n < 2
return 1;
else
Divide and conquer strategy has various application areas such as:
1. Defective chessboard
2. Binary search
4. Merge sort
5. Quicksort
In this approach, the array is divided into two halves. Then using recursive approach maximum and minimum
numbers in each halves are found. Later, return the maximum of two maxima of each half and the minimum
of two minima of each half.
In this given problem, the number of elements in an array is y – x + 1, where y is greater than or equal to x.
Max - Min(x,y) will return the maximum and minimum values of an array numbers [x….y].
if y x ≤ 1 then
return (max(numbers[x],numbers[y]),min((numbers[x],numbers[y]))
else
Example:
A = [5, 3, 8, 2, 7, 6]
Step 1: Divide
Step 3: Combine
Complexity Analysis:
Number of Comparisons: ≈ 3n/2 − 2 (less than the 2n−2 comparisons in the simple linear method)
Advantages:
Dynamic Programming is a commonly used algorithmic technique used to optimize recursive solutions
when same subproblems are called again.
The core idea behind DP is to store solutions to subproblems so that each is solved only once.
To solve DP problems, we first write a recursive solution in a way that there are overlapping
subproblems in the recursion tree (the recursive function is called with the same parameters multiple
times)
To make sure that a recursive value is computed only once (to improve time taken by algorithm), we
store results of the recursive calls.
There are two ways to store the results, one is top down (or memoization) and other is bottom up (or
tabulation).
Dynamic programming is used for solving problems that consists of the following characteristics:
1. Optimal Substructure:
The property Optimal substructure means that we use the optimal results of subproblems to achieve the
optimal result of the bigger problem.
Example:
Consider the problem of finding the minimum cost path in a weighted graph from a source node to a
destination node. We can break this problem down into smaller subproblems:
Find the minimum cost path from the source node to each intermediate node.
Find the minimum cost path from each intermediate node to the destination node.
The solution to the larger problem (finding the minimum cost path from the source node to the destination
node) can be constructed from the solutions to these smaller subproblems.
2. Overlapping Subproblems:
The same subproblems are solved repeatedly in different parts of the problem refer to Overlapping
Subproblems Property in Dynamic Programming.
Example:
Consider the problem of computing the Fibonacci series. To compute the Fibonacci number at index n, we
need to compute the Fibonacci numbers at indices n-1 and n-2. This means that the subproblem of computing
the Fibonacci number at index n-2 is used twice (note that the call for n - 1 will make two calls, one for n-2
and other for n-3) in the solution to the larger problem of computing the Fibonacci number at index n.
You may notice overlapping subproblems highlighted in the second recursion tree for Nth Fibonacci
diagram shown below.
In the top-down approach, also known as memoization, we keep the solution recursive and add a
memoization table to avoid repeated calls of same subproblems.
Before making any recursive call, we first check if the memoization table already has solution for it.
After the recursive call is over, we store the solution in the memoization table.
In the bottom-up approach, also known as tabulation, we start with the smallest subproblems and
gradually build up to the final solution.
We write an iterative solution (avoid recursion overhead) and build the solution in bottom-up
manner.
We use a dp table where we first fill the solution for base cases and then fill the remaining entries of
the table using recursive formula.
We only use recursive formula on table entries and do not make recursive calls.
Example of Dynamic Programming (DP)
Brute Force Approach: To find the nth Fibonacci number using a brute force approach, you would simply
add the (n-1)th and (n-2)th Fibonacci numbers.
Output
5
Below is the recursion tree of the above recursive solution.
The time complexity of the above approach is exponential and upper bounded by O(2n) as we make two
recursive calls in every function.
Let us now see the above recursion tree with overlapping subproblems highlighted with same color. We can
clearly see that that recursive solution is doing a lot work again and again which is causing the time complexity
to be exponential. Imagine time taken for computing a large Fibonacci number.
Here Same Colours denotes overlapping subproblems.
Identify Subproblems: Divide the main problem into smaller, independent subproblems, i.e., F(n-1)
and F(n-2)
Store Solutions: Solve each subproblem and store the solution in a table or array so that we do not
have to recompute the same again.
Build Up Solutions: Use the stored solutions to build up the solution to the main problem. For F(n),
look up F(n-1) and F(n-2) in the table and add them.
Avoid Recomputation: By storing solutions, DP ensures that each subproblem (for example,
F(2)) is solved only once, reducing computation time.
Fibonacci series:
Write an algorithm in dynamic programming that generates the Fibonacci series.
Member=[ ];
fibonacci(n){
if n in Member
return member[n];
else{
if( n < 2)
fib= 1;
else
fib = fib(n - 1) + fib(n -2);
}
mem[n] = fib;
return fib;
}
Applications of Dynamic Programming
1. Knapsack Problem: One of the classic examples of dynamic programming, the knapsack problem involves
selecting a subset of items with given weights and values to maximize total value without exceeding a weight
limit. This problem has practical applications in resource allocation, budgeting, and logistics.
2. Shortest Path Algorithms: Dynamic programming is used to find the shortest path in weighted graphs, such
as in the Bellman-Ford algorithm and Floyd-Warshall algorithm. These algorithms are essential in routing and
navigation systems, helping to determine the most efficient routes for transportation and delivery services.
4. Resource Allocation: Dynamic programming helps in optimizing resource allocation in various industries,
including manufacturing and project management. It ensures that resources are distributed efficiently across
competing tasks or time periods, maximizing utility while adhering to constraints.
5. Inventory Management: Businesses use dynamic programming to predict inventory needs, optimize stock
levels, and determine reorder points. This application helps maintain the right inventory levels while
minimizing costs, ensuring demand is met without excessive storage or stockouts.
7. Stage Coach Problem: This foundational example of dynamic programming models a scenario where a
stagecoach must travel from an initial point to a destination, passing through several stages. The goal is to
determine the most cost-effective route, illustrating how DP can optimize sequential decision-making under
constraints.
A Multistage graph is a directed, weighted graph in which the nodes can be divided into a set of stages such
that all edges are from a stage to next stage only (In other words there is no edge between vertices of same
stage and from a vertex of current stage to previous stage).
The vertices of a multistage graph are divided into n number of disjoint subsets
S = { S1 , S2 , S3 ........... Sn }, where S1 is the source and Sn is the sink (destination).
The cardinality of S1 and Sn are equal to 1. i.e., |S1| = |Sn| = 1.
We are given a multistage graph, a source and a destination, we need to find shortest path from source to
destination. By convention, we consider source at stage 1 and destination as last stage.
Now there are various strategies we can apply: -
The Brute force method of finding all possible paths between Source and Destination and then
finding the minimum. That's the WORST possible strategy.
Dijkstra's Algorithm of Single Source shortest paths. This method will find shortest paths from
source to all other nodes which is not required in this case. So it will take a lot of time, and it doesn't
even use the SPECIAL feature that this MULTI-STAGE graph has.
Simple Greedy Method - At each node, choose the shortest outgoing path. If we apply this approach
to the example graph given above, we get the solution as 1 + 4 + 18 = 23. But a quick look at the
graph will show much shorter paths available than 23. So, the greedy method fails!
The best option is Dynamic Programming. So we need to find Optimal Sub-structure, Recursive
Equations and Overlapping Sub-problems.
So, here we have drawn a very small part of the Recursion Tree and we can already see
Overlapping Sub-Problems. We can largely reduce the number of M(x, y) evaluations using
Dynamic Programming.
The below implementation assumes that nodes are numbered from 0 to N-1 from first stage (source)
to last stage (destination). We also assume that the input graph is multistage.
We use top to bottom approach and use dist[] array to store the value of overlapping sub-problem.
dist[i] will store the value of minimum distance from node i to node n-1 (target node).
Therefore, dist[0] will store minimum distance between from source node to target node.
Algorithm
Input: A weighted multistage graph G with s and t as source and target vertices, respectively.
Output: The shortest path from s to t in G.
Set d(t) = 0 and d(v) = ? for all other vertices v in G.
For i = k-1 to 1:
a. For each vertex v in stage i:
i. Set d(v) = min(w(v, u) + d(u)) for all vertices u in stage i+1.
Return d(s) as the shortest path from s to t.
In the above algorithm, we start by setting the shortest path distance to the target vertex t as 0 and all other
vertices as infinity.
We then work backwards from the target vertex t to the source vertex s.
Starting from the second-to-last stage (k-1), we loop over all the vertices in that stage and update their
shortest path distance based on the
shortest path distances of the vertices in the next stage (i+1). We update the shortest path distance of a vertex
v in stage i as the minimum of the sum of its
weight w(v,u) and the shortest path distance d(u) of all vertices u in stage i+1 that are reachable from v.
After we have processed all stages and all vertices, the final shortest path distance d(s) will contain the
shortest path from s to t.
Time Complexity: The time complexity of the given code is O(N^2), where N is the number of nodes in the
graph. This is because the code involves two nested loops that iterate over all pairs of nodes in the graph, and
each iteration performs a constant amount of work (i.e., comparing and updating distances). Since the graph
is represented using an adjacency matrix, accessing an element takes constant time. Therefore, the overall time
complexity of the algorithm is O(N^2).
Space Complexity: The space complexity of the given program is O(N), where N is the number of nodes in
the graph. This is because the program uses an array of size N to store the shortest distance from each node to
the destination node N-1.
A greedy algorithm solves problems by making the best choice at each step. Instead of looking at all
possible solutions, it focuses on the option that seems best right now.
Problem structure:
Most of the problems where greedy algorithms work follows these two properties:
1) Greedy Choice Property: - This property states that choosing the best possible option at each step will
lead to the best overall solution. If this is not true, a greedy approach may not work.
2) Optimal Substructure: - This means that you can break the problem down into smaller parts, and solving
these smaller parts by making greedy choices helps solve the overall problem.
1) Can we break the problem into smaller parts? If so and solving those parts helps us solve the main
problem, it probably would be solved using greedy approach. For example - In activity selection problem,
once we have selected an activity then remaining subproblem is to choose those activities that start after the
selected activity.
2) Will choosing the best option at each step lead to the best overall solution? If yes, then a greedy
algorithm could be a good choice. For example - In Dijkstra’s shortest path algorithm, choosing the minimum-
cost edge at each step guarantees the shortest path.
A selection function − Used to choose the best candidate to be added to the solution.
A feasibility function − Used to determine whether a candidate can be used to contribute to the
solution.
A solution function − Used to indicate whether a complete solution has been reached.
1). Greedy algorithm works when the problem has Greedy Choice Property and Optimal Substructure,
Dynamic programming also works when a problem has optimal substructure, but it also
requires Overlapping Subproblems.
2). In greedy algorithm each local decision leads to an optimal solution for the entire problem whereas in
dynamic programming solution to the main problem depends on the overlapping subproblems.
1) Sorting
Job Sequencing:- To maximize profits, we prioritize jobs with higher profits. So we sort them in descending
order based on profit. For each job, we try to schedule it as late as possible within its deadline to leave earlier
slots open for other jobs with closer deadlines.
Disjoint Intervals:- The approach for this problem is exactly similar to previous one, we sort the intervals
based on their start or end times in ascending order. Then, select the first interval and continue adding next
intervals that start after the previous one ends.
Fractional Knapsack:- The basic idea is to calculate the ratio profit/weight for each item and sort the item on
the basis of this ratio. Then take the item with the highest ratio and add them as much as we can (can be the
whole element or a fraction of it).
Kruskal Algorithm:- To find the Minimum Spanning Tree (MST), we prioritize edges with the smallest
weights to minimize the overall cost. We start by sorting all the edges in ascending order based on their
weights. Then, we iteratively add edges to the MST while ensuring that adding an edge does not form a cycle.
Dijkstra Algorithm:- To find the shortest path from a source node to all other nodes in a graph, we prioritize
nodes based on the smallest distance from the source node. We begin by initializing the distances and using
a min-priority queue. In each iteration, we extract the node with the minimum distance from the priority
queue and update the distances of its neighboring nodes. This process continues until all nodes have been
processed, ensuring that we find the shortest paths efficiently.
Connect N ropes:- In this problem, the lengths of the ropes picked first are counted multiple times in the total
cost. Therefore, the strategy is to connect the two smallest ropes at each step and repeat the process for the
remaining ropes. To implement this, we use a min-heap to store all the ropes. In each operation, we extract
the top two elements from the heap, add their lengths, and then insert the sum back into the heap. We continue
this process until only one rope remains.
Huffman Encoding:- To compress data efficiently, we assign shorter codes to more frequent characters and
longer codes to less frequent ones. We start by creating a min-heap that contains all characters and their
frequencies. In each iteration, we extract the two nodes with the smallest frequencies, combine them into a
new node, and insert this new node back into the heap. This process continues until there is only one node left
in the heap.
3) Arbitrary
Minimum Number of Jumps To Reach End:- In this problem we maintain a variable to store maximum
reachable position at within the current jump's range and increment the jump counter when the current jump
range has been traversed. We stop this process when the maximum reachable position at any point is greater
than or equal to the last index value.
We use Greedy Algorithms in our day-to-day life to find minimum number of coins or notes for a given
amount. We fist begin with largest denomination and try to use maximum number of the largest and
then second largest and so on.
Dijkstra's shortest path algorithm: Finds the shortest path between two nodes in a graph.
Kruskal's and Prim's minimum spanning tree algorithm: Finds the minimum spanning tree for a
weighted graph. Minimum Spanning Trees are used in Computer Networks Designs and have many
real-world applications
Huffman coding: Creates an optimal prefix code for a set of symbols based on their frequencies.
Fractional knapsack problem: Determines the most valuable items to carry in a knapsack with a
limited weight capacity.
Activity selection problem: Chooses the maximum number of non-overlapping activities from a set
of activities.
Job Sequencing and Job Scheduling Problems.
Finding close to the optimal solution for NP-Hard problems like TSP. ide range of network design
problems, such as routing, resource allocation, and capacity planning.
Machine learning: Greedy algorithms can be used in machine learning applications, such as feature
selection, clustering, and classification. In feature selection, greedy algorithms are used to select a
subset of features that are most relevant to a given problem. In clustering and classification, greedy
algorithms can be used to optimize the selection of clusters or classes
Image processing: Greedy algorithms can be used to solve a wide range of image processing problems,
such as image compression, denoising, and segmentation. For example, Huffman coding is a greedy
algorithm that can be used to compress digital images by efficiently encoding the most frequent pixels.
Game theory: Greedy algorithms can be used in game theory applications, such as finding the optimal
strategy for games like chess or poker. In these applications, greedy algorithms can be used to identify
the most promising moves or actions at each turn, based on the current state of the game.
The Counting Coins problem is to count to a desired value by choosing the least possible coins and the greedy
approach forces the algorithm to pick the largest possible coin. If we are provided coins of 1, 2, 5 and 10 and
we are asked to count 18 then the greedy procedure will be −
Though, it seems to be working fine, for this count we need to pick only 4 coins. But if we slightly change the
problem then the same approach may not be able to produce the same optimum result.
For the currency system, where we have coins of 1, 7, 10 value, counting coins for value 18 will be absolutely
optimum but for count like 15, it may use more coins than necessary. For example, the greedy approach will
use 10 + 1 + 1 + 1 + 1 + 1, total 6 coins. Whereas the same problem could be solved by using only 3 coins (7
+ 7 + 1)
Hence, we may conclude that the greedy approach picks an immediate optimized solution and may fail where
global optimization is a major concern.
Knapsack Problem Basics - Implementation of Knapsack Problem
Definition:
The Knapsack Problem is a classic optimization problem in computer science and mathematics.
A knapsack (bag) that can carry only a limited maximum weight (W).
Goal: Select items to maximize the total value in the knapsack without exceeding the weight capacity.
Each item can be taken only once or not taken at all. (No fractions allowed.)
You cannot break items: either take the entire item or leave it.
Cannot generally be solved by the greedy method. Needs dynamic programming for an optimal answer.
You can break items into fractions (e.g., take half of a gold bar).
Given a collection of items, each with a value (profit) and weight, and a knapsack with limited capacity, the
goal is to maximize value without exceeding the weight limit.
Imagine you are a thief with a bag (knapsack) that can hold a limited amount of weight, and you must fill it
from a set of items, each having a weight and a profit (value). Your goal? Maximize the total value in the
bag without exceeding its weight limit.
Problem Statement
Given:
o A set of nnn items, each with a profit pip_ipi and weight wiw_iwi.
Constraint: Each item can be chosen at most once (either 0 or 1 times, hence the name).
The greedy idea (select by highest value/weight ratio) does not always lead to the optimal solution, since
you can’t split an item as in fractional knapsack.
A 60 10
B 100 20
C 120 30
Greedy picks A ([Link]), then B ([Link]), then tries to pick C but no space left.
But the optimal solution: pick B and C (20+30=5020+30=5020+30=50). Total profit = 100+120=220100 +
120 = 220100+120=220; better than greedy.
1. Create a table: Rows for items, columns for possible weights up to WWW.
2. Fill the table: For each item and weight, decide whether including it yields a better total value or not.
Given:
Items list:
Item Value Weight
(Profit)
1 60 10
2 100 20
3 120 30
Order:
1. Item 1 (6.0)
2. Item 2 (5.0)
3. Item 3 (4.0)
Take Item 1:
o Weight: 10 (fits!)
o Total value = 0 + 60 = 60
Take Item 2:
o Weight: 20 (fits!)
Final Solution
1 10 60
2 20 100
3 20 (of 30) 80
Key Points to Remember
Greedy choice is optimal for the fractional version, not for 0/1 knapsack.
Summary Table
2. Explore: Proceed with the chosen option and move to the next step.
3. Check: If the current solution is invalid or incomplete, backtrack by undoing the last
decision and try the next alternative.
Backtracking is often implemented using recursion, where the function calls itself to explore
further possibilities.
Applications of Backtracking
1. N-Queens Problem:
o Place N queens on an N×N chessboard such that no two queens threaten each
other.
o Assign colors to vertices of a graph such that no two adjacent vertices share the
same color.
3. Hamiltonian Cycle:
o Find a cycle in a graph that visits each vertex exactly once and returns to the
starting vertex.
5. Sudoku Solver:
o Solve Sudoku puzzles by filling in numbers while ensuring the rules of the game
are followed.
6. Word Search:
o Find a sequence of moves for a knight on a chessboard such that it visits every
square exactly once.
8. String Permutations:
9. Maze Solving:
Given a set[] of non-negative integers and a value sum, the task is to print the subset of the
given set whose sum is equal to the given sum.
Examples:
Subset sum can also be thought of as a special case of the 0–1 Knapsack problem. For each
item, there are two possibilities:
Include the current element in the subset and recur for the remaining elements with
the remaining Sum.
Exclude the current element from the subset and recur for the remaining elements.
Finally, if Sum becomes 0 then print the elements of current subset. The recursion’s base
case would be when no items are left, or the sum becomes negative, then simply return.
Algorithm Explanation
o Include the element in the subset and check if the sum matches the target.
o Exclude the element and move to the next.
Example
target = 9
Output
For the input nums = [3, 34, 4, 12, 5, 2] and target = 9, the output will be:
Complexity analysis:
Time Complexity: O(2n) The above solution may try all subsets of the given set in the
worst case. Therefore, time complexity of the above solution is exponential.
Key Concepts:
Bounding: Compute a lower (or upper) bound on the subproblem's best possible solution.
Pruning: Discard subproblems if their bound is worse than the current best solution.
Applications
1. Combinatorial Optimization:
Traveling Salesman Problem (TSP): Branch and Bound is used to find the shortest possible
route that visits a set of cities and returns to the origin city. It systematically explores all
possible routes while pruning those that exceed the current best solution.
Knapsack Problem: This algorithm helps in determining the most valuable combination of
items to include in a knapsack without exceeding its weight limit. It evaluates different
combinations and eliminates those that do not yield optimal value.
2. Resource Allocation:
The algorithm is effective in solving problems where a set of constraints must be satisfied, such
as scheduling tasks or assigning resources while adhering to specific limitations.
4. Production Planning:
Branch and Bound is applied in production scheduling and planning to optimize the use of
resources and time, ensuring that production goals are met efficiently.
It is used to solve complex network flow issues, where the goal is to find the optimal way to
route flow through a network while minimizing costs or maximizing throughput.
TSP is an NP-hard problem, meaning there is no known efficient solution for large datasets,
but various algorithms can provide exact or approximate solutions.
The algorithm for salesman Travelling problem is important because it represents real-world
problems that many industries face, such as:
Delivery services need to plan routes to drop off packages at multiple locations.
Logistics companies need to find the shortest path to transport goods efficiently.
Solving TSP helps reduce time, fuel costs, and energy, making operations faster and cheaper.
1. Initialization: Set initial best cost (e.g., infinity) and prepare starting node representing an
empty path from the source city.
2. Branching: At each node (partial path), generate branches by extending the path to unvisited
cities.
3. Bounding: Calculate a lower bound on the total route cost achievable from this node (partial
solution).
4. Pruning: If the bound exceeds the current best cost, skip expanding that node.
5. Search: Use best-first or depth-first strategy to explore and update best solution.
6. Termination: Once all viable nodes are explored, return the best-found route.
def tsp(dist):
n = len(dist) # Number of cities
# dp[mask][i] will store the minimum distance to visit all cities in 'mask' and end at city 'i'
dp = [[float('inf')] * n for _ in range(1 << n)]
dp[1][0] = 0 # Start from city 0 (only city 0 visited)
# Find the minimum cost to return to the starting city (0) from any city
min_cost = float('inf')
for u in range(1, n):
min_cost = min(min_cost, dp[(1 << n) - 1][u] + dist[u][0])
return min_cost
Travelling Salesman Problem: Example
Let's say we have a salesperson who needs to visit four popular Indian cities: Mumbai, Delhi,
Bengaluru, and Chennai, and they want to find the shortest route that visits each city exactly
once and returns to the starting city.
Problem: The salesperson starts in Mumbai and must visit Delhi, Bengaluru, and Chennai
exactly once, then return to Mumbai. The goal is to find the shortest route.
Possible Routes: Let’s calculate the total distance for a few possible routes:
In both cases, the total travel distance is the same, and this is the most efficient route for the
salesperson to minimize travel distance.
In this example, we used the salesman travelling problem to determine the shortest route
between four major Indian cities. By calculating different possible routes, we found the shortest
one, which helps reduce travel time and costs. However, real-life problems can be much more
complex.
The Travelling Salesperson Problem is an NP-hard problem, and finding an optimal solution
becomes difficult as the number of cities increases. Various approaches have been developed
to solve TSP, each with different trade-offs between accuracy and efficiency:
1. Brute-Force Approach
This method explores all possible routes (permutations) between cities, calculates the total
distance for each route, and selects the shortest one. It guarantees an optimal solution but is
inefficient.
Disadvantages: Extremely slow for large datasets due to factorial growth in possible
routes.
The travelling salesman problem using dynamic programming breaks the problem into smaller
subproblems. It solves these subproblems and stores the results to avoid redundant calculations.
Time Complexity: O(n² * 2^n).
3. Approximation Algorithms
These algorithms do not guarantee the optimal solution but provide near-optimal solutions
more quickly than exact approaches.
Starts at a random city and repeatedly visits the nearest unvisited city.
Disadvantages: The solution may not be very close to the optimal route.
Greedy Algorithm:
Selects the shortest available edge between cities at each step without forming a cycle.
Christofides Algorithm:
Finds a solution that is at most 1.5 times the optimal for metric TSP (where the triangle
inequality holds).