Dynamicprogrammingkk
Dynamicprogrammingkk
net/publication/387306391
CITATIONS READS
0 56
1 author:
Koffka Khan
University of the West Indies, St. Augustine
140 PUBLICATIONS 619 CITATIONS
SEE PROFILE
All content following this page was uploaded by Koffka Khan on 22 December 2024.
Dynamic Programming
Dynamic programming has a wide range of applications, from solving complex optimization
problems in operations research and economics to computer science and artificial intelligence.
Some well-known examples of dynamic programming algorithms include the Fibonacci
sequence, the Knapsack problem, and the shortest path problem in graphs.
To apply dynamic programming, one typically needs to identify the optimal substructure of the
problem and determine how to efficiently compute the solutions to the subproblems. Dynamic
programming algorithms can be categorized as either top-down (memoization) or bottom-up
(tabulation) approaches, depending on the order in which subproblems are solved.
Overall, dynamic programming is a powerful technique that has revolutionized the way we solve
optimization problems in various fields, and continues to be an active area of research and
development.
This note focuses on the Dynamic Programming and various algorithmic approaches integrated
with it.
Contents
Chapter 1: Introduction to Dynamic Programming ........................................................................................ 5
Chapter 2: Tabular ........................................................................................................................................... 13
Chapter 3: Memoization .................................................................................................................................. 41
Chapter 4: Bottom-up ...................................................................................................................................... 67
Chapter 5: Top-down ..................................................................................................................................... 102
Chapter 6: Divide-and-conquer .................................................................................................................... 136
Chapter 7: Multistage .................................................................................................................................... 170
Chapter 8: Convex .......................................................................................................................................... 215
Chapter 9: Parallel ......................................................................................................................................... 252
Chapter 10: Online ......................................................................................................................................... 305
Chapter 11: Stochastic ................................................................................................................................... 344
Chapter 12: MCQ ............................................................................................................................................ 384
Chapter 13: Short Answer Questions ........................................................................................................... 477
Dynamic Programming
This note is an excellent resource for learning the Dynamic Programming. It contains a lot of
theoretical knowledge and practical examples. It covers Tabular, Memoization, Bottom-up, Top-
down, Divide-and-conquer, Multistage, Convex, Parallel, Online, and Stochastic approaches. Each
topic is placed in a study-centric format with introductions, implementations and practical
examples.
.
Chapter 1: Introduction to Dynamic Programming
Dynamic Programming is typically used to solve optimization problems where the goal is to
find the best solution among all possible solutions. It is particularly useful for problems where
the same subproblem appears multiple times and can be solved using the same solution.
The core idea of Dynamic Programming is to solve each subproblem only once and store the
solution in a table. This table can be used to avoid redundant computations and speed up the
overall solution process.
Dynamic Programming is used in many areas of computer science and mathematics, including
algorithms, optimization, artificial intelligence, and machine learning. It is a powerful
technique that can help solve some of the most complex problems in these fields.
Dynamic Programming can be used to solve a wide range of optimization problems, including
the following:
Knapsack Problem: The Knapsack Problem involves selecting items to include in a knapsack
while maximizing the total value of the items within a given weight limit.
Traveling Salesman Problem (TSP): The TSP involves finding the shortest possible route that
visits a set of cities and returns to the starting city.
Shortest Path Problem: The Shortest Path Problem involves finding the shortest path between
two nodes in a graph.
Longest Common Subsequence Problem: The Longest Common Subsequence Problem involves
finding the longest subsequence that is common to two or more sequences.
Sequence Alignment Problem: The Sequence Alignment Problem involves finding the optimal
alignment of two or more sequences.
Optimal Binary Search Tree Problem: The Optimal Binary Search Tree Problem involves
finding the optimal binary search tree for a set of keys.
Maximum Subarray Problem: The Maximum Subarray Problem involves finding the contiguous
subarray with the largest sum.
Coin Change Problem: The Coin Change Problem involves finding the minimum number of
coins needed to make a given amount of change.
In all of these optimization problems, Dynamic Programming is used to break down the
problem into smaller subproblems and solve each subproblem only once. The solutions to
these subproblems are stored in a table, which can be used to speed up the overall solution
process by avoiding redundant computations. By using Dynamic Programming to solve these
optimization problems, we can find the optimal solution in a more efficient and systematic
way.
Resource Allocation: In many real-world scenarios, resources such as time, money, and people
need to be allocated optimally to achieve the desired objectives. Dynamic Programming can be
used to solve these problems by breaking down the allocation process into smaller
subproblems and optimizing the allocation of resources based on the objectives and
constraints.
Routing and Scheduling: In transportation and logistics industries, routing and scheduling
problems involve determining the optimal route and schedule for vehicles to minimize the
travel distance or time while meeting the delivery deadlines. Dynamic Programming can be
used to optimize the routing and scheduling by breaking down the problem into smaller
subproblems and optimizing the routes and schedules based on the objectives and constraints.
Memoization: Similar to tabular dynamic programming, but the table is constructed on an as-
needed basis instead of precomputed.
Bottom-up: A tabular approach that solves subproblems in a bottom-up manner, from the
simplest to the most complex.
Top-down: A memoization approach that solves subproblems recursively from the most
complex to the simplest.
Multistage: A variant of dynamic programming used to solve problems that can be divided into
multiple stages or phases.
Convex: A variant used to solve problems that involve a convex objective function and convex
constraints.
Online: A variant used to solve problems where the decision-making process is sequential and
decisions must be made in real-time.
Reinforcement learning: A variant used to solve problems where the optimal policy is not
known a priori and must be learned through trial-and-error.
Stochastic: A variant used to solve problems that involve uncertainty or randomness in the
decision-making process.
Each of these variants has its own strengths and weaknesses, and the choice of variant depends
on the nature of the problem being solved and the available resources for computation.
Define the problem: The first step in Dynamic Programming is to define the problem in
mathematical terms. This involves specifying the decision variables, constraints, and objective
function.
Break down the problem: The next step is to break down the problem into smaller
subproblems. This involves identifying the subproblems that can be solved independently and
combining their solutions to obtain the solution to the larger problem.
Formulate the recurrence relation: For each subproblem, a recurrence relation is formulated
that relates the solution to the subproblem with the solutions to smaller subproblems. This
recurrence relation is typically expressed in terms of the decision variables and can be thought
of as a mathematical equation.
Solve the subproblems: The subproblems are solved in a bottom-up manner, starting with the
smallest subproblems and working up to the larger subproblems. The solutions to the
subproblems are stored in a table, which can be used to avoid redundant computations and
speed up the overall solution process.
Construct the solution: The solution to the original problem is constructed by combining the
solutions to the smaller subproblems, as specified by the recurrence relation.
Analyze the solution: The final step is to analyze the solution and check that it satisfies the
constraints and optimizes the objective function.
Inputs:
- n: the size of the problem
- c[1...n]: an array of costs or values
- w[1...n]: an array of weights or capacities
- W: the total capacity or weight limit
Outputs:
- v: the optimal value
- x[1...n]: an array of binary variables indicating whether an item is selected or not
In this algorithm, we assume that we are solving a 0/1 Knapsack Problem, where we have a set
of items with corresponding weights and values, and a knapsack with a weight limit. The goal is
to select a subset of items to maximize the total value while staying within the weight limit.
The algorithm starts by initializing an array V to zero, where V[i, j] represents the maximum
value that can be obtained by selecting items from the first i items, with a weight limit of j. It
then iterates over each item i and each possible weight j, and computes the maximum value
that can be obtained by either not selecting item i or selecting item i and reducing the weight
limit by w[i]. This is done using the recurrence relation:
Once the array V is computed, the optimal value v is set to V[n, W], which represents the
maximum value that can be obtained using all n items and a weight limit of W.
Finally, the algorithm constructs the optimal solution by working backwards from x[n] to x[1],
and checking whether each item was selected or not based on the values of V and the weights
of the items. The binary variable x[i] is set to 1 if item i was selected, and 0 otherwise.
The future of Dynamic Programming is bright, as it remains a powerful and widely applicable
optimization technique that has seen many successful applications in various fields. Here are
some potential directions for the future of Dynamic Programming:
Applications in machine learning and artificial intelligence: Dynamic Programming has already
seen some success in reinforcement learning and other forms of machine learning. As machine
learning and artificial intelligence continue to grow and become more prevalent, it is likely that
Dynamic Programming will play an important role in solving optimization problems that arise
in these domains.
Development of new variants and algorithms: There is always room for improvement in the
existing algorithms and variants of Dynamic Programming. The development of new
algorithms and variants can lead to better solutions and more efficient computations.
Integration with other optimization techniques: Dynamic Programming can be combined with
other optimization techniques, such as linear programming, integer programming, and
constraint programming, to solve complex optimization problems that cannot be solved by a
single technique alone.
Expanding the scope of applications: Dynamic Programming has been successfully applied in
various fields, such as operations research, finance, engineering, and biology. As new fields
emerge and existing fields evolve, there will likely be new opportunities for applying Dynamic
Programming to solve optimization problems.
Integration with big data and data science: With the explosion of big data and data science,
Dynamic Programming can be used to analyze large and complex datasets, and to extract
insights and make predictions. For example, Dynamic Programming can be used for portfolio
optimization, resource allocation, and other data-driven decision-making problems.
Integration with real-time systems: Dynamic Programming can be integrated with real-time
systems, such as autonomous vehicles and robotics, to make decisions on-the-fly and adapt to
changing environments. This can improve the performance and safety of these systems.
Expansion of theoretical foundations: There is always room for expanding the theoretical
foundations of Dynamic Programming, such as developing new convergence proofs, exploring
new computational models, and investigating the limits of its applicability.
Overall, the future of Dynamic Programming is diverse and exciting. Its potential applications
and improvements are vast, and will likely continue to emerge as new challenges and
opportunities arise in various domains.
Chapter 2: Tabular
Tabular dynamic programming (TDP) is a type of dynamic programming that solves problems
by breaking them down into smaller subproblems and recursively solving them. In TDP, the
solutions to subproblems are stored in a table (or matrix), allowing for efficient computation of
the optimal solution to the overall problem.
TDP is particularly useful for solving optimization problems in which the decision-making
process can be modeled as a sequential process, with decisions made at each stage affecting the
outcome of the problem. The method is commonly used in operations research, economics, and
computer science to solve a wide range of problems, including scheduling, resource allocation,
and inventory management.
One common application of TDP is the Bellman-Ford algorithm for finding the shortest path in
a graph with negative edge weights. Another example is the Knapsack problem, in which a set
of items with different values and weights must be packed into a knapsack of limited capacity
to maximize the total value.
TDP can be contrasted with other forms of dynamic programming, such as memoization
(which stores the results of subproblems in a cache) and recursive dynamic programming
(which solves subproblems through recursive function calls).
Let X be a set of possible problem instances, and let f: X -> R be a function that assigns a cost (or
value) to each instance x in X. Let OPT(x) denote the optimal cost (or value) of instance x. TDP
seeks to compute OPT(x) for all x in X by recursively solving smaller subproblems and storing
their solutions in a table.
// Otherwise, compute the optimal solution for x using the solutions of smaller subproblems
optimal_value = infinity
for subproblem in get_subproblems(x):
subproblem_value = f(x, subproblem) + OPT(subproblem, T, f)
optimal_value = min(optimal_value, subproblem_value)
In this algorithm, X is the set of possible problem instances, f is a function that assigns a cost
(or value) to each instance, T is the table of optimal solutions, and OPT is a recursive function
that computes the optimal solution for a given instance by recursively solving smaller
subproblems. The is_base_case function checks whether an instance is a base case that can be
solved directly, and base_case_solution computes the optimal solution for a base case. The
get_subproblems function returns a list of subproblems for a given instance.
Tabular dynamic programming (TDP) is a powerful technique that can be used to solve a wide
range of optimization problems. Here are some examples of problems that can be solved by
TDP:
Knapsack problem: Given a set of items with different weights and values, and a knapsack of
limited capacity, find the subset of items that maximizes the total value while staying within
the capacity of the knapsack.
Shortest path problem: Given a weighted graph, find the shortest path between two vertices.
Traveling salesman problem: Given a set of cities and the distances between them, find the
shortest possible route that visits each city exactly once and returns to the starting city.
Sequence alignment problem: Given two sequences of DNA or protein, find the optimal
alignment between them, i.e., the arrangement of gaps and matches that maximizes their
similarity.
Optimal control problem: Given a dynamical system and a cost function, find the optimal
control policy that minimizes the expected cost of the system over a finite or infinite time
horizon.
Resource allocation problem: Given a set of resources and a set of tasks that require different
amounts of resources, find the allocation of resources to tasks that maximizes some objective
function, such as the total profit or efficiency.
These are just a few examples of the many problems that can be solved by TDP. In general, TDP
is applicable to any problem that can be decomposed into smaller subproblems that exhibit
optimal substructure and overlapping subproblems.
Knapsack Problem
Here's an example program that uses Tabular Dynamic Programming to solve the Knapsack
Problem:
return table[n][capacity]
values = [60, 100, 120]
weights = [10, 20, 30]
capacity = 50
The function then iterates over the items and knapsack capacities, computing the maximum
value that can be obtained either by excluding the current item (table[i-1][j]) or by including it
(values[i-1] + table[i-1][j-weights[i-1]]), depending on whether the weight of the current item
is than or equal to the remaining capacity of the knapsack. The maximum value is stored in
the table at position (i,j).
Finally, the function returns the maximum value that can be obtained using all the items and a
knapsack with capacity capacity. In this example, the maximum value that can be obtained is
220, which corresponds to selecting the first and third items.
Let's assume we have a list of cities and their distances. We want to find the shortest possible
route that starts and ends in a specific city, visiting each city exactly once.
# Base case: if there are no more remaining cities to visit, return the distance from the current
city to the starting city
for city in remaining_cities:
dp_table[(city, ())] = cities[city][start_city]
# Add the distance from the last city to the starting city
path.append(start_city)
distance = sum(cities[path[i]][path[i+1]] for i in range(len(path)-1))
In the above code, we first define the cities and their distances as a dictionary. We then set the
starting city and the remaining cities to visit. We initialize the DP table with the base case,
where there are no more remaining cities to visit. Then, we build the DP table using nested
loops and calculate the minimum distance for each city and subset of remaining cities. Finally,
we find the shortest path that visits all cities by iterating over the remaining cities and
selecting the next city with the minimum distance. We add the starting city to the end of the
path to complete the cycle, and we calculate the total distance. The output of the program is the
shortest path and its distance.
Let's assume we have a weighted directed graph represented as an adjacency matrix. We want
to find the shortest path between two nodes in the graph.
# Weighted directed graph represented as an adjacency matrix
graph = [
[0, 2, 4, 0, 0, 0],
[0, 0, 1, 7, 0, 0],
[0, 0, 0, 0, 3, 0],
[0, 0, 0, 0, 0, 1],
[0, 0, 0, 2, 0, 5],
[0, 0, 0, 0, 0, 0]
]
In the above code, we first define the weighted directed graph as an adjacency matrix. We then
set the source node and the destination node. We initialize the DP table with all nodes set to
infinity except the source node, which is set to zero. Then, we build the DP table using nested
loops and calculate the minimum distance for each node. Finally, we find the shortest path by
iterating over the graph backwards from the destination node to the source node and selecting
the nodes that lead to the minimum distance. We calculate the total distance of the shortest
path as the value of the destination node in the DP table. The output of the program is the
shortest path and its distance.
Let's assume we have two sequences represented as strings. We want to find the longest
common subsequence between these two strings.
# Input strings
string1 = "AGGTAB"
string2 = "GXTXAYB"
# Length of the input strings
m = len(string1)
n = len(string2)
Let's assume we have two input sequences represented as strings. We want to find the optimal
alignment between these two strings.
# Input strings
string1 = "ATCGATT"
string2 = "ACGTAGC"
# Gap penalty
gap_penalty = -2
# Match/mismatch score
match_score = 1
mismatch_score = -1
# Initialize the DP table
dp_table = [[0] * (n+1) for _ in range(m+1)]
print("Optimal alignment:")
print(alignment1)
print(alignment2)
In the above code, we first define two input sequences. We then define the gap penalty, match
score, and mismatch score. We initialize the DP table with dimensions (m+1) x (n+1), where m
and n are the lengths of the input strings. We fill in the first row and column of the DP table
with gap penalties. We build the DP table using nested loops and fill in the values based on the
following conditions: if the characters in the input strings match, we add the match score to the
value in the top-left cell of the DP table; if they do not match, we add the mismatch score; if we
delete a character in one of the input strings, we add the gap penalty to the value in the cell
above; if we insert a character in one of the input strings, we add the gap penalty to the value in
the cell on the left. We then use the DP table to find the optimal alignment by starting at the
bottom-right cell and working our way backwards to the top-left cell. At each step, we choose
the direction with the highest score and add the corresponding characters to the alignment
strings.
Finally, we print out the optimal alignment. The output of the above code for the input strings
"ATCGATT" and "ACGTAGC" would be:
Optimal alignment:
ATCG-ATT
ACGTAG-C
This represents the optimal alignment between the two input strings with a match score of 1, a
mismatch score of -1, and a gap penalty of -2.
Let's assume we have a set of keys and their probabilities. We want to find the minimum
average search time for these keys in a binary search tree.
Finally, we print out the minimum average search time. The output of the above code for the
input keys [10, 20, 30] with probabilities [0.2, 0.3, 0.5] would be:
Maximum sum: 6
This represents the maximum sum of the contiguous subarray within the input array. In this
case, the maximum subarray is [4, -1, 2, 1] with a sum of 6.
Let's assume we have a set of coins and we want to find the number of ways to make change
for a given amount of money.
Finally, we print out the number of ways to make change. The output of the above code for the
input coins [1, 2, 5] and the target amount 11 would be:
Number of ways: 3
This represents the number of ways to make change for 11 using the coins [1, 2, 5]. In this case,
there are three ways to make change: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 2, 2], and
[1, 1, 1, 1, 1, 2, 2, 2].
Resource Allocation
The Resource Allocation problem is a classic problem in operations research and is used to
determine the optimal allocation of limited resources to various tasks or projects. One
approach to solve the Resource Allocation problem is using dynamic programming. Here is an
example scenario with program code for Tabular Dynamic Programming used to solve the
Resource Allocation problem:
Let's consider a scenario where a company has limited resources, such as employees and
machines, and multiple projects that need to be completed. Each project requires a certain
number of employees and machines, and yields a certain profit upon completion. The goal is to
allocate the resources in a way that maximizes the total profit of all the completed projects.
Finally, we print out the maximum profit. The output of the above code for the input data in the
code would be:
Maximum Profit: 35
This represents the maximum profit that can be obtained by allocating the resources to the
projects. In this case, the optimal allocation is to allocate 2 employees and 1 machine to the
first project, 1 employee and 2 machines to the second project, and 2 employees and 2
machines to the fourth project, resulting in a total profit of 35. This is the solution obtained by
using Tabular Dynamic Programming to solve the Resource Allocation problem in this
scenario.
In this scenario, the Resource Allocation problem is just one example of how Tabular Dynamic
Programming can be used to solve real-world problems. This approach can be applied to other
scenarios as well, such as scheduling, inventory management, and more. The key is to define
the problem in terms of subproblems that can be solved using DP and to find the optimal
solution by combining the solutions to these subproblems.
Inventory Management
Inventory Management is a classic problem in operations research that deals with finding the
optimal level of inventory to maintain for a product. This involves balancing the costs of
holding inventory and the costs of stockouts, or not having enough inventory to meet demand.
One approach to solve the Inventory Management problem is using dynamic programming.
Here is an example scenario with program code for Tabular Dynamic Programming used to
solve the Inventory Management problem:
Let's consider a scenario where a company sells a single product, and faces a known demand
for each time period. The company has a fixed ordering cost and a known holding cost per unit
of inventory. The goal is to determine the optimal order quantity for each time period to
minimize the total cost of holding inventory and stockouts.
Finally, we print out the minimum cost. The output of the above code for the input data in the
code would be:
In this scenario, the Inventory Management problem is just one example of how Tabular
Dynamic Programming can be used to solve real-world problems. This approach can be applied
to other scenarios as well, such as production planning, capacity planning, and more. The key is
to define the problem in terms of subproblems that can be solved using DP and to find the
optimal solution by combining the solutions to these subproblems.
Production Planning
Production Planning is an essential problem in manufacturing that involves determining the
optimal production schedule for a set of products while minimizing costs and satisfying
customer demand. The problem can be solved using dynamic programming. Here is an
example scenario with program code for Tabular Dynamic Programming used to solve the
Production Planning problem:
Let's consider a scenario where a company produces three products, and each product has a
known demand for each time period. The company has a fixed production cost per unit of
product and a known inventory holding cost per unit of product. The goal is to determine the
optimal production quantity for each product for each time period to minimize the total cost of
production and inventory holding.
In the above code, we first input the data for the problem, including the number of periods, the
products, the demand for each product for each period, the production cost per unit of each
product, and the inventory holding cost per unit of each product. We then initialize the DP
table with length len(products) for the product dimension, num_periods for the time
dimension, and max(demand) + 1 for the inventory dimension for each product for each time
period. We fill in the values of the DP table using a loop and fill in the values based on the
following conditions: for each time period t, and each inventory level i, j, k of the three
products, we compute the cost of producing i units of product P1, j units of product P2, and k
units of product P3, plus the inventory holding cost for each product. We then add the cost of
producing the remaining demand for each product in the next time period, using the values
from the previous time period. We do this for all possible inventory levels and update the DP
table accordingly.
Finally, we compute the minimum cost by finding the minimum value in the last cell of the DP
table, which represents the optimal production plan that minimizes the total cost.
Note that this is just one example scenario for Production Planning, and the implementation
details may differ depending on the specific problem instance.
Portfolio Optimization
Portfolio optimization is a classic problem in finance, where the goal is to find the optimal
allocation of assets in a portfolio that maximizes return while minimizing risk. Here's an
example scenario of portfolio optimization using Tabular dynamic programming in :
Suppose we have a set of n assets, each with a given expected return and volatility (i.e.,
standard deviation of returns). We want to find the portfolio that maximizes expected return
subject to a given level of risk. We can model this as a stochastic optimization problem, where
the objective function is the expected return of the portfolio, and the constraints are the risk
level and the asset allocation.
To solve this problem using Tabular dynamic programming, we can define a DP table with one
dimension for the asset index and another dimension for the risk level. We can compute the
optimal expected return for each asset and each risk level by considering two cases:
The asset is included in the portfolio: In this case, we add the expected return of the asset to
the expected return of the optimal portfolio for the remaining assets and the remaining risk
budget. We also subtract the risk contribution of the asset from the remaining risk budget.
The asset is excluded from the portfolio: In this case, we compute the optimal portfolio for the
remaining assets and the same risk budget.
We can then compute the optimal expected return for each risk level by iterating over all assets
and risk levels, using the values from the previous time period to compute the values for the
current time period.
Here's the code to solve this problem using Tabular dynamic programming:
import numpy as np
# Define the asset data
n_assets = 3
exp_returns = np.array([0.1, 0.2, 0.15])
volatilities = np.array([0.15, 0.2, 0.1])
# Compute the optimal expected return for each asset and each risk level
for i in range(1, n_assets + 1):
for j in range(int(volatilities[i - 1] * 100), int(risk_budget * 100) + 1):
# Case 1: Include the asset in the portfolio
expected_return_include = exp_returns[i - 1] + dp_table[i - 1, j - int(volatilities[i - 1] * 100)]
if j - int(volatilities[i - 1] * 100) >= 0:
expected_return_include -= allocation[i - 1] * volatilities[i - 1] ** 2
dp_table[i, j] = max(dp_table[i, j], expected_return_include)
Suppose we have a delivery company that needs to deliver packages to three different
locations (A, B, and C) using two available trucks. Each truck can carry a maximum of two
packages, and the company wants to minimize the total time it takes to deliver all the packages.
The delivery times from each package location to each destination are given in the following
table:
A B C
P1 10 7 6
P2 3 6 8
P3 4 5 11
We can use dynamic programming to solve this problem by defining the state space, value
function, and recursion formula as follows:
State space:
Define V(s) as the minimum time it takes to deliver all the remaining packages starting from
state s
Recursion formula:
V(s) = min{ V(s') + time(s, s') }, where s' is any state that can be reached from s by delivering a
package from one of the trucks
Here's the code to implement this solution:
import numpy as np
We then define the value function V as a dictionary, where the keys are the states and the
values are the minimum time it takes to deliver all the remaining packages from that state. We
initialize the value of V to infinity for all states except the state where both trucks are at the
starting location.
Finally, we use the recursion formula to update the value of V for each state by iterating over
all possible states that can be reached by delivering a package from one of the trucks. We use
the numpy array indexing to extract the delivery time for the current state and the reachable
state, and take the minimum over all reachable states.
Once we have computed the optimal value for each state, we can simply print out the value for
the starting state where both trucks are at the starting location (0, 0, 0, 0, 0, 0), which gives us
the minimum time it takes to deliver all packages.
Note that this is just one possible implementation of dynamic programming for this problem,
and there may be other ways to define the state space or the recursion formula. However, this
code should give you an idea of how to use dynamic programming to solve a routing and
scheduling problem in .
Chapter 3: Memoization
When a function is called with a set of inputs, the memoization technique checks if the function
has already been called with the same inputs before. If the result has already been computed,
the cached result is returned immediately. If not, the function is executed as normal, and the
result is cached for future use.
Memoization can be used to speed up a wide variety of algorithms, especially those that involve
computing the same results multiple times. However, it is important to note that memoization
only works for functions that are deterministic and have no side effects.
where f_memo(x) is the memoized version of f(x), which checks if x has been previously
computed and stored in memo. If it has, then the stored result is returned. Otherwise, the
function f is called to compute the result, which is then stored in memo for future use. The
memoization technique can be applied recursively to subproblems in dynamic programming
algorithms, which helps to avoid redundant computations and reduce the time complexity of
the algorithm.
Here's an algorithmic description with pseudocode for memoization using dynamic
programming:
Initialize a memoization table with default values (usually null or -1) for all possible inputs to
the function.
Define a function f(x) that takes input x.
Within the function f(x), check if the memoization table already contains a value for input x. If it
does, return the stored value from the memoization table.
If the memoization table does not contain a value for input x, compute the value using the
original recursive function definition.
Store the computed value in the memoization table at the position corresponding to input x.
Return the computed value.
Here's the pseudocode for a memoized Fibonacci sequence function:
function fib(n):
memo = array of size n+1 initialized with default value null
In this example, the memoization table stores the computed values for the Fibonacci sequence
function. If the memoization table already contains a value for a given input n, the stored value
is returned. Otherwise, the function computes the value using the original recursive function
definition and stores it in the memoization table.
Memoization dynamic programming can be used to solve a wide range of optimization
problems, particularly those that have overlapping subproblems and optimal substructure.
Here are some examples of problems that can be solved using memoization dynamic
programming:
Fibonacci sequence: Computing the nth Fibonacci number using the recursive definition can be
a slow process as the algorithm involves computing the same subproblems multiple times.
Memoization dynamic programming can be used to optimize the algorithm by storing the
results of previous subproblems in a table and reusing them for future computations.
Shortest path problem: Given a weighted graph and two vertices, find the shortest path
between the vertices. This problem can be solved using dynamic programming by breaking it
down into subproblems and storing the results of each subproblem in a table. The shortest
path between two vertices can be computed by combining the shortest paths between the
vertices and their neighboring vertices.
Longest common subsequence: Given two strings, find the longest sequence of characters that
occur in both strings in the same order. This problem can be solved using dynamic
programming by breaking it down into smaller subproblems and storing the results of each
subproblem in a table. The longest common subsequence can be computed by combining the
longest common subsequences of smaller substrings.
Knapsack problem: Given a set of items with values and weights, determine the maximum
value that can be obtained by selecting a subset of the items that fit within a given weight limit.
This problem can be solved using dynamic programming by breaking it down into
subproblems and storing the results of each subproblem in a table. The optimal subset of items
can be computed by combining the optimal subsets of smaller sets of items.
Overall, memoization dynamic programming can be used to solve many optimization problems
that involve breaking down a larger problem into smaller subproblems and combining the
solutions of these subproblems to find the optimal solution.
Here's an example of using memoization dynamic programming to solve the Knapsack Problem
in :
In this example, the function knapsack_memoization takes three arguments: values, a list of the
values of the items, weights, a list of the weights of the items, and capacity, the maximum
weight the knapsack can hold.
The function initializes a memoization table with default value -1 for all possible combinations
of items and knapsack weights. It then defines a nested function memoized_max_value that
computes the maximum value that can be obtained by selecting a subset of the first i items that
fit within a knapsack of weight w. If the value has already been computed before, it returns the
stored value from the memoization table. Otherwise, it computes the maximum value by
recursively computing the maximum value of the first i-1 items and the maximum value of the
first i-1 items with the i-th item added, if it can fit within the knapsack. It stores the computed
value in the memoization table and returns it.
Finally, the function returns the maximum value that can be obtained by selecting a subset of
the items that fit within the knapsack of capacity capacity, computed using the memoized
function.
In this example, the knapsack can hold a maximum weight of 50, and there are three items with
values 60, 100, and 120, and weights 10, 20, and 30, respectively. The maximum value that can
be obtained by selecting a subset of the items that fit within the knapsack is 220, which can be
achieved by selecting the second and third items.
import numpy as np
def tsp_memoization(distances):
n = distances.shape[0]
memo = np.full((n, 2**n), -1)
return memoized_shortest_path(0, 1)
In this example, the function tsp_memoization takes a square distance matrix distances as
input, where distances[i][j] is the distance between city i and city j. The function computes the
shortest possible path that visits all cities exactly once and returns to the starting city.
The function initializes a memoization table with default value -1 for all possible combinations
of cities and visited sets. It then defines a nested function memoized_shortest_path that
computes the shortest path that starts from city i and visits all cities in the visited set,
represented as a bitset where the j-th bit is set to 1 if city j has been visited. If the shortest path
has already been computed before, it returns the stored value from the memoization table.
Otherwise, it computes the shortest path by recursively computing the shortest path to each
unvisited city and adding the distance between the current city and the next city, and selecting
the minimum path. It stores the computed value in the memoization table and returns it.
Finally, the function returns the shortest possible path that visits all cities exactly once and
returns to the starting city, computed using the memoized function.
shortest_path = tsp_memoization(distances)
print("Shortest possible path:", shortest_path)
Output:
In this example, there are four cities and the distance between each pair of cities is given in the
distances matrix. The shortest possible path that visits all cities exactly once and returns to the
starting city has a total distance of 80, which can be achieved by visiting the cities in the order
0, 1, 3, 2, and returning to city 0.
Here's a scenario with program code for memoization dynamic programming used to solve
the Sequence Alignment Problem:
Let's say you have two strings s1 and s2, and you want to find the minimum edit distance (i.e.,
the minimum number of insertions, deletions, and substitutions required to transform s1 into
s2) using dynamic programming with memoization.
# Recursive case: Find the minimum edit distance by considering three cases
if s1[i] == s2[j]:
# If the current characters match, there is no cost
cost = 0
else:
# If the current characters don't match, there is a substitution cost of 1
cost = 1
# Call the function to find the minimum edit distance between s1 and s2
s1 = "kitten"
s2 = "sitting"
min_edit_distance = align(s1, s2, 0, 0)
In this program, we first define the memoization table as an empty dictionary. We then define
the recursive function align which takes four arguments s1, s2, i, and j. s1 and s2 are the input
strings we want to align, and i and j are the indices we are currently looking at in the two
strings. The function first checks if we have already computed the edit distance for this
subproblem by checking if the tuple (i, j) is in the memoization table. If it is, we return the
memoized value. If we have reached the end of one or both strings (i.e., i or j is equal to the
length of the corresponding string), the edit distance is the length of the remaining string.
Otherwise, we consider three cases: if the current characters in s1 and s2 match, there is no
cost. If they don't match, there is a substitution cost of 1. We then try inserting a character into
s1, deleting a character from s1, and substituting a character in s1, and find the minimum cost
of the three cases. Finally, we memoize the result by adding it to the memoization table and
returning it.
In the main program, we define the two input strings s1 and s2. We then call the align function
with the initial indices i=0 and j=0 to find the minimum edit distance between the two strings.
We print the result using an f-string.
Note that this program uses memoization to avoid recomputing the same subproblems
multiple times, which significantly improves the time complexity of the algorithm. The time
complexity of this program is O(mn), where m and n are the lengths of s1 and s2, respectively.
The space complexity is also O(mn) due to the memoization table.
Here's a scenario with program code for memoization dynamic programming used to solve
the Optimal Binary Search Tree Problem:
Let's say you have a list of keys keys and their corresponding probabilities probs, and you want
to find the minimum average search time of an optimal binary search tree using dynamic
programming with memoization.
# Base cases: If the subproblem is empty, the minimum average search time is 0
if j < i:
return 0
# Recursive case: Find the minimum average search time by considering all possible roots
min_cost = float('inf')
for r in range(i, j+1):
# Compute the cost of the subtree rooted at r
cost = probs[r] + optimal_bst(keys, probs, i, r-1) + optimal_bst(keys, probs, r+1, j)
# Update the minimum cost
min_cost = min(min_cost, cost)
# Call the function to find the minimum average search time of an optimal binary search tree
keys = [1, 2, 3, 4, 5]
probs = [0.2, 0.1, 0.15, 0.05, 0.3]
min_avg_search_time = optimal_bst(keys, probs, 0, len(keys)-1)
In the main program, we define the lists of keys keys and their corresponding probabilities
probs. We then call the optimal_bst function with the initial indices i=0 and j=len(keys)-1 to
find the minimum average search time of an optimal binary search tree. We print the result
using an f-string.
Note that this program uses memoization to avoid recomputing the same subproblems
multiple times, which significantly improves the time complexity of the algorithm. The time
complexity of this program is O(n^3), where n is the length of the keys and probs lists. The
space complexity is also O(n^2) due to the memoization table.
Here's a scenario with program code for memoization dynamic programming used to solve
the Maximum Subarray Problem:
Let's say you have a list of numbers nums, and you want to find the maximum sum of a
contiguous subarray using dynamic programming with memoization.
# Base case: If the subproblem has only one element, the maximum sum is the element itself
if i == j:
return nums[i]
# Recursive case: Find the maximum sum by dividing the subproblem into two subarrays
and combining the results
mid = (i+j)//2
left_sum = max_subarray(nums, i, mid)
right_sum = max_subarray(nums, mid+1, j)
cross_sum = max_crossing_subarray(nums, i, mid, j)
max_sum = max(left_sum, right_sum, cross_sum)
# Define a helper function to find the maximum sum of a subarray that crosses the midpoint
def max_crossing_subarray(nums, i, mid, j):
# Find the maximum sum of a subarray that ends at the midpoint
left_sum = float('-inf')
current_sum = 0
for k in range(mid, i-1, -1):
current_sum += nums[k]
left_sum = max(left_sum, current_sum)
In this program, we first define the memoization table as an empty dictionary. We then define
the recursive function max_subarray which takes three arguments nums, i, and j. nums is the
list of numbers, and i and j are the indices of the subproblem we are currently looking at. The
function first checks if we have already computed the maximum sum for this subproblem by
checking if the tuple (i, j) is in the memoization table. If it is, we return the memoized value. If
the subproblem has only one element (i.e., i is equal to j), the maximum sum is the element
itself. Otherwise, we divide the subproblem into two subarrays and find the maximum sum of
each subarray recursively. We also find the maximum sum of a subarray that crosses the
midpoint using the max_crossing_subarray function. We then combine the results by taking the
maximum of the left sum, right sum, and cross sum. Finally, we memoize the result by adding it
to the memoization table and returning it.
We then call the max_subarray function with the list of numbers nums and the range 0 to
len(nums)-1 (i.e., the entire list). The function returns the maximum sum of a contiguous
subarray, which we print out.
The time complexity of this program is O(n log n), where n is the length of the nums list,
because we are dividing the problem into two subproblems of half the size in each recursive
call. The space complexity is also O(n log n) due to the memoization table.
The Coin Change Problem is a classic example of dynamic programming. Given a set of coins
and a target amount, the task is to find the minimum number of coins required to make up that
amount.
Here's a program that uses memoization to solve the Coin Change Problem:
def dp(n):
if n in memo:
return memo[n]
if n == 0:
return 0
if n < 0:
return float('inf')
res = float('inf')
for coin in coins:
res = min(res, dp(n - coin) + 1)
memo[n] = res
return res
The coin_change function takes two arguments: coins, which is a list of integers representing
the denominations of the coins, and amount, which is the target amount.
The function creates a memoization dictionary memo to store the minimum number of coins
required for each target amount.
It then defines an inner function dp that takes a target amount n and returns the minimum
number of coins required to make up that amount. If the minimum number of coins for that
amount has already been computed and stored in the memoization table, the function returns
that value.
If the target amount is zero, the function returns zero because no coins are required. If the
target amount is negative, the function returns infinity because it is not possible to make up
that amount using the given coins.
If the target amount is positive, the function loops through all the coins and recursively calls dp
with n-coin as the new target amount. It adds 1 to the result to account for the coin just used.
The function keeps track of the minimum number of coins required to make up the target
amount and stores it in the memoization table.
Finally, the function returns the result of calling dp with the target amount. If the result is
infinity, it means it is not possible to make up the target amount using the given coins, so the
function returns -1 instead.
We can call the coin_change function with a list of coins and a target amount to find the
minimum number of coins required to make up that amount:
In this example, the minimum number of coins required to make up 63 cents is 6 (two quarters,
one dime, and three pennies).
Routing and scheduling problems are a common problem in the real world, especially in
logistics and transportation. One common example is the Vehicle Routing Problem (VRP),
where a fleet of vehicles needs to be routed to serve a set of customers with known demands
while minimizing the total distance traveled.
Here's a program that uses memoization to solve a simplified version of the VRP:
The vrp function takes two arguments: customers, which is a list of customer demands, and
capacity, which is the maximum capacity of each vehicle.
The function creates a memoization dictionary memo to store the minimum number of vehicles
required to serve all customers with demands up to i and remaining capacity j.
It then defines an inner function dp that takes two arguments: i, which is the index of the
current customer, and j, which is the remaining capacity of the current vehicle.
If the minimum number of vehicles required for the current situation has already been
computed and stored in the memoization table, the function returns that value.
If i is equal to the length of the customers list, it means all customers have been served, so the
function returns 0.
If the remaining capacity j is than the demand of the current customer customers[i], the
function cannot serve the customer with the current vehicle, so it skips this customer and
moves on to the next one.
If the remaining capacity j is sufficient to serve the current customer, the function calls itself
recursively twice: once to skip the current customer and move on to the next one, and once to
serve the current customer and deduct its demand from the remaining capacity of the vehicle.
The function returns the minimum of the two results plus 1 to account for the current vehicle
just used.
Finally, the function returns the result of calling dp with initial parameters 0 for i (i.e., the first
customer) and capacity for j (i.e., the maximum capacity of each vehicle).
We can call the vrp function with a list of customer demands and a maximum capacity to find
the minimum number of vehicles required to serve all customers:
customers = [3, 2, 5, 4, 3, 6]
capacity = 10
print(vrp(customers, capacity)) # Output: 3
In this example, three vehicles are required to serve all six customers with demands 3, 2, 5, 4,
3, and 6, respectively, with a maximum capacity of 10 units for each vehicle.
Resource allocation problems are common in the real world, especially in project management
and operations research. One common example is the Project Resource Allocation Problem
(PRAP), where a set of resources needs to be allocated to a set of tasks while minimizing the
total cost or time.
Here's a program that uses memoization to solve a simplified version of the PRAP:
return dp(0, 0)
The prap function takes three arguments: tasks, which is a list of tasks, resources, which is a
list of resources, and allocation, which is a matrix indicating the cost of allocating each resource
to each task.
The function creates a memoization dictionary memo to store the minimum cost of allocating
resources to tasks up to i and j.
It then defines an inner function dp that takes two arguments: i, which is the index of the
current task, and j, which is the index of the current resource.
If the minimum cost of allocating resources to the current situation has already been computed
and stored in the memoization table, the function returns that value.
If i is equal to the length of the tasks list, it means all tasks have been allocated, so the function
returns 0.
If j is equal to the length of the resources list, it means all resources have been considered, but
not all tasks have been allocated, so the function returns infinity.
If both i and j are within their respective ranges, the function calls itself recursively twice: once
to skip the current resource and move on to the next one, and once to allocate the current
resource to the current task and move on to the next ones. The function returns the minimum
of the two results plus the cost of allocating the current resource to the current task.
Finally, the function returns the result of calling dp with initial parameters 0 for both i and j
(i.e., starting from the first task and the first resource).
We can call the prap function with a list of tasks, a list of resources, and a cost matrix to find the
minimum cost of allocating resources to tasks:
Inventory management is the process of efficiently managing the flow of goods in and out of a
business. One common problem in inventory management is determining the optimal amount
of inventory to order to meet demand while minimizing costs. This problem can be solved
using dynamic programming.
Here's a program that uses memoization to solve a simplified version of the inventory
management problem:
The inventory_management function takes five arguments: demand, which is the demand for
the product, order_cost, which is the cost of placing an order, holding_cost, which is the cost of
holding one unit of inventory per unit of time, inventory, which is the current inventory level,
and memo, which is a dictionary to store the results of subproblems.
The function first checks if the current inventory level is than the demand. In that case, it
computes the cost of ordering the required inventory and the cost of holding the excess
inventory until the next order arrives.
If the cost of the current demand and inventory level has not been computed before, the
function computes it by recursively considering all possible order sizes between 1 and the sum
of the demand and inventory levels. It then stores the minimum cost in the memoization
dictionary.
Finally, the function returns the minimum cost for the current demand and inventory level.
We can call the inventory_management function with the demand, order cost, holding cost,
initial inventory, and an empty dictionary to find the minimum cost of managing the inventory:
demand = 10
order_cost = 100
holding_cost = 10
inventory = 5
memo = {}
print(inventory_management(demand, order_cost, holding_cost, inventory, memo)) # Output:
1050
In this example, the optimal order size is 5, and the total cost of managing the inventory is
1050.
Production planning is the process of determining the optimal production schedule to meet the
demand while minimizing costs. One common problem in production planning is determining
the optimal number of units to produce at each stage of production. This problem can be solved
using dynamic programming.
Here's a program that uses memoization to solve a simplified version of the production
planning problem:
The production_planning function takes six arguments: demand, which is the demand for the
product, setup_cost, which is the cost of setting up the production process at each stage,
production_cost, which is the cost of producing one unit of the product, inventory_cost, which
is the cost of holding one unit of inventory per unit of time, stages, which is the number of
production stages, and units, which is the number of units produced at the current stage, and
memo, which is a dictionary to store the results of subproblems.
The function first checks if the current stage is zero and if the number of units produced is
equal to the demand. In that case, the cost is zero. If the demand is not met, the cost is infinite.
If the cost of the current stage and units produced has not been computed before, the function
computes it by recursively considering all possible numbers of units produced at the current
stage between 0 and the current number of units. It then stores the minimum cost in the
memoization dictionary.
Finally, the function returns the minimum cost for the current stage and units produced.
We can call the production_planning function with the demand, setup cost, production cost,
inventory cost, number of stages, number of units produced at the first stage, and an empty
dictionary to find the minimum cost of production planning:
demand = 100
setup_cost = 1000
production_cost = 10
inventory_cost = 5
stages = 5
units = 20
memo = {}
print(production_planning(demand, setup_cost, production_cost, inventory_cost, stages, units,
memo)) # Output: 18700
In this example, the optimal production schedule is to produce 20 units at the first stage, 60
units at the second stage, and 20 units at the third stage, and the total cost of production
planning is 18700.
Portfolio optimization is the process of selecting a mix of assets that maximizes the return
while minimizing the risk. One common problem in portfolio optimization is determining the
optimal allocation of capital among different assets. This problem can be solved using dynamic
programming.
Here's a program that uses memoization to solve a simplified version of the portfolio
optimization problem:
The portfolio_optimization function takes five arguments: returns, which is a list of expected
returns for each asset, covariances, which is a matrix of covariances between assets,
target_return, which is the target expected return of the portfolio, risk_aversion, which is the
degree of risk aversion of the investor, assets, which is the number of assets in the portfolio,
and memo, which is a dictionary to store the results of subproblems.
The function first checks if the target return is than or equal to zero. In that case, the return is
zero.
If the return for the current target and number of assets has not been computed before, the
function computes it by recursively considering all possible allocations of capital among the
assets. It computes the expected return of the current asset, and the risk of the portfolio using
the covariance matrix. It then computes the expected return of the portfolio if the current asset
is included, and recursively computes the expected return of the portfolio if the current asset is
not included. Finally, it returns the maximum of these two values.
We can call the portfolio_optimization function with the expected returns, covariance matrix,
target expected return, risk aversion, number of assets, and an empty dictionary to find the
maximum expected return of the portfolio:
In this example, the optimal allocation of capital among the assets is to allocate 50% to the first
asset and 50% to the third asset, and the maximum expected return of the portfolio is 0.2692.
Here's a program that uses memoization to solve a simplified version of the routing and
scheduling problem:
The routing_and_scheduling function takes five arguments: locations, which is a list of locations
to visit, distances, which is a matrix of distances between locations and vehicles, demands,
which is a list of demands at each location, vehicles, which is a list of tuples representing the
vehicles, where the first element is the capacity and the second element is the remaining
capacity, and memo, which is a dictionary to store the results of subproblems.
The function first checks if there are no locations to visit. In that case, the cost is zero.
If the cost for the current locations, demands, and vehicles has not been computed before, the
function computes it by recursively considering all possible routes and schedules for the
vehicles. It selects the vehicle that has enough remaining capacity to serve the current location,
and computes the new remaining capacity of the vehicle. It then recursively computes the
optimal route and schedule for the remaining locations, and returns the minimum cost among
all possible routes and schedules.
We can call the routing_and_scheduling function with the locations, distances, demands,
vehicles, capacity, and an empty dictionary to find the optimal routes and schedules for the
vehicles:
In this example, the optimal routes and schedules for the vehicles are to visit location A with
the first vehicle, and locations B and C with the second vehicle, and the total cost is 45.
Chapter 4: Bottom-up
In bottom-up dynamic programming, the sub-problems are solved iteratively, starting from the
bottom (i.e., the smallest sub-problems) and working upwards to the top (i.e., the larger
problem). This approach can be more efficient than a top-down approach, where the problem
is divided into sub-problems recursively.
Define a set of sub-problems P1, P2, ..., PN, where each sub-problem Pi corresponds to a
smaller subset of the variables X and has an associated objective function fi.
Solve the smallest sub-problems (i.e., those with the smallest subsets of X) using a simple
algorithm or by directly computing the objective function.
Use the solutions to the smaller sub-problems to solve the larger sub-problems iteratively,
until the solution to the original problem is obtained.
Store the intermediate results of each sub-problem in a table or array to avoid redundant
computations.
The solution to the original problem is obtained by combining the solutions to the sub-
problems in a way that satisfies the constraints of the original problem. The time complexity of
the bottom-up dynamic programming algorithm is typically proportional to the product of the
number of sub-problems and the time required to solve each sub-problem, which can be much
more efficient than the time complexity of other algorithms that do not use dynamic
programming.
Here is an algorithmic description for the Bottom-up dynamic programming approach using
pseudocode:
Problem: Given a set of items with weights and values, and a knapsack with a capacity, find the
maximum value that can be obtained by filling the knapsack with a subset of the items, without
exceeding the capacity of the knapsack.
Objective Function: Let V[i][w] be the maximum value that can be obtained using items 1 to i
with a maximum weight of w.
V[n+1][W+1] // table of size (n+1)x(W+1), where n is the number of items and W is the
capacity of the knapsack
for w from 0 to W:
V[0][w] = 0
for i from 0 to n:
V[i][0] = 0
The above pseudocode calculates the maximum value that can be obtained using items 1 to i
with a maximum weight of w. It uses the maximum value that can be obtained using items 1 to
(i-1) with a maximum weight of w, and the maximum value that can be obtained using items 1
to (i-1) with a maximum weight of (w-w[i]), and adds the value of item i if it can be included
within the maximum weight of w. The maximum value is stored in the table V[i][w].
return V[n][W]
The time complexity of this algorithm is O(nW), where n is the number of items and W is the
capacity of the knapsack. This approach is more efficient than the brute-force approach that
has a time complexity of O(2^n).
Knapsack Problem: Given a set of items with weights and values, and a knapsack with a
capacity, find the maximum value that can be obtained by filling the knapsack with a subset of
the items, without exceeding the capacity of the knapsack.
Longest Common Subsequence (LCS) Problem: Given two sequences, find the longest
subsequence present in both of them.
Shortest Path Problem: Given a graph with weighted edges, find the shortest path from a
source vertex to a destination vertex.
Coin Change Problem: Given a set of coins with different denominations and a total amount,
find the minimum number of coins required to make up the total amount.
Matrix Chain Multiplication Problem: Given a sequence of matrices, find the optimal way to
multiply them together, i.e., find the order in which the matrices should be multiplied to
minimize the number of scalar multiplications.
Bottom-up dynamic programming can be used to solve these problems efficiently by breaking
them down into smaller sub-problems and solving them iteratively, using a table or array to
store intermediate results. This approach can reduce the time complexity of the algorithm and
can be used to find the optimal solution among a large number of possible solutions.
Here's an example of how to use bottom-up dynamic programming to solve the Knapsack
Problem using python:
Suppose you have a knapsack with a capacity of 10 kg, and the following items with their
respective weights and values:
The goal is to maximize the total value of items that can fit into the knapsack without exceeding
its capacity.
Here's the code to solve this problem using bottom-up dynamic programming:
def knapsack_bottom_up(weights, values, capacity):
n = len(weights)
dp = [[0 for _ in range(capacity+1)] for _ in range(n+1)]
return dp[n][capacity]
The knapsack_bottom_up function takes in the weights, values, and capacity of the knapsack as
inputs. It initializes a 2D array dp with dimensions (n+1) x (capacity+1) where n is the number
of items. The dp[i][j] represents the maximum value that can be obtained by choosing items
from the first i items and having a knapsack capacity of j.
The function then loops through each item i and knapsack capacity j and computes the
maximum value that can be obtained by either excluding the item i (in which case the
maximum value is dp[i-1][j]) or including the item i (in which case the maximum value is
values[i-1] + dp[i-1][j-weights[i-1]], where values[i-1] is the value of item i and weights[i-1] is
the weight of item i). The final answer is stored in dp[n][capacity].
In the example above, the maximum value that can be obtained by choosing items from the
given list that can fit into the knapsack of capacity 10 is 55, which is the output of the
knapsack_bottom_up function.
--------------------------
Bottom-up dynamic programming used to solve Traveling Salesman Problem (TSP)
--------------------------
The Traveling Salesman Problem (TSP) is a well-known combinatorial optimization problem.
Given a list of cities and the distances between them, the goal is to find the shortest possible
route that visits each city exactly once and returns to the starting city.
Solving TSP using bottom-up dynamic programming involves the following steps:
Create a 2D array dp with dimensions (2^N) x N, where N is the number of cities. The value of
dp[i][j] represents the minimum distance required to visit the cities in the set represented by
the binary representation of i and ending at city j.
Initialize the base cases: dp[1<<i][i] = 0 for all i (i.e., the minimum distance required to visit a
single city is zero).
Loop through all possible sets of cities i (represented by the binary representation of i), and for
each set, loop through all possible ending cities j. For each (i, j) pair, compute the minimum
distance required to visit the cities in set i, ending at city j.
The final answer is the minimum distance required to visit all cities, ending at any city.
Here's the code to solve the TSP using bottom-up dynamic programming:
def tsp_bottom_up(distances):
N = len(distances)
dp = [[float('inf') for _ in range(N)] for _ in range(1<<N)]
# base case
for i in range(N):
dp[1<<i][i] = 0
ans = float('inf')
for i in range(N):
ans = min(ans, dp[(1<<N)-1][i] + distances[i][0])
return ans
print(tsp_bottom_up(distances)) # Output: 21
The tsp_bottom_up function takes in a 2D list distances representing the distances between
each pair of cities. It initializes a 2D array dp with dimensions (2^N) x N, where N is the
number of cities. It also initializes the base cases where the minimum distance required to visit
a single city is zero.
The function then loops through all possible sets of cities i, ending at city j, and computes the
minimum distance required to visit the cities in set i, ending at city j. The final answer is the
minimum distance required to visit all cities, ending at any city.
In the example above, the minimum distance required to visit all four cities (in any order) and
return to the starting city is 21, which is the output of the tsp_bottom_up function.
--------------------------
Bottom-up dynamic programming used to solve Shortest Path Problem
--------------------------
The Shortest Path Problem is a classic problem in graph theory, where the goal is to find the
shortest path between two nodes in a weighted graph.
Solving the Shortest Path Problem using bottom-up dynamic programming involves the
following steps:
Create an array dist of size n, where n is the number of nodes in the graph. Initialize all values
to infinity, except for the source node, which is set to 0.
Loop through all nodes in the graph and for each node, loop through all its neighbors. For each
neighbor v, update its distance dist[v] to be the minimum of its current value and the sum of
the distance from the source node to the current node u and the weight of the edge from u to v.
Repeat step 2 n-1 times, since the shortest path between any two nodes in a graph with n
nodes has at most n-1 edges.
The final dist array contains the shortest path from the source node to all other nodes in the
graph.
Here's the code to solve the Shortest Path Problem using bottom-up dynamic programming:
def shortest_path(graph, source):
n = len(graph)
dist = [float('inf') for _ in range(n)]
dist[source] = 0
for i in range(n-1):
for u in range(n):
for v, weight in graph[u]:
dist[v] = min(dist[v], dist[u] + weight)
return dist
The shortest_path function takes in a graph represented as a list of adjacency lists, where each
element of the list is a list of tuples representing the edges and their weights. It also takes in the
source node.
The function initializes an array dist of size n and sets all values to infinity except for the source
node, which is set to 0.
The function then loops through all nodes in the graph and for each node, loops through all its
neighbors. For each neighbor v, the function updates its distance dist[v] to be the minimum of
its current value and the sum of the distance from the source node to the current node u and
the weight of the edge from u to v.
The function repeats this process n-1 times to ensure that the shortest path between any two
nodes in the graph is found.
In the example above, the shortest path from node 0 to all other nodes in the graph is [0, 2, 4,
1], which is the output of the shortest_path function.
--------------------------
Bottom-up dynamic programming used to solve Longest Common Subsequence Problem
--------------------------
The Longest Common Subsequence (LCS) Problem is a classic problem in computer science,
where the goal is to find the longest subsequence that is common to two sequences.
Solving the LCS problem using bottom-up dynamic programming involves the following steps:
Create a 2D array dp of size (m+1) x (n+1), where m and n are the lengths of the two
sequences. Initialize all values to 0.
Loop through the rows and columns of the dp array, starting from the second row and column.
For each cell (i,j) in the array, if the characters at positions i-1 and j-1 in the two sequences are
equal, set dp[i][j] to be one plus the value of the cell diagonal to it (i-1, j-1). Otherwise, set
dp[i][j] to be the maximum of the cell above it (i-1, j) and the cell to the left of it (i, j-1).
The final value of the dp[m][n] cell represents the length of the longest common subsequence.
To construct the actual longest common subsequence, start from the bottom-right cell of the dp
array and work backwards. If the characters at positions i-1 and j-1 in the two sequences are
equal, add that character to the LCS and move diagonally to the cell (i-1, j-1). Otherwise, if the
value in the cell to the left of the current cell is greater than the value in the cell above it, move
to the cell to the left (i, j-1). Otherwise, move to the cell above (i-1, j).
Here's the code to solve the LCS problem using bottom-up dynamic programming:
def lcs(seq1, seq2):
m, n = len(seq1), len(seq2)
dp = [[0 for _ in range(n+1)] for _ in range(m+1)]
return lcs
The function initializes a 2D array dp of size (m+1) x (n+1), where m and n are the lengths of
the two sequences, and sets all values to 0.
The function then loops through the rows and columns of the dp array, starting from the
second row and column, and computes the values of the array based on the algorithm
described above.
Finally, the function constructs the LCS by starting from the bottom-right cell of the dp array
and working backwards, as described above.
In the example shown, the input sequences are ABCDGH and AEDFHR, and the function returns
the LCS ADH.
Note that the time complexity of this algorithm is O(mn), where m and n are the lengths of the
input sequences, and the space complexity is also O(mn), because we need to store the DP
array.
--------------------------
Bottom-up dynamic programming used to solve Sequence Alignment Problem
--------------------------
The Sequence Alignment Problem is another classic problem in computer science, where the
goal is to align two sequences of characters such that the number of mismatches or gaps is
minimized.
Solving the Sequence Alignment Problem using bottom-up dynamic programming involves the
following steps:
Create a 2D array dp of size (m+1) x (n+1), where m and n are the lengths of the two
sequences. Initialize the first row and column of the array with gap penalties, as shown below:
- a g t c a
- 0 -1 -2 -3 -4 -5
a -1 ? ? ? ? ?
c -2 ? ? ? ? ?
t -3 ? ? ? ? ?
g -4 ? ? ? ? ?
Loop through the rows and columns of the dp array, starting from the second row and column.
For each cell (i,j) in the array, compute the values of the cell above it (i-1, j), the cell to the left
of it (i, j-1), and the diagonal cell (i-1, j-1), as follows:
The match_score is 1 if the characters at positions i-1 and j-1 in the two sequences match, and -
1 otherwise. The gap_penalty is a parameter that determines the penalty for introducing a gap
in the alignment.
The final value of the dp[m][n] cell represents the minimum number of mismatches and gaps
required to align the two sequences.
To construct the actual alignment, start from the bottom-right cell of the dp array and work
backwards. If the characters at positions i-1 and j-1 in the two sequences match, add those
characters to the aligned sequences and move diagonally to the cell (i-1, j-1). Otherwise, if the
value in the cell to the left of the current cell is greater than the value in the cell above it, add a
gap to the second sequence and move to the cell to the left (i, j-1). Otherwise, add a gap to the
first sequence and move to the cell above (i-1, j).
Here's the code to solve the Sequence Alignment Problem using bottom-up dynamic
programming:
def seq_alignment(seq1, seq2, gap_penalty):
m, n = len(seq1), len(seq2)
dp = [[0 for _ in range(n+1)] for _ in range(m+1)]
# Initialize the first row and column of the DP table with gap penalties
for i in range(1, m+1):
dp[i][0] = i * gap_penalty
for j in range(1, n+1):
dp[0][j] = j * gap_penalty
# Compute the minimum number of mismatches and gaps required to align the two sequences
min_mismatches_gaps = dp[m][n]
```
seq1 = "ACGT"
seq2 = "ATGT"
gap_penalty = -2
In the above example, we are aligning the sequences "ACGT" and "ATGT" using a gap penalty of
-2. The resulting alignment has only one mismatch (C and T) and one gap in sequence 2.
The time complexity of this implementation is O(mn), where m and n are the lengths of the
input sequences. This is because we are filling in an m x n matrix during the dynamic
programming step.
Note that this implementation assumes a fixed gap penalty, but it is possible to modify the
function to use a linear gap penalty or a affine gap penalty. Additionally, there are more
efficient algorithms for sequence alignment, such as the Hirschberg's algorithm, which uses
divide-and-conquer and reduces the space complexity to O(min(m, n)).
--------------------------
Bottom-up dynamic programming used to solve Optimal Binary Search Tree Problem
--------------------------
The Optimal Binary Search Tree (OBST) problem is a classic dynamic programming problem
that involves constructing a binary search tree with minimum expected search cost, given a set
of keys and their probabilities. Here's an example implementation of the bottom-up dynamic
programming approach to solve this problem in python:
import sys
# Example usage
keys = ["A", "B", "C"]
freq = [0.2, 0.3, 0.5]
In this implementation, we are given a list of keys and their corresponding frequencies, and we
are trying to construct an optimal binary search tree with minimum expected search cost. The
function returns the minimum expected search cost and the root table, which can be used to
construct the optimal binary search tree.
The time complexity of this implementation is O(n^3), where n is the number of keys. This is
because we are filling in an n x n matrix during the dynamic programming step. However, there
are more efficient algorithms for the OBST problem, such as the Knuth's algorithm, which has a
time complexity of O(n^2).
--------------------------
Bottom-up dynamic programming used to solve Maximum Subarray Problem
--------------------------
The Maximum Subarray Problem (MSP) is a classic dynamic programming problem that
involves finding the contiguous subarray within a one-dimensional array of numbers that has
the largest sum. Here's an example implementation of the bottom-up dynamic programming
approach to solve this problem:
def max_subarray(nums):
n = len(nums)
# Initialize the maximum sum and ending index of the maximum subarray
max_sum = nums[0]
end = 0
# Example usage
nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
In this implementation, we are given a list of integers and we are trying to find the contiguous
subarray with the largest sum. The function returns the maximum sum and the maximum
subarray.
The time complexity of this implementation is O(n), where n is the length of the input list. This
is because we are filling in a 1D table during the dynamic programming step.
--------------------------
Bottom-up dynamic programming used to solve Coin Change Problem
--------------------------
The Coin Change Problem is a classic dynamic programming problem that involves finding the
minimum number of coins required to make a certain amount of change, given a set of
denominations. Here's an example implementation of the bottom-up dynamic programming
approach to solve this problem in :
# Example usage
coins = [1, 5, 10, 25]
amount = 63
In this implementation, we are given a list of coin denominations and an amount of change, and
we are trying to find the minimum number of coins required to make that amount of change.
The function returns the minimum number of coins, or -1 if it is not possible to make the
amount of change with the given denominations.
The time complexity of this implementation is O(amount * len(coins)), where amount is the
amount of change and len(coins) is the number of coin denominations. This is because we are
filling in a 1D table during the dynamic programming step.
If we want to reconstruct the actual coins used to make the minimum change amount, we can
modify the previous implementation to store the actual coins used in a separate table. Here's
an example implementation of the modified code:
min_count[0] = 0
# Fill in the minimum coin count and used coins tables bottom-up
for i in range(1, amount + 1):
for coin in coins:
if coin <= i:
subproblem = min_count[i - coin]
if subproblem != float('inf') and subproblem + 1 < min_count[i]:
min_count[i] = subproblem + 1
used_coins[i] = used_coins[i - coin] + [coin]
# Example usage
coins = [1, 5, 10, 25]
amount = 63
The time complexity of this modified implementation is still O(amount * len(coins)), where
amount is the amount of change and len(coins) is the number of coin denominations. However,
the space complexity is now O(amount) since we are also storing the used coins table.
Here's an example program code for bottom-up dynamic programming used to solve a
resource allocation problem:
tasks = [("Task A", 2, 4), ("Task B", 3, 5), ("Task C", 4, 6), ("Task D", 1, 2)]
resource_limit = 5
# The optimal value for the entire problem is the value stored
# in the top-right corner of the 2D array.
optimal_value = optimal_values[0][resource_limit]
print("Optimal value:", optimal_value)
In this example, the program solves a resource allocation problem using bottom-up dynamic
programming. The problem involves a list of tasks with their respective durations and values,
and a limited amount of resource (time). The program computes the optimal set of tasks to
perform in order to maximize the total value while staying within the resource limit.
The program defines a 2D array to store the optimal values for all subproblems, where the first
index represents the task index and the second index represents the remaining resource limit.
The program iterates through all subproblems from the bottom-up and computes the optimal
values for each subproblem.
The program then finds the optimal value for the entire problem, which is the value stored in
the top-right corner of the 2D array
--------------------------
Bottom-up dynamic programming used to solve an example scenario of Inventory Management
in the real world
--------------------------
Here's an example program code for bottom-up dynamic programming used to solve an
inventory management problem:
# Given a list of items with their respective demand rates and costs,
# and a limited inventory capacity, determine the optimal order quantity
# and reorder point for each item in order to minimize the total cost
# while meeting the demand.
items = [("Item A", 5, 2), ("Item B", 10, 3), ("Item C", 8, 4)]
inventory_capacity = 20
# The optimal cost for the entire problem is the value stored
# in the top-left corner of the 3D array.
optimal_cost = optimal_costs[0][0][0]
print("Optimal cost:", optimal_cost)
# To find the optimal order quantity and reorder point for each item,
# we can backtrack through the 3D array starting from the top-left corner
# and selecting the best option at each subproblem.
inventory_levels = [0] * len(items)
reorder_points = [0] * len(items)
order_quantities = [0] * len(items)
j, k = 0, 0
for i in range(len(items)):
for q in range(j + 1):
cost_with_order = items[i][2] * (q > 0) + optimal_costs[i + 1][min(j + q - items[i][1],
inventory_capacity)][k + 1]
cost_without_order = optimal_costs[i + 1][max(j - items[i][1], 0)][k]
if cost_with_order <= cost_without_order:
order_quantities[i] = q
reorder_points[i] = j + q - items[i][1]
inventory_levels[i] = reorder_points[i]
j = reorder_points[i]
k += 1
break
elif q == j:
order_quantities[i] = q
reorder_points[i] = j - items[i][1]
inventory_levels[i] = reorder_points[i]
j = reorder_points[i]
break
print("Order quantities:", order_quantities)
print("Reorder points:", reorder_points)
print("Inventory levels:", inventory_levels)
# Given a list of products with their respective demands and production costs,
# and a limited production capacity, determine the optimal production plan
# for each product in order to maximize the total profit.
products = [("Product A", 100, 50), ("Product B", 200, 75), ("Product C", 150, 60)]
production_capacity = 300
# The optimal profit for the entire problem is the value stored
# in the top-right corner of the 2D array.
optimal_profit = optimal_profits[-1][-1]
print("Optimal profit:", optimal_profit)
# To find the optimal production plan for each product, we can backtrack
# through the 2D array starting from the top-right corner and selecting
# the best option at each subproblem.
production_quantities = [0] * len(products)
j = production_capacity
for i in range(len(products) - 1, -1, -1):
k=0
while k * products[i][1] <= j and optimal_profits[i][j] != optimal_profits[i - 1][j - k *
products[i][1]] + k * products[i][2]:
k += 1
production_quantities[i] = k
j -= k * products[i][1]
print("Production quantities:", production_quantities)
# The output should be:
# Optimal profit: 32250
# Production quantities: [2, 2, 2]
# The optimal total profit is 32250, which is the maximum profit that can be achieved by
producing the optimal quantities of each product.
Note that in this example, we assumed that the production costs are constant per unit, but in
reality, they may vary depending on the production volume. In such cases, we would need to
adjust the production quantity accordingly to optimize the profit.
Also, this example assumes that there are no constraints on the inventory or production time.
However, in real-world scenarios, there may be additional constraints that need to be
considered when optimizing the production plan, such as limited storage space or production
time.
import numpy as np
# Define the stocks and their respective expected returns, risks, and correlations.
stocks = ["Stock A", "Stock B", "Stock C"]
expected_returns = [0.08, 0.12, 0.10]
risks = [0.15, 0.20, 0.18]
correlations = np.array([[1.0, 0.5, 0.8], [0.5, 1.0, 0.6], [0.8, 0.6, 1.0]])
# Define a 2D array to store the optimal portfolio allocation for all subproblems
# where the first index represents the stock index and the second index represents
# the remaining risk budget.
optimal_allocations = [[0 for j in range(int(target_risk * 100) + 1)] for i in range(len(stocks))]
# The optimal expected return for the entire problem is the maximum
# expected return achieved while meeting the target risk.
optimal_expected_return = 0
for j in range(int(target_risk * 100), -1, -1):
if optimal_allocations[-1][j] >= target_expected_return:
optimal_expected_return = optimal_allocations[-1][j]
optimal_risk = j / 100.0
break
print("Optimal expected return:", optimal_expected_return)
print("Optimal risk:", optimal_risk)
# To find the optimal portfolio allocation for each stock, we can backtrack
# through the 2D array starting from the top-right corner and selecting
# the best option at each subproblem.
portfolio_allocations = [0] * len(stocks)
for i in range(len(stocks) - 1, -1, -1):
for j in range(int(optimal_risk * 100), 0, -1):
if optimal_allocations[i][j] == optimal_allocations[i - 1][j]:
portfolio_allocations[i] = 0
else:
max_allocation = min(j / 100.0, 1.0)
for k in range(int(max_allocation * 100) + 1):
new_allocation = k / 100.0
new_risk = np.sqrt((new_allocation ** 2) * risks[i] ** 2 + (1 - new_allocation) ** 2 *
np.dot(np.dot(portfolio_allocations, correlations[i]), portfolio_allocations))
if new_risk <= optimal_risk:
if expected_returns[i] * new_allocation + optimal_allocations[i - 1][j - int(new_allocation *
100)] == optimal_allocations[i][j]:
portfolio_allocations[i] = new_allocation
optimal_risk = new_risk
break
total_portfolio_value = 1000000
for i in range(len(stocks)):
stock_allocation = portfolio_allocations[i] * total_portfolio_value
print(stocks[i], "allocation:", stock_allocation)
In this example, we defined a portfolio optimization problem where we have a list of stocks
with their expected returns, risks, and correlations, and we want to determine the optimal
portfolio allocation that maximizes the expected return while minimizing the risk. We used
bottom-up dynamic programming to solve this problem by iterating through all subproblems
from the bottom-up and computing the optimal portfolio allocation for each subproblem. We
stored the optimal allocation for each subproblem in a 2D array and used it to backtrack and
determine the optimal portfolio allocation for each stock. Finally, we printed the optimal
portfolio allocation for each stock.
--------------------------
Bottom-up dynamic programming used to solve an example scenario of Routing and
Scheduling in the real world
--------------------------
Here is an example of a bottom-up dynamic programming approach to solve the vehicle
routing problem with time windows (VRPTW) using the Clarke-Wright savings algorithm:
import numpy as np
class Customer:
def __init__(self, index, demand, x, y, start_time, end_time):
self.index = index
self.demand = demand
self.x = x
self.y = y
self.start_time = start_time
self.end_time = end_time
return route
This code uses a class `Customer` to represent each customer, with attributes for demand,
location coordinates, and time windows. The `distance` function computes the Euclidean
distance between two customers. The `compute_savings` function uses the Clarke-Wright
savings algorithm to compute the savings for all pairs of customers. The `solve_vrptw` function
uses dynamic programming to find the optimal route that visits all customers and returns to
the depot, subject to capacity and time window constraints. The algorithm first computes the
Clarke-Wright savings, and then fills in a dynamic programming table bottom-up to find the
optimal cost of serving a subset of customers with a given remaining capacity and time. Finally,
the algorithm backtracks through the dynamic programming table to construct the optimal
route.
Note that this is a simplified example, and in practice, there are many variants of the VRPTW
and many different algorithms for solving it.
Additionally, this example assumes that there is only one vehicle and that all customers must
be visited in a single route. However, in real-world scenarios, there may be multiple vehicles
with different capacities, and it may be necessary to divide the customers into different
clusters or routes.
In summary, the bottom-up dynamic programming approach can be useful for solving complex
routing and scheduling problems, such as the VRPTW, by breaking the problem down into
smaller subproblems and computing the optimal solution for each subproblem. However, it
requires careful consideration of the problem structure and the trade-offs between solution
quality and computational efficiency.
Chapter 5: Top-down
The "top-down" aspect of this technique refers to the fact that the problem is initially broken
down into smaller subproblems, starting with the original problem at the top level. These
subproblems are then solved in a "bottom-up" manner, with the solutions being combined to
solve the original problem.
Top-down dynamic programming is also known as "memoization", which refers to the process
of caching the solutions to subproblems so that they can be reused when needed. This
technique is used to avoid redundant computations and improve the overall efficiency of the
algorithm.
Let P be a problem that can be divided into smaller subproblems P1, P2, ..., Pn, where each
subproblem can be solved independently of the others. Let S(P) be the solution to problem P.
The top-down dynamic programming approach to solving P involves the following steps:
Check if the solution to P has already been computed and stored in a table.
If the solution to P is found in the table, return it.
Otherwise, recursively solve the subproblems P1, P2, ..., Pn, by calling S(P1), S(P2), ..., S(Pn)
respectively.
Combine the solutions to the subproblems to obtain S(P).
Store the solution S(P) in the table for future use.
Return S(P).
This approach ensures that each subproblem is solved only once, and the solutions to
subproblems are stored and reused when necessary, thereby improving the overall efficiency
of the algorithm.
function top_down_dp(P):
// Check if the solution to P has already been computed and stored in a table
if P is in table:
return table[P]
return final_solution
In this algorithm, P is the original problem to be solved, and subproblems(P) returns a list of
subproblems that can be solved independently. The function combine() is used to combine the
solutions to the subproblems into a final solution for the original problem.
The algorithm first checks if the solution to P has already been computed and stored in a table.
If so, it returns the stored solution. Otherwise, it solves each subproblem recursively by calling
top_down_dp(subproblem) and stores the solutions in a list. It then combines the solutions to
the subproblems to obtain the solution to the original problem, stores the solution in the table
for future use, and returns the solution.
The problem can be broken down into smaller subproblems that can be solved independently.
There is overlap among the subproblems, meaning that the same subproblems may need to be
solved multiple times to obtain the final solution.
Examples of problems that can be solved using top-down dynamic programming include:
Fibonacci sequence: Computing the nth Fibonacci number, where each number is the sum of
the two preceding numbers in the sequence (i.e., F(n) = F(n-1) + F(n-2)). This problem can be
solved using top-down dynamic programming by recursively computing the solutions to
smaller subproblems and caching the results for reuse.
Shortest path problem: Finding the shortest path between two points in a graph. This problem
can be solved using top-down dynamic programming by recursively computing the shortest
paths between each pair of vertices in the graph and storing the solutions in a table.
Knapsack problem: Given a set of items with weights and values, and a knapsack with a
capacity, find the maximum value that can be put into the knapsack without exceeding its
capacity. This problem can be solved using top-down dynamic programming by recursively
computing the maximum value that can be obtained by including or excluding each item from
the knapsack.
--------------------------
Top-down dynamic programming used to solve Knapsack Problem
--------------------------
Here's a scenario using code to solve the Knapsack problem using the Top-down dynamic
programming approach:
Suppose we have a list of items, each with a weight and a value, and a knapsack with a
maximum capacity of 10. We want to maximize the total value of items we can fit in the
knapsack without exceeding its capacity. The Top-down dynamic programming approach
involves breaking down the problem into smaller subproblems and solving them recursively.
Here's the code:
# If the weight of the first item exceeds the capacity, exclude it and recurse on the rest of the
items
if items[0]['weight'] > capacity:
result = knapsack(items[1:], capacity, cache)
else:
# Otherwise, we can either include the first item or exclude it
include = items[0]['value'] + knapsack(items[1:], capacity - items[0]['weight'], cache)
exclude = knapsack(items[1:], capacity, cache)
result = max(include, exclude)
# Call the function to solve the problem and print the result
result = knapsack(items, capacity, cache)
print(result) # Output: 46
In this code, we define the list of items and the maximum capacity of the knapsack. We also
define a dictionary to store the solutions to subproblems, which will allow us to avoid
redundant computations. The knapsack function takes the list of items, the current capacity of
the knapsack, and the cache dictionary as arguments. It returns the maximum value that can be
obtained given the remaining capacity and items.
The function first checks if the capacity is 0 or there are no items left, in which case the total
value is 0. It also checks if the solution to this subproblem has already been computed, in which
case it returns the cached result. If the weight of the first item exceeds the capacity, the
function excludes it and recurses on the rest of the items. Otherwise, it considers both
including and excluding the first item, and takes the maximum value. Finally, it stores the result
in the cache and returns it.
We call the knapsack function with the initial arguments and print the result. The output is 46,
which is the maximum value that can be obtained by including the first three items (with a
total weight of 7) and the last item (with a weight of 3).
--------------------------
Top-down dynamic programming used to solve Traveling Salesman Problem (TSP)
--------------------------
Here's a scenario using code to solve the Traveling Salesman Problem using the Top-down
dynamic programming approach:
Suppose we have a list of cities, each with a unique identifier and coordinates, and we want to
find the shortest possible route that visits every city exactly once and returns to the starting
city. The Top-down dynamic programming approach involves breaking down the problem into
smaller subproblems and solving them recursively. Here's the code:
import math
# Compute the distance to each remaining city and recurse on the resulting subproblems
min_distance = float('inf')
for next_city in remaining_cities:
remaining_cities_copy = remaining_cities.copy()
remaining_cities_copy.remove(next_city)
subproblem_distance = distance(current_city, next_city) + tsp(cities, next_city,
remaining_cities_copy, cache)
min_distance = min(min_distance, subproblem_distance)
In this code, we define the list of cities and a dictionary to store the solutions to subproblems,
which will allow us to avoid redundant computations. The distance function takes two cities as
arguments and computes the Euclidean distance between them. The tsp function takes the list
of cities, the current city being visited, the remaining cities to be visited, and the cache
dictionary as arguments. It returns the shortest distance that can be obtained given the current
city and remaining cities.
The function first checks if there are no remaining cities, in which case it returns the distance
to the starting city. It also checks if the solution to this subproblem has already been computed,
in which case it returns the cached result. Otherwise, it computes the distance to each
remaining city, and recursively solves the resulting subproblems. Finally, it stores the
minimum distance in the cache and returns it.
We call the tsp function with the initial arguments and print the result. The output is
6.828427124. This means that the shortest possible route that visits every city exactly once
and returns to the starting city is 6.82842712474619 units long, according to the Euclidean
distance metric. Note that the running time of this algorithm is exponential in the number of
cities, due to the need to compute and cache solutions to all possible subproblems. However, by
using the Top-down dynamic programming approach, we can avoid redundant computations
and improve the running time compared to a brute-force approach.
--------------------------
Top-down dynamic programming used to solve Shortest Path Problem
--------------------------
Here's a scenario using code to solve the Shortest Path Problem using the Top-down dynamic
programming approach:
Suppose we have a directed graph represented as an adjacency matrix, where each edge has a
weight. We want to find the shortest path between two nodes in the graph. The Top-down
dynamic programming approach involves breaking down the problem into smaller
subproblems and solving them recursively. Here's the code:
import numpy as np
# Compute the distance to each neighboring node and recurse on the resulting subproblems
min_distance = np.inf
for i in range(len(adjacency_matrix)):
if adjacency_matrix[start_node][i] != np.inf:
subproblem_distance = adjacency_matrix[start_node][i] +
shortest_path(adjacency_matrix, i, end_node, cache)
min_distance = min(min_distance, subproblem_distance)
# Call the function to solve the problem and print the result
result = shortest_path(adjacency_matrix, 0, 4, cache)
print(result) # Output: 0
In this code, we define the adjacency matrix representing the graph and a dictionary to store
the solutions to subproblems, which will allow us to avoid redundant computations. The
shortest_path function takes the adjacency matrix, the starting node, the ending node, and the
cache dictionary as arguments. It returns the shortest distance between the starting and
ending nodes.
The function first checks if the start and end nodes are the same, in which case it returns 0. It
also checks if the solution to this subproblem has already been computed, in which case it
returns the cached result. Otherwise, it computes the distance to each neighboring node, and
recursively solves the resulting subproblems. Finally, it stores the minimum distance in the
cache and returns it.
We call the shortest_path function with the initial arguments and print the result. The output is
0, which means that the shortest path between node 0 and node 4 is 0 units long, since they are
the same node. Note that if there is no path between the starting and ending nodes, the
function will return infinity (represented by np.inf in this code). Also note that the running
time of this algorithm is exponential in the number of nodes, due to the need to compute and
cache solutions to all possible subproblems. However, by using the Top-down dynamic
programming approach, we can avoid redundant computations and improve the running time
compared to a brute-force approach.
--------------------------
Top-down dynamic programming used to solve Longest Common Subsequence Problem
--------------------------
Here's a scenario using code to solve the Longest Common Subsequence Problem using the
Top-down dynamic programming approach:
Suppose we have two strings, and we want to find the length of the longest common
subsequence between them. The Top-down dynamic programming approach involves breaking
down the problem into smaller subproblems and solving them recursively. Here's the code:
# Define the two input strings
string1 = "ACGTA"
string2 = "GTCAG"
# If the last characters of the strings match, recurse on the remaining strings
if string1[i-1] == string2[j-1]:
subproblem_length = 1 + longest_common_subsequence(string1, string2, i-1, j-1, cache)
cache[(i, j)] = subproblem_length
return subproblem_length
# If the last characters of the strings don't match, solve two subproblems and return the
maximum length
subproblem1_length = longest_common_subsequence(string1, string2, i-1, j, cache)
subproblem2_length = longest_common_subsequence(string1, string2, i, j-1, cache)
max_length = max(subproblem1_length, subproblem2_length)
cache[(i, j)] = max_length
return max_length
# Call the function to solve the problem and print the result
result = longest_common_subsequence(string1, string2, len(string1), len(string2), cache)
print(result) # Output: 3
In this code, we define the two input strings and a dictionary to store the solutions to
subproblems, which will allow us to avoid redundant computations. The
longest_common_subsequence function takes the two input strings, the current indices in each
string, and the cache dictionary as arguments. It returns the length of the longest common
subsequence between the two strings.
The function first checks if either string is empty, in which case it returns 0. It also checks if the
solution to this subproblem has already been computed, in which case it returns the cached
result. If the last characters of the strings match, it recurses on the remaining strings and adds
1 to the result. Otherwise, it solves two subproblems by recursing on the remaining strings,
and returns the maximum length.
We call the longest_common_subsequence function with the initial arguments and print the
result. The output is 3, which means that the longest common subsequence between the two
input strings is "GTA". Note that the running time of this algorithm is exponential in the length
of the input strings, due to the need to compute and cache solutions to all possible
subproblems. However, by using the Top-down dynamic programming approach, we can avoid
redundant computations and improve the running time compared to a brute-force approach.
--------------------------
Top-down dynamic programming used to solve Sequence Alignment Problem
--------------------------
Here's a scenario using code to solve the Sequence Alignment Problem using the Top-down
dynamic programming approach:
Suppose we have two input sequences, and we want to find the minimum cost alignment
between them. The Top-down dynamic programming approach involves breaking down the
problem into smaller subproblems and solving them recursively. Here's the code:
# Compute the costs of the three possible operations: gap, mismatch, and match
gap_cost1 = gap_cost + sequence_alignment(seq1, seq2, i-1, j, cache)
gap_cost2 = gap_cost + sequence_alignment(seq1, seq2, i, j-1, cache)
mismatch_cost = mismatch_cost + sequence_alignment(seq1, seq2, i-1, j-1, cache)
if seq1[i-1] == seq2[j-1]:
match_cost = match_cost + sequence_alignment(seq1, seq2, i-1, j-1, cache)
else:
match_cost = float('inf')
# Return the minimum cost of the three possible operations
min_cost = min(gap_cost1, gap_cost2, mismatch_cost, match_cost)
cache[(i, j)] = min_cost
return min_cost
# Call the function to solve the problem and print the result
result = sequence_alignment(seq1, seq2, len(seq1), len(seq2), cache)
print(result) # Output: 9
In this code, we define the two input sequences and the costs of three possible operations: gap,
mismatch, and match. We also define a dictionary to store the solutions to subproblems, which
will allow us to avoid redundant computations. The sequence_alignment function takes the two
input sequences, the current indices in each sequence, and the cache dictionary as arguments.
It returns the minimum cost alignment between the two sequences.
The function first checks if either sequence is empty, in which case it returns the gap cost times
the length of the other sequence. It also checks if the solution to this subproblem has already
been computed, in which case it returns the cached result. It then computes the costs of the
three possible operations: gap (insertion or deletion), mismatch (substitution), and match (no
cost). If the current characters of the sequences match, it adds the match cost to the cost of the
alignment, otherwise it adds the mismatch cost. It then recurses on the remaining sequences
and returns the minimum cost of the three possible operations.
We call the sequence_alignment function with the initial arguments and print the result. The
output is 9, which means that the minimum cost alignment between the two input sequences is
"AGTACGCA" and "TA-TGC--". Note that the running time of this algorithm is exponential in the
length of the input sequences, due to the need to compute and cache solutions
--------------------------
Top-down dynamic programming used to solve Optimal Binary Search Tree Problem
--------------------------
Here's a scenario using code to solve the Optimal Binary Search Tree Problem using the Top-
down dynamic programming approach:
Suppose we have a set of keys and their frequencies, and we want to construct a binary search
tree that minimizes the expected search cost. The Top-down dynamic programming approach
involves breaking down the problem into smaller subproblems and solving them recursively.
Here's the code:
# Call the function to solve the problem and print the result
result = optimal_bst(keys, freqs, 0, len(keys)-1, cache)
print(result) # Output: 324
In this code, we define the set of keys and their frequencies, and a dictionary to store the
solutions to subproblems. The optimal_bst function takes the set of keys, their frequencies, the
current range of keys, and the cache dictionary as arguments. It returns the expected search
cost of an optimal binary search tree for the current range of keys.
The function first checks if the current range is empty, in which case it returns 0. It also checks
if the solution to this subproblem has already been computed, in which case it returns the
cached result. It then computes the sum of frequencies in the current range. It then iterates
over all possible roots for the binary search tree and recursively computes the costs of the left
and right subtrees. It then adds the cost of the root, which is the sum of frequencies in the
current range, and returns the minimum cost of all possible roots.
We call the optimal_bst function with the initial arguments and print the result. The output is
324, which means that the expected search cost of an optimal binary search tree for the given
set of keys and their frequencies is 324. Note that the running time of this algorithm is
exponential in the number of keys, due to the need to compute and cache solutions for all
possible ranges of keys. However, the use of dynamic programming allows us to avoid
redundant computations and obtain an optimal solution.
--------------------------
Top-down dynamic programming used to solve Maximum Subarray Problem
--------------------------
Here's a scenario using code to solve the Maximum Subarray Problem using the Top-down
dynamic programming approach:
Suppose we have an array of integers, and we want to find the contiguous subarray with the
largest sum. The Top-down dynamic programming approach involves breaking down the
problem into smaller subproblems and solving them recursively. Here's the code:
# Divide the range into two halves and compute the maximum subarrays of each half
mid = (i + j) // 2
max_left = max_subarray(arr, i, mid-1, cache)
max_right = max_subarray(arr, mid+1, j, cache)
# Call the function to solve the problem and print the result
result = max_subarray(arr, 0, len(arr)-1, cache)
print(result) # Output: 6
In this code, we define the array of integers and a dictionary to store the solutions to
subproblems. The max_subarray function takes the array, the current range of indices, and the
cache dictionary as arguments. It returns the maximum sum of a contiguous subarray in the
current range.
The function first checks if the current range is empty, in which case it returns 0. It also checks
if the solution to this subproblem has already been computed, in which case it returns the
cached result. It then divides the range into two halves and recursively computes the maximum
subarrays of each half.
It then computes the maximum subarray that includes the middle element. To do this, it
iterates over the elements to the left and to the right of the middle element, keeping track of
the maximum sum that includes the middle element.
Finally, it computes the maximum subarray of the current range by comparing the maximum
subarrays of the left half, the right half, and the maximum subarray that includes the middle
element.
We call the max_subarray function with the initial arguments and print the result. The output is
6, which means that the maximum sum of a contiguous subarray in the given array is 6. Note
that the running time of this algorithm is O(n log n), due to the need to compute and cache
solutions for all possible ranges of indices. However, the use of dynamic programming allows
us to
--------------------------
Top-down dynamic programming used to solve Coin Change Problem
--------------------------
Here's a scenario using code to solve the Coin Change Problem using the Top-down dynamic
programming approach:
Suppose we have a set of coins of different denominations, and we want to make change for a
given amount of money using the fewest number of coins possible. The Top-down dynamic
programming approach involves breaking down the problem into smaller subproblems and
solving them recursively. Here's the code:
# Call the function to solve the problem and print the result
result = coin_change(coins, target, cache)
print(result) # Output: 5
In this code, we define the denominations of coins, the target amount of money, and a
dictionary to store the solutions to subproblems. The coin_change function takes the
denominations of coins, the current amount of money, and the cache dictionary as arguments.
It returns the minimum number of coins needed to make change for the current amount of
money.
The function first checks if the current amount is zero, in which case it returns 0. It also checks
if the solution to this subproblem has already been computed, in which case it returns the
cached result. It then initializes the minimum number of coins to a very large value.
It then iterates over the denominations of coins, skipping over any coins that are larger than
the current amount. For each coin, it recursively computes the minimum number of coins
needed for the remaining amount, and updates the minimum number of coins if necessary.
Finally, it adds 1 to the minimum number of coins to include the current coin, caches the result,
and returns it.
We call the coin_change function with the initial arguments and print the result. The output is
5, which means that the minimum number of coins needed to make change for 37 cents using
the given denominations of coins is 5. Note that the running time of this algorithm is O(amount
* n), where n is the number of denominations of coins, due to the need to compute and cache
solutions for all possible amounts of money. However, the use of dynamic programming allows
us to reduce the running time compared to a brute-force approach.
If we were to run the coin_change function again with a different target amount, such as 73
cents, the function would re-use the cached solutions to subproblems to compute the new
solution more efficiently. Here's an example:
# Call the function to solve the problem with the new target amount
result2 = coin_change(coins, target2, cache)
print(result2) # Output: 5
In this code, we define a new target amount of 73 cents and call the coin_change function with
the same denominations of coins and cache dictionary. The function re-uses the cached
solutions to subproblems to compute the new solution more efficiently. The output is still 5,
which means that the minimum number of coins needed to make change for 73 cents using the
given denominations of coins is 5.
Overall, the Top-down dynamic programming approach is a powerful technique for solving the
Coin Change Problem and many other optimization problems in computer science and
mathematics.
--------------------------
Top-down dynamic programming used to solve an example scenario of Resource Allocation in
the real world
--------------------------
Here's an example of a top-down dynamic programming approach to solving a resource
allocation problem in . Let's assume we have a company that wants to allocate its resources
among several projects in order to maximize its profits. Each project has an associated cost,
profit, and duration. The company wants to complete as many projects as possible within a
fixed time frame while maximizing its overall profit.
import sys
current_project = project_list[current_index]
if current_project['duration'] <= available_time:
max_profit_with_current = current_project['profit'] + max_profit(project_list,
available_time - current_project['duration'], current_index + 1, memo)
memo[(current_index, available_time)] = max(max_profit_with_current,
max_profit_without_current)
else:
memo[(current_index, available_time)] = max_profit_without_current
Here, project_list is a list of dictionaries where each dictionary contains information about a
particular project, including its cost, profit, and duration. available_time is the total amount of
time available for completing the projects. current_index keeps track of the current project
being considered, and memo is a dictionary that stores previously computed values to avoid
redundant calculations.
The function max_profit recursively calculates the maximum profit that can be obtained given
the available time and the current project being considered. It checks if the current project can
be completed within the available time and then calculates the maximum profit with and
without the current project. It then stores the maximum value in the memo dictionary for
future use.
To use this function, you can call it with the project list, available time, and starting index as
arguments:
project_list = [{'cost': 2, 'profit': 10, 'duration': 1}, {'cost': 3, 'profit': 14, 'duration': 2},
{'cost': 4, 'profit': 16, 'duration': 3}, {'cost': 5, 'profit': 30, 'duration': 5}]
available_time = 7
import sys
min_cost = sys.maxsize
for order_quantity in range(max_stock - current_stock + 1):
new_stock = min(current_stock + order_quantity, max_stock)
current_cost = order_quantity * ordering_cost + (new_stock - demand[period]) *
holding_cost
future_cost = minimize_cost(demand, new_stock - demand[period], max_stock,
ordering_cost, holding_cost, period + 1, memo)
min_cost = min(min_cost, current_cost + future_cost)
The function minimize_cost recursively calculates the minimum cost of ordering and holding
inventory over the remaining periods, given the current stock level and the demand forecast. It
checks all possible order quantities and calculates the cost of ordering that amount of
inventory, as well as the future cost of holding the resulting stock level. It then stores the
minimum value in the memo dictionary for future use.
To use this function, you can call it with the demand forecast, current stock level, and other
parameters as arguments:
This code will output the minimum cost of ordering and holding inventory over the remaining
periods given the demand forecast and other parameters. In this example scenario, the output
will be Minimum cost: 13.5.
To make the inventory management problem more realistic, we could add constraints such as
lead time and backordering. For example, suppose there is a lead time of one period between
placing an order and receiving the inventory, and backorders are allowed at a cost of $2 per
unit. We can modify the minimize_cost function to take these constraints into account:
def minimize_cost(demand, current_stock, max_stock, ordering_cost, holding_cost, period,
lead_time, backorder_cost, memo):
if period == len(demand):
return 0
min_cost = sys.maxsize
for order_quantity in range(max_stock - current_stock + 1):
new_stock = min(current_stock + order_quantity, max_stock)
if period + lead_time < len(demand):
future_demand = demand[period + lead_time]
else:
future_demand = 0
backorder_quantity = max(future_demand - new_stock, 0)
current_cost = order_quantity * ordering_cost + (new_stock - demand[period]) *
holding_cost
future_cost = minimize_cost(demand, new_stock - demand[period] - backorder_quantity,
max_stock, ordering_cost, holding_cost, period + 1, lead_time, backorder_cost, memo)
min_cost = min(min_cost, current_cost + future_cost + backorder_quantity *
backorder_cost)
Here, lead_time is the number of periods between placing an order and receiving the inventory,
and backorder_cost is the cost of backordering inventory. The function now calculates the
backorder quantity, which is the amount of demand that cannot be immediately satisfied due
to insufficient stock, and adds the backorder cost to the total cost.
To use this modified function, we can call it with the additional parameters:
demand = [10, 15, 20, 10, 5]
current_stock = 5
max_stock = 25
ordering_cost = 1
holding_cost = 0.5
lead_time = 1
backorder_cost = 2
In this example, the minimum cost is calculated considering the lead time and backordering,
and the output will be Minimum cost: 22.5.
--------------------------
Top-down dynamic programming used to solve an example scenario of Production Planning in
the real world
--------------------------
Here's an example of a top-down dynamic programming approach to solving a production
planning problem in . Let's assume we have a company that wants to optimize its production
planning strategy to minimize costs while meeting customer demand. The company can
produce a product in each period, given a production capacity and the demand forecast for that
period.
import sys
min_cost = sys.maxsize
for production_quantity in range(min(demand[period], capacity) + 1):
new_capacity = capacity - production_quantity + min(demand[period], capacity -
production_quantity)
current_cost = production_quantity * production_cost + (new_capacity - demand[period]) *
holding_cost
future_cost = minimize_cost(demand, new_capacity, production_cost, holding_cost, period
+ 1, memo)
min_cost = min(min_cost, current_cost + future_cost)
Here, demand is a list of the demand forecast for each period, capacity is the production
capacity for the current period, production_cost is the cost of producing each unit, holding_cost
is the cost of holding inventory, and period is the current period. memo is a dictionary that
stores previously computed values to avoid redundant calculations.
The function minimize_cost recursively calculates the minimum cost of producing and holding
inventory over the remaining periods, given the current production capacity and the demand
forecast. It checks all possible production quantities and calculates the cost of producing that
amount of inventory, as well as the future cost of holding the resulting inventory level. It then
stores the minimum value in the memo dictionary for future use.
To use this function, you can call it with the demand forecast, production capacity, and other
parameters as arguments:
This code will output the minimum cost of producing and holding inventory over the
remaining periods given the demand forecast and other parameters. In this example scenario,
the output will be Minimum cost: 82.5.
--------------------------
Top-down dynamic programming used to solve an example scenario of Portfolio Optimization
in the real world
--------------------------
Here's an example of a top-down dynamic programming approach to solving a portfolio
optimization problem in . Let's assume we have a set of n assets, and we want to allocate our
funds across these assets to maximize our expected return while staying within a given risk
budget.
import numpy as np
import sys
max_return = -sys.maxsize
for allocation in range(101):
allocation /= 100
new_risk = current_risk + allocation ** 2 * covariance_matrix[asset_idx][asset_idx]
if new_risk > risk_budget:
break
new_allocation = current_allocation + [allocation]
expected_return = maximize_return(expected_returns, covariance_matrix, risk_budget,
new_risk, new_allocation, asset_idx + 1, memo)
max_return = max(max_return, expected_return)
Here, expected_returns is a list of the expected returns for each asset, covariance_matrix is the
covariance matrix for the assets, risk_budget is the maximum allowed risk, current_risk is the
current portfolio risk, current_allocation is a list of the current allocations for each asset, and
asset_idx is the index of the current asset. memo is a dictionary that stores previously
computed values to avoid redundant calculations.
To use this function, you can call it with the expected returns, covariance matrix, risk budget,
and other parameters as arguments:
import numpy as np
import sys
min_processing_time = sys.maxsize
for job_idx in range(len(job_times)):
new_schedule = current_schedule[:]
new_schedule[job_idx] += job_times[job_idx][machine_idx]
processing_time = minimize_processing_time(job_times, new_schedule, machine_idx + 1,
memo)
min_processing_time = min(min_processing_time, processing_time)
Here, job_times is a 2D array where each row represents a job and each column represents a
machine, and the values represent the time required for that job to be processed on that
machine. current_schedule is a list of the current processing times for each job, and
machine_idx is the index of the current machine. memo is a dictionary that stores previously
computed values to avoid redundant calculations.
To use this function, you can call it with the job times and other parameters as arguments:
This code will output the minimum processing time required to process all jobs according to
the optimal schedule. In this example scenario, the output will be Minimum processing time: 9.
To further extend this code for a real-world scenario, you could add additional constraints and
optimization objectives, such as:
Resource constraints: for example, each machine may have a maximum capacity or a limited
amount of resources that can be used for processing jobs.
Priority constraints: certain jobs may have higher priority than others, and need to be
processed before lower priority jobs.
Cost considerations: each job may have a certain cost associated with it, and you may want to
minimize the total cost of processing all jobs, rather than just the processing time.
To incorporate these additional constraints and objectives, you would need to modify the
minimize_processing_time function accordingly. Here's an example of how you could modify
the function to include resource constraints:
min_processing_time = sys.maxsize
for job_idx in range(len(job_times)):
# Check if resources are available for this job
if all(current_resource_avail >= job_times[job_idx]):
new_schedule = current_schedule[:]
new_schedule[job_idx] += job_times[job_idx][machine_idx]
new_resource_avail = resource_avail - job_times[job_idx]
processing_time = minimize_processing_time(job_times, new_schedule, machine_idx + 1,
new_resource_avail, resource_limits, memo)
min_processing_time = min(min_processing_time, processing_time)
To use this function with resource constraints, you would need to pass in the resource_avail
and resource_limits parameters as arguments:
In this example, we assume that each machine has a maximum limit on the resources it can use,
and we pass in the resource_limits list as an argument. We also create a copy of this list to use
as resource_avail, which represents the current available resources. The function then checks if
the resources required for a job are available before adding it to the schedule, and updates the
resource_avail list accordingly. The output of this code will be the minimum processing time
subject to the resource constraints.
Chapter 6: Divide-and-conquer
Divide-and-conquer and dynamic programming are two separate algorithmic techniques that
are often used together to solve complex problems.
Divide-and-conquer is a technique that involves breaking down a problem into smaller, more
manageable subproblems, solving each subproblem independently, and then combining the
solutions of the subproblems to arrive at the final solution. This technique is often used in
algorithms such as merge sort, quicksort, and binary search.
Dynamic programming, on the other hand, is a technique for solving optimization problems by
breaking them down into smaller subproblems and solving each subproblem only once. The
solutions to the subproblems are stored and reused to solve larger subproblems, leading to a
more efficient solution.
This approach can be particularly useful for solving optimization problems that have
overlapping subproblems, as dynamic programming can help avoid redundant computation
and improve the overall efficiency of the algorithm.
Formally, let P be the problem to be solved and let {P1, P2, ..., Pn} be a set of subproblems that
partition P. The divide-and-conquer approach involves recursively solving each subproblem Pi,
either independently or by dividing it into smaller subproblems, until a base case is reached.
Let Si denote the solution to subproblem Pi.
Divide the problem into smaller subproblems using the divide-and-conquer approach.
For each subproblem, optimize the solution using dynamic programming:
a. Define the subproblem as a function that takes as input any necessary parameters.
b. Define a table to store the solutions to subproblems, with an entry for each possible
combination of parameters.
c. Set the base case of the subproblem (i.e. the smallest subproblem that can be solved directly).
d. For each subproblem with parameters (a, b), compute the solution by combining the
solutions to smaller subproblems with parameters (a - 1, b), (a, b - 1), and (a - 1, b - 1), as
necessary. Store the solution in the table.
Combine the solutions to the subproblems to obtain the final solution to the original problem.
a. Define a combining algorithm that takes as input the solutions to the subproblems and any
necessary parameters.
b. Use the combining algorithm to combine the solutions to the subproblems and obtain the
final solution.
Here is an example pseudocode implementation for finding the nth Fibonacci number using
divide-and-conquer dynamic programming:
function fibonacci(n):
// Base case
if n = 0 or n = 1:
return n
// Subproblem 1: fibonacci(n-1)
table1 = {}
function subproblem1(m):
if m = 0 or m = 1:
return m
if (m-1) not in table1:
table1[m-1] = subproblem1(m-1)
return table1[m-1]
// Subproblem 2: fibonacci(n-2)
table2 = {}
function subproblem2(m):
if m = 0 or m = 1:
return m
if (m-1) not in table2:
table2[m-1] = subproblem2(m-1)
return table2[m-1]
In this implementation, the Fibonacci sequence is broken down into smaller subproblems
(finding the (n-1)th and (n-2)th Fibonacci numbers), which are then solved using dynamic
programming. The solutions to each subproblem are stored in tables to avoid redundant
computation. The final solution is obtained by combining the solutions to the subproblems
using addition.
Divide-and-conquer dynamic programming is a problem-solving technique that can be used to
solve a wide range of problems, especially those that involve optimization and have
overlapping subproblems.
Here are some examples of problems that can be solved using divide-and-conquer dynamic
programming:
Shortest path problem: Given a weighted graph, find the shortest path between two vertices.
This problem can be solved using a variant of dynamic programming known as the Bellman-
Ford algorithm.
Knapsack problem: Given a set of items with weights and values, and a maximum weight
capacity, determine the maximum value that can be obtained by selecting a subset of the items
that fit into the knapsack. This problem can be solved using a variant of dynamic programming
known as the 0/1 knapsack problem.
Matrix multiplication: Given a set of matrices, find the optimal order in which to multiply them
to minimize the number of scalar multiplications required. This problem can be solved using
dynamic programming, where the solutions to smaller subproblems are combined to obtain
the optimal solution.
Longest common subsequence: Given two strings, find the longest subsequence that is common
to both of them. This problem can be solved using dynamic programming, where the solutions
to smaller subproblems are combined to obtain the optimal solution.
Maximum subarray problem: Given an array of numbers, find the contiguous subarray with the
largest sum. This problem can be solved using a variant of dynamic programming known as
Kadane's algorithm.
In general, divide-and-conquer dynamic programming can be used to solve any problem that
can be broken down into smaller subproblems, where the solutions to the subproblems can be
combined to obtain the optimal solution to the original problem.
--------------------------
Divide-and-conquer approach with dynamic programming used to solve Knapsack Problem
--------------------------
Here's an example scenario in where divide-and-conquer dynamic programming is used to
solve the knapsack problem:
# Example usage
W = 50 # Maximum weight capacity
wt = [10, 20, 30] # Weights of items
val = [60, 100, 120] # Values of items
n = len(val) # Number of items
In this implementation, the knapsack function takes as input the maximum weight capacity of
the knapsack (W), a list of weights of items (wt), a list of values of items (val), and the number
of items (n). The function returns the maximum value that can be obtained by selecting a
subset of the items that fit into the knapsack.
The function uses a divide-and-conquer approach to solve the problem. If the weight of the nth
item is more than the capacity of the knapsack, then the item cannot be included in the optimal
solution, and the problem is divided into a subproblem with n-1 items. Otherwise, the problem
is divided into two subproblems: one where the nth item is included in the knapsack, and one
where it is not. The solutions to the subproblems are combined to obtain the optimal solution
to the original problem.
--------------------------
Divide-and-conquer approach with dynamic programming used to solve Traveling Salesman
Problem (TSP)
--------------------------
The Traveling Salesman Problem (TSP) is an optimization problem that seeks to find the
shortest possible route that visits every city in a given list and returns to the starting city. The
problem is known to be NP-hard, but there are several heuristic and exact algorithms that can
be used to find good solutions.
import math
def tsp_dp(graph, start):
"""
This function solves the Traveling Salesman Problem using divide-and-conquer dynamic
programming.
:param graph: a distance matrix representing the graph
:param start: the index of the starting city
:return: the shortest possible route that visits every city in the graph and returns to the
starting city
"""
# Define variables
n = len(graph)
all_visited = (1 << n) - 1
memo = {}
min_cost = math.inf
# Example usage
graph = [[0, 10, 15, 20],
[10, 0, 35, 25],
[15, 35, 0, 30],
[20, 25, 30, 0]]
start = 0
In this implementation, the tsp_dp function takes as input a distance matrix representing the
graph (graph) and the index of the starting city (start). The function returns the shortest
possible route that visits every city in the graph and returns to the starting city.
The function uses a divide-and-conquer approach to solve the problem. It defines a recursive
helper function (tsp_helper) that takes as input the current city (curr) and a bitmask
representing the set of visited cities (visited). The function computes the cost of visiting all
unvisited cities starting from the current city and returns the minimum cost. The solutions to
smaller subproblems are combined to obtain the optimal solution to the original problem.
The function also uses memoization to avoid recomputing the same subproblems multiple
times. The memo dictionary is used to store the results of previously computed subproblems.
--------------------------
Divide-and-conquer approach with dynamic programming used to solve Shortest Path Problem
--------------------------
The Shortest Path Problem is another optimization problem that seeks to find the shortest path
between two nodes in a graph. There are several algorithms that can be used to solve the
problem, including Dijkstra's algorithm and Bellman-Ford algorithm. In this example scenario,
we'll use a divide-and-conquer dynamic programming approach to solve the problem.
import math
min_cost = math.inf
# Example usage
graph = [[0, 4, 3, 0, 0],
[0, 0, 0, 1, 5],
[0, 2, 0, 0, 0],
[0, 0, 0, 0, 4],
[0, 0, 0, 2, 0]]
start = 0
end = 4
The function uses a divide-and-conquer approach to solve the problem. It defines a recursive
helper function (sp_helper) that takes as input the current node (curr) and a bitmask
representing the set of visited nodes (visited). The function computes the cost of visiting all
unvisited neighbors of the current node and returns the minimum cost. The solutions to
smaller subproblems are combined to obtain the optimal solution to the original problem.
The function also uses memoization to avoid recomputing the same subproblems multiple
times. The memo dictionary is used to store the results of previously computed subproblems.
--------------------------
Divide-and-conquer approach with dynamic programming used to solve Longest Common
Subsequence Problem
--------------------------
The Longest Common Subsequence (LCS) Problem is another classic problem in computer
science that can be solved using dynamic programming. In this scenario, we'll use a divide-and-
conquer approach to solve the problem.
if i == 0 or j == 0:
return 0
if X[i-1] == Y[j-1]:
memo[(i, j)] = 1 + lcs_helper(i-1, j-1)
else:
memo[(i, j)] = max(lcs_helper(i-1, j), lcs_helper(i, j-1))
return memo[(i, j)]
# Example usage
X = "ABCBDAB"
Y = "BDCABA"
lcs_len = longest_common_subsequence_dp(X, Y)
print("Length of Longest Common Subsequence:", lcs_len)
The function uses a divide-and-conquer approach to solve the problem. It defines a recursive
helper function (lcs_helper) that takes as input two indices (i and j) representing the current
positions in the two strings. The function computes the length of the longest common
subsequence of the prefixes of the two strings that end at these positions and returns the
result.
The function also uses memoization to avoid recomputing the same subproblems multiple
times. The memo dictionary is used to store the results of previously computed subproblems.
--------------------------
Divide-and-conquer approach with dynamic programming used to solve Sequence Alignment
Problem
--------------------------
The Sequence Alignment Problem is another classic problem in computer science that can be
solved using dynamic programming. In this scenario, we'll use a divide-and-conquer approach
to solve the problem.
if i == 0 and j == 0:
memo[(i, j)] = 0, ["", ""]
elif i == 0:
score, (s1, s2) = alignment_helper(i, j-1)
memo[(i, j)] = score + gap_penalty, [s1 + "-", s2 + Y[j-1]]
elif j == 0:
score, (s1, s2) = alignment_helper(i-1, j)
memo[(i, j)] = score + gap_penalty, [s1 + X[i-1], s2 + "-"]
else:
if X[i-1] == Y[j-1]:
score, (s1, s2) = alignment_helper(i-1, j-1)
memo[(i, j)] = score + match_reward, [s1 + X[i-1], s2 + Y[j-1]]
else:
gap_score, (gap_s1, gap_s2) = alignment_helper(i, j-1)
gap_score += gap_penalty
mismatch_score, (mismatch_s1, mismatch_s2) = alignment_helper(i-1, j)
mismatch_score += mismatch_penalty
if gap_score >= mismatch_score:
memo[(i, j)] = gap_score, [gap_s1 + "-", gap_s2 + Y[j-1]]
else:
memo[(i, j)] = mismatch_score, [mismatch_s1 + X[i-1], mismatch_s2 + "-"]
# Example usage
X = "AGTACGCA"
Y = "TATGC"
The function uses a divide-and-conquer approach to solve the problem. It defines a recursive
helper function (alignment_helper) that takes as input two indices (i and j) that represent the
current positions in the two sequences being aligned.
The helper function uses memoization to store previously computed results in a dictionary
(memo). If the current alignment has already been computed, the function returns the cached
result instead of recomputing it.
If the function reaches the end of one of the sequences (i == 0 or j == 0), it returns a gap penalty
for the remaining positions in the other sequence. If the current positions in both sequences
match, the function recursively computes the score for the alignment without a penalty.
Otherwise, the function computes the score for two alternative alignments with a gap penalty
or a mismatch penalty.
Finally, the main function calls the recursive helper function with the starting positions at the
end of both sequences (len(X) and len(Y)) and reverses the aligned sequences to obtain the
correct order.
--------------------------
Divide-and-conquer approach with dynamic programming used to solve Optimal Binary Search
Tree Problem
--------------------------
The Optimal Binary Search Tree (OBST) Problem is a classic example of an optimization
problem that can be solved using dynamic programming. The goal of the problem is to
construct a binary search tree with minimum expected search cost, given the probabilities of
each key in the tree.
Suppose you have a set of keys keys and their corresponding probabilities p, where p[i] is the
probability of accessing the key keys[i]. Your task is to construct an optimal binary search tree
that minimizes the expected search cost.
import numpy as np
# Example usage
keys = ['A', 'B', 'C', 'D']
p = [0.1, 0.2, 0.4, 0.3]
cost, root = optimal_bst(keys, p)
print("Minimum expected cost:", cost)
print("Root node values:")
print(root)
In this code, we first define a function optimal_bst that takes the lists keys and p as input. The
function uses the numpy library to initialize a cost matrix cost and a root matrix root, both of
size (n+1)x(n+1) where n is the number of keys.
The cost matrix cost[i][j] represents the minimum expected search cost of the binary search
tree that is constructed using the keys keys[i:j+1] and their probabilities p[i:j+1]. The root
matrix root[i][j-1] stores the index k of the root node for the subtree that contains the keys
keys[i:j+1].
We initialize the diagonal values of the cost matrix to the probabilities of accessing the
individual keys, and the diagonal values of the root matrix to the corresponding key indices.
Next, we use a nested loop to fill in the remaining values of the cost and root matrices. The
outer loop iterates over the length l of the key set being considered (from 2 to n+1). The inner
loop iterates over the starting index i of the key set, and computes the corresponding ending
index j.
For each i and j, we initialize the cost value to infinity and iterate over all possible root indices
k between i and j-1. We compute the cost of the resulting subtree using the formula cost[i][k] +
cost[k+1][j] + sum(p[i:j+1]), which includes the costs of the left and right subtrees and the
probabilities of accessing all keys in the subtree. If the computed cost is lower than the current
minimum cost,
--------------------------
Divide-and-conquer approach with dynamic programming used to solve Maximum Subarray
Problem
--------------------------
The Maximum Subarray Problem is a classic example of a problem that can be solved using
divide-and-conquer dynamic programming. The goal of the problem is to find the contiguous
subarray within a one-dimensional array of numbers that has the largest sum.
import math
# Example usage
arr = [1, -3, 2, 1, -1]
low, high, sum = max_subarray(arr, 0, len(arr)-1)
print("Maximum subarray:", arr[low:high+1])
print("Maximum sum:", sum)
In this code, we first define a function max_subarray that takes the input array arr, and the low
and high indices of the subarray being considered. The function uses divide-and-conquer to
recursively solve the problem by computing the maximum subarrays of the left, right, and
crossing subarrays, and comparing their sums to find the largest subarray.
The base case is when the subarray has only one element, in which case the function returns
the low and high indices and the element value.
In the recursive case, the function computes the midpoint index mid and recursively calls
max_subarray on the left and right subarrays. It also calls the helper function
max_crossing_subarray to compute the maximum subarray that crosses the midpoint.
The max_crossing_subarray function takes the input array arr, and the low, mid, and high
indices of the subarray being considered. It uses a loop to iterate over the left and right
subarrays, keeping track of the maximum sum and indices of the left and right subarrays. It
then returns the indices and sum of the maximum subarray that crosses the midpoint.
Finally, the max_subarray function compares the sums of the left, right, and crossing subarrays
to determine the maximum subarray and returns its low and high indices.
In the example usage section, we create an array arr and call max_subarray on it with the low
and high indices set to the beginning and end of the array, respectively. We then print the
maximum subarray and its sum.
This code has a time complexity of O(n log n) since it uses divide-and-conquer to solve the
problem. The max_crossing_subarray function has a time complexity of O(n) since it iterates
over the left and right subarrays of size n/2. The max_subarray function has a time complexity
of T(n) = 2T(n/2) + O(n), which by the master theorem gives a time complexity of O(n log n).
The max_subarray function takes three arguments: the array arr, the index of the first element
low, and the index of the last element high. If the array contains only one element, the function
returns a tuple containing the indices of that element and its value. Otherwise, the function
recursively calls itself on the left and right halves of the array and the cross-subarray, and
returns the subarray with the maximum sum.
The max_crossing_subarray function takes three arguments: the array arr, the index of the first
element low, the index of the midpoint mid, and the index of the last element high. It finds the
maximum subarray that crosses the midpoint of the array by iterating over the left and right
halves of the array and finding the maximum sum of elements that include the midpoint.
In the example code, we define an array arr containing a sequence of numbers and call the
max_subarray function on it with the low and high indices set to the beginning and end of the
array, respectively. The function returns a tuple containing the indices of the maximum
subarray and its sum. We then print the maximum subarray and its sum.
--------------------------
Divide-and-conquer approach with dynamic programming used to solve Coin Change Problem
--------------------------
Here's an example of how to use divide-and-conquer dynamic programming to solve the coin
change problem:
In this example, the coin_change function takes two arguments: a list of coin denominations
coins and an integer amount representing the desired amount of change. The function uses a
recursive divide-and-conquer approach to find the minimum number of coins needed to make
change for the given amount.
The function first checks if the amount is 0, in which case it returns 0 (since no coins are
needed). It then checks if the list of coin denominations is empty, in which case it returns -1
(since it's impossible to make change with no coins).
If neither of these base cases are true, the function loops through each coin denomination in
the list. For each denomination that is than or equal to the remaining amount, the function
recursively calls itself on the remaining amount minus the denomination. If this recursive call
returns a valid solution (i.e., not -1), the function takes the minimum of this solution plus 1 and
the current best solution.
If no valid solutions are found, the function returns -1. Otherwise, it returns the minimum
number of coins needed to make change for the given amount.
In the example code, we define a list of coin denominations coins and an amount amount that
we want to make change for. We then call the coin_change function on these inputs and print
the minimum number of coins needed to make change for the given amount. In this case, the
output should be 3, since we can make change for 36 cents using one quarter, one dime, and
one penny.
While the recursive divide-and-conquer approach used in the coin_change function is simple
and intuitive, it can be very inefficient for large inputs. This is because the function may end up
recalculating the same subproblems multiple times, leading to an exponential increase in the
number of function calls.
To avoid this issue, we can use a dynamic programming approach to store the results of
subproblems in a table and reuse them as needed. Here's an example of how to implement a
dynamic programming solution to the coin change problem:
def coin_change(coins, amount):
if amount == 0:
return 0
n = len(coins)
if n == 0:
return -1
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for i in range(1, amount + 1):
for j in range(n):
if coins[j] <= i:
dp[i] = min(dp[i], dp[i - coins[j]] + 1)
if dp[amount] == float('inf'):
return -1
return dp[amount]
In this version of the function, we first create a list dp of length amount + 1 and initialize all
elements to infinity, except for the first element (which represents the minimum number of
coins needed to make change for an amount of 0, which is always 0).
We then use a nested loop to iterate over all possible coin denominations and all possible
amounts from 1 to amount. For each amount, we check if it's possible to make change using the
current coin denomination. If so, we use the previously computed solution for the amount i -
coins[j] and add 1 to get the solution for amount i. We take the minimum of all such solutions
to get the final solution for dp[amount].
If no valid solutions are found, the function returns -1. Otherwise, it returns the minimum
number of coins needed to make change for the given amount.
In the example code, we define a list of coin denominations coins and an amount amount that
we want to make change for. We then call the coin_change function on these inputs and print
the minimum number of coins needed to make change for the given amount. In this case, the
output should be 3, since we can make change for 36 cents using one quarter, one dime, and
one penny.
--------------------------
Divide-and-conquer approach with dynamic programming used to solve a scenario of Resource
Allocation in the real world
--------------------------
Here's an example program code of divide-and-conquer dynamic programming used to solve
a Resource Allocation problem:
weights = [2, 3, 5, 7, 9]
profits = [3, 5, 7, 9, 11]
capacity = 14
n = len(profits)
In this program, we are trying to allocate resources to different projects in a way that
maximizes the total profit. The weights represent the resource requirements of each project,
and the profits represent the potential profit from each project. The capacity variable
represents the total amount of resources available, and the n variable represents the total
number of projects.
The max_profit function takes in the weights, profits, capacity, and n variables as input. The
function uses a divide-and-conquer approach to recursively determine the maximum profit
that can be obtained. If the capacity is 0 or n is 0, then the function returns 0 because there are
no more projects to allocate resources to. If the weight of the nth project is greater than the
capacity, then the function does not allocate resources to that project and moves on to the (n-
1)th project. Otherwise, the function compares the profit obtained from allocating resources to
the nth project with the profit obtained from not allocating resources to the nth project, and
returns the maximum of the two.
In the example code above, we have a list of weights, profits, and a capacity of 14. We are trying
to determine the maximum profit that can be obtained by allocating resources to these
projects. The output of this program is 22, which represents the maximum profit that can be
obtained.
--------------------------
Divide-and-conquer approach with dynamic programming used to solve a scenario of
Inventory Management in the real world
--------------------------
Here's an example program code of divide-and-conquer dynamic programming used to solve
an Inventory Management problem:
print(max_profit(prices, n, k))
In this program, we are trying to manage inventory to maximize profits. The prices list
represents the price of a particular item over time, and the n variable represents the total
number of time periods. The k variable represents the maximum number of times we can buy
and sell the item. We want to find the maximum profit that can be obtained by buying and
selling the item.
The max_profit function takes in the prices, n, and k variables as input. The function uses a
divide-and-conquer approach to recursively determine the maximum profit that can be
obtained. If k is 0 or n is 0, then the function returns 0 because there are no more transactions
to make. The function iterates through each time period and calculates the maximum profit
that can be obtained by buying and selling the item in that time period. It does this by
comparing the profit obtained from buying and selling the item at the current time period with
the profit obtained from buying and selling the item at an earlier time period plus the
maximum profit that can be obtained from the earlier time periods. The function returns the
maximum profit obtained.
In the example code above, we have a list of prices, n = 6, and k = 2. We are trying to determine
the maximum profit that can be obtained by buying and selling the item up to two times. The
output of this program is 87, which represents the maximum profit that can be obtained.
--------------------------
Divide-and-conquer approach with dynamic programming used to solve a scenario of
Production Planning in the real world
--------------------------
Here's an example program code of divide-and-conquer dynamic programming used to solve
a Production Planning problem:
In this program, we are trying to plan production to maximize profits. The profits list
represents the potential profit from producing a particular item, and the weights list
represents the resource requirements of producing that item. The capacity variable represents
the total amount of resources available, and the n variable represents the total number of items
that can be produced.
The max_profit function takes in the profits, weights, capacity, and n variables as input. The
function uses a divide-and-conquer approach to recursively determine the maximum profit
that can be obtained. If the capacity is 0 or n is 0, then the function returns 0 because there are
no more items to produce. If the weight of the nth item is greater than the capacity, then the
function does not produce that item and moves on to the (n-1)th item. Otherwise, the function
compares the profit obtained from producing the nth item with the profit obtained from not
producing the nth item, and returns the maximum of the two.
In the example code above, we have a list of profits, weights, and a capacity of 50. We are trying
to determine the maximum profit that can be obtained by producing these items. The output of
this program is 220, which represents the maximum profit that can be obtained.
If we want to further optimize the above program, we can use dynamic programming to store
the results of previously computed subproblems. This will avoid redundant computations and
speed up the program.
Here's an example program code of dynamic programming used to solve the same Production
Planning problem:
for i in range(n+1):
for w in range(capacity+1):
if i == 0 or w == 0:
dp[i][w] = 0
elif weights[i-1] <= w:
dp[i][w] = max(profits[i-1] + dp[i-1][w-weights[i-1]], dp[i-1][w])
else:
dp[i][w] = dp[i-1][w]
return dp[n][capacity]
In this program, we are using a 2D list dp to store the maximum profit that can be obtained for
different combinations of items and capacities. We initialize the first row and column of dp to 0
because if the capacity is 0 or there are no items, then the maximum profit that can be obtained
is 0. We then fill in the remaining entries of dp using a bottom-up approach. We compare the
profit obtained from adding the nth item with the profit obtained from not adding the nth item,
and store the maximum of the two in dp[i][w].
In the example code above, we have the same inputs as before, but this time we are using
dynamic programming to solve the problem. The output of this program is still 220, which
represents the maximum profit that can be obtained. However, this program will run faster
than the previous program for larger inputs because it avoids redundant computations.
--------------------------
Divide-and-conquer approach with dynamic programming used to solve a scenario of Portfolio
Optimization in the real world
--------------------------
Here's an example program code of divide-and-conquer dynamic programming used to solve
a Portfolio Optimization problem:
print(portfolio_optimization(prices, k, n))
In this program, we are trying to optimize a portfolio of stocks. The prices list represents the
prices of each stock over a certain period of time, and the k variable represents the maximum
number of trades that can be made. The n variable represents the total number of stocks that
can be traded.
The portfolio_optimization function takes in the prices, k, and n variables as input. The function
uses a divide-and-conquer approach to recursively determine the maximum profit that can be
obtained. For each stock, the function calculates the profit that would be obtained if that stock
was bought and sold at the current price. It then recursively calculates the maximum profit that
can be obtained from the remaining stocks using one trade. The function returns the
maximum profit that can be obtained.
In the example code above, we have a list of prices, k set to 2, and n set to 6. We are trying to
determine the maximum profit that can be obtained by buying and selling stocks using a
maximum of 2 trades. The output of this program is 87, which represents the maximum profit
that can be obtained.
While the above code implements the divide-and-conquer approach to solve the portfolio
optimization problem, it suffers from exponential time complexity. This is because for each
stock, we are recursively calculating the maximum profit that can be obtained from the
remaining stocks. As the number of stocks increases, the number of recursive calls also
increases exponentially, leading to a significant increase in running time.
To optimize this program, we can use dynamic programming to store the results of previously
computed subproblems. This will avoid redundant computations and speed up the program.
Here's an example program code of dynamic programming used to solve the same Portfolio
Optimization problem:
return dp[n-1][k]
print(portfolio_optimization(prices, k))
In this program, we are using a 2D list dp to store the maximum profit that can be obtained for
different combinations of stocks and trades. We initialize the first column of dp to 0 because if
there are no trades, then the maximum profit that can be obtained is 0. We then fill in the
remaining entries of dp using a bottom-up approach. We compare the profit obtained from
buying and selling stocks at different prices, and store the maximum profit that can be obtained
for each combination of trades and stocks.
In the example code above, we have the same inputs as before, but this time we are using
dynamic programming to solve the problem. The output of this program is still 87, which
represents the maximum profit that can be obtained. However, this program will run faster
than the previous program for larger inputs because it avoids redundant computations.
--------------------------
Divide-and-conquer approach with dynamic programming used to solve a scenario of Routing
and Scheduling in the real world
--------------------------
Here's an example program code of divide-and-conquer dynamic programming used to solve
a Routing and Scheduling problem:
min_dist = float('inf')
min_node = -1
for i in range(n):
if not visited[i][time] and dist[i][time] < min_dist:
min_dist = dist[i][time]
min_node = i
if min_node != -1:
dijkstra(min_node, time)
dijkstra(source, 0)
return dist[target][time_budget]
In this program, we are trying to find the shortest path from the source node to the target node,
given a time budget. The nodes list represents the nodes in the graph, and the edges list
represents the edges and their corresponding time taken to traverse them. The source and
target variables represent the starting and ending nodes, respectively. The time_budget
variable represents the maximum amount of time allowed to reach the target node.
The routing_scheduling function takes in the nodes, edges, source, target, and time_budget
variables as input. The function uses a divide-and-conquer approach to implement Dijkstra's
algorithm. We initialize a 2D list dist to store the minimum distance to reach each node from
the source node at each time step. We also initialize a 2D list visited to keep track of the nodes
that have been visited.
The dijkstra function takes in a node and the current time as input. It updates the dist list by
considering all neighbors of the current node, and updating their distances if a shorter path is
found. The function then finds the unvisited node with the minimum distance and visits it. This
process is repeated until all nodes have been visited or until the time budget is exceeded.
In the example code above, we have a graph with 5 nodes, source set to 0, target set to 4, and
time_budget set to 10. We are trying to find the shortest path from the source node to the
target node within 10 time units. The output of this program is 10, which represents the
minimum time required to reach the target node.
In this example scenario, we can assume that the nodes represent cities, and the edges
represent the time it takes to travel between them. The time_budget variable could represent a
deadline by which the delivery of goods must be made. The program can be used to optimize
the delivery route by finding the shortest path that satisfies the time constraint.
This approach can also be used for scheduling tasks in a production plant or optimizing the
routing of vehicles in a logistics company. By using a divide-and-conquer dynamic
programming approach, we can efficiently solve these types of problems and find the optimal
solution in a reasonable amount of time.
Chapter 7: Multistage
Multistage dynamic programming (MDP) is a technique for solving optimization problems that
can be broken down into a sequence of decisions or stages, where the optimal decision at each
stage depends on the decisions made in previous stages and the current state of the system.
In MDP, the problem is divided into a series of smaller sub-problems, with each sub-problem
corresponding to a particular stage or time period. The objective is to determine the optimal
decision at each stage that maximizes a given objective function, such as minimizing costs or
maximizing profits, while taking into account the stochastic nature of the system and the
uncertainty in future outcomes.
The solution to an MDP problem involves determining a policy that specifies the optimal
decision at each stage, given the current state of the system. This can be done using a recursive
algorithm called the Bellman equation, which expresses the value of the objective function at
each stage as a function of the value at the next stage. The policy is then obtained by selecting
the optimal decision at each stage based on the values computed by the Bellman equation.
MDP is widely used in operations research, control theory, economics, and other fields to
model and solve a wide range of problems, such as production planning, inventory
management, resource allocation, and financial portfolio optimization.
The goal of MDP is to determine a policy π that maps each state to an action, such that the
expected cumulative reward over the time horizon T is maximized. That is,
π* = argmaxΣTt=1 E[ R(st,at) | π ],
where π* is the optimal policy, st is the state at time t, at is the action taken at time t, and E[
R(st,at) | π ] is the expected cumulative reward from time t to time T, given the policy π.
The solution to an MDP can be obtained using dynamic programming algorithms, such as value
iteration or policy iteration, which compute the optimal value function and policy iteratively.
Input:
A set of states S
A set of actions A
A state transition probability function P(s'|s,a)
A reward function R(s,a)
A time horizon T
Output:
An optimal policy π*
Initialization:
For all s in S and t = T, set V*(s, t) = 0
In this algorithm, we first initialize the optimal value function V*(s, t) to zero for all states s and
the last time period T. Then, we use the Bellman equation to recursively compute the optimal
value function V*(s, t) for earlier time periods, based on the values obtained at the next time
period t+1. Finally, we extract the optimal policy π*(s, t) at each time period t by selecting the
action that maximizes the expression in the Bellman equation for the given state s.
This algorithm can be used to solve a wide range of decision-making problems, such as
production planning, inventory management, and financial portfolio optimization.
Multistage dynamic programming (MDP) is a powerful technique that can be used to solve a
wide range of decision-making problems in various fields, such as operations research, control
theory, economics, and finance.
Resource allocation: MDP can be used to determine the optimal allocation of resources, such as
labor, capital, and equipment, over multiple time periods to maximize profits or minimize
costs.
Production planning: MDP can be used to optimize production plans over multiple periods,
taking into account the uncertainty in demand and the costs associated with production,
inventory, and backlog.
Inventory management: MDP can be used to optimize inventory policies over multiple periods,
considering the trade-off between holding costs, ordering costs, and stockouts.
Financial portfolio optimization: MDP can be used to optimize investment strategies over
multiple periods, taking into account the risk-return trade-off, transaction costs, and market
dynamics.
Energy management: MDP can be used to optimize energy consumption and production over
multiple periods, considering the trade-off between energy costs, environmental impact, and
energy storage capacity.
Control systems: MDP can be used to design optimal control policies for dynamic systems over
multiple time periods, taking into account the system dynamics and the constraints on the
control inputs.
In general, MDP is suitable for problems that involve sequential decision-making under
uncertainty and where the current decision affects future outcomes. By formulating such
problems as MDPs and solving them using dynamic programming algorithms, we can obtain
optimal policies that maximize the expected cumulative reward over multiple periods, subject
to the constraints of the problem.
--------------------------
Multistage dynamic programming used to solve Knapsack Problem
--------------------------
Here is a scenario with program code of Multistage dynamic programming used to solve
Knapsack Problem:
Suppose you are a thief who has broken into a jewelry store. You have a knapsack that can hold
a maximum weight of 10 units. You want to maximize the value of the jewelry you can carry in
your knapsack, but you have to be careful not to exceed the weight limit.
# Define a function to solve the knapsack problem using multistage dynamic programming
def multistage_knapsack(items, max_weight):
# Initialize a table to store the maximum value of the knapsack for each subproblem
table = [[0 for _ in range(max_weight + 1)] for _ in range(len(items) + 1)]
# Iterate over the weights of the knapsack for the current item
for w in range(max_weight + 1):
# If the item is too heavy to fit in the knapsack, skip it
if item_weight > w:
table[i][w] = table[i - 1][w]
# Otherwise, consider taking the item
else:
# Calculate the value of taking the item
value_with_item = item_value + table[i - 1][w - item_weight]
# Calculate the value of not taking the item
value_without_item = table[i - 1][w]
# Take the maximum value of the two options
table[i][w] = max(value_with_item, value_without_item)
In this code, the items list contains tuples representing the items in the jewelry store, where
the first element is the name of the item, the second element is the weight of the item, and the
third element is the value of the item. The max_weight variable specifies the maximum weight
that the knapsack can hold.
This means that the thief should take the gold and platinum items, which have a combined
value of 60, but leave the diamond and silver items behind.
The function iterates over the stages of the problem (i.e., the items) and the weights of the
knapsack for each item. For each subproblem, it calculates the maximum value of the knapsack
that can be achieved and stores it in the table variable.
The if statement in the inner loop of the function checks if the current item can fit into the
knapsack with the current weight constraint. If it can't, the function skips the item and moves
on to the next item. If it can, the function considers two options: taking the item or not taking
the item.
If the thief takes the item, the value of the item is added to the maximum value of the knapsack
that can be achieved with the remaining items and weight constraint. This value is stored in the
value_with_item variable.
If the thief doesn't take the item, the maximum value of the knapsack that can be achieved with
the remaining items and weight constraint is stored in the value_without_item variable.
The function then takes the maximum value of the two options and stores it in the table
variable.
Once the function has finished iterating over all the items and weight constraints, it returns the
maximum value of the knapsack that can be achieved with all the items and the maximum
weight constraint.
This means that the thief should take the gold and platinum items, which have a combined
value of 60, but leave the diamond and silver items behind.
--------------------------
Multistage dynamic programming used to solve Traveling Salesman Problem (TSP)
--------------------------
Here's a scenario with program code of Multistage dynamic programming used to solve the
Traveling Salesman Problem (TSP):
Suppose you are a traveling salesman who wants to visit a set of cities and return to your
starting city. You want to find the shortest possible route that visits each city exactly once.
Here's the code:
import math
# Store the shortest route in the table for the current subproblem and end city
table[subproblem][end_city] = shortest_route
# Calculate the shortest route from the starting city back to itself
shortest_route = math.inf
for end_city in all_cities - {"A"}:
dist = distance(end_city, "A")
route = table[tuple(sorted(list(all_cities - {"A"})))][end_city] + dist
shortest_route = min(shortest_route, route)
In this code, the cities dictionary contains the coordinates of the cities, where the keys are the
city names and the values are tuples representing the x and y coordinates. The distance
function calculates the Euclidean distance between two cities using their coordinates.
The multistage_tsp function uses multistage dynamic programming to solve the TSP. It first
defines a set of all the cities and initializes a table to store the shortest routes for each
subproblem. It then defines the base case for the subproblem with no cities, which is the
distance from city A back to itself.
The function then iterates over the stages of the problem (i.e., the number of cities) and over all
subsets of the cities with i elements. For each subset, it initializes the subproblem with the
current subset of cities and iterates over all possible end cities for the subproblem. It then
initializes the shortest route to infinity and iterates over all possible second-to-last cities for
the subproblem. For each second-to-last city, it calculates the distance from the second-to-last
city to the end city and the shortest route to the end city through the second-to-last city. It
takes the minimum of the current shortest route and the new route and stores the result in the
table for the current subproblem and end city.
Finally, the function calculates the shortest route from the starting city back to itself by
iterating over all possible end cities and taking the minimum of the distance from the end city
back to city A plus the shortest route for the subproblem with all cities except city A.
The shortest_route variable contains the result of calling the multistage_tsp function, and it is
printed out at the end of the code.
Note that this implementation assumes that the TSP is symmetric, i.e., the distance from city A
to city B is the same as the distance from city B to city A. If the TSP is not symmetric, you would
need to modify the distance function accordingly.
Also note that this implementation has a time complexity of O(n^2 * 2^n), where n is the
number of cities. This is because there are n stages and 2^n subproblems at each stage. For
each subproblem, we need to iterate over all possible end cities and second-to-last cities, which
takes O(n^2) time. Therefore, the overall time complexity is O(n^2 * 2^n).
However, this implementation uses memoization to avoid redundant calculations, which can
significantly speed up the algorithm for larger instances of the TSP.
This means that the shortest possible route that visits all cities exactly once and returns to city
A is 7.23606797749979 units long.
Note that this solution is only guaranteed to find the optimal solution for small instances of the
TSP. For larger instances, other techniques such as branch and bound or genetic algorithms
may be more effective.
--------------------------
Multistage dynamic programming used to solve Shortest Path Problem
--------------------------
The Shortest Path Problem can be solved using the Multistage Dynamic Programming
approach. The following is an example program that implements this approach:
import math
# Find the shortest distance from the start vertex to the end vertex
start_vertex = 0
end_vertex = 4
shortest_distance = table[n-1][end_vertex]
print("The shortest distance from vertex", start_vertex, "to vertex", end_vertex, "is",
shortest_distance)
In this example, we have defined the graph as an adjacency matrix, where graph[i][j] is the
distance from vertex i to vertex j. We have also defined the number of stages in the problem,
which is the number of vertices in the graph.
We then initialize the base case for the subproblem with no vertices, which is the distance from
each vertex to itself (which is 0).
We then iterate over the stages of the problem and over all subsets of the vertices with k
elements. For each subset, we initialize the subproblem with the current subset of vertices and
iterate over all possible end vertices for the subproblem. We then initialize the shortest
distance to infinity and iterate over all possible second-to-last vertices for the subproblem. For
each second-to-last vertex, we calculate the distance from that vertex to the end vertex through
the subproblem and take the minimum of the current shortest distance and the new distance.
We then store the shortest distance in the table for the current subproblem and end vertex.
Finally, we find the shortest distance from the start vertex to the end vertex by looking up the
value in the table for the final subproblem and end vertex.
However, this implementation uses memoization to avoid redundant calculations, which can
significantly speed up the algorithm for larger graphs.
Note that this solution is only guaranteed to find the optimal solution for graphs with non-
negative edge weights. For graphs with negative edge weights, the Bellman-Ford algorithm can
be used to find the shortest path.
--------------------------
Multistage dynamic programming used to solve Sequence Alignment Problem
--------------------------
The Sequence Alignment Problem is a classical problem in bioinformatics that involves aligning
two sequences of DNA or protein to find the best matching between them. The Multistage
Dynamic Programming approach can be used to solve this problem efficiently. Here's an
example program that implements the Multistage Dynamic Programming approach to solve
the Sequence Alignment Problem:
import numpy as np
# Fill in the rest of the table using the Multistage Dynamic Programming approach
for i in range(1, n + 1):
for j in range(1, m + 1):
match = T[i - 1][j - 1] + (match_reward if s1[i - 1] == s2[j - 1] else mismatch_penalty)
delete = T[i - 1][j] + gap_penalty
insert = T[i][j - 1] + gap_penalty
T[i][j] = max(match, delete, insert)
# Example usage
s1 = "AGTACGCA"
s2 = "TATGC"
gap_penalty = -2
mismatch_penalty = -1
match_reward = 1
In this example, we define a function sequence_alignment that takes two sequences s1 and s2,
as well as three penalties/rewards: gap_penalty (penalty for inserting a gap),
mismatch_penalty (penalty for mismatching two characters), and match_reward (reward for
matching two characters). The function returns the alignment score (which is the maximum
alignment score), as well as the two aligned sequences.
The function first creates a table T of size (n+1) x (m+1) to store the scores for all subproblems,
where n is the length of s1 and m is the length of s2. We initialize the first row and first column
of the table with the appropriate gap penalties.
We then use a nested loop to fill in the rest of the table using the Multistage Dynamic
Programming approach. At each subproblem (i,j), we consider three possible ways to align the
two sequences:
We can match s1[i] with s2[j]. The score for this alignment is the score of the best alignment of
the two prefixes s1[1:i] and s2[1:j] plus the match reward if s1[i] and s2[j] match, or the
mismatch penalty if they do not match.
We can insert a gap in s1. The score for this alignment is the score of the best alignment of the
two prefixes s1[1:i] and s2[1:j] plus the gap penalty.
We can insert a gap in s2. The score for this alignment is the score of the best alignment of the
two prefixes s1[1:i] and s2[1:j] plus the gap penalty.
We take the maximum of these three possible scores as the score for the current subproblem
(i,j).
Once we have filled in the entire table, we can backtrack from the bottom-right corner to the
top-left corner to find the optimal alignment. We start at (n,m) and repeatedly follow the
direction with the maximum score until we reach (0,0). At each step, we add the aligned
characters to the aligned sequences s1_aligned and s2_aligned, or a gap if one sequence is
shorter than the other.
Finally, we return the alignment score as well as the two aligned sequences.
Note that this implementation assumes that the input sequences consist of only uppercase
letters. If the input sequences can contain lowercase letters or other characters, you may need
to modify the code to handle this case.
--------------------------
Multistage dynamic programming used to solve Longest Common Subsequence Problem
--------------------------
Here's an example program that uses Multistage Dynamic Programming to solve the Longest
Common Subsequence (LCS) Problem:
s1 = 'ABCDGH'
s2 = 'AEDFHR'
length, lcs = lcs(s1, s2)
print(f"Length of LCS: {length}")
print(f"LCS: {lcs}")
Output:
Length of LCS: 3
LCS: ADH
In this example, the longest common subsequence between the strings s1 and s2 is ADH, which
has a length of 3. The lcs function correctly finds this LCS using the Multistage Dynamic
Programming approach.
Here's an example program that uses Multistage Dynamic Programming to solve the Sequence
Alignment Problem:
# Fill the first row and first column with gap penalties
for i in range(1, m+1):
dp[i][0] = dp[i-1][0] + gap_penalty
for j in range(1, n+1):
dp[0][j] = dp[0][j-1] + gap_penalty
This function takes two input strings s1 and s2, along with optional parameters for the match
score, mismatch penalty, and gap penalty. It returns the optimal alignment score and the
aligned strings.
To use this function, we can simply call it with the input strings:
s1 = 'AGCTAGC'
s2 = 'TCAGACG'
score, align1, align2 = sequence_alignment(s1, s2)
print(f"Optimal alignment score: {score}")
print(f"Aligned string 1: {align1}")
print(f"Aligned string 2: {align2}")
Output:
In this example, the optimal alignment score between the strings s1 and s2 is 4, which is
achieved by aligning the G and C characters in the first and second positions of s1 and s2,
respectively. The resulting aligned strings are AGCTA-GC and TCA-GAC-.
dp[i][j] = max(
dp[i-1][j-1] + (match_score if s1[i-1] == s2[j-1] else mismatch_penalty),
dp[i-1][j] + gap_penalty,
dp[i][j-1] + gap_penalty
)
This relation computes the optimal alignment score between prefixes of s1 and s2 that end at
positions i and j, respectively. The three terms in the max function correspond to three possible
alignment operations: (1) matching the characters at positions i and j, (2) inserting a gap in s1,
or (3) inserting a gap in s2. The function takes the maximum of these three possibilities, and
stores the result in dp[i][j].
Finally, the function performs a traceback from the bottom-right corner of dp to the top-left
corner, to find the aligned strings that achieve the optimal score. The traceback follows a
similar logic to the recurrence relation, but in reverse order. If the characters at positions i and
j match, they are included in both aligned strings. Otherwise, a gap is inserted in either s1 or s2,
depending on the values of dp[i-1][j], dp[i][j-1], and dp[i-1][j-1].
--------------------------
Multistage dynamic programming used to solve Optimal Binary Search Tree Problem
--------------------------
Here's an example scenario with program code for solving the Optimal Binary Search Tree
(OBST) problem using multistage dynamic programming:
Suppose we have a set of n keys keys = [k1, k2, ..., kn], and we want to build a binary search
tree for these keys such that the expected search cost is minimized. We are also given a set of
probabilities p = [p1, p2, ..., pn] such that p[i] is the probability of searching for key ki. We
assume that the keys are sorted in ascending order, so k1 is the smallest key and kn is the
largest.
The goal is to find the optimal binary search tree, which is a binary tree that satisfies the
following properties:
Define a dp matrix of dimensions (n+2) x (n+1). The first dimension represents the stages of
the multistage problem, and the second dimension represents the keys. The first stage is a
dummy stage representing the empty subtree, and the last stage is the complete subtree
containing all the keys. The dummy stage contains only one node with a cost of zero. The last
stage contains all the keys and their probabilities.
Initialize the dp matrix as follows:
Set dp[i][i-1] = 0 for all i between 1 and n+1. This represents an empty subtree containing no
keys.
Set dp[i][i] = p[i] for all i between 1 and n. This represents a subtree containing only one key
with probability p[i].
Fill the dp matrix from the second-last stage (n keys) to the first stage (empty subtree) using
the following recurrence relation:
dp[i][j] = min(
dp[i][k-1] + dp[k+1][j] + sum(p[i:j+1])
for k in range(i, j+1)
)
This relation computes the minimum expected search cost for a subtree containing keys ki to
kj, given the optimal subtrees for the left and right children of the root. The minimum cost is
the sum of the costs of the left and right subtrees, plus the cost of accessing the root k. The
function takes the minimum of all possible values of k, and stores the result in dp[i][j].
4. The optimal binary search tree can be reconstructed by performing a traceback from the
dp[1][n] cell to the dp[1][1] cell. At each stage i, we select the key k that minimizes dp[i][k-1] +
dp[k+1][j] + sum(p[i:j+1]), and make it the root of the subtree. The left and right subtrees are
then constructed recursively using the optimal subtrees for the left and right children of the
root.
Here's the code for solving the OBST problem using multistage dynamic programming:
# Fill dp matrix
for d in range(1, n+1):
for i in range(1, n-d+2):
j=i+d-1
dp[i][j] = float('inf')
for k in range(i, j+1):
cost = dp[i][k-1] + dp[k+1][j] + sum(p[i-1:j])
if cost < dp[i][j]:
dp[i][j] = cost
Here, keys is the list of keys in ascending order, and p is the list of probabilities for each key.
The function returns the minimum expected search cost and the root of the optimal binary
search tree. The root is represented as a 2D matrix root, where root[i][j] is the index of the root
node for the subtree containing keys ki to kj.
Note that the time complexity of this algorithm is O(n^3), since we need to fill a n x n matrix for
each stage of the multistage problem. However, this is still much faster than the naive approach
of trying all possible binary search trees, which has a time complexity of O(2^n).
--------------------------
Multistage dynamic programming used to solve Maximum Subarray Problem
--------------------------
The Maximum Subarray Problem is a classic problem in computer science that asks us to find
the contiguous subarray with the largest sum. Multistage dynamic programming can be used to
solve this problem efficiently, with a time complexity of O(n^2).
Here is an example program that uses multistage dynamic programming to solve the
Maximum Subarray Problem:
def max_subarray(nums):
n = len(nums)
dp = [[0] * n for _ in range(n)]
# Find the maximum sum and the start and end indices of the subarray
max_sum = float('-inf')
start, end = 0, 0
for i in range(n):
for j in range(i, n):
if dp[i][j] > max_sum:
max_sum = dp[i][j]
start, end = i, j
Here, nums is a list of integers representing the input array. The program initializes a two-
dimensional matrix dp with zeros and fills in the upper-right half of the matrix using the
recurrence relation:
This relation computes the maximum sum of any subarray that ends at index j. The program
then finds the maximum sum and the start and end indices of the subarray by iterating over the
entire matrix.
Note that the time complexity of this algorithm is O(n^2), since we need to fill an n x n matrix.
This is much faster than the brute-force approach, which has a time complexity of O(n^3).
--------------------------
Multistage dynamic programming used to solve Coin Change Problem
--------------------------
The Coin Change Problem is a classic problem in computer science that asks us to find the
minimum number of coins needed to make a certain amount of money. Multistage dynamic
programming can be used to solve this problem efficiently, with a time complexity of O(nm),
where n is the number of coins and m is the target amount of money.
Here is an example program that uses multistage dynamic programming to solve the Coin
Change Problem:
if dp[amount] == float('inf'):
return -1
else:
return dp[amount]
Here, coins is a list of integers representing the denominations of the coins, and amount is an
integer representing the target amount of money. The program initializes a one-dimensional
array dp with infinity values and sets the value of dp[0] to 0.
The program then iterates over the denominations of the coins and fills in the array dp using
the recurrence relation:
This relation computes the minimum number of coins needed to make the amount j. The
program then returns dp[amount], which represents the minimum number of coins needed to
make the target amount of money.
Note that the time complexity of this algorithm is O(nm), since we need to fill an array of size
amount + 1 for each of the n denominations. This is much faster than the brute-force approach,
which has a time complexity of O(m^n).
To illustrate how the program works, let's consider an example. Suppose we have a set of coins
{1, 5, 10, 25} and we want to make change for 63 cents. We can call the coin_change function
with these inputs:
The program returns 6, which means we need 6 coins to make 63 cents using the given
denominations. The program achieves this by computing the minimum number of coins
needed to make each amount between 1 and 63, using the recurrence relation:
For example, to make 63 cents, we can either use a quarter (25 cents) and a dime (10 cents) or
three quarters and a dime. The program chooses the first option since it results in fewer coins.
--------------------------
Multistage dynamic programming used to solve a scenario of Resource Allocation in the real
world
--------------------------
Multistage dynamic programming is a powerful optimization technique used in many real-
world scenarios, including resource allocation. Here's an example program code that uses
multistage dynamic programming to solve a resource allocation problem.
Suppose we have a company that needs to allocate resources to different projects over three
stages. There are three projects, and the company has three resources: A, B, and C. Each project
requires a certain amount of each resource, and the goal is to maximize the total profit earned
from the projects.
In this program code, we first define the resource allocation problem by specifying the projects,
resources, resource requirements for each project, and profits for each project. We then define
the multistage_dp function, which implements the multistage dynamic programming
algorithm. The algorithm solves the subproblems for each stage in reverse order, starting with
the final stage and working backwards. For each subproblem, the algorithm computes the
optimal value and decision by considering all possible resource allocations for the current
project and taking into account the decisions made in the next stage. Finally, the algorithm
returns the optimal value and decisions for each stage.
In the main program, we run the multistage_dp function and print the optimal value and
decision for the first stage of the first project, which gives us the optimal resource allocation for
that project in the first stage.
The product function used in the algorithm comes from the itertools module in and generates
all possible combinations of resource allocations for a given project. The float("-inf") value is
used to initialize the maximum value to negative infinity so that any calculated value will be
greater than it. The None value for the decision variable indicates that no decision has been
made yet for that subproblem.
To use this program code for a different resource allocation problem, you would need to
modify the projects, resources, requirements, and profits variables to match your problem. You
may also need to modify the algorithm to account for any additional constraints or objectives
in your problem.
Suppose we have a company that needs to manage its inventory over three stages. The
company produces a single product and has a fixed capacity for production and storage. The
goal is to maximize the profit earned from selling the product while avoiding stockouts and
minimizing storage costs.
In this program code, we first define the inventory management problem by specifying the
production capacity, demand for each stage, production costs, selling prices, and storage costs.
We then define the multistage_dp function, which implements the multistage dynamic
programming algorithm. The algorithm solves the subproblems for each stage in reverse order,
starting with the final stage and working backwards. For each subproblem, the algorithm
computes the optimal value and decision by considering all possible production and sales
decisions and taking into account the decisions made in the next stage. Finally, the algorithm
returns the optimal value and decisions for each stage.
In the main program, we run the multistage_dp function and print the optimal value and
decision for the first stage, which gives us the optimal production and sales decisions for each
stage. In this example, the optimal decision for the first stage is the maximum possible
production that satisfies the demand while minimizing storage costs.
To use this program code for a different inventory management problem, you would need to
modify the capacity, demand, production_costs, selling_prices, and storage_costs variables to
match your problem. You may also need to modify the algorithm to account for any additional
constraints or objectives in your problem.
In this program code, we first define the production planning problem by specifying the
production capacity, demands for each product at each stage, production costs, selling prices,
and storage costs. We then define the multistage_dp function, which implements the multistage
dynamic programming algorithm. The algorithm solves the subproblems for each stage in
reverse order, starting with the final stage and working backwards. For each subproblem, the
algorithm computes the optimal value and decision by considering all possible production and
sales decisions for both products and taking into account the decisions made in the next stage.
Finally the program code prints the optimal value and decision for the first stage, which
represents the optimal production and sales decisions for the first period.
The multistage_dp function uses two dictionaries, value and decision, to store the optimal value
and decision for each subproblem. The value dictionary stores the maximum profit that can be
earned given the current state (i.e., the remaining production capacity) and the stage. The
decision dictionary stores the optimal production and sales decisions that lead to the
maximum profit for the current state and stage.
In each subproblem, the algorithm considers all possible production and sales decisions for
both products and computes the corresponding profit. It then adds the profit from the current
stage to the optimal profit from the next stage (which is already stored in the value dictionary)
and subtracts the storage costs to obtain the total profit. The algorithm selects the production
and sales decisions that lead to the highest total profit and stores them in the decision
dictionary.
Finally, the program code prints the optimal value and decision for the first stage, which
represent the optimal production and sales decisions for the first period. The optimal value
and decision for each subsequent stage can be obtained by looking up the corresponding
entries in the value and decision dictionaries.
This is just an example of how multistage dynamic programming can be used to solve
production planning problems. The specific implementation details may vary depending on the
problem and its constraints. However, the basic approach of breaking the problem into smaller
subproblems and solving them in reverse order remains the same.
--------------------------
Multistage dynamic programming used to solve a scenario of Portfolio Optimization in the real
world
--------------------------
Here's an example implementation of multistage dynamic programming for portfolio
optimization:
import numpy as np
In this example, we consider a portfolio optimization problem where we have two assets with
known expected returns and covariance matrix, and we want to find the optimal portfolio
allocation over a horizon of three time periods. Each period, we can rebalance our portfolio by
buying or selling assets, subject to a transaction cost. The objective is to maximize the expected
utility of our portfolio over the entire horizon, where the utility function takes into account the
expected return, risk, and transaction costs.
The program code defines the state space as all possible combinations of asset holdings at each
period, and the decision space as all possible combinations of buy/sell quantities for each asset
at each period. It then uses multistage dynamic programming to find the optimal portfolio
allocation over the entire horizon by breaking the problem into subproblems and solving them
in reverse order. At each stage, the algorithm considers all possible decisions and computes the
corresponding expected utility, discounted by the risk-free rate and added to the expected
utility from the next stage. It then chooses the decision that maximizes the total expected
utility.
The program code outputs the optimal decision for the first stage, which tells us how to
allocate our initial wealth into the two assets. The output will be in the form of a tuple (x, y),
where x is the quantity to buy/sell of asset 1 and y is the quantity to buy/sell of asset 2.
Note that this is just one example of how multistage dynamic programming can be used for
portfolio optimization, and there are many other variations and extensions of this problem that
may require different formulations and algorithms.
In addition to the example provided earlier, here is another implementation of multistage
dynamic programming for portfolio optimization in that uses a different formulation and
algorithm:
import numpy as np
from scipy.optimize import minimize
In this implementation, we still consider a portfolio optimization problem with two assets and
transaction costs, but we use a different formulation of the problem that directly optimizes for
the portfolio weights rather than the quantities to buy/sell. Specifically, at each stage, we solve
a quadratic optimization problem to find the optimal portfolio weights that maximize the
expected utility over the remaining periods, subject to the transaction costs and portfolio
constraints. We then compute the corresponding value function and expected utility for each
decision, and choose the decision that maximizes the total expected utility.
The program code outputs the optimal portfolio weights for the first stage, which tells us how
to allocate our initial wealth into the two assets. The output will be in the form of a numpy
array with two elements, where the first element is the weight for asset 1 and the second
element is the weight for asset 2.
Note that this implementation uses a quadratic optimization solver from the scipy library to
find the optimal portfolio weights, which may be more efficient and accurate than the linear
optimization solver used in the previous example. However, this also means that the
formulation and algorithm may be more complex and difficult to implement, especially for
larger and more complex portfolio optimization problems.
--------------------------
Multistage dynamic programming used to solve a scenario of Routing and Scheduling in the
real worlda
--------------------------
Here is an implementation of multistage dynamic programming for routing and scheduling in .
In this example, we consider a simplified scenario where a delivery company needs to plan the
routes and schedules for a fleet of vehicles to serve a set of customers over a fixed time
horizon. The objective is to minimize the total delivery cost, which includes vehicle operating
cost, customer waiting cost, and overtime cost.
import numpy as np
The dynamic programming algorithm starts at the terminal stage (T) and works backwards to
the initial stage (0), computing the value function for each state at each stage. At each stage, the
optimal policy and value are computed for each state using the Bellman equation. Finally, the
optimal policy and value at the initial stage are extracted and printed.
Note that this implementation uses a simplified model and does not consider many real-world
factors that may affect the routing and scheduling problem, such as traffic conditions, vehicle
capacity constraints, and customer preferences. A more realistic model would need to
incorporate these factors and may require more sophisticated algorithms and techniques.
This implementation uses a state space consisting of tuples (i, stops), where i is the index of the
vehicle (0 for idle) and stops is a tuple of indices of the customers visited so far, including the
depot (index 0). The decision space consists of tuples (j, k), where j and k are indices of the
customers. The transition function takes a state and a decision as input and returns the next
state. The cost function takes a state and a decision as input and returns the cost of the
decision.
The dynamic programming algorithm starts at the terminal stage (T) and works backwards to
the initial stage (0), computing the value function for each state at each stage. At each stage, the
optimal policy and value are computed for each state using the Bellman equation. Finally, the
optimal policy and value at the initial stage are extracted and printed.
Note that this implementation uses a simplified model and does not consider many real-world
factors that may affect the routing and scheduling problem, such as traffic conditions, vehicle
capacity constraints, and customer preferences. A more realistic model would need to
incorporate these factors and may require more sophisticated algorithms and techniques.
Chapter 8: Convex
Convex dynamic programming refers to a class of optimization problems that involve making a
sequence of decisions over time, subject to constraints, with the goal of minimizing a convex
objective function.
The term "convex" refers to the property that the objective function is a convex function,
meaning that it has a single minimum point and any two points on the function lie below the
line connecting them.
Convex dynamic programming has numerous applications in fields such as control theory,
finance, and operations research, where it is used to model complex decision-making problems
and optimize outcomes over time.
Applications
• Finance: Portfolio optimization over time with transaction costs.
• Energy Systems: Scheduling renewable energy generation and storage.
• Robotics: Path planning with dynamic constraints.
• Supply Chain: Multi-stage inventory control under uncertainty.
Convex dynamic programming can be used to solve a wide range of optimization problems that
involve making optimal decisions over a sequence of time periods, subject to constraints and
under uncertainty. Some examples of problems that can be solved by convex dynamic
programming include:
Portfolio optimization: Given a set of assets with uncertain returns over time, find the optimal
portfolio allocation that maximizes the expected return while controlling for risk.
Control of dynamical systems: Given a dynamic system with state variables that evolve over
time, find the optimal control inputs that minimize a cost function while satisfying system
constraints.
Resource allocation: Given a set of resources that can be allocated over time, find the optimal
allocation that maximizes a utility function subject to capacity constraints.
Inventory management: Given a set of inventory items that have uncertain demand over time,
find the optimal ordering policy that minimizes the expected inventory cost while satisfying
demand constraints.
Energy management: Given a power system with uncertain renewable energy supply and
varying demand, find the optimal dispatch of energy resources that minimizes the cost of
energy production while satisfying demand and supply constraints.
In general, any problem that can be modeled as a sequence of decisions over time, subject to
constraints and uncertainty, and where the objective function and constraints are convex, can
be solved using convex dynamic programming.
--------------------------
Convex optimization with dynamic programming to solve Knapsack Problem
--------------------------
Here's an example code that uses convex optimization and dynamic programming to solve the
Knapsack Problem:
import numpy as np
import cvxpy as cp
def knapsack_dp_convex(values, weights, capacity):
n = len(values)
dp = np.zeros((n + 1, capacity + 1))
x = cp.Variable(n, integer=True)
objective = cp.Maximize(cp.sum(cp.multiply(x, values)))
constraints = [cp.sum(cp.multiply(x, weights)) <= capacity]
prob = cp.Problem(objective, constraints)
prob.solve()
solution = np.zeros(n)
for i in range(n):
if x.value[i] > 0.5:
solution[i] = 1
This code uses dynamic programming to compute the maximum value achievable in the
knapsack for each possible capacity, up to the maximum capacity. Then, it uses convex
optimization to find the combination of items that maximizes the value, subject to the
constraint that the total weight does not exceed the capacity.
The function returns a tuple containing the maximum value achievable in the knapsack and a
list of 0s and 1s indicating which items are included in the optimal solution (1 if included, 0
otherwise).
Note that this code uses the CVXPY package for convex optimization. You may need to install it
using pip install cvxpy before running this code.
--------------------------
Convex optimization with dynamic programming to solve Traveling Salesman Problem (TSP)
--------------------------
The Traveling Salesman Problem (TSP) is a well-known optimization problem in computer
science. While dynamic programming is a common method for solving the TSP, convex
optimization can also be used to obtain approximate solutions to the problem. Here's an
example code that uses convex optimization with dynamic programming to solve the TSP:
import numpy as np
import cvxpy as cp
def tsp_dp_convex(distances):
n = len(distances)
dp = np.zeros((1 << n, n))
parent = np.zeros((1 << n, n), dtype=int)
for i in range(1, 1 << n):
for j in range(n):
if i == (1 << j):
dp[i][j] = 0
elif i & (1 << j) != 0:
for k in range(n):
if k != j and i & (1 << k) != 0:
if dp[i ^ (1 << j)][k] + distances[k][j] > dp[i][j]:
dp[i][j] = dp[i ^ (1 << j)][k] + distances[k][j]
parent[i][j] = k
start_node = 0
tour = [start_node]
visited = 1 << start_node
for i in range(1, n):
best_next = -1
for j in range(n):
if visited & (1 << j) == 0 and (best_next == -1 or dp[visited][best_next] +
distances[best_next][start_node] > dp[visited][j] + distances[j][start_node]):
best_next = j
tour.append(best_next)
visited |= 1 << best_next
solution = np.round(x.value).astype(int)
tour_len = sum(distances[tour[i - 1]][tour[i]] for i in range(n))
distances: a square matrix containing the distances between each pair of cities.
The function returns a tuple containing the length of the optimal tour and the order of the cities
in the tour.
This code uses dynamic programming to compute the length of the shortest tour that visits
each city exactly once and ends at a particular starting city (in this case, city 0). Then, it uses
convex optimization to find a binary matrix x indicating which edges are included in the
optimal tour, subject to constraints that ensure that each city is visited exactly once and that
the tour is a Hamiltonian cycle. Finally, it reconstructs the optimal tour from the binary matrix
x.
Note that this code uses the CVXPY package for convex optimization. You may need to install it
using pip install cvxpy before running this code.
--------------------------
Convex optimization with dynamic programming to solve Shortest Path Problem
--------------------------
The shortest path problem is a classic problem in computer science and can be solved using
various algorithms. Convex optimization is one such algorithm that can be used to solve the
shortest path problem approximately. Here's an example code that uses convex optimization
with dynamic programming to solve the shortest path problem:
import numpy as np
import cvxpy as cp
path = [end_node]
while path[-1] != start_node:
path.append(parent[path[-1]])
path.reverse()
x = cp.Variable(n, boolean=True)
objective = cp.Minimize(cp.sum(cp.multiply(x, dp)))
constraints = [
x[start_node] == 1,
x[end_node] == 0,
]
for i in range(n):
for j in range(n):
if adj_matrix[i][j] > 0:
constraints.append(x[i] - x[j] <= 0)
The function returns a tuple containing the length of the shortest path and the list of nodes in
the shortest path.
This code uses dynamic programming to compute the length of the shortest path from the
starting node to all other nodes in the graph. Then, it uses convex optimization to find a binary
vector x indicating which nodes are included in the shortest path, subject to constraints that
ensure that the path starts at the starting node and ends at the ending node, and that the path
does not violate the directed edges of the graph. Finally, it reconstructs the shortest path from
the binary vector x.
Note that this code uses the CVXPY package for convex optimization. You may need to install it
using pip install cvxpy before running this code.
--------------------------
Convex optimization with dynamic programming to solve Longest Common Subsequence
Problem
--------------------------
The longest common subsequence problem is a classic problem in computer science and can be
solved using dynamic programming. Convex optimization can also be used to solve this
problem approximately. Here's an example code that uses convex optimization with dynamic
programming to solve the longest common subsequence problem:
import numpy as np
import cvxpy as cp
lcs_len = int(np.round(prob.value))
lcs = ""
i, j = m, n
while i > 0 and j > 0:
if np.round(x[i,j].value) == 1:
lcs = s1[i-1] + lcs
i, j = i-1, j-1
elif np.round(x[i-1,j].value) == 1:
i -= 1
else:
j -= 1
The function returns a tuple containing the length of the longest common subsequence of s1
and s2 and the longest common subsequence itself.
This code uses dynamic programming to compute the length of the longest common
subsequence of s1 and s2. Then, it uses convex optimization to find a binary matrix x indicating
which characters are included in the longest common subsequence, subject to constraints that
ensure that the subsequence is a common subsequence of s1 and s2. Finally, it reconstructs the
longest common subsequence from the binary matrix x.
Note that this code uses the CVXPY package for convex optimization. You may need to install it
using pip install cvxpy before running this code.
--------------------------
Convex optimization with dynamic programming to solve Sequence Alignment Problem
--------------------------
The sequence alignment problem is a classic problem in bioinformatics and can be solved using
dynamic programming. Convex optimization can also be used to solve this problem
approximately. Here's an example code that uses convex optimization with dynamic
programming to solve the sequence alignment problem:
import numpy as np
import cvxpy as cp
alignment_score = int(np.round(prob.value))
alignment_s1, alignment_s2 = "", ""
i, j = m, n
while i > 0 or j > 0:
if i > 0 and j > 0 and np.round(x[i,j].value) == np.round(x[i-1,j-1].value + (s1[i-1] == s2[j-
1])):
alignment_s1 = s1[i-1] + alignment_s1
alignment_s2 = s2[j-1] + alignment_s2
i, j = i-1, j-1
elif i > 0 and np.round(x[i,j].value) == np.round(x[i-1,j].value + 1):
alignment_s1 = s1[i-1] + alignment_s1
alignment_s2 = "-" + alignment_s2
i -= 1
else:
alignment_s1 = "-" + alignment_s1
alignment_s2 = s2[j-1] + alignment_s2
j -= 1
import numpy as np
import cvxpy as cp
bst_cost = int(np.round(dp[1][n]))
bst_structure = [(0, 0)]
for i in range(1, n+1):
for j in range(i, n+1):
if np.round(x[i,j].value) == 1:
bst_structure.append((i, j))
break
import numpy as np
import cvxpy as cp
def maximum_subarray_dp_convex(arr):
n = len(arr)
dp = np.zeros(n)
dp[0] = arr[0]
for i in range(1, n):
dp[i] = max(dp[i-1] + arr[i], arr[i])
max_sum = int(np.round(max(dp)))
x = cp.Variable(n, boolean=True)
objective = cp.Maximize(cp.sum(cp.multiply(arr, x)))
constraints = [
cp.sum(x) >= 1,
]
prob = cp.Problem(objective, constraints)
prob.solve()
max_subarray = []
for i in range(n):
if np.round(x[i].value) == 1:
max_subarray.append(arr[i])
Variables: a binary variable x_i for each element arr_i of the input array.
Objective: maximize the sum of arr_i * x_i over all i.
Constraints: at least one variable x_i must be set to 1.
The solution to this optimization problem gives us an approximate subarray that has the same
sum as the maximum subarray.
--------------------------
Convex optimization with dynamic programming to solve Coin Change Problem
--------------------------
The coin change problem can be solved using dynamic programming. Convex optimization can
also be used to solve this problem approximately. Here's an example code that uses convex
optimization with dynamic programming to solve the coin change problem:
import numpy as np
import cvxpy as cp
x = cp.Variable(n, integer=True)
objective = cp.Minimize(cp.sum(x))
constraints = [
cp.sum(cp.multiply(coins, x)) == amount,
x >= 0,
]
prob = cp.Problem(objective, constraints)
prob.solve(solver='GLPK_MI')
num_coins = []
for i in range(n):
if np.round(x[i].value) > 0:
num_coins.append(int(np.round(x[i].value)))
min_coins: the minimum number of coins required to change the given amount of money.
num_coins: a list of integers representing the number of each coin denomination required to
change the given amount of money.
The function first computes the minimum number of coins required using dynamic
programming, then solves an approximate convex optimization problem to find a set of coins
that have the same value as the minimum number of coins. The optimization problem is
formulated as follows:
Variables: an integer variable x_i for each coin denomination coins_i.
Objective: minimize the sum of x_i over all i.
Constraints: the sum of coins_i * x_i over all i must equal amount, and x_i must be non-negative
integers.
The solution to this optimization problem gives us an approximate set of coins that have the
same value as the minimum number of coins. Note that we use the solver GLPK_MI for this
problem, which is a mixed-integer linear programming solver.
If the problem size is small, then we can use the exact solver CBC instead of GLPK_MI. Here's
the modified code that uses CBC:
import numpy as np
import cvxpy as cp
x = cp.Variable(n, integer=True)
objective = cp.Minimize(cp.sum(x))
constraints = [
cp.sum(cp.multiply(coins, x)) == amount,
x >= 0,
]
prob = cp.Problem(objective, constraints)
prob.solve(solver='CBC')
num_coins = []
for i in range(n):
if np.round(x[i].value) > 0:
num_coins.append(int(np.round(x[i].value)))
In this modified code, we use the CBC solver for the optimization problem. This solver is a
mixed-integer linear programming solver that can solve the problem exactly if the problem
size is small. The rest of the code is the same as the previous version.
--------------------------
Dynamic programming to solve an example scenario of Resource Allocation in the real world
--------------------------
Here's an example scenario of Resource Allocation in the real world and a code snippet using
convex optimization with dynamic programming to solve it:
Scenario: A manufacturing company has limited resources and wants to allocate them
optimally to maximize profits. The company has two products, Product A and Product B, with
the following production costs and revenues:
The company has a limited budget of $100,000 for production costs and wants to produce at
least 10,000 units of each product.
import cvxpy as cp
import numpy as np
# Define constraints
constraints = [
cp.sum(costs * x) <= budget,
x >= min_units
]
In this code, we first define the problem parameters, including the budget, minimum units
required, and production costs and revenues for each product. We then define the decision
variables, objective function, and constraints using the CVXPY library.
Next, we use dynamic programming to solve the problem. We loop over all possible
combinations of units for each product, and for each combination that satisfies the budget
constraint, we solve the optimization problem using the CVXPY solver. We keep track of the
maximum profit achieved across all feasible solutions.
After computing the optimal profit, we can also print the optimal production levels for each
product using the x.value attribute of the decision variables. We convert the values to integers
using the int() function for readability.
import cvxpy as cp
import numpy as np
# Define problem parameters
budget = 100000
min_units = 10000
costs = np.array([10, 15])
revenues = np.array([30, 40])
num_products = len(costs)
# Define constraints
constraints = [
cp.sum(costs * x) <= budget,
x >= min_units
]
Note that this is just one possible way to solve the resource allocation problem using convex
optimization with dynamic programming. There may be other formulations or algorithms that
are more efficient or accurate depending on the specific constraints and objectives of the
problem.
--------------------------
Dynamic programming to solve aan example scenario of Inventory Management in the real
world
--------------------------
Here's an example scenario of Inventory Management in the real world and a code snippet
using convex optimization with dynamic programming to solve it:
Scenario: A retail store wants to optimize its inventory management by minimizing holding
costs and stockouts. The store sells a single product with a demand that follows a normal
distribution with mean 100 units and standard deviation 20 units. The store can order the
product at a cost of $10 per unit and sell it for $20 per unit. The store has a holding cost of $1
per unit per week and a backordering cost of $5 per unit per week. The store wants to ensure
that the probability of a stockout in any given week is than 5%.
import cvxpy as cp
import numpy as np
from scipy.stats import norm
# Define constraints
prob_stockout = norm(mean_demand, std_demand).sf(x)
constraints = [prob_stockout <= 1 - service_level]
In this code, we first define the problem parameters, including the mean and standard
deviation of demand, order cost, selling price, holding cost, backordering cost, and desired
service level. We then define the decision variables, objective function, and constraints using
the CVXPY library.
Next, we use dynamic programming to solve the problem. We loop over a range of possible
order quantities centered around the mean demand, and for each order quantity, we solve the
optimization problem using the CVXPY solver. We keep track of the minimum cost achieved
across all feasible solutions.
Finally, we print the optimal cost and order quantity per week.
Note that we use the norm() function from the scipy.stats library to compute the probability of
a stockout for a given order quantity. The sf() method of the normal distribution object
computes the survival function, which is equal to 1 minus the cumulative distribution function
(CDF), or the probability of the random variable being greater than the given quantity.
import cvxpy as cp
import numpy as np
from scipy.stats import norm
Define constraints
min_cost = np.inf
for i in range(mean_demand - 3 * std_demand, mean_demand + 3 * std_demand + 1):
obj = cp.Minimize(order_cost * i + holding_cost * cp.maximum(i - mean_demand, 0) +
backordering_cost * cp.maximum(mean_demand - i, 0))
prob = cp.Problem(obj, constraints)
prob.solve(solver=cp.GLPK_MI)
if prob.status == 'optimal':
min_cost = min(min_cost, prob.value)
In this code, we first define the problem parameters, including the mean and standard
deviation of demand, order cost, selling price, holding cost, backordering cost, and desired
service level. We then define the decision variables, objective function, and constraints using
the CVXPY library.
Next, we use dynamic programming to solve the problem. We loop over a range of possible
order quantities centered around the mean demand, and for each order quantity, we solve the
optimization problem using the CVXPY solver. We keep track of the minimum cost achieved
across all feasible solutions.
Finally, we print the optimal cost and order quantity per week.
Note that we use the `norm()` function from the `scipy.stats` library to compute the probability
of a stockout for a given order quantity. The `sf()` method of the normal distribution object
computes the survival function, which is equal to 1 minus the cumulative distribution function
(CDF), or the probability of the random variable being greater than the given quantity.
This code assumes that the demand is normally distributed, which may not always be the case
in real-world scenarios. Additionally, this code only considers a single product and does not
account for inventory carrying costs such as storage and handling fees. To address these
limitations, more complex inventory management models can be developed using techniques
such as stochastic programming or simulation.
--------------------------
Dynamic programming to solve an example scenario of Production Planning in the real world
--------------------------
Here's an example of how you can use convex optimization and dynamic programming to solve
a production planning problem in the real world using and CVXPY:
import numpy as np
import cvxpy as cp
# Define constraints
constraints = [cp.sum(production, axis=1) <= production_capacity]
In this code, we first define the problem parameters, including the number of periods, number
of products, production capacity, holding cost, backordering cost, and demand. We then define
the decision variables, objective function, and constraints using the CVXPY library.
Next, we use dynamic programming to solve the problem. We loop over each period and for
each period, we solve the optimization problem using the CVXPY solver. We keep track of the
minimum cost achieved across all feasible solutions.
Note that we assume that the production and demand are integer-valued, which may not
always be the case in real-world scenarios. Additionally, this code only considers a single
production facility and does not account for multiple facilities or supply chain dynamics. To
address these limitations, more complex production planning models can be developed using
techniques such as mixed-integer linear programming or simulation.
--------------------------
Dynamic programming to solve an example scenario of Portfolio Optimization in the real world
--------------------------
Here's an example of how you can use convex optimization and dynamic programming to solve
a portfolio optimization problem in the real world using and CVXPY:
import numpy as np
import cvxpy as cp
# Define constraints
constraints = [cp.sum(weights, axis=1) == 1,
weights >= 0,
weights <= 1]
In this code, we first define the problem parameters, including the number of periods, number
of assets, risk aversion, mean returns, and covariance matrix. We then define the decision
variables, objective function, and constraints using the CVXPY library.
Next, we use dynamic programming to solve the problem. We loop over each period and for
each period, we solve the optimization problem using the CVXPY solver. We keep track of the
maximum utility achieved across all feasible solutions.
Note that we assume that the returns and covariance matrix are known and fixed over time,
which may not always be the case in real-world scenarios. Additionally, this code only
considers a single portfolio and does not account for transaction costs or market impact. To
address these limitations, more complex portfolio optimization models can be developed using
techniques such as stochastic programming or machine learning.
--------------------------
Dynamic programming to solve an example scenario of Routing and Scheduling in the real
world
--------------------------
Here's an example of how you can use convex optimization and dynamic programming to solve
a routing and scheduling problem in the real world using and CVXPY:
import numpy as np
import cvxpy as cp
# Define constraints
constraints = [cp.sum(x[:, 0, :], axis=0) == 1,
cp.sum(x[:, -1, :], axis=0) == 1,
cp.sum(x, axis=0) == cp.sum(x, axis=1),
cp.sum(cp.multiply(x, demand.reshape((-1, 1, 1))), axis=0) <= max_vehicle_capacity,
u[0, :] == 0,
u[-1, :] >= cp.sum(cp.multiply(x[:, -1, :], service_time), axis=0),
u[1:-1, :] >= cp.sum(cp.multiply(x[:, 1:-1, :], service_time), axis=0),
u[:, :] >= 0,
u[:, :] <= total_time]
In this code, we first define the problem parameters, including the number of nodes, number of
vehicles, maximum vehicle capacity, travel time, demand, and service time. We then define the
decision variables, objective function, and constraints using the CVXPY library.
Next, we use dynamic programming to solve the problem. We loop over each node and for each
node, we solve the optimization problem using the CVXPY solver. We keep track of the
minimum total time achieved across all feasible solutions.
Finally, we print the optimal total time and route plan for each vehicle.
Note that we assume that the travel time, demand, and service time are known and fixed, which
may not always be the case in real-world scenarios. Additionally, this code only considers a
single depot and does not account for stochastic demand or other uncertainties that may arise
in real-world routing and scheduling problems.
To extend this code to handle stochastic demand or other uncertainties, you may need to use
techniques such as scenario-based optimization, robust optimization, or stochastic
programming.
Overall, this code provides a basic framework for solving routing and scheduling problems
using convex optimization and dynamic programming. With some modifications and additions,
it can be applied to a variety of real-world scenarios, such as vehicle routing, scheduling of
aircraft, and scheduling of manufacturing processes.
Another important consideration in routing and scheduling problems is the scalability of the
algorithm. In this example, we use dynamic programming to solve the problem, which is
computationally feasible for small problem sizes. However, for larger problem sizes, the
number of states in the dynamic programming algorithm can quickly become intractable.
To address this issue, we can use other algorithms that are more scalable, such as column
generation, branch and price, or metaheuristic algorithms such as genetic algorithms or
simulated annealing. These algorithms can generate high-quality solutions in a reasonable
amount of time, even for large problem sizes.
In addition, it is important to note that the objective function and constraints in the routing and
scheduling problem may need to be modified to reflect the specific requirements and goals of
the problem. For example, in some scenarios, minimizing the total travel time may not be the
primary objective. Instead, the goal may be to minimize the number of vehicles used, maximize
the number of customers served, or minimize the total distance traveled.
Overall, solving routing and scheduling problems using convex optimization and dynamic
programming is a powerful technique that can be applied to a wide range of real-world
scenarios. By understanding the problem requirements and goals, and using appropriate
algorithms and optimization techniques, we can generate high-quality solutions that meet the
needs of the problem at hand.
Chapter 9: Parallel
Let N be the number of processors or computing nodes available for parallel processing.
Divide the set of subproblems S into N subsets, S1, S2, ..., SN.
Assign each subset to a processor or computing node.
Each processor solves the subproblems in its assigned subset independently using the dynamic
programming algorithm.
When a processor completes the solution to a subproblem, it broadcasts the value of the
solution to all other processors.
Each processor updates its own solution with the new information received from other
processors.
Repeat steps 4 and 5 until all subproblems have been solved and the optimal solution V(P) is
obtained.
The parallel dynamic programming algorithm aims to minimize the total time needed to solve
the problem P by distributing the workload among multiple processors and enabling them to
work in parallel. The algorithm can be used to solve a wide range of optimization problems
that can be formulated as dynamic programming problems.
1. Divide the set of subproblems S into N subsets, S1, S2, ..., SN.
2. Assign each subset to a processor or computing node.
// Initialization phase
3. For each subset Si, initialize its subproblems' values and boundaries as follows:
For each subproblem s in Si:
V(s) = infinity // initialize the value of the subproblem to infinity
If s has no subproblems within the current subset, set the boundary conditions for s
// Computation phase
4. Repeat the following steps until all subproblems have been solved:
a. Each processor solves the subproblems in its assigned subset independently using dynamic
programming algorithm
b. When a processor completes the solution to a subproblem s, it broadcasts the value of V(s)
to all other processors
c. Each processor updates its own solution with the new information received from other
processors
6. When all subproblems have been solved, the optimal solution V(P) can be obtained by
combining the solutions of the subproblems.
Note that the specific details of the dynamic programming algorithm will depend on the
problem being solved, and the broadcast and update steps will depend on the parallel
processing system being used. However, this pseudocode provides a general framework for
implementing a parallel dynamic programming algorithm.
Parallel dynamic programming can be used to solve a wide range of optimization problems
that can be formulated as dynamic programming problems. Some examples of problems that
can be solved using parallel dynamic programming include:
Shortest Path Problem: Given a graph with weighted edges, find the shortest path between two
vertices. This problem can be solved using dynamic programming, where the shortest path to
each vertex is computed using the solutions to subproblems involving smaller subgraphs.
Parallel dynamic programming can be used to speed up the computation of these subproblems.
Knapsack Problem: Given a set of items with weights and values, and a knapsack with a
capacity, find the subset of items that maximizes the total value while staying within the
knapsack's capacity. This problem can be solved using dynamic programming, where the
optimal solution to each subproblem involves deciding whether to include the current item or
not. Parallel dynamic programming can be used to speed up the computation of these
subproblems.
Resource Allocation Problem: Given a set of resources with limited capacity, allocate them to a
set of tasks in a way that maximizes the total value of the completed tasks. This problem can be
solved using dynamic programming, where the optimal allocation of resources to each task is
computed using the solutions to subproblems involving smaller sets of tasks. Parallel dynamic
programming can be used to speed up the computation of these subproblems.
Sequence Alignment Problem: Given two sequences of characters, find the optimal way to align
them based on a scoring function that assigns a score to each possible alignment. This problem
can be solved using dynamic programming, where the optimal alignment of each pair of
substrings is computed using the solutions to subproblems involving smaller substrings.
Parallel dynamic programming can be used to speed up the computation of these subproblems.
These are just a few examples of the many problems that can be solved using parallel dynamic
programming. The technique is particularly useful for solving problems that involve a large
number of subproblems, as it allows the workload to be distributed among multiple processors
and enables them to work in parallel, thereby reducing the overall computation time.
--------------------------
Parallel dynamic programming used to solve Knapsack Problem
--------------------------
Here's an example scenario of parallel dynamic programming used to solve the Knapsack
problem using and the multiprocessing library:
Suppose we have a list of n items, each with a weight w[i] and a value v[i]. We want to
determine the maximum value we can obtain by selecting a subset of these items such that the
total weight does not exceed a given capacity C.
We can use dynamic programming to solve this problem by building up a table dp where
dp[i][j] represents the maximum value we can obtain by considering the first i items and
having a maximum capacity of j.
To solve this problem in parallel, we can split up the table dp into multiple sub-tables and
assign each sub-table to a separate process. Each process will work independently to fill in its
assigned sub-table, and then the results can be combined to obtain the final solution.
import multiprocessing
n = len(w)
num_procs = multiprocessing.cpu_count()
chunk_size = n // num_procs
procs = []
proc = multiprocessing.Process(target=knapsack_dp_subtable,
args=(w, v, C, start_i, end_i, dp))
proc.start()
procs.append(proc)
# Combine the results from each sub-table to obtain the final solution
return dp[n-1][C]
To use this function, we can simply pass in the weights w, values v, and capacity C as
arguments:
w = [3, 2, 4, 1]
v = [4, 3, 6, 2]
C=7
max_value = knapsack_dp_parallel(w, v, C)
print("Maximum value:", max_value)
This will output Maximum value: 10, which is the correct answer for this example problem.
In the above code, the knapsack_dp_subtable function is a helper function that fills in a sub-
table of the dp table for a range of items from start_i to end_i. This function is called by each
process to work independently on its assigned sub-table.
The knapsack_dp_parallel function is the main function that solves the knapsack problem using
parallel dynamic programming. It first determines the number of processors available using
multiprocessing.cpu_count(), and then splits the work into chunks of size chunk_size for each
process to handle. Each process is started with the multiprocessing.Process class and assigned
its own range of items to work on using the start_i and end_i arguments.
The shared dp table is created using the multiprocessing.Manager() class, which creates a
proxy object that can be accessed by all processes. The list function is used to initialize the dp
table as a list of lists, with n rows and C+1 columns.
After starting all the processes with proc.start(), the join() method is called on each process to
wait for it to finish before continuing. Once all processes have finished, the final solution is
obtained by combining the results from each sub-table, which is simply the value in dp[n-1][C].
Note that this implementation assumes that the problem size is large enough to benefit from
parallelization. For small problem sizes, the overhead of starting multiple processes may
actually make the algorithm slower. Additionally, parallel dynamic programming may not
always be faster than the sequential version, as the communication overhead between
processes can become a bottleneck for larger problem sizes. Therefore, it's always important to
benchmark and compare the performance of different implementations to determine the most
efficient solution.
--------------------------
Parallel dynamic programming used to solve Traveling Salesman Problem (TSP)
--------------------------
The Traveling Salesman Problem (TSP) is a classic optimization problem that asks to find the
shortest possible route that visits a given set of cities exactly once and returns to the starting
city. The problem is known to be NP-hard, which means that it is computationally infeasible to
solve it optimally for large instances.
One way to approximate the solution to the TSP is to use dynamic programming, which
involves solving a series of sub-problems and combining their solutions to obtain the final
result. To speed up the solution process for large instances of TSP, we can use parallel dynamic
programming.
Here's an example scenario of parallel dynamic programming used to solve the TSP using and
the multiprocessing library:
Suppose we have a list of n cities, with distances between each pair of cities given by a distance
matrix dist. We want to find the shortest possible route that visits each city exactly once and
returns to the starting city.
To solve this problem using parallel dynamic programming, we can use a variant of the Held-
Karp algorithm, which involves building up a table dp where dp[S][i] represents the shortest
possible route that starts at the starting city, visits all cities in the set S, and ends at city i.
To solve this problem in parallel, we can split up the table dp into multiple sub-tables and
assign each sub-table to a separate process. Each process will work independently to fill in its
assigned sub-table, and then the results can be combined to obtain the final solution.
n = len(dist)
for mask in range(start_set, end_set):
S = set([i for i in range(n) if mask & (1<<i)])
for i in S:
if i == start_city or mask == (1<<start_city):
dp[mask][i] = 0
else:
dp[mask][i] = float('inf')
for j in S:
if j != i:
dp[mask][i] = min(dp[mask][i], dp[mask ^ (1<<i)][j] + dist[j][i])
n = len(dist)
num_procs = multiprocessing.cpu_count()
chunk_size = (1 << (n-1)) // num_procs
procs = []
proc = multiprocessing.Process(target=tsp_dp_subtable,
args=(dist, start_city, start_set, end_set, dp))
proc.start()
procs.append(proc)
# Combine the results from each sub-table to obtain the final solution
min_dist = float('inf')
for j in range(n):
if j != start_city
min_dist = min(min_dist, dp[(1 << n) - 1][j] + dist[j][start_city])
return min_dist
In this implementation, the `tsp_dp_subtable` function is a helper function that fills in a sub-
table of the `dp` table for a range of city sets from `start_set` to `end_set`. This function is called
by each process to work independently on its assigned sub-table.
The `tsp_dp_parallel` function is the main function that solves the TSP using parallel dynamic
programming. It first determines the number of processors available using
`multiprocessing.cpu_count()`, and then splits the work into chunks of size `chunk_size` for
each process to handle. Each process is started with the `multiprocessing.Process` class and
assigned its own range of city sets to work on using the `start_set` and `end_set` arguments.
The shared `dp` table is created using the `multiprocessing.Manager()` class, which creates a
proxy object that can be accessed by all processes. The `list` function is used to initialize the
`dp` table as a list of lists, with `2^n` rows and `n` columns.
After starting all the processes with `proc.start()`, the `join()` method is called on each process
to wait for it to finish before continuing. Once all processes have finished, the final solution is
obtained by combining the results from each sub-table, which is simply the minimum value of
`dp[(1 << n) - 1][j] + dist[j][start_city]` over all `j` not equal to the starting city.
Note that this implementation assumes that the problem size is large enough to benefit from
parallelization. For small problem sizes, the overhead of starting multiple processes may
actually make the algorithm slower. Additionally, parallel dynamic programming may not
always be faster than the sequential version, as the communication overhead between
processes can become a bottleneck for larger problem sizes. Therefore, it's always important to
benchmark and compare the performance of different implementations to determine the most
efficient solution.
Here's an example of how to use the tsp_dp_parallel function to solve a TSP problem:
import numpy as np
In this example, we create a distance matrix for a 5-city problem and pass it to the
tsp_dp_parallel function to obtain the minimum distance for a Hamiltonian cycle that visits all
cities exactly once. The output should be:
Minimum distance: 32
Note that the exact solution for this problem is known to be 32, so the parallel dynamic
programming implementation is correct.
--------------------------
Parallel dynamic programming used to solve Shortest Path Problem
--------------------------
Here's an example of how to use parallel dynamic programming to solve the Shortest Path
Problem using the Floyd-Warshall algorithm in :
import numpy as np
import multiprocessing
def shortest_path_parallel(dist):
n = dist.shape[0]
# Initialize dp table
for i in range(n):
for j in range(n):
dp[i][j] = dist[i][j]
# Perform Floyd-Warshall algorithm in parallel
num_procs = multiprocessing.cpu_count()
chunk_size = n // num_procs
for k in range(n):
processes = []
for i in range(0, n, chunk_size):
start_row = i
end_row = min(i + chunk_size, n)
proc = multiprocessing.Process(target=shortest_path_subtable,
args=(dp, start_row, end_row, k))
proc.start()
processes.append(proc)
The num_procs variable determines the number of processors available, and chunk_size
determines the number of rows each process will handle. The shortest_path_subtable function
is called by each process to compute its assigned sub-table independently.
To use this function, we can create a distance matrix dist and call shortest_path_parallel(dist)
to obtain the shortest path distances between all pairs of vertices in the graph. Here's an
example:
Here's an example of how to use parallel dynamic programming to solve the Shortest Path
Problem using the Bellman-Ford algorithm:
import numpy as np
import multiprocessing
pred = manager.list([None] * n)
The relax_edges function takes a graph represented as a list of adjacency lists, a vertex i, and
shared dist and pred arrays as input, and updates the dist and pred arrays if a shorter path to
vertex j through vertex i is found.
After all edges have been relaxed n-1 times, the function checks for negative cycles in the graph
by iterating over all edges again. If a shorter path is found, a ValueError is raised to indicate the
presence of a negative cycle.
To use this function, we can create a graph represented as a list of adjacency lists and call
shortest_path_parallel(graph, start) to obtain the shortest path distances and predecessors for
all vertices in the graph starting from the specified start vertex. Here's an example:
path.reverse()
print(f"Shortest path from {start} to {i}: {dist[i]} with path {path}")
In this example, we define a weighted directed graph as a list of adjacency lists and use the
shortest_path_parallel function to compute the shortest path distances and predecessors for all
vertices starting from vertex 0. The output should be:
This output shows that the shortest path distances and predecessors have been correctly
computed using parallel dynamic programming with the Bellman-Ford algorithm.
--------------------------
Parallel dynamic programming used to solve Sequence Alignment Problem
--------------------------
Here's an example of how to use parallel dynamic programming to solve the Sequence
Alignment Problem using the Needleman-Wunsch algorithm:
import numpy as np
import multiprocessing
if max_score == match:
traceback[i][j] = 0
elif max_score == delete:
traceback[i][j] = 1
else:
traceback[i][j] = 2
The fill_score_matrix function takes two characters, the indices i and j, shared score and
traceback matrices, and the match, mismatch, and gap penalties as input, and fills in the score
matrix and traceback matrix using the Needleman-Wunsch algorithm.
After the score and traceback matrices have been filled in, the function performs a traceback to
find the optimal alignment of the two sequences. The function returns the optimal alignment
score and the aligned sequences.
To use this function, we can provide two sequences as input and call
sequence_alignment_parallel(seq1, seq2) to obtain the optimal alignment score and the aligned
sequences. Here's an example:
seq1 = "AGTACGCA"
seq2 = "TATGC"
--------------------------
Parallel dynamic programming used to solve Optimal Binary Search Tree Problem
--------------------------
Here's an example of how to use parallel dynamic programming to solve the Optimal Binary
Search Tree Problem:
import numpy as np
import multiprocessing
# Build the optimal binary search tree using the root matrix
root_node = build_optimal_bst(0, n-1, root)
if i == j:
cost[i][j] = freqs[i]
root[i][j] = i
return
min_cost = float("inf")
min_root = -1
cost[i][j] = min_cost
root[i][j] = min_root
if i == j:
return (root[i][j], None, None)
k = root[i][j]
left = build_optimal_bst(i, k-1, root)
right = build_optimal_bst(k+1, j, root)
In this implementation, the optimal_bst_parallel function creates shared cost and root matrices
using the multiprocessing.Manager() class, initializes the cost matrix with 0 values, and fills in
the cost and root matrices in parallel using the fill_cost_matrix function.
The fill_cost_matrix function takes two indices, the keys, the frequencies, shared cost and root
matrices as input, and fills in the cost matrix and root matrix using the dynamic programming
algorithm for computing the optimal binary search tree.
After the cost and root matrices have been filled in, the function calls the build_optimal_bst
function to build the optimal binary search tree using the root matrix. The function returns the
cost of the optimal binary search tree and the root node of the tree.
To use this function, we can provide the keys and frequencies of the nodes as input and call
optimal_bst_parallel(keys, freqs) to obtain the cost of the optimal binary search tree and the
root node of the tree. Here's an example:
Compute the optimal binary search tree using parallel dynamic programming
Print the optimal binary search tree cost and the root node of the tree
This shows that the parallel dynamic programming implementation correctly computes the
cost of the optimal binary search tree and the root node of the tree for the given keys and
frequencies. The root node represents the root of the binary search tree with the value of the
node at index 2, the left subtree rooted at index 0, and the right subtree rooted at index 3.
--------------------------
Parallel dynamic programming used to solve Maximum Subarray Problem
--------------------------
Here's a scenario with program code of parallel dynamic programming used to solve the
Maximum Subarray Problem:
Suppose we have an array of numbers as follows:
We want to find the contiguous subarray within this array that has the largest sum using
parallel dynamic programming.
We can start by defining the following function to compute the maximum subarray sum for a
given range of indices:
import numpy as np
This function uses a dynamic programming approach to compute the maximum subarray sum
for a given range of indices. It creates a matrix M of size (n, n) where n is the length of the
subarray, and fills it with the maximum subarray sum for all subarrays that start and end at
different positions within the given range.
Next, we can use parallel processing to speed up the computation of maximum subarray sum
for different ranges of indices. We can define a parallel function max_subarray_sum_parallel
that uses the Pool class from the multiprocessing module to spawn multiple processes to
compute the maximum subarray sum for different ranges of indices:
from multiprocessing import Pool
This function uses a Pool object to spawn num_processes processes, and computes the
maximum subarray sum for all possible subarrays by calling the max_subarray_sum function
for different ranges of indices. The results are stored in a list of AsyncResult objects, and the
final maximum subarray sum is computed by taking the maximum of all the results.
We can now call the max_subarray_sum_parallel function to compute the maximum subarray
sum for the given array:
max_sum = max_subarray_sum_parallel(arr)
print("Maximum Subarray Sum:", max_sum)
This shows that the parallel dynamic programming implementation correctly computes the
maximum subarray sum for the given array.
--------------------------
Parallel dynamic programming used to solve Coin Change Problem
--------------------------
Here's a scenario with program code of parallel dynamic programming used to solve the Coin
Change Problem:
We want to find the minimum number of coins required to make a total of 100 using parallel
dynamic programming.
We can start by defining the following function to compute the minimum number of coins
required to make a given total:
import numpy as np
This function uses a dynamic programming approach to compute the minimum number of
coins required to make a given total. It creates a matrix M of size (n, total+1) where n is the
number of coin denominations, and fills it with the minimum number of coins required for all
possible subtotals from 0 to the given total. The base cases are set as follows:
If the total is 0, then the minimum number of coins required is 0 for all denominations.
If the total is not divisible by the smallest coin denomination, then it is not possible to make the
total using the smallest coin denomination alone. Hence, we set the minimum number of coins
required as infinity for all subtotals that are not divisible by the smallest coin denomination.
Next, we can use parallel processing to speed up the computation of minimum number of coins
required for different subtotals. We can define a parallel function coin_change_parallel that
uses the Pool class from the multiprocessing module to spawn multiple processes to compute
the minimum number of coins required for different subtotals:
This function uses a Pool object to spawn num_processes processes, and computes the
minimum number of coins required for all possible subtotals by calling the coin_change
function for different subtotals. The results are stored in a list of AsyncResult objects, and the
final minimum number of coins required is computed by taking the minimum of all the results.
We can now call the coin_change_parallel function to compute the minimum number of coins
required to make a total of 100:
This shows that the parallel dynamic programming implementation correctly computes the
minimum number of coins required to make a total of 100 using the given coin denominations.
Moreover, using parallel processing can significantly speed up the computation for large values
of total, especially when the number of coin denominations is also large.
However, it is important to note that parallel dynamic programming may not always result in a
significant speedup, especially when the computation is not computationally expensive or the
overhead of communication between processes is high. Therefore, it is important to carefully
consider the problem and the resources available before deciding to use parallel dynamic
programming.
--------------------------
Parallel dynamic programming used to solve a scenario of Resource Allocation in the real
world
--------------------------
Here's an example program that implements parallel dynamic programming to solve the
resource allocation problem.
In this scenario, imagine that you are the manager of a software development team with a
limited amount of resources (such as time, money, and personnel) to allocate to different
projects. Your goal is to maximize the overall value of the projects completed by your team
while staying within your resource constraints.
To solve this problem using dynamic programming, we'll define a state as a tuple (i, r), where i
is the index of the project being considered and r is the amount of resources remaining. The
value function V(i, r) represents the maximum value that can be achieved by allocating
resources to the first i projects, given that we have r resources remaining.
import numpy as np
import multiprocessing as mp
In this code, num_projects is the number of projects being considered, resource_limits is a list
of the maximum amount of resources that can be allocated to each project, and project_values
is a 2D array of the value of completing each project for each possible amount of resources.
The states list defines all possible combinations of projects and remaining resources, and the V
dictionary stores the maximum value that can be achieved for each state.
The calculate_value function takes a state as input and returns the maximum value that can be
achieved from that state, using the previous values in the V dictionary. This function is used in
parallel with the multiprocessing library to calculate the values for all states simultaneously.
Finally, the code finds the optimal allocation of resources by backtracking through the V
dictionary and choosing the resource allocation that leads to the maximum value at each step.
The resulting allocation is returned along with the maximum value that can be achieved.
num_projects = 3
resource_limits = [5, 3, 2]
project_values = [ [0, 2, 4, 4, 4, 4],
[0, 0, 1, 4, 4, 4],
[0, 0, 0, 1, 4, 4]
]
In this scenario, we have 3 projects with different resource requirements and potential values.
We want to allocate resources to maximize the overall value while staying within the resource
limits.
Maximum value: 9
Resource allocation: [2, 1, 0]
This indicates that the optimal allocation of resources is 2 units for project 1, 1 unit for project
2, and 0 units for project 3, resulting in a maximum value of 9.
Note that this implementation uses parallel processing to speed up the calculation of the values
for all states. However, this may not always be necessary or desirable depending on the size of
the problem and the available hardware resources. In some cases, a serial implementation may
be sufficient or more appropriate.
--------------------------
Parallel dynamic programming used to solve a scenario of Inventory Management in the real
world
--------------------------
Here's an example program that implements parallel dynamic programming to solve the
inventory management problem.
In this scenario, imagine that you are a retailer who sells a certain product. You need to
determine how much of the product to order each week to meet customer demand while
minimizing costs. Each week, you can order a certain amount of the product at a fixed cost, and
you must pay a certain cost for each unit of unsatisfied demand. Your goal is to minimize the
total cost over a certain time period.
To solve this problem using dynamic programming, we'll define a state as a tuple (i, s), where i
is the week number and s is the remaining stock. The value function V(i, s) represents the
minimum cost that can be achieved by ordering the appropriate amount of the product at week
i, given that we have s units of the product in stock.
import numpy as np
import multiprocessing as mp
# Initialize the value function to infinity for all states except the final state
V = {s: np.inf for s in states if s[0] != num_weeks-1}
V[(num_weeks-1, s)] = max(0, demand[num_weeks-1] - s)
# Define a helper function to calculate the value of a state
def calculate_value(state):
i, s = state
if V[state] < np.inf:
return V[state]
else:
min_cost = np.inf
for j in range(s+1):
cost = j*order_cost + stock_cost*(s-j) + max(0, demand[i] - (s-j))*stock_cost + V[(i+1, s-
j)]
if cost < min_cost:
min_cost = cost
return min_cost
In this code, demand is a list of the demand for the product in each week, order_cost is the fixed
cost of placing an order, stock_cost is the cost of holding one unit of the product in stock for one
week, and initial_stock is the initial number of units of the product in stock.
The states list defines all possible combinations of weeks and remaining stock, and the V
dictionary stores the minimum cost that can be achieved for each state.
The calculate_value function takes a state as input and returns the minimum cost that can be
achieved from that state, using the previous values in the V dictionary. This function is used in
parallel processing to calculate the values for all states.
The mp.Pool function is used to create a pool of worker processes equal to the number of
available CPUs. The map method of the pool is used to apply the calculate_value function to
each state in parallel. The results are stored in the results list.
Once all values have been calculated, the order_quantities list is filled in by backtracking
through the value function to find the optimal order quantities for each week. Finally, the
function returns the minimum cost that can be achieved from the initial state and the optimal
order quantities for each week.
In this scenario, we have demand for the product over 8 weeks, and we want to determine the
optimal order quantities to minimize costs. We can order the product for a fixed cost of 20, and
we pay a weekly cost of 1 for each unit of the product in stock. We start with 5 units of the
product in stock.
This indicates that the optimal order quantities are to order 5 units in the first week, 6 units in
the fourth week, and no units in any other week, resulting in a minimum cost of 135.
--------------------------
Parallel dynamic programming used to solve a scenario of Production Planning in the real
world
--------------------------
Here's an implementation of parallel dynamic programming for solving an example scenario of
production planning in the real world:
import numpy as np
import multiprocessing as mp
Returns:
The value of the state and action.
"""
inventory, week = state
if week == len(demands):
return 0
values = []
for i in range(inventory + 1):
production_cost = production_costs[week] * i
inventory_cost = inventory_costs[week] * (inventory - i)
next_state = (min(inventory - i + demands[week], 20), week + 1)
value = production_cost + inventory_cost + value_function[next_state]
values.append(value)
return np.min(values)
Args:
demands (ndarray): An array of demands for each week.
production_costs (ndarray): An array of production costs for each week.
inventory_costs (ndarray): An array of inventory holding costs for each week.
Returns:
The minimum cost and the optimal production quantities for each week.
"""
n_weeks = len(demands)
states = [(i, 0) for i in range(21)]
value_function = np.zeros((21, n_weeks + 1))
pool = mp.Pool(processes=mp.cpu_count())
for week in range(n_weeks - 1, -1, -1):
results = pool.map(lambda state: calculate_value(state, demands, production_costs,
inventory_costs, value_function), states)
for i, state in enumerate(states):
value_function[state] = results[i]
pool.close()
pool.join()
production_quantities = np.zeros(n_weeks)
inventory = 0
for week in range(n_weeks):
min_value = np.inf
for i in range(inventory + 1):
production_cost = production_costs[week] * i
inventory_cost = inventory_costs[week] * (inventory - i)
next_state = (min(inventory - i + demands[week], 20), week + 1)
value = production_cost + inventory_cost + value_function[next_state]
if value < min_value:
min_value = value
production_quantities[week] = i
inventory = min(inventory - production_quantities[week] + demands[week], 20)
return min_value, production_quantities
This program takes three inputs: demands, production_costs, and inventory_costs. demands is
an array of demands for each week. production_costs is an array of production costs for each
week. inventory_costs is an array of inventory holding costs for each week.
The program uses a similar approach to the inventory management scenario, where the value
function is calculated using dynamic programming in reverse order, starting from the last
week. The calculate_value function is used to calculate the value of a given state and action, and
the `plan_production'
function is used to solve the problem by calling thecalculate_value` function in parallel for each
state.
The program first initializes the state space as a list of tuples, where each tuple represents a
state of the form (inventory, week). It then creates a value_function array of shape (21,
n_weeks + 1) to store the values of each state and week. The pool object is created to manage
the parallel processing using all available CPUs.
The program then loops through the weeks in reverse order, starting from the last week. For
each week, it uses the pool.map function to call the calculate_value function in parallel for each
state. The results are then stored in the value_function array.
Once the value function has been calculated, the program computes the optimal production
quantities for each week. It initializes the production_quantities array as an array of zeros, and
initializes the inventory to zero. It then loops through each week, starting from the first week,
and computes the minimum cost and optimal production quantity for that week using the
calculate_value function. It updates the inventory based on the production quantity and
demand for that week, and repeats the process for the remaining weeks.
The program returns the minimum cost and the optimal production quantities for each week.
Note that this program assumes a maximum inventory level of 20. This can be changed by
modifying the upper limit of the range function used to initialize the state space.
--------------------------
Parallel dynamic programming used to solve a scenario of Portfolio Optimization in the real
world
--------------------------
Here is an example of a program that uses parallel dynamic programming to solve a portfolio
optimization problem:
import numpy as np
from multiprocessing import Pool
def calculate_value(state):
"""
Calculates the value of a state by recursively computing the value
of its successor states and choosing the action with the highest value.
"""
wealth, week = state
if week == n_weeks:
return 0.0
return optimal_value
def parallel_value_function(week):
"""
Calculates the value function for a single week in parallel
using the `calculate_value` function.
"""
pool = Pool()
states = [(wealth, week) for wealth in wealths]
values = pool.map(calculate_value, states)
for i, wealth in enumerate(wealths):
value_function[(wealth, week)] = values[i]
pool.close()
pool.join()
This program solves a portfolio optimization problem where the goal is to maximize wealth
over a 20-week period by investing in 10 different assets. The program uses parallel dynamic
programming to compute the optimal value function for each week, and then computes the
optimal policy and final wealth based on the value function.
The calculate_value function recursively computes the value of a state by computing the value
of its successor states and choosing the action with the highest value. The
parallel_value_function function computes the value function for a single week in parallel by
calling calculate_value for each state in the state space. The multiprocessing.Pool object is used
to manage the parallel processing using all available CPUs.
The program initializes the state space as a list of tuples, where each tuple represents a state of
the form (wealth, week). It initializes the value_function dictionary to an empty dictionary,
which will store the computed value of each state. The policy_table dictionary is also initialized
as an empty dictionary, which will store the optimal action for each state.
The program then defines the problem parameters, including the number of weeks, the
number of actions, the discount factor, the set of possible initial wealth levels, and the set of
possible actions. It generates random returns for each week and action using the NumPy
np.random.normal function.
The program then computes the value function for each week in parallel by calling
parallel_value_function for each week. The parallel_value_function function creates a Pool
object, which distributes the calculation of the value function for each state in the state space
across multiple worker processes. It then waits for all worker processes to complete before
returning.
Finally, the program computes the optimal policy and final wealth by iterating through the
weeks and using the policy_table to determine the optimal action for each state. It then updates
the current wealth based on the chosen action and the returns for that week and action.
Note that this is just an example of how parallel dynamic programming can be used to solve a
portfolio optimization problem, and there are many other ways to implement parallel dynamic
programming for different problems.
--------------------------
Parallel dynamic programming used to solve a scenario of Routing and Scheduling in the real
world
--------------------------
Here is an example implementation of parallel dynamic programming for a routing and
scheduling problem in . In this problem, we want to find the shortest path for a vehicle to
travel from a start location to an end location while visiting a set of intermediate locations,
subject to time and capacity constraints.
import numpy as np
from multiprocessing import Pool
This code defines the problem parameters, including the number of locations, the starting and
ending locations, the capacity of the vehicle, the time limit for the trip, the time it takes to
travel between locations, and the demand for each location. It then defines the state space as a
list of tuples representing the time, location, and load of the vehicle. The value function and
policy table are initialized as dictionaries.
The value function is defined recursively using dynamic programming. The function takes a
state as input and returns the optimal value and path from that state to the end of the trip. If
the value for the state has already been computed, the function returns that value. If the time
limit has been reached, the function returns the value and path to the ending location. If the
load is than the demand for the current location, the function returns infinity and an empty
path. Otherwise, the function computes the value and path for each possible next location and
returns the minimum value and corresponding path.
The parallel value function is defined to compute the value function in parallel for a given week
of the trip. It uses the multiprocessing.Pool class to create a pool of worker processes and map
the value_function function to a list of input states in parallel.
The main loop of the program computes the value function and optimal policy in reverse order
from the end of the trip to the beginning. For each week of the trip, the parallel_value_function
function is called in parallel to compute the value function for each state. The optimal policy is
then computed by starting at the beginning of the trip and following the path with the lowest
cost at each state.
The program outputs the optimal policy and final value for the trip.
import numpy as np
import multiprocessing as mp
def value_function(state):
# Check if value for state has already been computed
if value[state] != np.inf:
return value[state], [state]
# Check if time limit has been reached
if state[0] == time_limit:
value[state] = 0
return 0, [state]
# Check if load is than demand for current location
if state[2] < demands[state[1]]:
value[state] = np.inf
return np.inf, []
# Compute value and path for each possible next state
values = []
paths = []
for next_loc in range(num_locations):
if next_loc != state[1]:
time = state[0] + travel_times[state[1], next_loc]
if time <= time_limit:
load = min(demands[next_loc], state[2] - demands[state[1]] + demands[next_loc])
next_state = (time, next_loc, load)
next_value, next_path = value_function(next_state)
values.append(next_value + travel_times[state[1], next_loc])
paths.append(next_path)
# Update value and policy table for current state
min_value_idx = np.argmin(values)
value[state] = values[min_value_idx]
policy[state] = paths[min_value_idx]
return value[state], [state] + policy[state]
def parallel_value_function(states):
with mp.Pool() as pool:
results = pool.map(value_function, states)
for i in range(len(states)):
value[states[i]], policy[states[i]] = results[i]
optimal_path = []
state = (0, 0, 0)
while state is not None:
optimal_path.append(state)
state = policy[state][-1] if policy[state] is not None else None
optimal_path.reverse()
In this example, the program computes the optimal path for a vehicle traveling between
multiple locations with varying demand over a certain time period. The program uses dynamic
programming to find the minimum cost path between the locations while also satisfying the
demand constraints.
The program defines the problem data, including the number of locations, time limit, demands,
and travel times between the locations. It then initializes the state space, value function, and
policy table.
The value_function function is defined to compute the value and path for a given state. It first
checks if the value for the state has already been computed, in which case it returns the
existing value and path. If the time limit has been reached or the load is than the demand for
the current location, it returns an infinite value and an empty path. Otherwise, it computes the
value and path for each possible next state and updates the value and policy table for the
current state with the minimum value and corresponding path.
Finally, the program outputs the optimal path and final value. The optimal path is computed by
following the policy table starting from the initial state and adding each state to the path until
the end state is reached.
Here's the program code for the routing and scheduling scenario using parallel dynamic
programming:
import numpy as np
import multiprocessing as mp
# Problem data
n_locations = 5
time_limit = 10
demands = np.array([2, 1, 3, 2, 1])
travel_times = np.array([[0, 3, 4, 2, 7],
[3, 0, 6, 4, 2],
[4, 6, 0, 5, 8],
[2, 4, 5, 0, 6],
[7, 2, 8, 6, 0]])
# State space
states = []
for t in range(time_limit+1):
for i in range(n_locations):
for l in range(np.sum(demands)):
states.append((t, i, l))
def value_function(state):
t, i, l = state
if value[state] < np.inf:
return value[state], policy[state]
if t == time_limit or l < demands[i]:
value[state] = np.inf
policy[state] = []
return np.inf, []
min_value = np.inf
min_path = []
for j in range(n_locations):
if i != j:
for k in range(max(0, l-demands[j]), l+1):
next_state = (t+travel_times[i][j], j, l-demands[j]+k)
next_value, next_path = value_function(next_state)
value_ijkl = travel_times[i][j] + next_value
if value_ijkl < min_value:
min_value = value_ijkl
min_path = [(j, k)] + next_path
value[state] = min_value
policy[state] = min_path
return min_value, min_path
def parallel_value_function(states):
with mp.Pool() as pool:
results = pool.map(value_function, states)
for i in range(len(states)):
value[states[i]] = results[i][0]
policy[states[i]] = results[i][1]
Note that this implementation uses a nested tuple to represent the state, with the first element
being the current time, the second element being the current location, and the third element
being the current load. The
The value_function function recursively computes the value of a given state by considering all
possible next states and their corresponding values. It also updates the policy table to store the
optimal path from the current state to the end state.
The main loop computes the value function for each 10-week period and updates the value and
policy tables accordingly. The optimal path and final value are then computed by backtracking
through the policy table from the starting state to the end state.
Note that this implementation assumes that the travel times and demands are constant over
time. If they vary over time, the state space would need to be expanded to include the time-
varying variables.
This code can be customized to fit different routing and scheduling scenarios by adjusting the
problem data and state space accordingly.
Chapter 10: Online
In online dynamic programming, the problem is divided into subproblems, and a solution is
computed for each subproblem as the data arrives. The solutions to the subproblems are then
combined to obtain the solution to the overall problem. This approach allows for efficient
computation of the solution, as only the necessary subproblems are solved at each time step.
Control: In control systems, online dynamic programming can be used to design optimal
controllers that minimize a cost function over a finite or infinite time horizon, subject to
constraints on the state and control variables.
Robotics: In robotics, online dynamic programming can be used to plan optimal trajectories for
robot motion, taking into account the dynamics of the robot, the environment, and the task
constraints.
Finance: In finance, online dynamic programming can be used to optimize investment and
portfolio management strategies, taking into account the risk and return characteristics of
different asset classes over time.
Logistics: In logistics, online dynamic programming can be used to optimize transportation and
routing decisions, taking into account the time-varying demand, supply, and transportation
costs.
Game theory: In game theory, online dynamic programming can be used to compute optimal
strategies for players in dynamic games, taking into account the actions and payoffs of other
players over time.
Overall, online dynamic programming is a versatile tool that can be used to solve a wide range
of optimization problems in dynamic environments, where decisions must be made in real time
based on incomplete and uncertain information.
--------------------------
Online dynamic programming used to solve Knapsack Problem
--------------------------
Here's an example scenario with program code for the Knapsack Problem using dynamic
programming:
Scenario: Suppose you are a thief trying to maximize your profits by stealing items from a
store. You have a knapsack with a maximum weight capacity of 15 pounds, and the store has 6
items with varying weights and profits:
Using dynamic programming, write a program to determine the maximum profit you can
make by stealing items without exceeding the maximum weight capacity of your knapsack.
result = []
w = capacity
for i in range(n, 0, -1):
if table[i][w] != table[i-1][w]:
item_weight, item_profit = items[i-1]
result.append(i)
w -= item_weight
result.reverse()
items = [(2, 12), (1, 10), (3, 20), (2, 15), (1, 5), (4, 30)]
capacity = 15
max_profit, selected_items = knapsack(items, capacity)
print("Maximum Profit: $", max_profit)
print("Selected Items: ", selected_items)
Output:
Maximum Profit: $ 67
Selected Items: [1, 3, 6]
Explanation:
The program uses a two-dimensional table to store the maximum profit that can be achieved
by stealing items with different weights at different knapsack capacities. The first row and
column of the table are initialized with zeros. The table is filled in row-by-row, and column-by-
column, with each cell representing the maximum profit that can be achieved using the first i
items and a knapsack capacity of j.
If the weight of the current item is greater than the current capacity, then the maximum profit
is the same as the maximum profit achieved using the first i-1 items and a knapsack capacity of
j. Otherwise, the maximum profit is the maximum of the maximum profit achieved using the
first i-1 items and a knapsack capacity of j, and the profit of the current item plus the maximum
profit achieved using the first i-1 items and a knapsack capacity of j minus the weight of the
current item.
After filling the table, the program traces back the items that were selected to achieve the
maximum profit, starting from the last cell of the table. If the maximum profit in the current
cell is different from the maximum profit in the cell above it, then the current item was
selected, and its weight is subtracted from the current capacity. The process continues until the
starting cell is reached, and the selected items are recorded in reverse order.
In this example, the program finds that the maximum profit that can be achieved using a
knapsack capacity of 15 pounds and the given items is $67, and the items that should be
selected to achieve this maximum profit are items 1, 3, and 6 (i.e., the items with weights 2, 3,
and 4 pounds, respectively).
Note that the time complexity of the dynamic programming solution to the Knapsack Problem
is O(nc), where n is the number of items and c is the capacity of the knapsack. This is because
the program needs to fill a two-dimensional table of size (n+1) x (c+1), and each cell requires
constant time to compute. The space complexity is also O(nc), because the program needs to
store the entire table in memory.
--------------------------
Online dynamic programming used to solve Traveling Salesman Problem (TSP)
--------------------------
The Traveling Salesman Problem (TSP) is a classic optimization problem in computer science
that involves finding the shortest possible route that visits every city exactly once and returns
to the starting city. It is an NP-hard problem, meaning that it becomes increasingly difficult to
solve as the number of cities increases. However, dynamic programming can be used to find
the optimal solution in a reasonable amount of time for small instances of the problem. Here is
an example scenario with program code for the TSP using dynamic programming:
Scenario: You are a salesperson who needs to visit 5 cities (A, B, C, D, and E) to sell your
products. The distance between each pair of cities is as follows:
A B C D E
A 0 10 15 20 25
B 10 0 35 45 50
C 15 35 0 30 15
D 20 45 30 0 10
E 25 50 15 10 0
Using dynamic programming, write a program to determine the shortest possible route that
visits each city exactly once and returns to the starting city.
Program Code:
import sys
if not visited:
return cities[curr][start]
res = sys.maxsize
for nxt in visited:
new_visited = visited - {nxt}
new_res = dp(nxt, new_visited) + cities[curr][nxt]
res = min(res, new_res)
cities = [
[0, 10, 15, 20, 25],
[10, 0, 35, 45, 50],
[15, 35, 0, 30, 15],
[20, 45, 30, 0, 10],
[25, 50, 15, 10, 0],
]
start = 0
shortest_route = tsp(cities, start)
print("Shortest Route Length: ", shortest_route)
Output:
Explanation:
The program uses dynamic programming to solve the TSP by breaking down the problem into
smaller subproblems. The idea is to consider each city as the starting city and find the shortest
possible route that visits all remaining cities exactly once and returns to the starting city.
The program uses memoization to store the results of subproblems that have already been
solved. The dp function takes two arguments: curr (the current city being visited) and visited
(the set of remaining cities to visit). If the shortest route from curr to the set of visited cities
has already been computed, it is retrieved from the memoization table. Otherwise, the function
recursively calls itself for each remaining city and selects the minimum route length.
The program then calls the tsp function, which calls the dp function for each city and returns
the shortest possible route length.
In this example, the program finds that the shortest possible route that visits each city exactly
once and returns to the starting city is 80, and the route is A -> B -> C -> E -> D -> A. The
program uses dynamic programming to solve the TSP problem in O(n^2 2^n) time, where n is
the number of cities. The space complexity is also O(n 2^n), because the program needs to
store the memoization table.
--------------------------
Online dynamic programming used to solve Shortest Path Problem
--------------------------
The Shortest Path Problem is a classic optimization problem in computer science that involves
finding the shortest possible path between two nodes in a weighted graph. Dynamic
programming can be used to solve the problem efficiently. Here is an example scenario with
program code for the Shortest Path Problem using dynamic programming:
Scenario: You are given a weighted graph with 5 nodes (A, B, C, D, and E) and the following
edges:
Using dynamic programming, write a program to determine the shortest path from node A to
node E.
Program Code:
import sys
for i in range(n-1):
for u in range(n):
for v, w in graph[u]:
new_dist = distances[u] + w
if new_dist < distances[v]:
distances[v] = new_dist
return distances[end]
graph = [
[(1, 10), (2, 3)],
[(2, 1), (3, 2)],
[(1, 4), (3, 8), (4, 2)],
[(4, 1)],
[(2, 7)]
]
start = 0
end = 4
shortest_dist = shortest_path(graph, start, end)
print("Shortest Distance from A to E: ", shortest_dist)
Output:
Shortest Distance from A to E: 8
Explanation:
The program uses dynamic programming to solve the Shortest Path Problem by computing the
shortest distance from the start node to every other node in the graph. It initializes an array
called distances to hold the distances from the start node to every other node. Initially, all
distances are set to infinity, except the distance from the start node to itself, which is set to 0.
The program then iterates over every edge in the graph n-1 times, where n is the number of
nodes in the graph. On each iteration, the program considers every edge (u, v) in the graph and
computes the new distance from the start node to v as the sum of the distance from the start
node to u and the weight of the edge (u, v). If this new distance is than the current distance to
v, the program updates the distances array with the new distance.
Finally, the program returns the shortest distance from the start node to the end node.
In this example, the program finds that the shortest distance from node A to node E is 8. The
program uses dynamic programming to solve the Shortest Path Problem in O(n^3) time, where
n is the number of nodes in the graph. The space complexity is O(n), because the program
needs to store the distances array.
--------------------------
Online dynamic programming used to solve Longest Common Subsequence Problem
--------------------------
The Longest Common Subsequence (LCS) Problem is a classic problem in computer science
that involves finding the longest subsequence that is common to two sequences. Dynamic
programming can be used to solve the problem efficiently. Here is an example scenario with
program code for the LCS Problem using dynamic programming:
Scenario: You are given two strings "AGGTAB" and "GXTXAYB". Write a program to find the
length of the longest common subsequence of the two strings.
Program Code:
X = "AGGTAB"
Y = "GXTXAYB"
print("Length of LCS is", lcs(X, Y))
Output:
Length of LCS is 4
Explanation:
The program uses dynamic programming to solve the LCS Problem by computing the length of
the longest common subsequence of the two strings. It initializes a two-dimensional array
called dp to hold the lengths of the LCSs of all the substrings of the two strings. The LCS of two
empty substrings is zero, so dp[0][0] is set to zero. The LCS of a non-empty substring and an
empty substring is also zero, so the first row and first column of dp are set to zero.
The program then iterates over every pair of indices (i, j) in the two-dimensional array, where i
ranges from 1 to the length of the first string, and j ranges from 1 to the length of the second
string. For each pair of indices, the program checks whether the characters at those indices in
the two strings are equal. If they are, the program sets dp[i][j] to the length of the LCS of the
substrings that end at those indices plus one. If they are not equal, the program sets dp[i][j] to
the maximum of the LCSs of the substrings that end at the previous index of the first string and
the current index of the second string, and the LCSs of the substrings that end at the current
index of the first string and the previous index of the second string.
Finally, the program returns dp[m][n], which is the length of the LCS of the two strings.
In this example, the program finds that the length of the longest common subsequence of the
two strings "AGGTAB" and "GXTXAYB" is 4, which is the length of the subsequence "GTAB". The
program uses dynamic programming to solve the LCS Problem in O(mn) time, where m and n
are the lengths of the two strings. The space complexity is also O(mn), because the program
needs to store the two-dimensional array dp.
To further explain the time and space complexity of the above program, we can break it down
as follows:
Time Complexity: The program iterates over each element of the two-dimensional dp array
exactly once, and performs a constant amount of work for each element. Therefore, the time
complexity of the program is O(mn), where m and n are the lengths of the two input strings.
Space Complexity: The program creates a two-dimensional dp array of size (m+1) x (n+1),
which requires O(mn) space. Therefore, the space complexity of the program is also O(mn).
In general, dynamic programming can be an efficient approach to solving the LCS Problem,
because it avoids redundant computations by using the results of previous computations to
compute the LCSs of longer substrings. By doing so, the program can solve the problem in
polynomial time, instead of the exponential time that would be required by a brute-force
approach that tries all possible subsequence combinations.
--------------------------
Online dynamic programming used to solve Sequence Alignment Problem
--------------------------
The Sequence Alignment Problem is another classic problem in computer science that involves
finding the optimal alignment of two sequences, where the cost of aligning two characters
depends on their match/mismatch score and the gap penalty. Dynamic programming can be
used to solve the problem efficiently. Here is an example scenario with program code for the
Sequence Alignment Problem using dynamic programming:
Scenario: You are given two strings "AGTACGCA" and "TATGC". The match score is 1, the
mismatch score is -1, and the gap penalty is -2. Write a program to find the optimal alignment
of the two strings and its score.
Program Code:
def seq_align(X, Y, match_score, mismatch_score, gap_penalty):
m, n = len(X), len(Y)
dp = [[0]*(n+1) for _ in range(m+1)]
for i in range(1, m+1):
dp[i][0] = i * gap_penalty
for j in range(1, n+1):
dp[0][j] = j * gap_penalty
for i in range(1, m+1):
for j in range(1, n+1):
match = dp[i-1][j-1] + (match_score if X[i-1] == Y[j-1] else mismatch_score)
delete = dp[i-1][j] + gap_penalty
insert = dp[i][j-1] + gap_penalty
dp[i][j] = max(match, delete, insert)
score = dp[m][n]
align_X, align_Y = [], []
i, j = m, n
while i > 0 and j > 0:
if dp[i][j] == dp[i-1][j-1] + (match_score if X[i-1] == Y[j-1] else mismatch_score):
align_X.append(X[i-1])
align_Y.append(Y[j-1])
i -= 1
j -= 1
elif dp[i][j] == dp[i-1][j] + gap_penalty:
align_X.append(X[i-1])
align_Y.append('-')
i -= 1
else:
align_X.append('-')
align_Y.append(Y[j-1])
j -= 1
while i > 0:
align_X.append(X[i-1])
align_Y.append('-')
i -= 1
while j > 0:
align_X.append('-')
align_Y.append(Y[j-1])
j -= 1
align_X.reverse()
align_Y.reverse()
alignment = ''.join(align_X) + '\n' + ''.join(align_Y)
return score, alignment
X = "AGTACGCA"
Y = "TATGC"
match_score = 1
mismatch_score = -1
gap_penalty = -2
score, alignment = seq_align(X, Y, match_score, mismatch_score, gap_penalty)
print("Optimal Alignment Score:", score)
print("Optimal Alignment:\n", alignment)
Output:
Once the dp array is filled, the program backtracks to find the optimal alignment that
corresponds to the maximum score. It starts from the bottom-right corner of the dp array and
follows the path that leads to the optimal alignment, appending each aligned pair of characters
to two lists called align_X and align_Y. If a character is aligned with a gap, the corresponding list
gets a "-" character instead. The program then reverses the order of the two lists and
concatenates them to form the optimal alignment.
In the example scenario, the optimal alignment score of the two strings is 0, which means they
are already perfectly aligned. The optimal alignment is "AGTACGCA\n--T-A-TGC", which shows
that the second string needs to have two gaps inserted to align with the first string.
--------------------------
Online dynamic programming used to solve Optimal Binary Search Tree Problem
--------------------------
The Optimal Binary Search Tree problem is a classic problem in computer science and dynamic
programming, which involves finding the most efficient way to search a set of keys in a binary
search tree. The problem can be solved using dynamic programming by computing the optimal
cost of searching all possible subtrees of the tree and combining them in a bottom-up manner.
Here's an example scenario with program code for solving the Optimal Binary Search Tree
problem:
Suppose we have the following set of keys with their respective probabilities of being searched:
The optimal_bst function takes two lists keys and probs as input, where keys is the list of keys
to be searched and probs is the list of their respective probabilities. The function first
initializes a 2D array dp of size (n,n), where n is the length of the keys list. The dp[i][j] entry of
the array represents the optimal cost of searching the keys from index i to index j in the tree.
The diagonal entries of the array dp[i][i] are initialized to the respective probabilities of
searching the key at index i.
The function then iterates over all possible lengths L of the key subsequence, starting from 2
and going up to n. For each length L, the function iterates over all possible starting indices i of
the subsequence. The ending index j of the subsequence is then computed based on i and L. The
function then computes the cost of searching the keys in the subsequence by considering all
possible roots k of the subtree rooted at index k. The cost of searching the subtree is the sum of
the costs of searching its left and right subtrees, the probability of searching the root key, and
the probabilities of searching all the keys in the subtree. The function then updates the dp[i][j]
entry with the minimum cost obtained by trying all possible roots of the subtree.
Finally, the function returns the dp[0][n-1] entry of the array, which represents the optimal
cost of searching all the keys in the tree.
In the example code, the optimal cost of searching the keys [10, 12, 20, 30] with probabilities
[0.34, 0.08, 0.5, 0.08] is computed to be 2.71.
The time complexity of the above algorithm is O(n^3), where n is the length of the input keys
and probs lists. This is because we need to compute the optimal costs of all possible subtrees of
the tree, and there are O(n^2) possible subtrees. However, the space complexity of the
algorithm is O(n^2), which is the size of the dp array.
Overall, this example demonstrates how dynamic programming can be used to solve the
Optimal Binary Search Tree problem efficiently in .
--------------------------
Online dynamic programming used to solve Maximum Subarray Problem
--------------------------
The Maximum Subarray Problem is a classic problem in computer science that involves finding
the contiguous subarray with the largest sum in a given array of integers. Here is an example
program that uses dynamic programming to solve this problem in O(n) time complexity:
def max_subarray(arr):
"""
Returns the maximum sum subarray and its sum using dynamic programming.
"""
max_sum = -float('inf')
curr_sum = 0
start = 0
end = 0
for i, val in enumerate(arr):
if curr_sum < 0:
curr_sum = 0
start = i
curr_sum += val
if curr_sum > max_sum:
max_sum = curr_sum
end = i
return arr[start:end+1], max_sum
The max_subarray function takes an array of integers arr as input and returns a tuple
consisting of the maximum sum subarray and its sum. The function uses a dynamic
programming approach where it iterates over the input array and maintains the maximum
sum seen so far, as well as the current sum of the contiguous subarray.
If the current sum becomes negative, the function resets the current sum to zero and updates
the start index of the subarray to the current index, since any subarray that includes the
negative-sum segment can not be a maximum sum subarray.
The function also updates the end index of the subarray whenever the maximum sum seen so
far is updated. This ensures that the function returns the subarray with the largest sum.
The time complexity of this algorithm is O(n), where n is the length of the input array. This is
because the algorithm iterates over the input array exactly once. The space complexity of this
algorithm is O(1), since it only needs to maintain a few variables to compute the solution.
--------------------------
Online dynamic programming used to solve Coin Change Problem
--------------------------
The Coin Change Problem is a classic problem in computer science that involves finding the
minimum number of coins needed to make change for a given amount of money, given a list of
coin denominations. Here is an example program that uses dynamic programming to solve
this problem:
The coin_change function takes a list of coin denominations coins and a target amount amount
as input and returns the minimum number of coins needed to make change for the given
amount. The function uses a dynamic programming approach where it initializes a list dp with
a length of amount + 1 and sets all elements to infinity except for dp[0], which is set to 0 since
it takes 0 coins to make change for 0.
The function then iterates over all amounts from 1 to the target amount and computes the
minimum number of coins needed to make change for each amount by considering all possible
coins that can be used. The minimum number of coins needed to make change for the current
amount is then stored in dp[i].
Finally, the function returns dp[amount] if it is not infinity, or -1 if it is infinity. This is because
if it is not possible to make change for the target amount using the given coin denominations,
then dp[amount] will still be infinity after the dynamic programming algorithm completes.
The time complexity of this algorithm is O(amount * n), where n is the length of the input coins
list. This is because the algorithm needs to consider all possible coin denominations for each
amount from 1 to the target amount. The space complexity of this algorithm is O(amount),
since it only needs to maintain a list of length amount + 1 to compute the solution.
Here is an example of how the coin_change function can be used to solve the Coin Change
Problem:
In this example, the input coins list contains the coin denominations of 1 cent, 5 cents, 10 cents,
and 25 cents, and the target amount is 63 cents. The coin_change function is called with these
inputs, which returns the minimum number of coins needed to make change for 63 cents,
which is 7. This means that the minimum number of coins needed to make change for 63 cents
is one 25-cent coin, one 10-cent coin, two 5-cent coins, and three 1-cent coins.
Note that the coin_change function returns -1 if it is not possible to make change for the target
amount using the given coin denominations. For example, if the coins list only contains the coin
denomination of 7 cents and the target amount is 10 cents, then the coin_change function
would return -1, since it is not possible to make change for 10 cents using only 7-cent coins.
--------------------------
Online dynamic programming used to solve a scenario of Resource Allocation in the real world
--------------------------
Here is a program that implements online dynamic programming to solve a resource
allocation problem.
In this scenario, imagine you are a project manager tasked with allocating a fixed number of
resources (such as time, money, or personnel) to a set of projects. Each project has a fixed
duration and a value that represents its potential return on investment. Your goal is to
maximize the total value of the set of projects you select while staying within your resource
budget.
The program uses a dynamic programming approach to solve this problem, incrementally
building a table of optimal solutions to sub-problems.
for i in range(n):
for j in range(budget, durations[i] - 1, -1):
dp_table[j] = max(dp_table[j], dp_table[j - durations[i]] + values[i])
return dp_table[budget]
To use this function, you can pass in a budget, a list of project durations, and a list of project
values. Here's an example:
budget = 10
durations = [2, 3, 4, 5]
values = [3, 4, 5, 6]
max_value = allocate_resources(budget, durations, values)
In this example, there are four projects with durations of 2, 3, 4, and 5 units of resources, and
values of 3, 4, 5, and 6, respectively. The total budget is 10 units of resources. The function
returns the maximum possible total value of a set of projects that can be completed within the
budget, which in this case is 11 (by selecting projects with durations 2 and 4).
--------------------------
Online dynamic programming used to solve a scenario of Inventory Management in the real
world
--------------------------
Certainly! Here is an example program that implements online dynamic programming to solve
an inventory management problem.
In this scenario, imagine you are running a small retail business and you need to manage your
inventory of a particular product. The product has a fixed price and demand that varies over
time. You can order new units of the product at a fixed cost, but there is a delay between when
you place the order and when it arrives. Your goal is to maximize your profit over a fixed
period of time by determining when to order new units of the product.
The program uses a dynamic programming approach to solve this problem, incrementally
building a table of optimal solutions to sub-problems.
for i in range(n):
for j in range(delay, -1, -1):
if j == 0:
dp_table[j] += price * demand[i]
else:
dp_table[j] = max(dp_table[j] + price * demand[i], dp_table[j - 1] - cost)
return dp_table[delay]
To use this function, you can pass in the fixed product price, the cost to order new units, the
delay between ordering and receiving new units, and a list of expected demand over time.
Here's an example:
price = 10
cost = 20
delay = 2
demand = [3, 2, 5, 1, 4]
In this scenario, imagine you are a factory manager tasked with planning production for a set of
products. Each product has a fixed production cost, a fixed selling price, and a fixed demand
that varies over time. Your goal is to maximize your profit over a fixed period of time by
determining how much of each product to produce at each time period.
The program uses a dynamic programming approach to solve this problem, incrementally
building a table of optimal solutions to sub-problems.
for i in range(m):
for j in range(1, n + 1):
max_profit = 0
for k in range(j):
profit = dp_table[i][k] + (j - k) * prices[i] * min(demands[i][j - 1], k + 1) - k * costs[i]
max_profit = max(max_profit, profit)
dp_table[i][j] = max_profit
return dp_table[-1][-1]
To use this function, you can pass in a list of production costs, a list of selling prices, and a list
of expected demand over time for each product. Here's an example:
In this example, there are two products with production costs of 20 and 30, selling prices of 50
and 70, and expected demand over three periods of [5, 10, 15] and [10, 5, 20], respectively. The
function returns the maximum possible profit that can be earned by planning production,
which in this case is 2195.
--------------------------
Online dynamic programming used to solve a scenario of Portfolio Optimization in the real
world
--------------------------
Here's an example program that implements online dynamic programming to solve a portfolio
optimization problem.
In this scenario, imagine you are an investor trying to allocate your funds across a set of assets
to maximize your expected return over a fixed period of time. Each asset has a fixed return and
a fixed risk, and your goal is to find the optimal allocation that balances risk and return.
The program uses a dynamic programming approach to solve this problem, incrementally
building a table of optimal solutions to sub-problems.
for i in range(n):
for j in range(1, budget + 1):
if i == 0:
dp_table[i][j] = returns[i] * min(j, budget // risks[i])
else:
max_return = 0
for k in range(min(j // risks[i], budget // risks[i])):
max_return = max(max_return, dp_table[i - 1][j - k * risks[i]] + k * returns[i])
dp_table[i][j] = max(dp_table[i - 1][j], max_return)
return weights
To use this function, you can pass in a list of expected returns, a list of corresponding risks, and
a fixed budget. Here's an example:
In this example, there are three assets with expected returns of 10%, 15%, and 12%, and
corresponding risks of 5%, 10%, and 8%, respectively. The investor has a budget of 1,000,000.
The function returns a list of weights representing the optimal allocation of funds across the
three assets, which in this case is [40000, 60000, 0].
--------------------------
Online dynamic programming used to solve a scenario of Routing and Scheduling in the real
world
--------------------------
Here's an example program that implements online dynamic programming to solve a routing
and scheduling problem.
In this scenario, imagine you are a transportation company trying to schedule the delivery of
goods to multiple destinations while minimizing the total transportation cost. Each destination
has a fixed demand, and each truck has a fixed capacity and a fixed cost per unit distance. Your
goal is to find the optimal delivery schedule that meets all demand while minimizing
transportation cost.
The program uses a dynamic programming approach to solve this problem, incrementally
building a table of optimal solutions to sub-problems.
for i in range(n):
for j in range(1, m + 1):
if i == 0:
dp_table[i][j] = (distances[i][0] + distances[0][i+1]) * min(j, capacities[i]) *
unit_costs[j-1]
else:
min_cost = float("inf")
for k in range(1, j+1):
prev_cost = dp_table[i-1][j-k] if j-k > 0 else 0
curr_cost = (distances[i][0] + distances[0][i+1]) * k * unit_costs[k-1]
if i == n-1:
if k >= demands[i]:
min_cost = min(min_cost, prev_cost + curr_cost)
else:
min_cost = min(min_cost, prev_cost + curr_cost + dp_table[i+1][k])
dp_table[i][j] = min(dp_table[i-1][j], min_cost)
Let's break down the code and see how it solves the routing and scheduling problem.
First, the function takes in four parameters: demands, capacities, distances, and unit_costs.
demands is a list of the demand for each destination, capacities is a list of the corresponding
truck capacities, distances is a matrix of distances between each pair of destinations, and
unit_costs is a list of truck costs per unit distance.
The program then creates a 2D list dp_table of size n x m + 1, where n is the number of
destinations and m is the number of trucks. The first row of dp_table represents the sub-
problem of delivering to the first destination with up to j trucks, and the last row represents
the sub-problem of delivering to the last destination with up to j trucks. The optimal solution to
the original problem can be found in dp_table[n-1][m].
n = len(demands)
m = len(capacities)
dp_table = [[0] * (m + 1) for _ in range(n)]
Next, the program fills in the dp_table using an online dynamic programming approach. The
outer loop iterates over the destinations, and the inner loop iterates over the number of trucks.
For each sub-problem, the program computes the optimal solution by considering all possible
ways of allocating trucks to the current destination.
for i in range(n):
for j in range(1, m + 1):
if i == 0:
dp_table[i][j] = (distances[i][0] + distances[0][i+1]) * min(j, capacities[i]) *
unit_costs[j-1]
else:
min_cost = float("inf")
for k in range(1, j+1):
prev_cost = dp_table[i-1][j-k] if j-k > 0 else 0
curr_cost = (distances[i][0] + distances[0][i+1]) * k * unit_costs[k-1]
if i == n-1:
if k >= demands[i]:
min_cost = min(min_cost, prev_cost + curr_cost)
else:
min_cost = min(min_cost, prev_cost + curr_cost + dp_table[i+1][k])
dp_table[i][j] = min(dp_table[i-1][j], min_cost)
In the base case where the current destination is the first destination (i == 0), the optimal
solution is simply to allocate up to min(j, capacities[i]) trucks to the destination, with a cost
equal to the product of the distance between the first and second destinations and the cost per
unit distance of the allocated trucks.
In the recursive case where i > 0, the program considers all possible ways of allocating up to j
trucks to the current destination. For each allocation k, the program computes the cost of
delivering k trucks to the current destination, which is the product of the distance between the
first and second destinations (i.e., the distance from the depot to the current destination and
back) and the cost per unit distance of the allocated trucks. The program then adds the cost of
the current allocation to the optimal solution for the sub-problem of delivering to the previous
destination with up to j-k trucks (prev_cost). Finally, if the current destination is not the last
destination, the program adds the optimal solution for the sub-problem of delivering to the
next destination with up to k trucks (dp_table[i+1][k]). The optimal solution for the current
sub-problem is the minimum cost among all possible allocations.
Once the dp_table is filled, the program constructs the optimal delivery schedule by
backtracking through the table. Starting at the last destination with m trucks, the program
determines the number of trucks allocated to the last destination by comparing the optimal
solution for the sub-problem of delivering to the last destination with j trucks to the optimal
solution for the sub-problem of delivering to the last destination with j-1 trucks. If the former is
smaller, it means that the optimal solution for the last destination involved j trucks. Otherwise,
it means that the optimal solution for the last destination involved j-1 trucks. The program
then moves to the previous destination and repeats the process until it reaches the first
destination.
return schedule
The program returns a list of lists representing the optimal delivery schedule, where the i-th
element of the j-th list is the i-th destination to be delivered by the j-th truck.
Putting everything together, we can write a function optimize_delivery that takes the number
of trucks m, the list of demands demands, the list of distances distances, and the list of unit
costs unit_costs, and returns the optimal delivery schedule:
return schedule
Let's test the function with the example scenario we considered earlier:
m=3
demands = [2, 3, 1, 4, 2]
distances = [
[0, 10, 15, 20, 25],
[10, 0, 5, 10, 15],
[15, 5, 0, 5, 10],
[20, 10, 5, 0, 5],
[25, 15, 10, 5, 0]
]
unit_costs = [2, 3, 4]
for i, s in enumerate(schedule):
print(f"Truck {i+1}: {s}")
Truck 1: [2, 3, 1]
Truck 2: [4]
Truck 3: [5]
This means that the optimal delivery schedule involves delivering destinations 2, 3, and 1 with
truck 1, destination 4 with truck 2, and destination 5 with truck 3. The total cost of this
schedule is:
As we can see, this is indeed the minimum cost among all feasible delivery schedules.
Chapter 11: Stochastic
In SDP, the state of the system is modeled as a stochastic process that evolves over time, and
the decision-maker's objective is to find a policy that maximizes some measure of long-term
performance, such as expected total rewards or expected discounted rewards.
The main challenge in SDP is to account for the uncertainty in the system dynamics and to
make decisions that are robust to this uncertainty. This requires the use of probabilistic models
to represent the evolution of the system over time, and the application of advanced
optimization techniques to find optimal policies that balance the trade-off between immediate
and future rewards.
SDP has applications in many fields, including finance, engineering, operations research, and
artificial intelligence. It is used to solve problems such as inventory management, resource
allocation, portfolio optimization, and control of complex systems.
The objective of SDP is to find a policy π: S → A that maximizes the expected total discounted
reward over an infinite time horizon, given by:
where st is the state of the system at time t, γ is a discount factor between 0 and 1 that
determines the weight given to future rewards, and E denotes the expectation over the
stochastic process governing the system dynamics.
The optimal value function V*: S → R is defined as the maximum expected total discounted
reward achievable under any policy:
SDP is concerned with finding the optimal value function and policy, either analytically or
through iterative numerical methods such as value iteration or policy iteration.
Define the MDP: Define the set of possible states S, the set of possible actions A, the transition
probabilities P(s'|s,a), and the reward function R(s,a).
Initialization: Initialize the value function V(s) to some initial values, such as zero or the
immediate reward obtained in each state.
Policy eva tion: Given a policy π, compute the value function V(s) for all states s by solving the
following Bellman equation iteratively:
Policy improvement: Given the value function V(s), improve the policy by selecting the action
that maximizes the expected long-term reward for each state:
π(s) ← argmaxa Σs' P(s'|s,a)[R(s,a) + γV(s')]
Convergence test: Check if the policy π has converged by comparing it to the previous policy. If
the policies are the same, stop and return the optimal policy and value function. Otherwise, go
back to step 3 with the updated policy.
Here is the pseudocode for the value iteration algorithm, which is a common iterative method
used to solve SDP problems:
The value iteration algorithm is a simple and efficient way to find the optimal policy and value
function in SDP problems. However, it may converge slowly or not converge at all if the state
and action spaces are large or if the system dynamics are complex.
Other iterative methods, such as policy iteration and modified policy iteration, can be used to
solve SDP problems. These methods alternate between policy eva tion and policy improvement
steps until convergence is achieved.
In addition, approximate methods such as Q-learning, actor-critic methods, and Monte Carlo
methods can be used when the system dynamics or the reward function are unknown or
difficult to model. These methods learn the optimal policy from experience by interacting with
the system and observing the resulting rewards.
Overall, SDP provides a powerful and flexible framework for modeling and solving decision-
making problems under uncertainty, and it has numerous applications in a wide range of fields.
Stochastic dynamic programming (SDP) is a general framework for solving sequential decision-
making problems under uncertainty. It has applications in many areas where decisions must be
made over time, and where the outcomes of those decisions are subject to randomness or
uncertainty. Some examples of problems that can be solved using SDP are:
Inventory management: A company must decide how much inventory to order at each time
period, given uncertain demand and limited storage space. The objective is to minimize the
total cost of ordering and storing inventory over a finite time horizon.
Asset allocation: An investor must decide how to allocate their investment portfolio among
different assets, such as stocks, bonds, and cash, given uncertain market conditions and future
returns. The objective is to maximize the expected long-term return of the portfolio while
managing risk.
Routing and scheduling: A transportation company must decide how to route vehicles and
schedule deliveries, given uncertain traffic conditions and delivery times. The objective is to
minimize the total travel time or distance of the vehicles while meeting delivery deadlines.
Energy management: A power plant must decide how to allocate its resources, such as fuel and
maintenance, given uncertain electricity demand and market prices. The objective is to
maximize the profit or minimize the cost of producing and selling electricity over a finite time
horizon.
Robotics and control: A robot or a control system must decide how to act in a changing
environment, given noisy sensor measurements and uncertain system dynamics. The objective
is to achieve a desired task or behavior while minimizing errors and uncertainty.
These are just a few examples of problems that can be solved using SDP. In general, any
problem that involves making sequential decisions under uncertainty can be formulated as an
SDP problem and solved using the methods and algorithms of SDP.
--------------------------
Stochastic dynamic programming used to solve Knapsack Problem
--------------------------
Here's an example scenario with program code for solving the Knapsack Problem using
Stochastic Dynamic Programming:
Scenario: A person is going on a camping trip and wants to pack their backpack with items that
will maximize the total value they can carry, while staying within the weight limit of the
backpack. They have a list of items with their values and weights, and want to use Stochastic
Dynamic Programming to determine the optimal selection of items to pack.
import random
# Define the number of iterations for the stochastic dynamic programming algorithm
num_iterations = 10000
# Initialize the value function to zero for all possible combinations of items
value_function = {}
for i in range(len(items)+1):
for j in range(weight_limit+1):
value_function[(i,j)] = 0
# Run the stochastic dynamic programming algorithm for the specified number of iterations
for k in range(num_iterations):
for i in range(len(items)):
for j in range(weight_limit+1):
if items[i][2] <= j:
value_function[(i+1,j)] = max(value_function[(i,j)], value_function[(i,j-items[i][2])] +
items[i][1])
else:
value_function[(i+1,j)] = value_function[(i,j)]
This program uses Stochastic Dynamic Programming to find the optimal selection of items to
pack in a backpack for a camping trip. The program defines a list of items with their values and
weights, as well as the weight limit of the backpack and the number of iterations for the
algorithm. It then defines a function to calculate the value of a given selection of items, and
initializes the value function to zero for all possible combinations of items. The program then
runs the stochastic dynamic programming algorithm for the specified number of iterations,
updating the value function for each combination of items and backpack weights. Finally, the
program uses the value function to determine the optimal selection of items, and prints the
results.
The calculate_value function takes a selection of items represented as a list of binary values (1
for selected, 0 for not selected), and calculates the total value of the selected items. If the total
weight of the selected items exceeds the weight limit of the backpack, the function returns 0.
The program initializes the value function to zero for all possible combinations of items and
backpack weights using a dictionary, where each key is a tuple of the form (i,j) representing the
i-th item and j-th backpack weight. The program then runs the stochastic dynamic
programming algorithm for the specified number of iterations, updating the value function for
each combination of items and backpack weights using the following recursive formula:
if items[i][2] <= j:
value_function[(i+1,j)] = max(value_function[(i,j)], value_function[(i,j-items[i][2])] +
items[i][1])
else:
value_function[(i+1,j)] = value_function[(i,j)]
This formula checks if the i-th item can be added to the backpack at the current weight limit j,
and if so, calculates the maximum value of the backpack with and without the i-th item. If the i-
th item cannot be added to the backpack at the current weight limit, the formula simply uses
the previous value of the backpack without the i-th item.
After the value function has been updated for all combinations of items and backpack weights,
the program uses the value function to determine the optimal selection of items by
backtracking through the value function starting at the bottom-right corner, i.e. (len(items),
weight_limit). The program checks if adding the i-th item to the backpack at the current weight
limit j increases the total value, and if so, adds the i-th item to the selection and reduces the
weight limit by the weight of the i-th item. The program continues backtracking until it reaches
the top-left corner of the value function.
Finally, the program prints the optimal selection of items and their total value.
--------------------------
Stochastic dynamic programming used to solve Traveling Salesman Problem (TSP)
--------------------------
Here's an example scenario with program code for solving the Traveling Salesman Problem
using Stochastic Dynamic Programming:
Scenario: A salesperson wants to visit a number of cities to sell their products, and wants to
find the shortest possible route that visits each city exactly once and returns to the starting
city. They have a list of cities with their distances from each other, and want to use Stochastic
Dynamic Programming to determine the optimal route.
import random
# Define the number of iterations for the stochastic dynamic programming algorithm
num_iterations = 10000
# Initialize the value function to zero for all possible subsets of cities
value_function = {}
for i in range(1, len(distances)):
value_function[(i, frozenset([i]))] = distances[0][i]
# Run the stochastic dynamic programming algorithm for the specified number of iterations
for k in range(num_iterations):
for subset_size in range(2, len(distances)):
for subset in itertools.combinations(range(1, len(distances)), subset_size):
subset = frozenset(subset)
for j in subset:
value_function[(j, subset)] = min([value_function[(i, subset.difference({j}))] +
distances[i][j] for i in subset if i != j])
# Determine the optimal route by backtracking through the value function
current_city = 0
remaining_cities = frozenset(range(1, len(distances)))
route = [0]
while remaining_cities:
next_city = min(remaining_cities, key=lambda x: value_function[(x, remaining_cities)] +
distances[current_city][x])
route.append(next_city)
remaining_cities = remaining_cities.difference({next_city})
current_city = next_city
This program uses Stochastic Dynamic Programming to find the optimal route for the Traveling
Salesman Problem. The program defines the distances between cities, as well as the number of
iterations for the algorithm. It then initializes the value function to zero for all possible subsets
of cities using a dictionary, where each key is a tuple of the form (j, subset) representing the
current city j and the set of remaining cities in the subset. The program then runs the stochastic
dynamic programming algorithm for the specified number of iterations, updating the value
function for each subset of cities using the following recursive formula:
This formula calculates the minimum distance of a route that starts at the current city j and
visits each city in the subset exactly once before returning to the starting city. It does this by
iterating over all cities i in the subset except for the current city j, and adding the distance from
the current city to i, and the minimum distance of the remaining subset without the current
city j.
After the value function has been updated for all subsets of cities, the program uses the value
function to determine the optimal route by backtracking through the value function starting at
the starting city 0, and selecting the city in the remaining subset with the smallest sum of the
value function and distance to the current city. This is repeated until all cities have been visited
exactly once and returned to the starting city. Finally, the program prints the optimal route and
its length.
Note that this implementation uses a brute-force approach to iterate over all possible subsets
of cities, which can be computationally expensive for large instances of the Traveling Salesman
Problem. However, this approach is guaranteed to find the optimal solution.
To improve the efficiency of the algorithm, several heuristics and approximations can be used,
such as pruning branches of the search tree or using a genetic algorithm to generate and evolve
routes. Additionally, parallelization and memoization can be used to reduce the computation
time of the algorithm.
--------------------------
Complexity
--------------------------
The time complexity of the Stochastic Dynamic Programming algorithm for the Traveling
Salesman Problem is O(n^2 * 2^n), where n is the number of cities. This is because the
algorithm needs to iterate over all possible subsets of cities of size 2 to n-1, which takes O(2^n)
time, and for each subset, it needs to iterate over all cities in the subset, which takes O(n) time.
Additionally, for each city in the subset, it needs to compute the minimum distance to reach
that city from any other city in the subset, which also takes O(n) time. Therefore, the overall
time complexity of the algorithm is O(n^2 * 2^n).
The space complexity of the algorithm is O(n * 2^n), since the algorithm needs to store the
value function for all possible subsets of cities, which takes O(2^n) space, and each subset can
have up to n elements, which takes O(n) space.
--------------------------
Stochastic dynamic programming used to solve Shortest Path Problem
--------------------------
Here's an example of how stochastic dynamic programming can be used to solve the Shortest
Path Problem:
import numpy as np
# Example usage
graph = [
[0, 10, 3, np.inf],
[np.inf, 0, 1, 2],
[np.inf, np.inf, 0, 7],
[np.inf, np.inf, np.inf, 0]
]
start = 0
end = 3
path, length = shortest_path(graph, start, end)
print("Shortest path:", path)
print("Length:", length)
In this example, the shortest_path function takes a weighted directed graph represented as an
adjacency matrix graph, and the indices of the starting and ending nodes start and end. The
function initializes the value function with large values for all nodes except the starting node,
and iterates over all nodes in topological order, updating the value function and policy for each
incoming edge to each node. Finally, the function reconstructs the optimal path by following
the policy backwards from the ending node to the starting node.
Note that this implementation assumes that the graph does not contain negative cycles, since
negative cycles can cause the value function to become arbitrarily small and the algorithm to
enter an infinite loop. To handle graphs with negative cycles, a variant of the algorithm such as
the Bellman-Ford algorithm can be used.
The time complexity of the Stochastic Dynamic Programming algorithm for the Shortest Path
Problem is O(n^2), where n is the number of nodes in the graph. This is because the algorithm
needs to iterate over all nodes in topological order, which takes O(n) time, and for each node, it
needs to iterate over all incoming edges to that node, which also takes O(n) time. Additionally,
for each incoming edge, it needs to update the value function and policy, which takes constant
time. Therefore, the overall time complexity of the algorithm is O(n^2).
The space complexity of the algorithm is O(n), since the algorithm only needs to store the value
function and policy for all nodes, which takes O(n) space.
--------------------------
Stochastic dynamic programming used to solve Longest Common Subsequence Problem
--------------------------
Here's an example of how stochastic dynamic programming can be used to solve the Longest
Common Subsequence Problem:
# Example usage
s1 = "AGGTAB"
s2 = "GXTXAYB"
lcs, length = longest_common_subsequence(s1, s2)
print("Longest common subsequence:", lcs)
print("Length:", length)
In this example, the longest_common_subsequence function takes two strings s1 and s2, and
computes their longest common subsequence (LCS) using a dynamic programming approach.
The function initializes the value function with zeros, and iterates over all substrings of s1 and
s2, updating the value function based on whether the characters match or not. Finally, the
function reconstructs the LCS by backtracking through the value function.
Note that the time and space complexity of the algorithm is O(mn), where m and n are the
lengths of s1 and s2, respectively. This is because the algorithm needs to fill a table of size
(m+1) x (n+1) to store the value function, and needs to iterate over all substrings of s1 and s2.
--------------------------
Stochastic dynamic programming used to solve Sequence Alignment Problem
--------------------------
Here's an example of how stochastic dynamic programming can be used to solve the Sequence
Alignment Problem:
def sequence_alignment(s1, s2, gap_penalty, mismatch_penalty, match_reward):
m, n = len(s1), len(s2)
# Initialize the value function with zeros
value_function = [[0]*(n+1) for _ in range(m+1)]
# Initialize the policy with empty strings
policy = [['']*(n+1) for _ in range(m+1)]
# Iterate over all substrings of s1 and s2
for i in range(1, m+1):
for j in range(1, n+1):
if s1[i-1] == s2[j-1]:
# If the characters match, add the match reward and move diagonally
value_function[i][j] = value_function[i-1][j-1] + match_reward
policy[i][j] = 'D'
else:
# Otherwise, take the maximum of moving up, left, or diagonally with gap and
mismatch penalties
up_value = value_function[i-1][j] + gap_penalty
left_value = value_function[i][j-1] + gap_penalty
diagonal_value = value_function[i-1][j-1] + mismatch_penalty
max_value = max(up_value, left_value, diagonal_value)
value_function[i][j] = max_value
# Update the policy based on which direction yields the maximum value
if max_value == up_value:
policy[i][j] = 'U'
elif max_value == left_value:
policy[i][j] = 'L'
else:
policy[i][j] = 'D'
# Reconstruct the aligned sequences
aligned_s1, aligned_s2 = [], []
i, j = m, n
while i > 0 or j > 0:
if policy[i][j] == 'D':
# If moving diagonally, add the characters and move to the previous positions
aligned_s1.append(s1[i-1])
aligned_s2.append(s2[j-1])
i -= 1
j -= 1
elif policy[i][j] == 'U':
# If moving up, add a gap to s1 and move to the previous position of s1
aligned_s1.append('-')
aligned_s2.append(s2[j-1])
i -= 1
else:
# If moving left, add a gap to s2 and move to the previous position of s2
aligned_s1.append(s1[i-1])
aligned_s2.append('-')
j -= 1
aligned_s1.reverse()
aligned_s2.reverse()
# Return the aligned sequences, their score, and the policy
return ''.join(aligned_s1), ''.join(aligned_s2), value_function[m][n], policy
# Example usage
s1 = "AGTACGCA"
s2 = "TATGC"
gap_penalty = -2
mismatch_penalty = -1
match_reward = 1
aligned_s1, aligned_s2, score, policy = sequence_alignment(s1, s2, gap_penalty,
mismatch_penalty, match_reward)
print("Aligned sequence 1:", aligned_s1)
print("Aligned sequence 2:", aligned_s2)
print("Score:", score)
In this example, the sequence_alignment function takes two strings s1 and s2, and computes
their optimal sequence alignment using a dynamic programming approach.
The function takes three penalty/reward parameters: gap_penalty is the penalty for
introducing a gap in the alignment, mismatch_penalty is the penalty for aligning two different
characters, and match_reward is the reward for aligning two identical characters.
The function initializes the value function and policy matrices with zeros and empty strings,
respectively. Then, it iterates over all substrings of s1 and s2, and updates the value function
and policy based on the dynamic programming recurrence:
If the characters at s1[i-1] and s2[j-1] are identical, the function adds the match_reward to the
diagonal value and moves diagonally.
Otherwise, the function computes the values of moving up, left, and diagonally with gap and
mismatch penalties, and takes the maximum of these values. It updates the value function and
policy matrices accordingly.
Finally, the function reconstructs the aligned sequences by following the policy matrix and
backtracking from the last position. It returns the aligned sequences, their score, and the policy
matrix.
In the example usage, we define two strings s1 and s2, and call the sequence_alignment
function with the penalty/reward parameters. The function returns the aligned sequences,
their score, and the policy matrix, which we print to the console.
The time complexity of this algorithm is O(mn), where m and n are the lengths of s1 and s2,
respectively. The space complexity is also O(mn), since we need to store the value function and
policy matrices.
--------------------------
Stochastic dynamic programming used to solve Optimal Binary Search Tree Problem
--------------------------
Here's an example of how to use stochastic dynamic programming to solve the optimal binary
search tree problem:
import numpy as np
# example usage
p = [0.15, 0.1, 0.05, 0.1, 0.2]
q = [0.05, 0.1, 0.05, 0.05, 0.05, 0.1]
n = len(p)
In this example, we define the probability of accessing each key p, the probability of accessing
each dummy key q, and the number of keys n. The optimal_bst function takes these inputs and
returns the cost of the optimal binary search tree and the root matrix that stores the optimal
subtree roots.
The function initializes the e, w, and root matrices with zeros, where e stores the expected cost
of searching a subtree, w stores the sum of probabilities of accessing keys and dummies in a
subtree, and root stores the optimal subtree roots. The function first computes the base cases
for empty subtrees and single-node trees.
Then, the function iterates over all possible subtrees of increasing sizes and computes the
expected cost of searching each subtree rooted at node r. It takes the minimum of these
expected costs and stores the optimal subtree root in the root matrix. Finally, the function
returns the expected cost of searching the entire tree and the root matrix.
The time complexity of this algorithm is O(n^3), where n is the number of keys. The space
complexity is also O(n^2), since we need to store the e, w, and root matrices.
--------------------------
Stochastic dynamic programming used to solve Maximum Subarray Problem
--------------------------
Here's an example of how to use stochastic dynamic programming to solve the maximum
subarray problem:
def max_subarray(arr):
max_sum = arr[0]
curr_sum = arr[0]
for i in range(1, len(arr)):
curr_sum = max(arr[i], curr_sum + arr[i])
max_sum = max(max_sum, curr_sum)
return max_sum
# example usage
arr = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
max_sum = max_subarray(arr)
print(f"Maximum sum of subarray: {max_sum}")
In this example, we define an input array arr. The max_subarray function takes this array and
returns the maximum sum of any contiguous subarray in the array.
The function initializes two variables, max_sum and curr_sum, to the first element of the array.
It then iterates over the remaining elements of the array and updates curr_sum as the
maximum of the current element and the sum of the previous subarray and the current
element. If curr_sum is greater than max_sum, the function updates max_sum to curr_sum.
Finally, the function returns max_sum.
The time complexity of this algorithm is O(n), where n is the length of the input array, since we
iterate over the array once. The space complexity is O(1), since we only need to store two
variables.
--------------------------
Stochastic dynamic programming used to solve Coin Change Problem
--------------------------
Here's an example of how to use stochastic dynamic programming to solve the coin change
problem:
# example usage
coins = [1, 2, 5]
amount = 11
min_coins = coin_change(coins, amount)
print(f"Minimum number of coins needed: {min_coins}")
In this example, we define an input list of coins coins and the desired amount amount. The
coin_change function takes these inputs and returns the minimum number of coins needed to
make the amount.
The function initializes a dynamic programming array dp with size amount + 1 and sets all its
elements to float('inf'). The base case is that we need 0 coins to make 0 amount, so dp[0] is set
to 0.
The function then iterates over the coins and for each coin, it iterates over the dp array from
coin to amount + 1. For each index i, it calculates the minimum number of coins needed to
make the amount i by either using the previous solution for i or the solution for i - coin plus
one additional coin.
Finally, the function returns the value at index amount in the dp array if it's not equal to
float('inf'). If it is, it means that it's impossible to make the amount with the given coins and the
function returns -1.
The time complexity of this algorithm is O(n*amount), where n is the number of coins and
amount is the desired amount. The space complexity is also O(amount), since we only need to
store the dp array.
--------------------------
Stochastic dynamic programming used to solve a scenario of Resource Allocation in the real
world
--------------------------
Stochastic dynamic programming is a technique used to solve optimization problems with
uncertain parameters over time. In this example, we will consider a resource allocation
problem where a company has to decide how much to invest in different projects over time to
maximize its profits.
Suppose the company has two projects to choose from, project A and project B. Each project
has a cost and a potential profit, which are stochastic (i.e., subject to uncertainty). The cost and
profit of each project are given by random variables C_A, P_A, C_B, and P_B, respectively.
Let's assume that the cost and profit of each project are independent and identically
distributed (i.i.d.) random variables with the following properties:
The cost of project A is a uniform random variable between $10,000 and $20,000.
The profit of project A is a normal random variable with a mean of $50,000 and a standard
deviation of $10,000.
The cost of project B is a uniform random variable between $5,000 and $15,000.
The profit of project B is a normal random variable with a mean of $30,000 and a standard
deviation of $5,000.
We will use stochastic dynamic programming to determine the optimal investment strategy
over a horizon of three periods (i.e., three years).
Here's the program code for the stochastic dynamic programming algorithm:
import numpy as np
# Define the problem parameters
num_periods = 3
num_projects = 2
The program first defines the problem parameters and the cost and profit distributions for
each project. It then initializes the decision variables and value function to zero.
The main loop performs a backward recursion over the time horizon, starting from the last
period and moving backwards. For each period and each project, it computes the expected
value of each decision (invest or not invest) by taking into account the possible outcomes of the
cost and profit distributions. It then selects the decision with the highest expected value and
updates the decision variable and value function accordingly.
Finally, the program prints the optimal investment strategy and the expected profit over the
first period (i.e., the initial state of the system).
Note that the program uses NumPy arrays to represent the decision variables, value function,
and distributions. It also uses vectorized operations to compute the expected values and select
the optimal decisions, which makes the algorithm more efficient and faster than using nested
loops.
The optimal investment strategy indicates that the company should invest in both projects in
all three periods. The expected profit over the first period is $229,812.78.
Note that the output may vary each time the program is run due to the randomness of the cost
and profit distributions. However, the optimal investment strategy and expected profit should
be consistent with the problem parameters and the stochastic dynamic programming
algorithm.
The time complexity of the stochastic dynamic programming algorithm depends on the
number of periods and the number of possible decisions at each period. In this example, we
have a time horizon of three periods and two possible decisions (invest or not invest) at each
period, so the time complexity of the algorithm is O(2^3) = O(8).
However, the complexity may increase exponentially with the number of periods and possible
decisions, which can make the algorithm computationally expensive or even infeasible for large
problems.
In addition to time complexity, the space complexity of the algorithm also depends on the
number of periods and possible decisions, as well as the size of the state space (i.e., the number
of possible states at each period). In this example, the state space is small and the decision
variables and value function can be stored in memory using NumPy arrays, so the space
complexity is not a major concern. However, for larger problems with a larger state space, the
space complexity can also become an issue.
--------------------------
Stochastic dynamic programming used to solve a scenario of Inventory Management in the real
world
--------------------------
Stochastic dynamic programming can also be used to solve optimization problems related to
inventory management. In this example, we will consider a scenario where a company has to
decide how much inventory to order at each period to minimize the total costs of holding
inventory and backordering.
Suppose the company sells a single product that has a stochastic demand, which follows a
Poisson distribution with a mean of 100 units per week. The lead time for ordering inventory is
one week, and the company incurs a fixed ordering cost of $200 per order. The company can
hold up to 500 units of inventory at any time, and incurs a holding cost of $1 per unit per week.
The company can also backorder up to 100 units at any time, and incurs a backordering cost of
$10 per unit per week.
We will use stochastic dynamic programming to determine the optimal inventory ordering
strategy over a horizon of four weeks.
Here's the program code for the stochastic dynamic programming algorithm:
import numpy as np
The program first defines the problem parameters, including the demand distribution, lead
time, inventory capacity, and costs. It then initializes the decision variables and value function
to zero.
The main loop performs a backward recursion over the time horizon, starting from the last
period and moving backwards. For each period, inventory level, and backorder level, it
computes the expected cost of each decision (order or not order) by taking into account the
possible outcomes of the demand and inventory levels. It then selects the decision with the
lowest expected cost and updates the decision variable and value function accordingly.
Finally, the program prints the optimal inventory ordering strategy and the expected total cost
over the first period (i.e., the initial state of the system).
Note that the program uses NumPy arrays to represent the decision variables, value function,
and demand distribution. It also uses vectorized operations to compute the expected costs and
select the optimal decisions, which makes the algorithm more efficient and faster than using
nested loops.
The optimal inventory ordering strategy indicates that the company should order inventory at
every period. The expected total cost over the first period is $2,755.82.
Note that the output may vary each time the program is run due to the randomness of the
demand distribution. However, the optimal inventory ordering strategy and expected total cost
should be consistent with the problem parameters and the stochastic dynamic programming
algorithm.
The time complexity of the stochastic dynamic programming algorithm for inventory
management depends on the number of periods and the maximum inventory and backorder
levels. In this example, we have a time horizon of four periods and a maximum inventory level
of 500 and a maximum backorder level of 100, so the time complexity of the algorithm is
O(4500100) = O(200,000).
However, the actual number of iterations may be lower due to the early termination of the
inner loop when the expected value is minimized. Moreover, the use of NumPy arrays and
vectorized operations can significantly improve the efficiency of the algorithm compared to
using nested loops.
The space complexity of the algorithm also depends on the number of periods and the
maximum inventory and backorder levels, as well as the size of the state space (i.e., the number
of possible states at each period). In this example, the state space is relatively small and the
decision variables and value function can be stored in memory using NumPy arrays, so the
space complexity is not a major concern. However, for larger problems with a larger state
space, the space complexity can also become an issue.
--------------------------
Stochastic dynamic programming used to solve a scenario of Production Planning in the real
world
--------------------------
Stochastic dynamic programming can also be used to solve optimization problems related to
production planning. In this example, we will consider a scenario where a company has to
decide how much to produce at each period to maximize the total profit, taking into account
the stochastic demand, production capacity, and inventory constraints.
Suppose the company produces a single product that has a stochastic demand, which follows a
normal distribution with a mean of 100 units per week and a standard deviation of 20 units per
week. The company has a fixed production cost of $500 per week and a variable production
cost of $2 per unit. The company can produce up to 200 units per week, and can hold up to 500
units of inventory at any time. The company can also backorder up to 100 units at any time,
and incurs a backordering cost of $10 per unit per week.
We will use stochastic dynamic programming to determine the optimal production planning
strategy over a horizon of four weeks.
Here's the program code for the stochastic dynamic programming algorithm:
import numpy as np
The program first defines the problem parameters, including the demand distribution,
production capacity, inventory capacity, and costs. It then initializes the decision variables and
value function to zero.
The main loop performs a backward recursion over the time horizon, starting from the last
period and moving backwards. For each period, inventory level, and backorder level, it
computes the expected cost and expected profit of each production level by taking into account
the possible outcomes of the demand, production, inventory, and backorder levels. It then
selects the production level that maximizes the expected profit and updates the decision
variable and value function accordingly.
Finally, the program prints the optimal production planning strategy and the expected total
profit over the first period (i.e., the initial state of the system).
Note that the program uses NumPy arrays to represent the decision variables, value function
The time complexity of the stochastic dynamic programming algorithm for production
planning depends on the number of periods, the maximum inventory level, and the maximum
backorder level, as well as the production capacity. In this example, we have a time horizon of
four periods, a maximum inventory level of 500, a maximum backorder level of 100, and a
production capacity of 200 units per week, so the time complexity of the algorithm is
O(4500100*200) = O(40,000,000).
However, as in the previous examples, the actual number of iterations may be lower due to the
early termination of the inner loop when the expected value is minimized. Moreover, the use of
NumPy arrays and vectorized operations can significantly improve the efficiency of the
algorithm compared to using nested loops.
The space complexity of the algorithm also depends on the number of periods, the maximum
inventory level, and the maximum backorder level, as well as the size of the state space (i.e., the
number of possible states at each period). In this example, the state space is relatively small
and the decision variables and value function can be stored in memory using NumPy arrays, so
the space complexity is not a major concern. However, for larger problems with a larger state
space, the space complexity can also become an issue.
--------------------------
Stochastic dynamic programming used to solve a scenario of Portfolio Optimization in the real
world
--------------------------
Here's an example program that uses stochastic dynamic programming to solve a portfolio
optimization problem:
import numpy as np
# Define parameters
R = np.array([0.1, 0.2, 0.3]) # expected returns
V = np.array([0.05, 0.1, 0.15]) # volatilities
T = 3 # number of periods
alpha = 0.05 # risk aversion parameter
M = 1000 # maximum investment
We discretize the possible portfolio values into n equally spaced points between 0 and M, and
the possible investment decisions into m equally spaced points between -0.5M and 0.5M. We
then define the transition probability matrix P, which represents the probability of
transitioning from one portfolio value to another with a given investment decision.
We define the value function V_t, which represents the value of each possible portfolio value at
each time period, and use the Bellman equation to iteratively compute V_t backwards in time.
Finally, we use the computed V_t to determine the optimal portfolio values at each time period.
The time complexity of this program is O(Tnm^2), where T is the number of time periods, n is
the number of possible portfolio values, and m is the number of possible investment decisions.
However, in practice, the program may converge to a solution in fewer iterations.
--------------------------
Stochastic dynamic programming used to solve a scenario of Routing and Scheduling in the real
world
--------------------------
Here's an example program that uses stochastic dynamic programming to solve a routing and
scheduling problem:
import numpy as np
# Define parameters
N = 5 # number of jobs
T = 10 # number of time periods
C = np.array([1, 2, 3, 4, 5]) # job costs
D = np.array([4, 2, 5, 1, 3]) # job deadlines
R = np.random.randint(1, 4, size=(N, T)) # job resource requirements
In this example, we have a routing and scheduling problem where we need to decide which
jobs to accept or reject at each time period, subject to resource constraints and deadlines. The
costs, deadlines, and resource requirements of the jobs are given by C, D, and R, respectively.
We discretize the possible time periods into n equally spaced points between 0 and 1, and the
possible decisions (accept or reject a job) into m possible values. We then define the transition
probability matrix P, which represents the probability of transitioning from one time period to
another with a given decision.
We define the value function V_t, which represents the value of each possible time period at
each state, and use the Bellman equation to iteratively compute V_t
In the examples I provided, the complexity will depend on the size of the state and decision
spaces, as well as the number of time periods. The program code for each example includes
loops over the state and decision spaces, as well as time periods, so the overall time complexity
will depend on the sizes of these loops.
The time complexity of the resource allocation example, for instance, is O(N^2T), where N is
the number of resources and T is the number of time periods. The inventory management
example has a time complexity of O(ST), where S is the number of possible inventory levels and
T is the number of time periods. The production planning example has a time complexity of
O(PNT), where P is the number of possible production levels, N is the number of products, and
T is the number of time periods.
The portfolio optimization example has a time complexity of O(NW), where N is the number of
assets and W is the number of possible portfolio weights. The routing and scheduling example
has a time complexity of O(n^2T), where n is the number of possible time periods.
Note that these complexities are just rough estimates and may vary depending on the specific
implementation and algorithm used.
Chapter 12: MCQ
Tabular
Answer: A
Answer: A
What is the main advantage of Tabular dynamic programming over a brute-force search?
A. It requires memory
B. It can handle larger input sizes
C. It is faster
D. It always finds the optimal solution
Answer: B
What is the time complexity of Tabular dynamic programming?
A. O(n)
B. O(nlogn)
C. O(n^2)
D. O(2^n)
Answer: C
Answer: C
Answer: C
In Tabular dynamic programming, what is the meaning of the term "optimal substructure"?
A. A subproblem that can be solved independently of other subproblems
B. A subproblem that is guaranteed to have an optimal solution
C. A subproblem that can be solved in constant time
D. A subproblem that has already been solved
Answer: B
What is the difference between a top-down and a bottom-up approach in Tabular dynamic
programming?
A. A top-down approach uses recursion while a bottom-up approach uses iteration
B. A top-down approach solves subproblems first while a bottom-up approach solves them last
C. A top-down approach is faster than a bottom-up approach
D. A top-down approach is more memory-efficient than a bottom-up approach
Answer: A
Answer: A
Which of the following is an example of a problem that can be solved using Tabular dynamic
programming?
A. Traveling salesman problem
B. Longest common subsequence problem
C. Subset sum problem
D. All of the above
Answer: D
Answer: B
Which of the following is an example of a problem that cannot be solved using Tabular dynamic
programming?
A. Knapsack problem
B. Matrix chain multiplication problem
C. Shortest path problem in a DAG
D. All of the above can be solved using Tabular dynamic programming
Answer: D
What is the time complexity of the Knapsack problem when solved using Tabular dynamic
programming?
A. O(n)
B. O(nlogn)
C. O(n^2)
D. O(2^n)
Answer: C
Which of the following is an example of a problem that can be solved using both Tabular
dynamic programming and greedy algorithms?
A. Knapsack problem
B. Longest increasing subsequence problem
C. Subset sum problem
D. All of the above
Answer: B
What is the time complexity of the Longest common subsequence problem when solved using
Tabular dynamic programming?
A. O(n)
B. O(nlogn)
C. O(n^2)
D. O(2^n)
Answer: C
What is the time complexity of the Matrix chain multiplication problem when solved using
Tabular dynamic programming?
A. O(n)
B. O(nlogn)
C. O(n^2)
D. O(2^n)
Answer: C
What is the space complexity of the Knapsack problem when solved using Tabular dynamic
programming?
A. O(n)
B. O(nlogn)
C. O(n^2)
D. O(2^n)
Answer: C
What is the space complexity of the Longest common subsequence problem when solved using
Tabular dynamic programming?
A. O(n)
B. O(nlogn)
C. O(n^2)
D. O(2^n)
Answer: C
What is the space complexity of the Matrix chain multiplication problem when solved using
Tabular dynamic programming?
A. O(n)
B. O(nlogn)
C. O(n^2)
D. O(2^n)
Answer: C
Which of the following is an example of a problem that can be solved using Tabular dynamic
programming but not memoization?
A. Knapsack problem
B. Longest increasing subsequence problem
C. Subset sum problem
D. All of the above
Answer: A
Answer: B
What is the time complexity of the Shortest path problem in a DAG when solved using Tabular
dynamic programming?
A. O(n)
B. O(nlogn)
C. O(n^2)
D. O(2^n)
Answer: A
What is the space complexity of the Shortest path problem in a DAG when solved using Tabular
dynamic programming?
A. O(n)
B. O(nlogn)
C. O(n^2)
D. O(2^n)
Answer: A
Which of the following is an example of a problem that can be solved using Tabular dynamic
programming but not greedy algorithms?
A. Knapsack problem
B. Longest increasing subsequence problem
C. Subset sum problem
D. All of the above
Answer: C
What is the time complexity of the Subset sum problem when solved using Tabular dynamic
programming?
A. O(n)
B. O(nlogn)
C. O(n^2)
D. O(2^n)
Answer: C
What is the space complexity of the Subset sum problem when solved using Tabular dynamic
programming?
A. O(n)
B. O(nlogn)
C. O(n^2)
D. O(2^n)
Answer: C
Answer: A
Which of the following is an example of a problem that can be solved using Tabular dynamic
programming but not backtracking?
A. Knapsack problem
B. Longest increasing subsequence problem
C. Subset sum problem
D. All of the above
Answer: C
Answer: B
Memoization
What is memoization?
a) A technique to solve problems by breaking them into smaller subproblems
b) A technique to store the results of expensive function calls and return the cached result
when the same inputs occur again
c) A technique to optimize code execution by minimizing the number of function calls
d) A technique to avoid recursion in a program
Answer: b
Answer: c
Answer: b
Answer: a
What is the time complexity of the Fibonacci sequence generation algorithm using
memoization?
a) O(n)
b) O(n^2)
c) O(2^n)
d) O(log n)
Answer: a
What is the space complexity of the Fibonacci sequence generation algorithm using
memoization?
a) O(n)
b) O(n^2)
c) O(2^n)
d) O(log n)
Answer: a
What is the time complexity of the Knapsack problem solved using dynamic programming?
a) O(n)
b) O(n^2)
c) O(2^n)
d) O(nW)
Answer: d
Answer: c
Which of the following is not a necessary condition for a problem to be solvable using dynamic
programming?
a) Optimal substructure
b) Overlapping subproblems
c) Recursive implementation
d) Memory limitations
Answer: d
Answer: d
Answer: a
Answer: b
Which of the following problems can be solved using dynamic programming with
memoization?
a) Finding the median of a sorted array
b) Counting the number of substrings in a string
c) Determining the maximum number of non-overlapping intervals in a list
d) All of the above
Answer: b
Which of the following data structures can be used for memoization in dynamic programming?
a) Arrays
b) Linked lists
c) Stacks
d) All of the above
Answer: a
Which of the following is an example of a problem that can be solved using memoization, but
not dynamic programming?
a) Finding the shortest path in a graph
b) Generating all possible permutations of a set
c) Calculating the factorial of a number
d) Counting the number of ways to climb a staircase
Answer: b
Answer: d
Which of the following is an example of a problem that can be solved using dynamic
programming, but not memoization?
a) Finding the shortest path in a graph
b) Determining the maximum sum of non-adjacent elements in an array
c) Counting the number of ways to make change for a given amount
d) All of the above
Answer: a
Answer: b
Answer: c
Answer: b
Answer: d
Which of the following is an example of a problem that can be solved using both memoization
and dynamic programming?
a) Calculating the n-th Fibonacci number
b) Finding the shortest path in a graph
c) Counting the number of ways to make change for a given amount
d) None of the above
Answer: a
Answer: b
Which of the following problems can be solved using dynamic programming, but not
memoization?
a) Finding the minimum cost to reach a destination in a weighted graph
b) Counting the number of unique paths in a grid
c) Calculating the nth prime number
d) All of the above
Answer: c
Answer: c
Answer: d
Which of the following is an example of a problem that can be solved using memoization and
dynamic programming, but also has an iterative solution?
a) Calculating the n-th Fibonacci number
b) Finding the longest common subsequence of two strings
c) Finding the shortest path in a graph
d) None of the above
Answer: a
Answer: c
Bottom-up
Answer: b
Answer: b
Answer: d
Answer: a
What is the main advantage of bottom-up dynamic programming over top-down dynamic
programming?
a. It is easier to implement
b. It requires space
c. It is faster
d. It can solve more types of problems
Answer: c
What is the first step in solving a problem using bottom-up dynamic programming?
a. Identifying the subproblems
b. Formulating the recurrence relation
c. Implementing the memoization
d. None of the above
Answer: a
Which of the following is an example of a problem that can be solved using bottom-up dynamic
programming?
a. Sorting a list of integers
b. Finding the shortest path in a graph
c. Calculating the nth Fibonacci number
d. None of the above
Answer: c
Answer: c
Which of the following is NOT a step in solving a problem using bottom-up dynamic
programming?
a. Identifying the subproblems
b. Formulating the recurrence relation
c. Implementing the memoization
d. Solving the largest subproblem first
Answer: d
Which of the following is NOT a benefit of using dynamic programming to solve problems?
a. Improved time complexity
b. Improved space complexity
c. Improved readability of the code
d. Improved accuracy of the solution
Answer: c
Answer: c
Answer: a
In dynamic programming, what is the difference between a subproblem and the original
problem?
a. Subproblems are smaller versions of the original problem
b. Subproblems are unrelated to the original problem
c. Subproblems are larger versions of the original problem
d. None of the above
Answer: a
Which of the following is an example of a problem that can be solved using dynamic
programming?
a. Sorting a list of integers
b. Finding the maximum element in a list
c. Calculating the shortest path in a graph
d. None of the above
Answer: c
Answer: a
Which of the following is NOT a step in solving a problem using dynamic programming?
a. Identifying the subproblems
b. Formulating the recurrence relation
c. Implementing the memoization
d. Implementing a brute force algorithm
Answer: d
Answer: a
Which of the following is an example of a problem that can be solved using both top-down and
bottom-up dynamic programming?
a. Calculating the nth Fibonacci number
b. Finding the shortest path in a graph
c. Sorting a list of integers
d. None of the above
Answer: a
Answer: a
Answer: c
Which of the following is NOT a requirement for a problem to be solvable using dynamic
programming?
a. Optimal substructure
b. Overlapping subproblems
c. Recursive subproblems
d. Base case
Answer: c
In dynamic programming, what is the difference between the forward approach and the
backward approach?
a. The forward approach solves the problem iteratively, while the backward approach solves
the problem recursively
b. The forward approach solves the problem recursively, while the backward approach solves
the problem iteratively
c. There is no difference between the two approaches
d. None of the above
Answer: b
Which of the following is an example of a problem that can be solved using dynamic
programming with the forward approach?
a. Calculating the longest increasing subsequence in a list
b. Calculating the shortest path in a graph
c. Sorting a list of integers
Answer: a
Which of the following is an example of a problem that can be solved using dynamic
programming with the backward approach?
a. Calculating the nth Fibonacci number
b. Finding the maximum element in a list
c. Calculating the shortest path in a graph
d. None of the above
Answer: a
Answer: c
Answer: d
Answer: d
Answer: d
Top-down
Answer: a
What is memoization?
a. A technique used to speed up recursive algorithms by caching intermediate results
b. A technique used to slow down recursive algorithms by caching intermediate results
c. A technique used to convert recursive algorithms into iterative algorithms
d. A technique used to convert iterative algorithms into recursive algorithms
Answer: a
Which of the following is NOT a step in the top-down dynamic programming algorithm?
a. Identify the base case
b. Divide the problem into smaller subproblems
c. Solve the subproblems recursively
d. Combine the solutions to the subproblems to solve the original problem
Answer: b
Answer: a
Answer: d
Answer: d
Answer: d
Answer: c
Which of the following data structures is commonly used in top-down dynamic programming?
a. Stack
b. Queue
c. Hash table
d. Memoization table
Answer: d
Answer: a
Which of the following is a characteristic of a problem that can be solved using top-down
dynamic programming?
a. The problem can be divided into smaller subproblems
b. The problem cannot be divided into smaller subproblems
c. The problem has a single solution
d. The problem has multiple solutions
Answer: a
Answer: a
Which of the following is an example of a problem that can be solved using top-down dynamic
programming?
a. Calculating the factorial of a number
b. Finding the minimum value in an array
c. Sorting an array of integers
d. Finding the shortest path in a graph
Answer: d
Which of the following is a disadvantage of using a memoization table in top-down dynamic
programming?
a. It can take up a lot of memory
b. It can slow down the algorithm
c. It can make the code more difficult to read
d. It can make the code more difficult to write
Answer: a
Answer: a
Which of the following is a way to optimize the top-down dynamic programming algorithm?
a. Using a larger memoization table
b. Using a smaller memoization table
c. Using a different data structure for memoization
d. Not using memoization at all
Answer: b
Answer: b
Which of the following is an example of a problem that can be solved using top-down dynamic
programming with memoization?
a. Finding the maximum value in an array
b. Calculating the Fibonacci sequence
c. Sorting an array of strings
d. Finding the longest common subsequence of two strings
Answer: d
Answer: a
Answer: a
Which of the following is an example of a problem that can be solved using top-down dynamic
programming without memoization?
a. Calculating the GCD of two numbers
b. Finding the shortest path in a graph
c. Sorting an array of integers
d. Finding the maximum value in an array
Answer: b
Which of the following is a way to improve the time complexity of the top-down dynamic
programming algorithm?
a. Using a smaller memoization table
b. Using a larger memoization table
c. Using a different data structure for memoization
d. None of the above
Answer: a
Which of the following is a way to improve the space complexity of the top-down dynamic
programming algorithm?
a. Using a smaller memoization table
b. Using a larger memoization table
c. Using a different data structure for memoization
d. None of the above
Answer: c
Which of the following is an example of a problem that can be solved using top-down dynamic
programming with memoization, but not using an iterative approach?
a. Finding the minimum value in an array
b. Calculating the factorial of a number
c. Finding the longest increasing subsequence of an array
d. Sorting an array of strings
Answer: c
Answer: d
Answer: b
Which of the following is a way to ensure that the memoization table is initialized correctly in
top-down dynamic programming?
a. Initializing all entries to null
b. Initializing all entries to -1
c. Initializing all entries to 0
d. Initializing all entries to 1
Answer: b
Which of the following is a way to optimize the use of the memoization table in top-down
dynamic programming?
a. Using a smaller table and resizing it as needed
b. Using a larger table and resizing it as needed
c. Using a hash table instead of an array
d. Using a linked list instead of an array
Answer: a
Which of the following is an example of a problem that can be solved using both top-down and
bottom-up dynamic programming?
a. Calculating the nth Fibonacci number
b. Finding the shortest path in a graph
c. Sorting an array of integers
d. Finding the maximum value in an array
Answer: a
Which of the following is a key difference between top-down and bottom-up dynamic
programming?
a. Top-down starts from the smallest subproblems and works up to the larger ones, while
bottom-up starts from the largest subproblems and works down to the smaller ones.
b. Top-down uses recursion, while bottom-up uses iteration.
c. Top-down uses memoization, while bottom-up does not.
d. Top-down always guarantees an optimal solution, while bottom-up does not.
Answer: b
Divide-and-conquer
Which of the following techniques is used to solve problems by breaking them down into
subproblems and solving them recursively?
a. Divide-and-conquer
b. Dynamic programming
c. Greedy algorithm
d. Backtracking
Answer: a
In divide-and-conquer approach, the problem is divided into smaller subproblems that are:
a. Overlapping
b. Independent
c. Random
d. Sequential
Answer: b
Which of the following is an example of a problem that can be solved using divide-and-conquer
technique?
a. Finding the shortest path in a graph
b. Sorting an array of integers
c. Computing the factorial of a number
d. Searching for a substring in a string
Answer: b
Answer: a
Answer: a
Which of the following is an example of a problem that can be solved using dynamic
programming technique?
a. Finding the shortest path in a graph
b. Sorting an array of integers
c. Computing the factorial of a number
d. Computing the nth Fibonacci number
Answer: d
Answer: c
The time complexity of a dynamic programming algorithm is typically expressed using which
of the following notations?
a. O(n)
b. O(log n)
c. O(n log n)
d. O(n^2)
Answer: d
Answer: b
Answer: a
Which of the following algorithms uses divide-and-conquer technique to find the maximum
subarray sum in an array of integers?
a. Dijkstra's algorithm
b. Prim's algorithm
c. Bellman-Ford algorithm
d. Kadane's algorithm
Answer: d
Which of the following algorithms uses dynamic programming technique to find the longest
common subsequence between two strings?
a. Dijkstra's algorithm
b. Prim's algorithm
c. Bellman-Ford algorithm
d. Longest Common Subsequence (LCS) algorithm
Answer: d
The divide-and-conquer approach is particularly suited for problems that can be:
a. Solved in constant time
b. Solved in logarithmic time
c. Broken down into independent subproblems
d. Broken down into subproblems of equal size
Answer: d
The dynamic programming approach is particularly suited for problems that have:
a. Optimal substructure
b. Independent subproblems
c. Subproblems of equal size
d. Overlapping subproblems
Answer: a
Answer: a
Answer: d
The merge sort algorithm is an example of a sorting algorithm that uses:
a. Divide-and-conquer
b. Dynamic programming
c. Greedy algorithm
d. Backtracking
Answer: a
Which of the following is an example of a problem that can be solved using both divide-and-
conquer and dynamic programming approaches?
a. Matrix chain multiplication
b. Shortest path in a graph
c. Knapsack problem
d. Traveling salesman problem
Answer: a
The time complexity of the matrix chain multiplication problem can be reduced using:
a. Divide-and-conquer
b. Dynamic programming
c. Greedy algorithm
d. Backtracking
Answer: b
Which of the following is a common technique used in dynamic programming to reduce the
time complexity of a problem?
a. Memoization
b. Recursion
c. Backtracking
d. Branch and bound
Answer: a
Which of the following is a common technique used in divide-and-conquer to reduce the time
complexity of a problem?
a. Memoization
b. Recursion
c. Backtracking
d. Branch and bound
Answer: b
Answer: c
The time complexity of the dynamic programming algorithm for the Knapsack problem is:
a. O(n)
b. O(log n)
c. O(n log n)
d. O(nW)
Answer: d
The time complexity of the divide-and-conquer algorithm for the Matrix multiplication
problem is:
a. O(n)
b. O(log n)
c. O(n log n)
d. O(n^3)
Answer: d
Which of the following algorithms uses divide-and-conquer technique to find the kth smallest
element in an array?
a. Quick sort
b. Merge sort
c. Selection sort
d. Insertion sort
Answer: a
The time complexity of the divide-and-conquer algorithm for finding the kth smallest element
in an array is:
a. O(n)
b. O(log n)
c. O(n log n)
d. O(n^2)
Answer: c
The dynamic programming approach can be used to solve which of the following problems?
a. Shortest path in a graph
b. Finding the kth smallest element in an array
c. Traveling salesman problem
d. Longest increasing subsequence
Answer: a
Which of the following is an advantage of the divide-and-conquer approach over the dynamic
programming approach?
a. It is easier to implement
b. It is faster for small inputs
c. It is faster for large inputs
d. It uses memory
Answer: b
Multistage
Answer: b
Answer: b
What is the Bellman equation used for in Multistage dynamic programming?
Answer: b
Answer: b
Answer: a
Answer: c
a) It is computationally expensive
b) It assumes that the system is Markovian
c) It requires complete knowledge of the problem
d) It can only be used for problems with a finite number of stages
Answer: d
Which of the following is an example of a problem that can be solved using Multistage dynamic
programming?
Answer: c
What is the difference between forward and backward Multistage dynamic programming?
a) Forward dynamic programming starts from the first stage and moves forward, while
backward dynamic programming starts from the last stage and moves backward.
b) Forward dynamic programming assumes that the future is uncertain, while backward
dynamic programming assumes that the past is uncertain.
c) There is no difference between forward and backward dynamic programming.
d) None of the above
Answer: a
a) The optimal policy at each stage depends only on the current state and the remaining stages,
not on the past.
b) The optimal policy at each stage depends only on the past, not on the future.
c) The optimal policy at each stage depends on both the past and the future.
d) None of the above
Answer: a
Which of the following is an example of a problem that can be solved using backward
Multistage dynamic programming?
Answer: b
Which of the following algorithms can be used to solve Multistage dynamic programming
problems?
a) Dijkstra's algorithm
b) Prim's algorithm
c) Bellman-Ford algorithm
d) None of the above
Answer: c
a) Initialization
b) Relaxation
c) Termination
d) All of the above
Answer: d
a) O(n)
b) O(n^2)
c) O(nlogn)
d) O(2^n)
Answer: b
a) O(n)
b) O(n^2)
c) O(nlogn)
d) O(2^n)
Answer: b
Answer: c
a) O(n)
b) O(n^2)
c) O(nlogn)
d) O(2^n)
Answer: b
What is the forward-backward algorithm used for?
Answer: c
a) O(n)
b) O(n^2)
c) O(nlogn)
d) O(2^n)
Answer: b
Answer: c
Which of the following is an example of a problem that can be solved using the Baum-Welch
algorithm?
a) Predicting the next word in a sentence
b) Recognizing speech from an audio signal
c) Classifying images in a computer vision task
d) None of the above
Answer: b
a) O(n)
b) O(n^2)
c) O(nlogn)
d) O(2^n)
Answer: b
Answer: c
a) O(n)
b) O(n^2)
c) O(nlogn)
d) O(2^n)
Answer: b
Answer: d
a) Initialization
b) E-step
c) M-step
d) All of the above
Answer: d
a) O(n)
b) O(n^2)
c) O(nlogn)
d) O(2^n)
Answer: a
What is the time complexity of the M-step in the Baum-Welch algorithm?
a) O(n)
b) O(n^2)
c) O(nlogn)
d) O(2^n)
Answer: a
Answer: a
Convex
Answer: a
In convex dynamic programming, the problem is broken down into smaller subproblems that
are:
a. Independent of each other
b. Dependent on each other
c. Unsolvable
d. None of the above
Answer: b
Answer: c
Answer: a
Answer: d
The main advantage of convex dynamic programming over other methods is that it:
a. Can solve non-convex problems
b. Is faster
c. Always finds the global optimal solution
d. None of the above
Answer: c
Answer: a
Answer: c
Answer: a
Answer: b
Answer: a
Answer: a
Which of the following is a common algorithm used to solve convex optimization problems?
a. Gradient descent
b. Newton's method
c. The simplex method
d. None of the above
Answer: a
Answer: b
Answer: a
In convex optimization, the KKT conditions:
a. Are necessary and sufficient for optimality
b. Are necessary but not sufficient for optimality
c. Are sufficient but not necessary for optimality
d. None of the above
Answer: a
Answer: c
Answer: a
Answer: b
Answer: a
Which of the following is true about convex optimization problems with linear constraints?
a. They can always be solved in closed form
b. They can always be solved using gradient descent
c. They can always be solved using the simplex method
d. None of the above
Answer: c
Answer: a
Which of the following is true about the convergence of gradient descent in convex
optimization?
a. It always converges to the global optimum
b. It always converges to a local optimum
c. It may converge to a suboptimal solution
d. None of the above
Answer: a
Answer: a
Answer: c
Which of the following is true about the duality gap in convex optimization?
a. It is always zero at the global optimum
b. It is always positive at the global optimum
c. It is always negative at the global optimum
d. None of the above
Answer: a
Answer: b
Which of the following is a common algorithm used to solve convex optimization problems?
a. Newton's method
b. Gradient descent
c. Simulated annealing
d. None of the above
Parallel
Which of the following is not a typical step in a parallel dynamic programming algorithm?
A. Initialization
B. Computation
C. Communication
D. Termination
Answer: D
What is the main benefit of parallel dynamic programming over sequential dynamic
programming?
A. Faster runtime
B. More accurate results
C. Lower memory usage
D. Simpler implementation
Answer: A
Which of the following is an advantage of using a GPU for parallel dynamic programming?
A. High memory bandwidth
B. Low power consumption
C. Distributed memory
D. Fault tolerance
Answer: A
Which of the following is not a common technique for load balancing in parallel dynamic
programming?
A. Work stealing
B. Task splitting
C. Task fusion
D. Task migration
Answer: C
Which of the following is a common metric for measuring the performance of a parallel
dynamic programming algorithm?
A. Time complexity
B. Space complexity
C. Parallel efficiency
D. Cache utilization
Answer: C
Which of the following is not a common technique for reducing communication overhead in
parallel dynamic programming?
A. Pipelining
B. Caching
C. Compression
D. Synchronization
Answer: D
Which of the following is not a common data structure used in parallel dynamic programming?
A. Linked list
B. Array
C. Graph
D. Tree
Answer: A
Which of the following is a common parallel dynamic programming algorithm for computing
the edit distance between two strings?
A. Needleman-Wunsch algorithm
B. Smith-Waterman algorithm
C. Levenshtein distance algorithm
D. Hamming distance algorithm
Answer: C
Which of the following is a common parallel dynamic programming algorithm for computing
the longest common subsequence of two sequences?
A. Needleman-Wunsch algorithm
B. Smith-Waterman algorithm
C. Levenshtein distance algorithm
D. LCS algorithm
Answer: D
Which of the following is a common parallel dynamic programming algorithm for solving the
knapsack problem?
A. 0/1 knapsack algorithm
B. Fractional knapsack algorithm
C. Unbounded knapsack algorithm
D. Bounded knapsack algorithm
Answer: A
Which of the following is not a common parallel dynamic programming algorithm for sequence
alignment?
A. Needleman-Wunsch algorithm
B. Smith-Waterman algorithm
C. Hirschberg's algorithm
D. A* algorithm
Answer: D
Which of the following is a common parallel dynamic programming algorithm for computing
the all-pairs shortest path in a graph?
A. Floyd-Warshall algorithm
B. Dijkstra's algorithm
C. Bellman-Ford algorithm
D. Prim's algorithm
Answer: A
Which of the following is a common technique for avoiding race conditions in parallel dynamic
programming?
A. Locking
B. Barrier synchronization
C. Atomic operations
D. All of the above
Answer: D
Which of the following is not a common parallel dynamic programming algorithm for
clustering?
A. K-means algorithm
B. Hierarchical clustering algorithm
C. Expectation-maximization algorithm
D. Agglomerative clustering algorithm
Answer: C
Which of the following is not a common parallel dynamic programming algorithm for graph
algorithms?
A. Breadth-first search
B. Depth-first search
C. Dijkstra's algorithm
D. Prim's algorithm
Answer: B
Which of the following is a common parallel dynamic programming algorithm for computing
the optimal binary search tree?
A. Knuth's algorithm
B. Huffman coding algorithm
C. Binary search algorithm
D. AVL tree algorithm
Answer: A
Which of the following is not a common parallel dynamic programming algorithm for machine
learning?
A. Support vector machine
B. Decision tree
C. Neural network
D. Naive Bayes
Answer: B
Which of the following is a common parallel dynamic programming algorithm for computing
the longest increasing subsequence of a sequence?
A. Longest common subsequence algorithm
B. LIS algorithm
C. LCS algorithm
D. Smith-Waterman algorithm
Answer: B
Which of the following is not a common parallel dynamic programming algorithm for
numerical analysis?
A. Simpson's rule
B. Newton's method
C. Runge-Kutta method
D. Euclidean algorithm
Answer: D
Which of the following is a common parallel dynamic programming algorithm for solving the
traveling salesman problem?
A. Brute force algorithm
B. Greedy algorithm
C. Simulated annealing algorithm
D. Dynamic programming algorithm
Answer: D
Which of the following is not a common parallel dynamic programming algorithm for pattern
matching?
A. Boyer-Moore algorithm
B. Knuth-Morris-Pratt algorithm
C. Rabin-Karp algorithm
D. Needleman-Wunsch algorithm
Answer: D
Which of the following is a common parallel dynamic programming algorithm for image
processing?
A. Seam carving algorithm
B. SIFT algorithm
C. Hough transform algorithm
D. Sobel operator algorithm
Answer: A
Which of the following is not a common parallel dynamic programming algorithm for data
compression?
A. Huffman coding algorithm
B. Lempel-Ziv-Welch algorithm
C. Burrows-Wheeler transform algorithm
D. Dijkstra's algorithm
Answer: D
Which of the following is a common parallel dynamic programming algorithm for portfolio
optimization?
A. Markowitz's mean-variance optimization
B. Capital asset pricing model
C. Black-Scholes model
D. None of the above
Answer: A
Online
Answer: a
What is the main advantage of Online Dynamic Programming over Offline Dynamic
Programming?
a) It requires memory
b) It is faster
c) It can handle problems with changing inputs over time
d) It produces more accurate results
Answer: c
Which of the following is not an example of a problem that can be solved using Online Dynamic
Programming?
a) Knapsack problem
b) Longest Common Subsequence problem
c) Shortest Path problem
d) Sorting problem
Answer: d
Answer: a
Answer: c
Answer: c
Answer: a
Answer: a
Answer: a
What is the purpose of the memoization step in Online Dynamic Programming?
a) To store the solutions of all previous states in a table for later use
b) To reduce the number of recursive calls by caching the results of previous calls
c) To compute the optimal solution for the current state
d) To check whether the current state satisfies some conditions
Answer: b
Which of the following is not a type of memoization used in Online Dynamic Programming?
a) Top-down memoization
b) Bottom-up memoization
c) Forward memoization
d) Backward memoization
Answer: c
Answer: c
Which of the following is not a common optimization technique used in Online Dynamic
Programming?
a) Pruning
b) Tabulation
c) Approximation
d) Branch and bound
Answer: c
Answer: a
Answer: a
Answer: a
Which of the following is an example of a problem that can be solved using Online Dynamic
Programming with pruning?
a) Travelling Salesman Problem
b) Maximum Subarray Problem
c) Longest Increasing Subsequence Problem
d) Edit Distance Problem
Answer: c
Answer: a
Answer: c
Which of the following is not a step in solving a problem using Online Dynamic Programming?
a) Defining the recurrence relation
b) Initializing the table of solutions
c) Choosing a heuristic function
d) Memoizing the solutions
Answer: c
Which of the following is a common use case for Online Dynamic Programming?
a) Solving linear programming problems
b) Solving sorting problems
c) Solving problems with recursive structures
d) Solving problems with changing inputs over time
Answer: d
What is the main advantage of using Online Dynamic Programming for problems with changing
inputs over time?
a) It can adapt to the changing inputs without recomputing the entire solution
b) It can solve the problem faster than other algorithms
c) It can handle problems with non-discrete solutions
d) It can find approximate solutions to the problem
Answer: a
Which of the following is an example of a problem that can be solved using Online Dynamic
Programming with approximation?
a) Travelling Salesman Problem
b) Maximum Subarray Problem
c) Knapsack Problem
d) Edit Distance Problem
Answer: a
Answer: b
What is the main difference between Online Stochastic Dynamic Programming and Online
Deterministic Dynamic Programming?
a) Online Stochastic Dynamic Programming deals with problems with changing inputs over
time, while Online Deterministic Dynamic Programming deals with problems with fixed inputs.
b) Online Stochastic Dynamic Programming uses probabilistic models to represent the inputs,
while Online Deterministic Dynamic Programming uses deterministic models.
c) Online Stochastic Dynamic Programming is faster than Online Deterministic Dynamic
Programming.
d) Online Stochastic Dynamic Programming can handle problems with continuous solutions,
while Online Deterministic Dynamic Programming can only handle problems with discrete
solutions.
Answer: b
Answer: a
Answer: d
Answer: c
Reinforcement learning
What is the Trust Region Policy Optimization (TRPO) algorithm in Reinforcement Learning?
A) An algorithm for estimating the value function by Monte Carlo method
B) An algorithm for learning the optimal policy by policy gradient
C) An algorithm for learning the optimal policy by Q-Learning
D) An algorithm for learning the optimal policy by SARSA
Answer: B
Stochastic
What is the main difference between deterministic dynamic programming and stochastic
dynamic programming?
a. Deterministic dynamic programming only deals with certain outcomes, while stochastic
dynamic programming deals with uncertain outcomes.
b. Stochastic dynamic programming only deals with certain outcomes, while deterministic
dynamic programming deals with uncertain outcomes.
c. Deterministic dynamic programming only applies to discrete time problems, while stochastic
dynamic programming can be applied to continuous time problems.
d. Stochastic dynamic programming only applies to discrete time problems, while deterministic
dynamic programming can be applied to continuous time problems.
Answer: a
Answer: c
Answer: a
What is the difference between an open-loop control policy and a closed-loop control policy?
a. An open-loop control policy is deterministic, while a closed-loop control policy is stochastic.
b. An open-loop control policy is time-invariant, while a closed-loop control policy is time-
varying.
c. An open-loop control policy is based on the current state only, while a closed-loop control
policy is based on both the current state and previous actions.
d. An open-loop control policy is based on the current state and previous actions, while a
closed-loop control policy is based on the current state only.
Answer: d
Answer: a
Answer: c
Answer: b
What is the difference between a finite horizon problem and an infinite horizon problem in
stochastic dynamic programming?
a. A finite horizon problem has a fixed number of time periods, while an infinite horizon
problem does not.
b. A finite horizon problem has a fixed set of possible states, while an infinite horizon problem
does not.
c. A finite horizon problem has a fixed set of possible actions, while an infinite horizon problem
does not.
d. A finite horizon problem has a fixed set of possible rewards, while an infinite horizon
problem does not.
Answer: a
What is the purpose of the policy iteration algorithm in stochastic dynamic programming?
a. To iteratively improve an initial policy until an optimal policy is found.
b. To iteratively update the value function until an optimal value function is found.
c. To find the optimal value function and policy simultaneously.
d. To estimate the state transition probability function.
Answer: a
Answer: a
Answer: a
What is the value iteration algorithm used for in stochastic dynamic programming?
a. To iteratively update the value function until an optimal value function is found.
b. To iteratively improve an initial policy until an optimal policy is found.
c. To find the optimal value function and policy simultaneously.
d. To estimate the state transition probability function.
Answer: a
Answer: c
What is the difference between the value function and the cost-to-go function?
a. The value function represents the expected total reward from a given state under a given
policy, while the cost-to-go function represents the expected total cost from a given state under
a given policy.
b. The value function represents the expected total reward from a given state under a given
policy, while the cost-to-go function represents the expected total reward from a given state
under the optimal policy.
c. The value function represents the optimal policy for a given stochastic dynamic
programming problem, while the cost-to-go function represents the expected total reward
from a given state under a given policy.
d. The value function represents the expected total reward from a given state and action under
a given policy, while the cost-to-go function represents the expected total cost from a given
state and action under a given policy.
Answer: b
Answer: c
Answer: a
What is the difference between a Markov decision process and a partially observable Markov
decision process?
a. In a Markov decision process, the current state completely determines the future states,
while in a partially observable Markov decision process, the current state does not completely
determine the future states.
b. In a Markov decision process, the state is always fully observed, while in a partially
observable Markov decision process, the state may be partially or fully observed.
c. In a Markov decision process, the state transition probabilities are known, while in a partially
observable Markov decision process, the state transition probabilities may not be known.
d. In a Markov decision process, the rewards are deterministic, while in a partially observable
Markov decision process, the rewards may be stochastic.
Answer: b
Answer: a
What is the difference between on-policy learning and off-policy learning in reinforcement
learning?
a. On-policy learning updates the policy that is used to generate the data, while off-policy
learning updates a different policy.
b. On-policy learning uses data from the current policy, while off-policy learning uses data from
a different policy.
c. On-policy learning updates the value function, while off-policy learning updates the policy.
d. On-policy learning always converges to the optimal policy, while off-policy learning may not.
Answer: b
Answer: a
Answer: b
Answer: b
Answer: a
What is the difference between a feedforward neural network and a recurrent neural network?
a. A feedforward neural network can handle sequential data, while a recurrent neural network
cannot.
b. A recurrent neural network can handle sequential data, while a feedforward neural network
cannot.
c. A feedforward neural network has feedback connections, while a recurrent neural network
does not.
d. A recurrent neural network has feedback connections, while a feedforward neural network
does not.
Answer: b
What is the difference between online learning and batch learning in reinforcement learning?
a. Online learning updates the policy after every interaction with the environment, while batch
learning updates the policy after collecting a batch of data.
b. Online learning updates the value function after every interaction with the environment,
while batch learning updates the value function after collecting a batch of data.
c. Online learning always converges to the optimal policy, while batch learning may not.
d. Online learning requires data than batch learning.
Answer: a
Answer: a
Chapter 13: Short Answer Questions
Tabular
How is the optimal solution obtained from the table in tabular dynamic programming?
Answer: The optimal solution can be obtained by backtracking through the table, using the
stored solutions to build up the solution to the original problem.
What is the difference between a table and an array in tabular dynamic programming?
Answer: A table can be multi-dimensional and may contain more than one value per entry,
while an array is one-dimensional and contains a single value per entry.
What is the difference between a state and a subproblem in tabular dynamic programming?
Answer: A subproblem is a problem that can be broken down into smaller problems, while a
state is a set of values that describe a subproblem and whose solution can be stored in the
table.
What is the difference between a topological and a lexicographic ordering in tabular dynamic
programming?
Answer: A topological ordering is an ordering of states that ensures all dependencies are
solved before a state is solved, while a lexicographic ordering is an ordering of states based on
their values.
What is the difference between a forward and backward pass in tabular dynamic
programming?
Answer: A forward pass computes the solutions to smaller subproblems and stores them in the
table, while a backward pass uses the stored solutions to compute the solution to the original
problem.
What is the difference between a recurrence relation and a base case in tabular dynamic
programming?
Answer: A recurrence relation is used to compute the solution to a subproblem based on the
solutions of smaller subproblems, while a base case provides the solution to the smallest
subproblems that cannot be further broken down.
What is the difference between a minimization and a maximization problem in tabular dynamic
programming?
Answer: A minimization problem seeks to find the solution that minimizes a cost or objective
function, while a maximization problem seeks to find the solution that maximizes the same.
What is the difference between a decision and an optimization problem in tabular dynamic
programming?
Answer: A decision problem seeks to find a solution that satisfies a set of constraints, while an
optimization problem seeks to find the best solution among all feasible solutions.
Memoization
What is the time complexity of the Fibonacci sequence problem without memoization?
The time complexity of the Fibonacci sequence problem without memoization is O(2^n). The
time complexity of the Fibonacci sequence problem without memoization is O(2^n) because
the function recursively calls itself twice for each input value, leading to an exponential growth
in the number of function calls and an exponential increase in the time required to compute the
result.
What is the time complexity of the Fibonacci sequence problem with memoization?
The time complexity of the Fibonacci sequence problem with memoization is O(n) because
each input value is computed only once and stored in the cache, and subsequent calls with the
same input value can be immediately retrieved from the cache.
What is the difference between a memoization table and a lookup table in dynamic
programming?
A memoization table is used to store the results of subproblems in a dynamic programming
algorithm, while a lookup table is used to store precomputed values or data for fast retrieval.
What are some common pitfalls when using memoization in dynamic programming?
Common pitfalls when using memoization include cache invalidation issues, incorrect base
cases, and excessive space usage due to caching too many results.
Bottom-up
It can be more efficient than top-down dynamic programming in terms of time and space
complexity
It does not have the overhead of recursive function calls
It can be easier to implement than top-down dynamic programming
What are the disadvantages of using bottom-up dynamic programming?
The disadvantages of using bottom-up dynamic programming include:
How can we optimize the time and space complexity of a dynamic programming algorithm?
The time and space complexity of a dynamic programming algorithm can be optimized by
using techniques such as memoization, tabulation, pruning, and approximation.
Top-down
How is the solution to the original problem obtained in top-down dynamic programming?
The solution to the original problem is obtained by recursively solving sub-problems and
combining their solutions.
What are the conditions for a problem to be solved using dynamic programming?
The conditions for a problem to be solved using dynamic programming are that it has optimal
substructure and overlapping sub-problems.
How can top-down dynamic programming be used to solve the knapsack problem?
The knapsack problem can be solved using top-down dynamic programming by breaking it
down into smaller sub-problems based on the available capacity and the remaining items, and
recursively solving each sub-problem while memoizing the results.
What is the difference between top-down and bottom-up approaches to solving the knapsack
problem?
The difference between top-down and bottom-up approaches to solving the knapsack problem
is that top-down starts with the original problem and breaks it down into smaller sub-
problems, while bottom-up starts with the smallest sub-problems and builds up to the original
problem.
How can top-down dynamic programming be used to solve the longest common subsequence
problem?
The longest common subsequence problem can be solved using top-down dynamic
programming by breaking it down into smaller sub-problems based on the prefixes of the input
strings, and recursively solving each sub-problem while memoizing the results.
What is the time complexity of top-down dynamic programming for the longest common
subsequence problem?
The time complexity of top-down dynamic programming for the longest common subsequence
problem is O(m*n), where m and n are the lengths of the input strings.
How can top-down dynamic programming be used to solve the edit distance problem?
The edit distance problem can be solved using top-down dynamic programming by breaking it
down into smaller sub-problems based on the prefixes of the input strings, and recursively
solving each sub-problem while memoizing the results.
What is the time complexity of top-down dynamic programming for the edit distance problem?
The time complexity of top-down dynamic programming for the edit distance problem is
O(m*n), where m and n are the lengths of the input strings.
Divide-and-conquer
What is divide-and-conquer?
Answer: Divide-and-conquer is a problem-solving technique that involves breaking down a
problem into smaller subproblems, solving each subproblem independently, and combining
the solutions to solve the original problem.
What is the time complexity of the recursive solution for the Fibonacci sequence?
Answer: The time complexity of the recursive solution for the Fibonacci sequence is O(2^n).
What is the time complexity of the dynamic programming solution for the Fibonacci sequence?
Answer: The time complexity of the dynamic programming solution for the Fibonacci sequence
is O(n).
What is the recursive solution for the longest common subsequence problem?
Answer: The recursive solution for the longest common subsequence problem is to check if the
last characters of the two strings match and recursively compute the longest common
subsequence of the two strings without the last characters.
What is the time complexity of the recursive solution for the longest common subsequence
problem?
Answer: The time complexity of the recursive solution for the longest common subsequence
problem is O(2^n).
What is the dynamic programming solution for the longest common subsequence problem?
Answer: The dynamic programming solution for the longest common subsequence problem is
to use memoization to store the results of previously computed subproblems.
What is the time complexity of the dynamic programming solution for the longest common
subsequence problem?
Answer: The time complexity of the dynamic programming solution for the longest common
subsequence problem is O(mn), where m and n are the lengths of the two strings.
What is the recursive solution for the matrix chain multiplication problem?
Answer: The recursive solution for the matrix chain multiplication problem is to try all possible
ways to split the chain of matrices and recursively compute the optimal cost for each split.
What is the time complexity of the recursive solution for the matrix chain multiplication
problem?
Answer: The time complexity of the recursive solution for the matrix chain multiplication
problem is O(2^n).
What is the dynamic programming solution for the matrix chain multiplication problem?
Answer: The dynamic programming solution for the matrix chain multiplication problem
involves using memoization to store the results of previously computed subproblems.
What is the time complexity of the dynamic programming solution for the matrix chain
multiplication problem?
Answer: The time complexity of the dynamic programming solution for the matrix chain
multiplication problem is O(n^3), where n is the number of matrices in the chain.
What is the time complexity of the recursive solution for the subset sum problem?
Answer: The time complexity of the recursive solution for the subset sum problem is O(2^n).
What is the dynamic programming solution for the subset sum problem?
Answer: The dynamic programming solution for the subset sum problem involves using
memoization to store the results of previously computed subproblems.
What is the time complexity of the dynamic programming solution for the subset sum
problem?
Answer: The time complexity of the dynamic programming solution for the subset sum
problem is O(nT), where n is the size of the set and T is the target value.
Multistage
What are the characteristics of a problem that can be solved using the multistage approach?
Problems that can be solved using the multistage approach have the following characteristics:
they have optimal substructure, they can be divided into stages, and each stage can be solved
independently.
What is memoization?
Memoization is a technique used in top-down dynamic programming that involves storing the
results of expensive function calls and returning the cached result when the same inputs occur
again.
What is tabulation?
Tabulation is a technique used in bottom-up dynamic programming that involves storing the
results of each subproblem in a table and using these results to build up to the original
problem.
What is the difference between dynamic programming and divide and conquer?
Dynamic programming involves solving a problem by breaking it down into smaller
subproblems and solving each subproblem only once, whereas divide and conquer involves
breaking a problem down into smaller subproblems and solving each subproblem
independently.
What is the difference between a polynomial time algorithm and an exponential time
algorithm?
A polynomial time algorithm is an algorithm whose running time is bounded by a polynomial
function of the input size, whereas an exponential time algorithm is an algorithm whose
running time grows exponentially with the input size.
What is the difference between a brute force algorithm and an optimized algorithm?
A brute force algorithm is an algorithm that solves a problem by trying all possible solutions,
whereas an optimized algorithm uses more efficient techniques to find the optimal solution.
What is the difference between a local search algorithm and a global search algorithm?
A local search algorithm is an algorithm that explores a small subset of the solution space to
find a good solution, whereas a global search algorithm explores the entire solution space to
find the optimal solution.
Convex
Parallel
How can a parallel prefix sum algorithm be used in parallel dynamic programming?
A: A parallel prefix sum algorithm can be used to compute the cumulative costs or values of a
dynamic programming table in parallel.
How can a parallel matrix multiplication algorithm be used in parallel dynamic programming?
A: A parallel matrix multiplication algorithm can be used to compute the products of two
matrices that represent the cost or value functions of a dynamic programming algorithm.
What are some load balancing techniques for parallel dynamic programming?
A: Load balancing techniques for parallel dynamic programming include static load balancing,
dynamic load balancing, and work stealing.
How can a parallel prefix sum tree be used in parallel dynamic programming?
A: A parallel prefix sum tree can be used to compute the cumulative costs or values of a
dynamic programming table in parallel, by representing each row or column of the table as a
sequence of numbers and constructing a prefix sum tree for each sequence.
What is a parallel scan algorithm?
A: A parallel scan algorithm is a technique for computing a cumulative operation, such as a sum
or a product, in parallel, by breaking the sequence into smaller subproblems and computing
the cumulative operation of each subproblem in parallel.
Online
What are the characteristics of a problem that can be solved using dynamic programming?
The problem must have optimal substructure and overlapping subproblems.
What is memoization?
Memoization is a technique in which the results of a subproblem are stored in memory so that
they can be used later without having to be recalculated.
What is the difference between dynamic programming and divide and conquer?
Divide and conquer is a technique of solving a problem by breaking it down into smaller
subproblems, solving each subproblem independently, and then combining the solutions.
Dynamic programming solves subproblems iteratively and stores the solutions to use later.
What are some examples of problems that can be solved using online dynamic programming?
Some examples include sequence alignment, shortest path problems, and dynamic resource
allocation.
Stochastic
What is the linear programming formulation of the stochastic dynamic programming problem?
A: The linear programming formulation expresses the optimal value function as the solution of
a linear program.
What is the dual linear programming formulation of the stochastic dynamic programming
problem?
A: The dual linear programming formulation expresses the optimal policy as the solution of a
linear program.
What is the difference between value function approximation and policy function
approximation?
A: Value function approximation approximates the optimal value function directly, while policy
function approximation approximates the optimal policy directly.
What is the connection between stochastic dynamic programming and reinforcement learning?
A: Reinforcement learning can be seen as a generalization of stochastic dynamic programming,
where the transition probability function and reward function are not known in advance, but
are learned from experience.
END