Dynamic programming is not a specific algorithm, but rather a programming technique (like recursion). It is best described as creating solutions to large problems by combining solutions to smaller problems. On some problems, applying dynamic programming can change the efficiency from exponential to polynomial.
Consider the problem of computing the Nth Fibonacci number F(N). The Fibonacci sequence starts 1, 1, 2, 3, 5, 8, ..., with each number being the sum of the previous two. So a simple recursive implementation might look like this:
function fib(n : integer) : integer; begin if n <= 2 then fib := 1 else fib := fib(n - 2) + fib(n - 1); end;
That is all well and good, but how efficienct is it? It turns out to take time proportional to the answer, which is in fact exponential time (O(sn), where s is the golden ratio). This example is usually used to show the pitfalls of recursion, but also provides a simple example of converting a problem to dynamic programming.
The key observation is that the recursive function is a true function in the mathematical sense: it has no side effects, and the output depends only on the input n. This means that we don't have to start by asking for the Nth Fibonacci number and work downwards, but can start with the first one and work upwards. Here is the code (arr is an array that is assumed to be big enough):
function fib(n : integer) : integer; var i : integer; begin arr[1] := 1; arr[2] := 1; for i := 3 to n do arr[i] := arr[i - 1] + arr[i - 2]; fib := arr[n]; end;
This computes the smaller problems of finding F(1), F(2), F(3) and so on, and combines these to produce the answers to the bigger problem of finding F(n).
Identifying problems that can be solving with dynamic programming is something that comes with practice. To begin with, the following procedure will help you see the dynamic programming solutions.
Sometimes the parameter space (the set of all legal input parameters) is too large to store in an array. This will usually happen if there are several parameters. To get around this, it is sometimes possible to get around this by only storing a subset of the parameter space (this depends on the problem). To return to the Fibonacci problem, we see that we only need to keep the last two Fibonacci numbers, as the previous ones are never used again. The modified code is:
function fib(n : integer) : integer; var f1, f2, cur, i : integer; begin f1 := 1; f2 := 1; cur := 1; for i := 3 to n do begin f1 := f2; f2 := cur; cur := f1 + f2; end; fib := cur; end;
More generally, the array will have multiple dimensions, and sometimes only one or two "rows" of the array are needed at one time. These cases can be identitied from the dependencies: in the Fibonacci example, F(n) depends only on the previous two elements, so those are all that need to be kept.
Sometimes the dependencies in problems are rather complicated, and this can make the ordering of the loops rather tricky. This often arises in problems involving graphs, where the dependencies are edges in the graph. Another problem with writing loops is that it is sometimes more efficient to avoid solving sub-problems that are never actually used to solve the main problem.
Both of these problems are addressed by a technique known as memoisation or caching. It is equivalent to dynamic programming, but I find it to be more work and generally only implement it when these problems arise. The basic approach is to stop after step 4 in the process above, leaving you with a recursive function and an array for holding the outputs. Initially tag all the array elements to indicate that they have not been computed (e.g. by storing -1). Then modify the recursive function to return the array value if it has already been computed, and to do the work otherwise. The array thus acts as a cache of results that have already been computed. Reusing the Fibonacci example:
var cache : array[1..MAXN] of longint; procedure initialise; begin fill cache with -1's, indicating that the value isn't known fillchar(cache, sizeof(cache), 255); if we initialise the first 2 here, it simplies the real function cache[1] := 1; cache[2] := 1; end; function fib(n : integer) : integer; begin if (cache[n] = -1) then cache[n] := fib(n - 1) + fib(n - 2); fib := cache[n]; end;
Last updated Sat May 31 19:48:29.0000000000 2008. Copyright Bruce Merry (bmerry '@' gmail dot. com).