Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 217 additions & 0 deletions contrib/ds-algorithms/dynamic-programming.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,3 +234,220 @@ print("Length of lis is", lis(arr))
## Complexity Analysis
- **Time Complexity**: O(n * n) for both approaches, where n is the length of the array.
- **Space Complexity**: O(n * n) for the memoization table in Top-Down Approach, O(n) in Bottom-Up Approach.

# 5. String Edit Distance

The String Edit Distance algorithm calculates the minimum number of operations (insertions, deletions, or substitutions) required to convert one string into another.

**Algorithm Overview:**
- **Base Cases:** If one string is empty, the edit distance is the length of the other string.
- **Memoization:** Store the results of previously computed edit distances to avoid redundant computations.
- **Recurrence Relation:** Compute the edit distance by considering insertion, deletion, and substitution operations.

## String Edit Distance Code in Python (Top-Down Approach with Memoization)
```python
def edit_distance(str1, str2, memo={}):
m, n = len(str1), len(str2)
if (m, n) in memo:
return memo[(m, n)]
if m == 0:
return n
if n == 0:
return m
if str1[m - 1] == str2[n - 1]:
memo[(m, n)] = edit_distance(str1[:m-1], str2[:n-1], memo)
else:
memo[(m, n)] = 1 + min(edit_distance(str1, str2[:n-1], memo), # Insert
edit_distance(str1[:m-1], str2, memo), # Remove
edit_distance(str1[:m-1], str2[:n-1], memo)) # Replace
return memo[(m, n)]

str1 = "sunday"
str2 = "saturday"
print(f"Edit Distance between '{str1}' and '{str2}' is {edit_distance(str1, str2)}.")
```

#### Output
```
Edit Distance between 'sunday' and 'saturday' is 3.
```

## String Edit Distance Code in Python (Bottom-Up Approach)
```python
def edit_distance(str1, str2):
m, n = len(str1), len(str2)
dp = [[0 for _ in range(n + 1)] for _ in range(m + 1)]

for i in range(m + 1):
for j in range(n + 1):
if i == 0:
dp[i][j] = j
elif j == 0:
dp[i][j] = i
elif str1[i - 1] == str2[j - 1]:
dp[i][j] = dp[i - 1][j - 1]
else:
dp[i][j] = 1 + min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1])

return dp[m][n]

str1 = "sunday"
str2 = "saturday"
print(f"Edit Distance between '{str1}' and '{str2}' is {edit_distance(str1, str2)}.")
```

#### Output
```
Edit Distance between 'sunday' and 'saturday' is 3.
```

## **Complexity Analysis:**
- **Time Complexity:** O(m * n) where m and n are the lengths of string 1 and string 2 respectively
- **Space Complexity:** O(m * n) for both top-down and bottom-up approaches


# 6. Matrix Chain Multiplication

The Matrix Chain Multiplication finds the optimal way to multiply a sequence of matrices to minimize the number of scalar multiplications.

**Algorithm Overview:**
- **Base Cases:** The cost of multiplying one matrix is zero.
- **Memoization:** Store the results of previously computed matrix chain orders to avoid redundant computations.
- **Recurrence Relation:** Compute the optimal cost by splitting the product at different points and choosing the minimum cost.

## Matrix Chain Multiplication Code in Python (Top-Down Approach with Memoization)
```python
def matrix_chain_order(p, memo={}):
n = len(p) - 1
def compute_cost(i, j):
if (i, j) in memo:
return memo[(i, j)]
if i == j:
return 0
memo[(i, j)] = float('inf')
for k in range(i, j):
q = compute_cost(i, k) + compute_cost(k + 1, j) + p[i - 1] * p[k] * p[j]
if q < memo[(i, j)]:
memo[(i, j)] = q
return memo[(i, j)]
return compute_cost(1, n)

p = [1, 2, 3, 4]
print(f"Minimum number of multiplications is {matrix_chain_order(p)}.")
```

#### Output
```
Minimum number of multiplications is 18.
```


## Matrix Chain Multiplication Code in Python (Bottom-Up Approach)
```python
def matrix_chain_order(p):
n = len(p) - 1
m = [[0 for _ in range(n)] for _ in range(n)]

for L in range(2, n + 1):
for i in range(n - L + 1):
j = i + L - 1
m[i][j] = float('inf')
for k in range(i, j):
q = m[i][k] + m[k + 1][j] + p[i] * p[k + 1] * p[j + 1]
if q < m[i][j]:
m[i][j] = q

return m[0][n - 1]

p = [1, 2, 3, 4]
print(f"Minimum number of multiplications is {matrix_chain_order(p)}.")
```

#### Output
```
Minimum number of multiplications is 18.
```

## **Complexity Analysis:**
- **Time Complexity:** O(n^3) where n is the number of matrices in the chain. For an `array p` of dimensions representing the matrices such that the `i-th matrix` has dimensions `p[i-1] x p[i]`, n is `len(p) - 1`
- **Space Complexity:** O(n^2) for both top-down and bottom-up approaches

# 7. Optimal Binary Search Tree

The Matrix Chain Multiplication finds the optimal way to multiply a sequence of matrices to minimize the number of scalar multiplications.

**Algorithm Overview:**
- **Base Cases:** The cost of a single key is its frequency.
- **Memoization:** Store the results of previously computed subproblems to avoid redundant computations.
- **Recurrence Relation:** Compute the optimal cost by trying each key as the root and choosing the minimum cost.

## Optimal Binary Search Tree Code in Python (Top-Down Approach with Memoization)

```python
def optimal_bst(keys, freq, memo={}):
n = len(keys)
def compute_cost(i, j):
if (i, j) in memo:
return memo[(i, j)]
if i > j:
return 0
if i == j:
return freq[i]
memo[(i, j)] = float('inf')
total_freq = sum(freq[i:j+1])
for r in range(i, j + 1):
cost = (compute_cost(i, r - 1) +
compute_cost(r + 1, j) +
total_freq)
if cost < memo[(i, j)]:
memo[(i, j)] = cost
return memo[(i, j)]
return compute_cost(0, n - 1)

keys = [10, 12, 20]
freq = [34, 8, 50]
print(f"Cost of Optimal BST is {optimal_bst(keys, freq)}.")
```

#### Output
```
Cost of Optimal BST is 142.
```

## Optimal Binary Search Tree Code in Python (Bottom-Up Approach)

```python
def optimal_bst(keys, freq):
n = len(keys)
cost = [[0 for x in range(n)] for y in range(n)]

for i in range(n):
cost[i][i] = freq[i]

for L in range(2, n + 1):
for i in range(n - L + 1):
j = i + L - 1
cost[i][j] = float('inf')
total_freq = sum(freq[i:j+1])
for r in range(i, j + 1):
c = (cost[i][r - 1] if r > i else 0) + \
(cost[r + 1][j] if r < j else 0) + \
total_freq
if c < cost[i][j]:
cost[i][j] = c

return cost[0][n - 1]

keys = [10, 12, 20]
freq = [34, 8, 50]
print(f"Cost of Optimal BST is {optimal_bst(keys, freq)}.")
```

#### Output
```
Cost of Optimal BST is 142.
```

### Complexity Analysis
- **Time Complexity**: O(n^3) where n is the number of keys in the binary search tree.
- **Space Complexity**: O(n^2) for both top-down and bottom-up approaches