Dynamic Programming Tutorial — From Zero to Hero
A complete beginner-friendly tutorial on Dynamic Programming with examples from VTU ADA Lab: Floyd-Warshall, Warshall's, and 0/1 Knapsack with C code.
What is Dynamic Programming?
Dynamic Programming (DP) is an optimization technique that solves complex problems by:
- Breaking them into smaller overlapping subproblems
- Solving each subproblem exactly once
- Storing (memoizing) the results to avoid recomputation
“Those who cannot remember the past are condemned to repeat it.” — This applies to algorithms too!
The Two Pillars of DP
1. Optimal Substructure
The optimal solution to the problem contains optimal solutions to its subproblems.
2. Overlapping Subproblems
The same subproblems are solved multiple times when using a naive recursive approach.
If a problem has these two properties → Dynamic Programming is the right tool!
DP Approaches
Top-Down (Memoization)
Start with the original problem, recurse, and cache results.
int memo[100][100]; // Cache
int solve(int i, int j) {
if(memo[i][j] != -1) return memo[i][j]; // Already computed
// Compute and cache
return memo[i][j] = compute(i, j);
}
Bottom-Up (Tabulation)
Build solutions from base cases up to the final answer.
// Build table iteratively
for(int i = 0; i <= n; i++) {
for(int j = 0; j <= W; j++) {
dp[i][j] = ...; // Based on smaller subproblems
}
}
VTU ADA Lab programs use the bottom-up tabulation approach.
Dynamic Programming in VTU ADA Lab
1. Floyd’s Algorithm — All Pairs Shortest Path
DP State: dist[i][j] = shortest distance from vertex i to vertex j
Recurrence:
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
Interpretation: The shortest path from i to j, possibly going through vertex k, is either:
- The direct path (current
dist[i][j]), OR - The path through intermediate vertex k (
dist[i][k] + dist[k][j])
void floyd() {
for(int k = 1; k <= n; k++) // Try each intermediate vertex
for(int i = 1; i <= n; i++) // For all source vertices
for(int j = 1; j <= n; j++) // For all destination vertices
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
}
View complete Floyd’s program →
2. Warshall’s Algorithm — Transitive Closure
DP State: D[i][j] = 1 if vertex j is reachable from vertex i
Recurrence:
D[i][j] = D[i][j] OR (D[i][k] AND D[k][j])
Interpretation: j is reachable from i if:
- It was already reachable directly, OR
- We can reach k from i AND reach j from k
void warshall() {
for(int k = 1; k <= n; k++)
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
D[i][j] = D[i][j] || (D[i][k] && D[k][j]);
}
View complete Warshall’s program →
3. 0/1 Knapsack
DP State: v[i][j] = maximum profit using items 1..i with capacity j
Recurrence:
if wt[i] > j:
v[i][j] = v[i-1][j] // Can't include item i
else:
v[i][j] = max(v[i-1][j], // Exclude item i
v[i-1][j-wt[i]] + val[i]) // Include item i
Base cases: v[0][j] = 0 (no items) and v[i][0] = 0 (no capacity)
int knap() {
for(int i = 0; i <= n; i++) {
for(int j = 0; j <= c; j++) {
if(i == 0 || j == 0)
v[i][j] = 0;
else if(wt[i] > j)
v[i][j] = v[i-1][j];
else
v[i][j] = max(v[i-1][j], v[i-1][j-wt[i]] + val[i]);
}
}
return v[n][c];
}
View complete 0/1 Knapsack program →
DP Problem-Solving Framework
- Identify the optimal substructure and overlapping subproblems
- Define the DP state — what does
dp[i][j]represent? - Write the recurrence relation
- Define base cases
- Determine the fill order (which cells to compute first)
- Extract the answer from the table
When to Use DP vs Greedy
| Situation | Algorithm |
|---|---|
| Fractional items allowed | Greedy (Fractional Knapsack) |
| Items must be whole (0 or 1) | Dynamic Programming (0/1 Knapsack) |
| Single source shortest path, no negative weights | Greedy (Dijkstra’s) |
| All pairs shortest paths | Dynamic Programming (Floyd’s) |
Common DP Patterns
- 1D DP:
dp[i]depends ondp[i-1](Fibonacci, climbing stairs) - 2D DP:
dp[i][j]depends ondp[i-1][j],dp[i][j-1](Knapsack, LCS) - Interval DP:
dp[l][r]solved usingdp[l][k]anddp[k+1][r] - All-Pairs: Triple loop over k, i, j (Floyd-Warshall)