首页 > 代码库 > Dynamic Programming

Dynamic Programming

We began our study of algorithmic techniques with greedy algorithms, which in some sense form the most natural approach to algorithm design. Faced with a new computational problem, we‘ve seen that it‘s not hard to propose multiple possible greedy algorithms; the challenge is then to determine whether any of these algorithms provides a correct solution to the problem in all cases.

6.1 Weighted Interval Scheduling: A Recursive Procedure

We have seen that a particular greedy algorithm produces an optimal solution to the Interval Scheduling Problem, where the goal is to accept as large a set of nonoverlapping intervals as possible. The weighted Interval Scheduling Problem is a strictly more general version, in which each interval has a certain value (or weight), and we want to accept a set of maximum value.

Designing a Recursive Algorithm

Since the original Interval Scheduling Problem is simply the special case in which all values are equal to 1, we know already that most greedy algorithms will not solve this problem optimally. But even the algorithm that worked before (repeatedly choosing the interval that ends earliest) is no longer optimal in this more general setting.

Indeed, no natural greedy algorithm is known for this problem, which is what motivates our switch to dynamic programming. As discussed above, we will begin our introduction to dynamic programming with a recursive type of algorithm for this problem, and then in the next section we‘ll move to a more iterative method that is closer to the style we use in the rest of this chapter.

We use the notation from our discussion of Interval Scheduling. We have 技术分享 requests labeled 技术分享, with each request 技术分享 specifying a start time 技术分享 and a finish time 技术分享. Each interval 技术分享 now also has a value, or weight 技术分享. Two intervals are compatible if they do not overlap. The goal of our current problem is to select a subset 技术分享 of mutually compatible intervals, so as to maximize the sum of the values of the selected intervals, 技术分享

Let‘s suppose that the requests are sorted in order of nondecreasing finish time: 技术分享. We‘ll say a request 技术分享 comes before a request 技术分享 if 技术分享. This will be the natural left-to-right order in which we‘ll consider intervals. To help in talking about this order, we define 技术分享, for an interval 技术分享, to be the largest index 技术分享 such that intervals 技术分享 and 技术分享 are disjoint. In other words, 技术分享 is the leftmost interval that ends before 技术分享 begins. We define 技术分享 if no request 技术分享 is disjoint from 技术分享.

Now, given an instance of the Weighted Interval Scheduling Problem, let‘s consider an optimal solution 技术分享, ignoring for now that we have no idea what it is. Here‘s something completely obvious that we can say about 技术分享: either interval 技术分享 (the last one) belongs to 技术分享, or it doesn‘t. Suppose we explore both sides of this dichotomy a little further. If 技术分享, then clearly no interval indexed strictly between 技术分享 and 技术分享 can belong to 技术分享, because by the definition of 技术分享, we know that intervals 技术分享 all overlap interval 技术分享. Moreover, if 技术分享, then 技术分享 must include an optimal solution to the problem consisting of requests 技术分享 - for if it didn‘t, we could replace 技术分享‘s choice of requests from 技术分享 with a better one, with no danger of overlapping request 技术分享.

On the other hand, if 技术分享, then 技术分享 is simply equal to the optimal solution to the problem consisting of requests 技术分享. This is by completely analogous reasoning: we‘re assuming that 技术分享 does not include request 技术分享; so if it does not choose the optimal set of requests from 技术分享, we could replace it with a better one.

All this suggests that finding the optimal solution on intervals 技术分享 involves looking at the optimal solutions of smaller problems of the form 技术分享. Thus, for any value of 技术分享 between 技术分享 and 技术分享, let 技术分享 denote the optimal solution to the problem consisting of requests 技术分享, and let 技术分享 denote the value of this solution. (We define 技术分享, based on the convention that this is the optimum over an empty set of intervals.) The optimal solution we‘re seeking is precisely 技术分享, with value 技术分享. For the optimal solution 技术分享 on 技术分享, our reasoning above (generalizing from the case in which 技术分享) says that either 技术分享, in which case 技术分享, or技术分享 , in which case 技术分享. Since these are precisely the two possible choices (技术分享 or 技术分享), we can further say that.

技术分享 (1)

And how do we decide whether 技术分享 belongs t the optimal solution 技术分享. This too is easy: it belongs to the optimal solution if and only if the first of the options above is at least as good as the second; in other words,

Request 技术分享 belongs to an optimal solution on the set 技术分享 if and only if

技术分享 (2)

These facts form the first crucial component on which a dynamic programming solution is based: a recurrence equation that expresses the optimal solution (or its value) in terms of the optimal solutions to smaller subproblems.

Despite the simple reasoning that led to this point, (1) is already a significant development. It directly gives us a recursive algorithm to compute 技术分享, assuming that we have already sorted the requests by finishing time and computed the values of 技术分享 for each 技术分享.

技术分享

If 技术分享 then

Return 技术分享

Else

Return 技术分享

Endif

The correctness of the algorithm follows directly by induction on 技术分享:

技术分享 correctly computes 技术分享 for each 技术分享.

Proof. By definition 技术分享. Now, take some 技术分享, and suppose by way of induction that 技术分享 correctly computes 技术分享 for all 技术分享. By the induction hypothesis, we know that 技术分享 and 技术分享; and hence from (1) it follows that

技术分享

Unfortunately, if we really implemented the algorithm 技术分享 as just written, it would take exponential time to run in the worst case.

Memoizing the Recursion

In fact, though, we‘re not so far from having a polynomial-time algorithm. A fundamental observation, which forms the second crucial component of a dynamic programming solution, is that our recursive algorithm 技术分享 is really only solving 技术分享 different subproblems: 技术分享. The fact that it runs in exponential time as written is simply due to the spectacular redundancy in the number of times it issues each of these calls.

How could we eliminate all this redundancy? We could store the value of 技术分享 in a globally accessible place the first time we compute it and then simply use this precomputed value in place of all future recursive calls. This technique of saving values that have already been computed is referred to as memoization.

We implement the above strategy in the more “intelligent” procedure 技术分享. This procedure will make use of an array 技术分享; 技术分享 will start with the value “empty”, but will hold the value of 技术分享 as soon as it is first determined. To determine 技术分享, we invoke 技术分享.

技术分享

If 技术分享 then

Return 技术分享

Else if 技术分享 is not empty then

Return 技术分享

Else

Define 技术分享

Return 技术分享

Endif

Analyzing the Memoized Version

Clearly, this looks very similar to our previous implementation of the algorithm; however, memoization has brought the running time way down.

The running time of 技术分享 is 技术分享 (assuming the input intervals are sorted by their finish times).

Dynamic Programming