Dynamic Programming (DP) is a methodical approach to solving problems by breaking them down into simpler subproblems and solving each subproblem only once, storing its solution for future use. This technique is particularly useful for problems involving optimal substructure and overlapping subproblems. The elements of dynamic programming provide the foundation for structuring and solving DP problems efficiently. These elements are critical to the application of DP.
The main elements of dynamic programming are:
Let's explore these elements in more detail:
Optimal Substructure is one of the core properties of a problem that makes it suitable for dynamic programming. A problem exhibits optimal substructure if the optimal solution to the problem can be constructed efficiently from the optimal solutions to its subproblems. This means that the solution to the overall problem depends on the solutions to smaller subproblems.
Example: In the 0/1 Knapsack Problem, the maximum value that can be obtained by selecting items for a knapsack of capacity can be derived from the maximum values that can be obtained by considering subsets of items with smaller capacities.
Overlapping Subproblems occur when a problem can be broken down into smaller subproblems, and these subproblems are solved multiple times during the process of solving the overall problem. Instead of recomputing the solution for the same subproblem multiple times, dynamic programming stores the results of subproblems (often in a table or cache) and reuses them when needed. This eliminates the redundant computation of the same subproblem.
Example: In the Fibonacci Sequence problem, each Fibonacci number depends on the previous two Fibonacci numbers. As you recursively compute Fibonacci numbers, many intermediate Fibonacci values (like Fibonacci(3), Fibonacci(4), etc.) are recalculated repeatedly. In DP, once these values are computed, they are stored and reused instead of being recomputed.
Memoization (Top-down approach) and Tabulation (Bottom-up approach) are two ways to solve problems with overlapping subproblems.
These are the two common approaches used to store and reuse solutions to subproblems in dynamic programming.
Memoization (Top-Down Approach): Memoization involves writing a recursive function to solve the problem. When a subproblem is solved, its result is stored (typically in a hash table or array) to avoid recomputing the same subproblem. If a subproblem is encountered again, its result is retrieved from the stored values.
Advantages:
Disadvantages:
Example (Fibonacci Sequence):
def fib(n, memo={}):
if n <= 1:
return n
if n not in memo:
memo[n] = fib(n-1, memo) + fib(n-2, memo)
return memo[n]
Tabulation (Bottom-Up Approach): In this approach, we solve the problem iteratively, starting with the smallest subproblems and solving progressively larger subproblems. All solutions are stored in a table (usually a 2D or 1D array), and no recursion is used.
Advantages:
Disadvantages:
Example (Fibonacci Sequence):
def fib(n):
dp = [0] * (n + 1)
dp[1] = 1
for i in range(2, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp[n]
In dynamic programming, the problem is broken down into smaller subproblems, and the state of the problem represents a solution to one of the subproblems. A state transition defines how to move from one subproblem (state) to another.
State Space: This is the set of all possible subproblems or configurations in the problem. In many DP problems, each subproblem corresponds to a state, and solving the problem involves exploring and transitioning between these states.
For example, in the 0/1 Knapsack Problem, the state is typically defined by the item being considered and the remaining capacity of the knapsack. Therefore, the state space consists of all possible combinations of items and knapsack capacities.
State Transition: The process of moving from one state to another depends on the decisions made at each step. These transitions are guided by the problem's recurrence relation (or DP formula).
For example, in the 0/1 Knapsack Problem, the state transition occurs when we either choose to include an item in the knapsack (which reduces the capacity) or skip it.
A recurrence relation defines the relationship between the solution of the current problem and the solutions of its subproblems. This relation is the core of the dynamic programming approach and allows the construction of the solution in a bottom-up or top-down manner.
Example (0/1 Knapsack Problem):
Let represent the maximum value obtainable by considering the first items with a weight limit of .
The recurrence relation for the 0/1 knapsack problem is:
Where:
This recurrence defines how the solution to each subproblem depends on previous subproblems.
These elements work together to create efficient solutions to complex optimization problems. Dynamic programming is particularly effective for problems that involve making decisions over time or stages, where the optimal decision at each stage depends on the decisions made previously.
Open this section to load past papers