Unit - 4
Dynamic Programming
1. Introduction
1.1 Overview
- A dynamic-programming algorithm solves every sub-problem just once and then saves its answer in a table.
- It avoids the work of re-computing the answer every time the sub-problem is encountered.
- Example: If there are multiple items with weights and profits, and we need to fit them into a bag to maximize profit, DP helps to efficiently find the optimal answer.
2. Principle of Optimality
2.1 Concept
- The dynamic programming algorithm obtains the solution using the principle of optimality.
- It states that "in an optimal sequence of decisions or choices, each subsequence must also be optimal."
- If it is not possible to apply the principle of optimality, then it is almost impossible to obtain the solution using the dynamic programming approach.
3. Generalized Solution using DP
3.1 Four Steps
- Characterize the structure of an optimal solution.
- Recursively define the value of an optimal solution.
- Compute the value of an optimal solution in a bottom-up fashion.
- Construct an optimal solution from the computed information.
4. 0/1 Knapsack Problem
4.1 Concept
- We are given items, each with a weight and a value , and a knapsack of capacity .
- The goal is to maximize the value without exceeding the knapsack capacity.
- Unlike Fractional Knapsack, we cannot break items (it's either 0 or 1).
4.2 Step-by-Step Explanation
- Initialize a 2D array .
- Set for .
- Iterate through items from 1 to and weights from 1 to :
- If : The item can't be included. .
- Else: Take the max of excluding the item or including the item. .
- The answer is found at .
4.3 Algorithm Complexity
- Time Complexity: , where is the number of items and is the knapsack capacity.
- Space Complexity: for the 2D DP table. Can be optimized to using a 1D array.
4.4 Algorithm
- Simple (Easy to Remember)
- Formal Algorithm (Code)
💡 The Big Idea in Plain English
Unlike Fractional Knapsack where you could cut items into pieces, here each item is a solid object (like a laptop or TV): you either take the whole thing (1) or leave it behind (0). For every item, you make a very simple comparison:
- Leave it: Keep the best profit you already made using previous items with current space.
- Take it: Add this item's value, plus the best profit you could make with whatever weight capacity remains in your bag. Compare both options, pick the maximum, and store it in a table so you never re-calculate!
👣 3 Easy Steps to Remember
- Create a Grid: Rows = items (), Columns = capacities ().
- With 0 items or 0 capacity, profit is always (fill first row and column with 0).
- Decide for Each Item & Capacity:
- For item and bag capacity :
- Too heavy to fit? (): You have no choice copy the profit directly from the row above ().
- It fits!: Take the maximum of leaving it vs. taking it:
- Read the Solution: The bottom-right corner is the absolute maximum profit!
🧠 Memory Trick: 0 or 1 choice If too heavy: copy from above If it fits: ! Time and space complexity: .
Algorithm 01Knapsack(n, W, wt, val)
// Computes maximum achievable value for 0/1 Knapsack using dynamic programming
// Input: Number of items n, capacity W, weight array wt[1..n], profit array val[1..n]
// Output: Maximum profit stored in K[n, W]
{
for w <- 0 to W do
K[0, w] <- 0;
for i <- 1 to n do
{
K[i, 0] <- 0;
for w <- 1 to W do
{
if (wt[i] <= w) then
K[i, w] <- max(val[i] + K[i - 1, w - wt[i]], K[i - 1, w]);
else
K[i, w] <- K[i - 1, w];
}
}
return K[n, W];
}
5. Binomial Coefficient
5.1 Concept
- A binomial coefficient calculates the number of ways to choose items from items.
- .
- DP avoids redundant calculations by storing intermediate values.
5.2 Step-by-Step Explanation
- Initialize a 2D array of size .
- Loop from 0 to , and from 0 to .
- Base cases: If or , then .
- Otherwise: .
- Return .
5.3 Algorithm Complexity
- Time Complexity:
- Space Complexity: or with a 1D array optimization.
5.4 Algorithm
- Simple (Easy to Remember)
- Formal Algorithm (Code)
💡 The Big Idea in Plain English
Think of Pascal's Triangle: Every number inside the triangle is simply the sum of the two numbers directly above it! To pick friends out of candidates (), imagine looking at one specific friend (say, Alex):
- Alex is picked: You now need more people from the remaining friends ().
- Alex is skipped: You still need people from the remaining friends (). Adding both scenarios gives: . Dynamic Programming builds this table row by row from top to bottom, avoiding slow factorials completely!
👣 3 Easy Steps to Remember
- Set Edge Base Cases to 1:
- Picking 0 items from is always way ().
- Picking all items from is always way ().
- Fill from Top to Bottom:
- For every cell : add the cell diagonally above-left and the cell directly above:
- Lookup the Value: contains the final binomial coefficient.
🧠 Memory Trick: Pascal's Triangle Outer edges are 1 Every inner number is the sum of the two above it! Time complexity: .
Algorithm BinomialCoefficient(n, k)
// Computes C(n, k) using bottom-up dynamic programming (Pascal identity)
// Input: Non-negative integers n and k (0 <= k <= n)
// Output: Binomial coefficient C(n, k)
{
for i <- 0 to n do
{
for j <- 0 to min(i, k) do
{
// Base cases: C(n, 0) = 1 and C(n, n) = 1
if (j = 0 or j = i) then
C[i, j] <- 1;
else
C[i, j] <- C[i - 1, j - 1] + C[i - 1, j];
}
}
return C[n, k];
}
6. Floyd-Warshall Algorithm (All Pairs Shortest Path)
6.1 Concept
- Finds the shortest paths between all pairs of vertices in a weighted graph.
- Works for graphs with negative weight edges (but no negative weight cycles).
6.2 Step-by-Step Explanation
- Initialize the solution matrix same as the input graph matrix as a first step.
- Update the solution matrix by considering all vertices as an intermediate vertex.
- The idea is to one by one pick all vertices and update all shortest paths which include the picked vertex as an intermediate vertex in the shortest path.
- If , update .
6.3 Algorithm Complexity
- Time Complexity:
- Space Complexity:
6.4 Algorithm
- Simple (Easy to Remember)
- Formal Algorithm (Code)
💡 The Big Idea in Plain English
While Dijkstra finds paths from one source, Floyd-Warshall finds the shortest path between every pair of vertices at the same time! Think of booking flights between two cities and : You test whether taking a layover at city makes the trip faster: "Is direct flight faster, or flight plus flight ?" If the layover is cheaper, take the shortcut! By testing every possible layover city one by one, all shortest routes are solved.
👣 3 Easy Steps to Remember
- Initialize the Distance Grid: Copy direct edge weights into table . If there is no road between and , set . Distance from any city to itself is .
- Three Nested Loops (Layover MUST be the outer loop!):
- Outer loop : Try vertex as the intermediate stop ().
- Middle loop : Try vertex as the starting point ().
- Inner loop : Try vertex as the ending destination ().
- Shortcut Update Formula:
- If going through is shorter:
🧠 Memory Trick: 3 nested loops with intermediate outside . All-pairs shortest paths in time, handles negative edges (no negative cycles).
Algorithm FloydWarshall(W, V)
// Computes all-pairs shortest paths using dynamic programming
// Input: Weight matrix W[1..V, 1..V] where W[i, j] = infinity if no edge (i, j) exists
// Output: Distance matrix D[1..V, 1..V] of shortest path lengths
{
for i <- 1 to V do
for j <- 1 to V do
D[i, j] <- W[i, j];
for k <- 1 to V do // Intermediate vertex
for i <- 1 to V do // Source vertex
for j <- 1 to V do // Destination vertex
if (D[i, k] + D[k, j] < D[i, j]) then
D[i, j] <- D[i, k] + D[k, j];
return D;
}
7. Matrix Chain Multiplication
7.1 Concept
- Determines the most efficient way to multiply a given sequence of matrices.
- The problem is not actually to perform the multiplications, but merely to decide the sequence of the matrix multiplications involved.
- Many options exist because matrix multiplication is associative.
7.2 Step-by-Step Explanation
- Let the input array
prepresent matrix dimensions such that matrix has dimension . - Construct a 2D array
m[n][n]to store the minimum scalar multiplications. - For chain length to :
- For to , set .
- Compute
m[i][j]by checking all possible partition points from to . - Cost is
m[i][k] + m[k+1][j] + p[i-1]*p[k]*p[j]. - Take the minimum of all possible 's.
m[1][n-1]holds the optimal cost.
7.3 Algorithm Complexity
- Time Complexity:
- Space Complexity:
7.4 Algorithm
- Simple (Easy to Remember)
- Formal Algorithm (Code)
💡 The Big Idea in Plain English
Multiplying three matrices always produces the same numerical answer whether you do or . BUT the number of scalar multiplications can vary wildly:
- One way might take 7,500 operations.
- The other way might take 75,000 operations (10 times slower!). We don't actually multiply the numbers here — we just figure out where to put brackets (parentheses) to do the least amount of work! We test every possible place to put a split/cut point and pick the cheapest cut.
👣 3 Easy Steps to Remember
- Base Case (Single Matrix): Multiplying one matrix by itself costs 0 multiplications: (the main diagonal is all 0).
- Grow Chain Length from 2 to :
- Solve all 2-matrix products first ().
- Then solve all 3-matrix products ().
- Continue expanding the window until the entire chain is solved.
- Try Every Cut Point :
- For any chain from matrix to , test splitting at every position from to :
- Keep the minimum cost in and record the winning cut in .
- Optimal Cost: gives the minimum total operations.
🧠 Memory Trick: Try every cut between and : Left cost + Right cost + . Solve by increasing chain length . Time: , Space: .
Algorithm MatrixChainOrder(p, n)
// Computes minimum scalar multiplications for matrix chain A_1 * A_2 * ... * A_{n-1}
// Input: Dimension sequence p = <p_0, p_1, ..., p_{n-1}> where matrix A_i is p_{i-1} x p_i
// Output: Table m[1..n-1, 1..n-1] containing optimal multiplication costs
{
for i <- 1 to n - 1 do
m[i, i] <- 0;
for L <- 2 to n - 1 do // L is the chain length
{
for i <- 1 to (n - 1) - L + 1 do
{
j <- i + L - 1;
m[i, j] <- infinity;
for k <- i to j - 1 do
{
q <- m[i, k] + m[k + 1, j] + p[i - 1] * p[k] * p[j];
if (q < m[i, j]) then
{
m[i, j] <- q;
s[i, j] <- k; // Optimal split point
}
}
}
}
return m[1, n - 1];
}
8. Longest Common Subsequence (LCS)
8.1 Concept
- Given two sequences, find the length of the longest subsequence present in both of them.
- A subsequence is a sequence that appears in the same relative order, but not necessarily contiguous.
8.2 Step-by-Step Explanation
- Let the two strings be of length and of length .
- Create a 2D array .
- Loop through the characters of and .
- If , then .
- If they are different, .
- contains the length of the LCS.
8.3 Algorithm Complexity
- Time Complexity:
- Space Complexity:
8.4 Algorithm
- Simple (Easy to Remember)
- Formal Algorithm (Code)
💡 The Big Idea in Plain English
Think of how Git diff or DNA comparison works:
You have two words (like X = "ABCBDAB" and Y = "BDCABA"). You want to find the longest sequence of letters that appear in both words in the same relative order, even if other letters are in between.
As you compare letter with letter :
- Letters Match? Awesome! That's to your score: take the score from before these two letters existed (diagonally up-left) and add .
- Letters Don't Match? You can't use both letters together. Pick the better choice between ignoring (cell above) or ignoring (cell to the left)!
👣 3 Easy Steps to Remember
- Make a Grid: Rows for characters of , columns for characters of .
- Row 0 and Column 0 are all (comparing anything against an empty string gives 0).
- Fill Every Cell :
- Match (): Take the top-left diagonal value and add 1:
- Mismatch (): Take the maximum of the cell above and the cell to the left:
- Finish: Cell in the bottom-right corner gives the length of the LCS!
🧠 Memory Trick: Letters match? Go diagonal . Letters differ? Take ! Grid size: . Time & space: .
Algorithm LCS_Length(X, Y, m, n)
// Finds length of Longest Common Subsequence of sequences X[1..m] and Y[1..n]
// Input: Sequences X of length m and Y of length n
// Output: Table c[0..m, 0..n] containing lengths of LCS
{
for i <- 0 to m do
c[i, 0] <- 0;
for j <- 0 to n do
c[0, j] <- 0;
for i <- 1 to m do
{
for j <- 1 to n do
{
if (X[i] = Y[j]) then
c[i, j] <- c[i - 1, j - 1] + 1;
else if (c[i - 1, j] >= c[i, j - 1]) then
c[i, j] <- c[i - 1, j];
else
c[i, j] <- c[i, j - 1];
}
}
return c[m, n];
}