# Chapter 4: Basic Algorithm Types

## 4.3 Dynamic Programming Algorithms

Dynamic Programming (DP) is an algorithmic technique that breaks down an optimization problem into simpler subproblems. The solution to the overall problem depends on the optimal solution to its subproblems.

The advantage of DP over divide-and-conquer algorithms is that DP is applicable to dependent subproblems. In other words, when subproblems share sub-subproblems, DP will work while divide-and-conquer algorithms will not.

DP algorithms solve subproblems just once and then save the answer in a table. This avoids the need to re-compute the answer every time the subproblem is encountered. This saves time and frees up computing power to solve other problems.

DP is a powerful technique that has many applications in areas such as computer science, economics, and engineering. By breaking down complex problems into smaller, more manageable subproblems, DP enables us to solve problems that would otherwise be too difficult to tackle.

Consider the problem of calculating the Fibonacci sequence. A naive recursive solution would be as follows:

`def fibonacci(n):`

if n <= 1:

return n

else:

return (fibonacci(n-1) + fibonacci(n-2))

This algorithm works, but it's inefficient. It performs an excessive amount of repeated computation. If we draw the recursion tree, we'll find that we calculate the same subproblems multiple times. This is where dynamic programming comes in.

We can solve this problem using dynamic programming in two ways: top-down (or memoization) and bottom-up.

**Top-down (Memoization):** In computer science, the top-down approach is often used in solving complex problems. It involves breaking down a large problem into smaller subproblems, which are then solved one by one.

This method is especially useful when solving problems that have overlapping subproblems. To avoid redundant computations, the memoization technique is used. Memoization is a form of caching where the results of expensive function calls are saved and returned when the same inputs occur again. By doing so, the algorithm can avoid re-computing the same subproblem multiple times and improve its overall efficiency.

Here's how we could implement the Fibonacci sequence with top-down dynamic programming:

`def fibonacci(n, memo = {}):`

if n <= 1:

return n

elif n not in memo:

memo[n] = fibonacci(n-1, memo) + fibonacci(n-2, memo)

return memo[n]

**Bottom-up:** This approach is the opposite. We start with the simplest subproblem and iteratively solve larger subproblems. The bottom-up approach is a method in which we begin by breaking down a problem into its simplest subproblems and then, through an iterative process, solve progressively larger subproblems. This approach is often used in dynamic programming, where the solution to a problem is built upon solutions to previous subproblems.

By starting with smaller subproblems, we can gradually work our way up to solving the problem as a whole, allowing for a more efficient and effective solution. In contrast to the top-down approach, which involves breaking a problem down from the top-level and working our way down to the subproblems, the bottom-up approach allows for a more systematic and organized solution process that ensures that no subproblem is overlooked.

Here's the Fibonacci sequence again, this time with bottom-up dynamic programming:

`def fibonacci(n):`

fib = [0, 1] + [0]*(n-1)

for i in range(2, n+1):

fib[i] = fib[i-1] + fib[i-2]

return fib[n]

Both of these approaches give us the same answer, but they get there in different ways. The top-down approach is usually easier to understand because it's closer to the original problem statement. The bottom-up approach is often more efficient because it eliminates the overhead of recursion.

Dynamic Programming is a powerful technique that allows us to solve many types of problems in time O(n^2), O(n^3) or even linear time, which would be impossible otherwise. Examples of such problems are Knapsack problem, Traveling Salesman Problem, Matrix Chain Multiplication, etc.

One important thing to remember: dynamic programming only works when each subproblem is discrete—when it doesn't depend on other subproblems—so you can't use it to solve problems where there's ongoing state that affects the solution.

As an additional note on dynamic programming, it's worth mentioning that the process of designing dynamic programming algorithms can be broken down into a series of steps:

**Characterize the structure of an optimal solution.****Define the value of an optimal solution recursively in terms of smaller sub-problems.****Compute the value of an optimal solution (typically in a bottom-up fashion).****Construct an optimal solution to the problem from the computed information.**

It's also worth noting that Dynamic Programming is mainly used when the solution can be recurrently expressed in terms of previous results. This makes it a valuable method in cases where the number of overlapping subproblems in a brute-force approach explodes exponentially.

However, dynamic programming isn't always the most efficient or practical method for every problem. It's best used when you have a problem which can be divided into smaller subproblems, and those subproblems are 'reused' multiple times.

In the next section, we will dive into Recursive Algorithms, which is another common and fundamental category of algorithms used in computer science. As we progress through this book, you will acquire a solid understanding of different types of algorithms and gain the ability to determine which type or types are most appropriate for any given problem. This is a key skill in computer science and software engineering, and it will serve you well in any related field or endeavor. So let's continue our journey into the world of algorithms!

## 4.3 Dynamic Programming Algorithms

Dynamic Programming (DP) is an algorithmic technique that breaks down an optimization problem into simpler subproblems. The solution to the overall problem depends on the optimal solution to its subproblems.

The advantage of DP over divide-and-conquer algorithms is that DP is applicable to dependent subproblems. In other words, when subproblems share sub-subproblems, DP will work while divide-and-conquer algorithms will not.

DP algorithms solve subproblems just once and then save the answer in a table. This avoids the need to re-compute the answer every time the subproblem is encountered. This saves time and frees up computing power to solve other problems.

DP is a powerful technique that has many applications in areas such as computer science, economics, and engineering. By breaking down complex problems into smaller, more manageable subproblems, DP enables us to solve problems that would otherwise be too difficult to tackle.

Consider the problem of calculating the Fibonacci sequence. A naive recursive solution would be as follows:

`def fibonacci(n):`

if n <= 1:

return n

else:

return (fibonacci(n-1) + fibonacci(n-2))

This algorithm works, but it's inefficient. It performs an excessive amount of repeated computation. If we draw the recursion tree, we'll find that we calculate the same subproblems multiple times. This is where dynamic programming comes in.

We can solve this problem using dynamic programming in two ways: top-down (or memoization) and bottom-up.

**Top-down (Memoization):** In computer science, the top-down approach is often used in solving complex problems. It involves breaking down a large problem into smaller subproblems, which are then solved one by one.

This method is especially useful when solving problems that have overlapping subproblems. To avoid redundant computations, the memoization technique is used. Memoization is a form of caching where the results of expensive function calls are saved and returned when the same inputs occur again. By doing so, the algorithm can avoid re-computing the same subproblem multiple times and improve its overall efficiency.

Here's how we could implement the Fibonacci sequence with top-down dynamic programming:

`def fibonacci(n, memo = {}):`

if n <= 1:

return n

elif n not in memo:

memo[n] = fibonacci(n-1, memo) + fibonacci(n-2, memo)

return memo[n]

**Bottom-up:** This approach is the opposite. We start with the simplest subproblem and iteratively solve larger subproblems. The bottom-up approach is a method in which we begin by breaking down a problem into its simplest subproblems and then, through an iterative process, solve progressively larger subproblems. This approach is often used in dynamic programming, where the solution to a problem is built upon solutions to previous subproblems.

By starting with smaller subproblems, we can gradually work our way up to solving the problem as a whole, allowing for a more efficient and effective solution. In contrast to the top-down approach, which involves breaking a problem down from the top-level and working our way down to the subproblems, the bottom-up approach allows for a more systematic and organized solution process that ensures that no subproblem is overlooked.

Here's the Fibonacci sequence again, this time with bottom-up dynamic programming:

`def fibonacci(n):`

fib = [0, 1] + [0]*(n-1)

for i in range(2, n+1):

fib[i] = fib[i-1] + fib[i-2]

return fib[n]

Both of these approaches give us the same answer, but they get there in different ways. The top-down approach is usually easier to understand because it's closer to the original problem statement. The bottom-up approach is often more efficient because it eliminates the overhead of recursion.

Dynamic Programming is a powerful technique that allows us to solve many types of problems in time O(n^2), O(n^3) or even linear time, which would be impossible otherwise. Examples of such problems are Knapsack problem, Traveling Salesman Problem, Matrix Chain Multiplication, etc.

One important thing to remember: dynamic programming only works when each subproblem is discrete—when it doesn't depend on other subproblems—so you can't use it to solve problems where there's ongoing state that affects the solution.

As an additional note on dynamic programming, it's worth mentioning that the process of designing dynamic programming algorithms can be broken down into a series of steps:

**Characterize the structure of an optimal solution.****Define the value of an optimal solution recursively in terms of smaller sub-problems.****Compute the value of an optimal solution (typically in a bottom-up fashion).****Construct an optimal solution to the problem from the computed information.**

It's also worth noting that Dynamic Programming is mainly used when the solution can be recurrently expressed in terms of previous results. This makes it a valuable method in cases where the number of overlapping subproblems in a brute-force approach explodes exponentially.

However, dynamic programming isn't always the most efficient or practical method for every problem. It's best used when you have a problem which can be divided into smaller subproblems, and those subproblems are 'reused' multiple times.

In the next section, we will dive into Recursive Algorithms, which is another common and fundamental category of algorithms used in computer science. As we progress through this book, you will acquire a solid understanding of different types of algorithms and gain the ability to determine which type or types are most appropriate for any given problem. This is a key skill in computer science and software engineering, and it will serve you well in any related field or endeavor. So let's continue our journey into the world of algorithms!

## 4.3 Dynamic Programming Algorithms

Dynamic Programming (DP) is an algorithmic technique that breaks down an optimization problem into simpler subproblems. The solution to the overall problem depends on the optimal solution to its subproblems.

The advantage of DP over divide-and-conquer algorithms is that DP is applicable to dependent subproblems. In other words, when subproblems share sub-subproblems, DP will work while divide-and-conquer algorithms will not.

DP algorithms solve subproblems just once and then save the answer in a table. This avoids the need to re-compute the answer every time the subproblem is encountered. This saves time and frees up computing power to solve other problems.

DP is a powerful technique that has many applications in areas such as computer science, economics, and engineering. By breaking down complex problems into smaller, more manageable subproblems, DP enables us to solve problems that would otherwise be too difficult to tackle.

Consider the problem of calculating the Fibonacci sequence. A naive recursive solution would be as follows:

`def fibonacci(n):`

if n <= 1:

return n

else:

return (fibonacci(n-1) + fibonacci(n-2))

This algorithm works, but it's inefficient. It performs an excessive amount of repeated computation. If we draw the recursion tree, we'll find that we calculate the same subproblems multiple times. This is where dynamic programming comes in.

We can solve this problem using dynamic programming in two ways: top-down (or memoization) and bottom-up.

**Top-down (Memoization):** In computer science, the top-down approach is often used in solving complex problems. It involves breaking down a large problem into smaller subproblems, which are then solved one by one.

This method is especially useful when solving problems that have overlapping subproblems. To avoid redundant computations, the memoization technique is used. Memoization is a form of caching where the results of expensive function calls are saved and returned when the same inputs occur again. By doing so, the algorithm can avoid re-computing the same subproblem multiple times and improve its overall efficiency.

Here's how we could implement the Fibonacci sequence with top-down dynamic programming:

`def fibonacci(n, memo = {}):`

if n <= 1:

return n

elif n not in memo:

memo[n] = fibonacci(n-1, memo) + fibonacci(n-2, memo)

return memo[n]

**Bottom-up:** This approach is the opposite. We start with the simplest subproblem and iteratively solve larger subproblems. The bottom-up approach is a method in which we begin by breaking down a problem into its simplest subproblems and then, through an iterative process, solve progressively larger subproblems. This approach is often used in dynamic programming, where the solution to a problem is built upon solutions to previous subproblems.

By starting with smaller subproblems, we can gradually work our way up to solving the problem as a whole, allowing for a more efficient and effective solution. In contrast to the top-down approach, which involves breaking a problem down from the top-level and working our way down to the subproblems, the bottom-up approach allows for a more systematic and organized solution process that ensures that no subproblem is overlooked.

Here's the Fibonacci sequence again, this time with bottom-up dynamic programming:

`def fibonacci(n):`

fib = [0, 1] + [0]*(n-1)

for i in range(2, n+1):

fib[i] = fib[i-1] + fib[i-2]

return fib[n]

Both of these approaches give us the same answer, but they get there in different ways. The top-down approach is usually easier to understand because it's closer to the original problem statement. The bottom-up approach is often more efficient because it eliminates the overhead of recursion.

Dynamic Programming is a powerful technique that allows us to solve many types of problems in time O(n^2), O(n^3) or even linear time, which would be impossible otherwise. Examples of such problems are Knapsack problem, Traveling Salesman Problem, Matrix Chain Multiplication, etc.

One important thing to remember: dynamic programming only works when each subproblem is discrete—when it doesn't depend on other subproblems—so you can't use it to solve problems where there's ongoing state that affects the solution.

As an additional note on dynamic programming, it's worth mentioning that the process of designing dynamic programming algorithms can be broken down into a series of steps:

**Characterize the structure of an optimal solution.****Define the value of an optimal solution recursively in terms of smaller sub-problems.****Compute the value of an optimal solution (typically in a bottom-up fashion).****Construct an optimal solution to the problem from the computed information.**

It's also worth noting that Dynamic Programming is mainly used when the solution can be recurrently expressed in terms of previous results. This makes it a valuable method in cases where the number of overlapping subproblems in a brute-force approach explodes exponentially.

However, dynamic programming isn't always the most efficient or practical method for every problem. It's best used when you have a problem which can be divided into smaller subproblems, and those subproblems are 'reused' multiple times.

In the next section, we will dive into Recursive Algorithms, which is another common and fundamental category of algorithms used in computer science. As we progress through this book, you will acquire a solid understanding of different types of algorithms and gain the ability to determine which type or types are most appropriate for any given problem. This is a key skill in computer science and software engineering, and it will serve you well in any related field or endeavor. So let's continue our journey into the world of algorithms!

## 4.3 Dynamic Programming Algorithms

`def fibonacci(n):`

if n <= 1:

return n

else:

return (fibonacci(n-1) + fibonacci(n-2))

**Top-down (Memoization):** In computer science, the top-down approach is often used in solving complex problems. It involves breaking down a large problem into smaller subproblems, which are then solved one by one.

Here's how we could implement the Fibonacci sequence with top-down dynamic programming:

`def fibonacci(n, memo = {}):`

if n <= 1:

return n

elif n not in memo:

memo[n] = fibonacci(n-1, memo) + fibonacci(n-2, memo)

return memo[n]

**Bottom-up:** This approach is the opposite. We start with the simplest subproblem and iteratively solve larger subproblems. The bottom-up approach is a method in which we begin by breaking down a problem into its simplest subproblems and then, through an iterative process, solve progressively larger subproblems. This approach is often used in dynamic programming, where the solution to a problem is built upon solutions to previous subproblems.

Here's the Fibonacci sequence again, this time with bottom-up dynamic programming:

`def fibonacci(n):`

fib = [0, 1] + [0]*(n-1)

for i in range(2, n+1):

fib[i] = fib[i-1] + fib[i-2]

return fib[n]

**Characterize the structure of an optimal solution.****Define the value of an optimal solution recursively in terms of smaller sub-problems.****Compute the value of an optimal solution (typically in a bottom-up fashion).****Construct an optimal solution to the problem from the computed information.**