5

Many people think that dynamic programming is difficult, and even think that the question of dynamic programming in the interview is embarrassing the candidate. This may produce a false subconscious: that dynamic programming does not need to be mastered.

In fact, dynamic programming is very necessary to master:

  1. I exercise my thinking very much. Dynamic programming is a very mental exercise. Although there are routines, the solution ideas for each problem are very different. It is very suitable as a thinking exercise.
  2. Very practical. Dynamic programming sounds very advanced, but in fact the ideas and problems to be solved are very common.

Dynamic programming is used to solve the optimal solution under certain conditions, such as:

  • Which method is best for automatic path finding?
  • What items in the backpack have the greatest space utilization?
  • How to make change with the fewest coins?

In fact, these questions are very difficult at first glance, after all, they are not questions that can be answered at a glance. But getting the optimal solution is very important. Who can bear the detour of the path finding algorithm in the game? Who doesn't want more things in the backpack? So we must learn dynamic programming well.

intensive reading

Dynamic programming is not magic, it is also trying to answer through violent methods, but the way is more "smart", making the actual time complexity not high.

The difference between dynamic programming and brute force and backtracking algorithms

The above sentence also shows that all dynamic programming problems can be solved by violent methods! Yes, all optimal solution problems can be tried through brute force methods (and backtracking algorithms), and finally the optimal one can be found.

Brute force algorithms can solve almost all problems. The characteristic of the backtracking algorithm is to try different branches through brute force, and finally select the route with the best result.

And dynamic programming also has the concept of branching, but instead of trying each branch to the end, when you reach the bifurcation, you can directly derive the optimal solution for the next step based on the performance of the previous branches! However, both direct derivation and the previous branch judgments are conditional. The solvable problem of dynamic programming needs to meet the following three characteristics at the same time:

  1. There is an optimal substructure.
  2. There is a duplication sub-problem.
  3. There is no aftereffect.

There is an optimal substructure

That is, the optimal solution of the sub-problem can be derived from the global optimal solution.

What is the sub-problem? For example, in the path finding algorithm, the completion of the first few steps is a sub-problem relative to the completion of the journey. It must be ensured that the shortest path to the completion of the journey can be derived by completing the first few steps before dynamic programming can be used.

Don't underestimate the first one. Dynamic programming is difficult here. How do you establish a relationship between the optimal substructure and the global optimal solution?

  • For the problem of climbing stairs, since each step is climbed by the previous steps, there must be a linear relationship derivation.
  • What if it becomes a two-dimensional plane pathfinding? Then it is upgraded to a two-dimensional problem, and there i,j and the previous step.
  • If it is a backpack problem, what about the three variables of i , item weight j and item quality k Then escalate to a three-position problem, and you need to find the relationship between the three.

By analogy, the complexity can rise to N dimensions, the higher the dimension, the higher the complexity of thinking, and the more space complexity needs to be optimized.

There are duplicate sub-problems

That is, the same sub-problem has repeated calculations in different scenarios.

For example, in the path finding algorithm, in the calculation of the same two routes, a section of the route is public and is the only way to be calculated, so it is only necessary to calculate it once. When calculating the next road, when you encounter this sub-road, directly Just take the cache of the first calculation. A typical example is the Fibonacci number, for f(3) and f(4) , we must calculate f(1) and f(2) , because f(3) = f(2) + f(1) , and f(4) = f(3) + f(2) = f(2) + f(1) + f(2) .

This is the key difference between dynamic programming and brute force solutions. The reason for the high performance of dynamic programming is that does not recalculate the repeated sub-problems . The algorithm is generally solved by caching the calculation results or bottom-up iteration, but the core It is this scene that has a repetitive sub-problem.

When you think that the violent solution may be silly and there are a lot of repeated calculations, you have to think about where there are repeated sub-problems and whether it can be solved by dynamic programming.

No aftereffect

That is, the previous choice will not affect the subsequent game rules.

In the path-finding algorithm, the route behind will not be affected because route B is taken in the front. Fibonacci sequence because the Nth term is definitely related to the previous term, there is no choice, so there is no problem of aftereffect.

What scenarios have aftereffects? For example, can you find the optimal solution in your life through dynamic programming? In fact, it is not possible, because your choice today may affect your future life trajectory. For example, if you choose a computer career, it will directly affect the field of work and the people you come into contact with. Therefore, the life course behind will completely change, so it is impossible at all. Compare with you who chose civil engineering, because the course of life has changed.

Some students may think this limitation is very big? In fact, there are still many problems with no aftereffects. For example, which item to put in the backpack, which route is currently taken, and which change is used, will not affect the size of the entire backpack, the terrain of the entire map, and the most important payment you pay. Amount.

Solution Routine-State Transition Equation

The core of solving the dynamic programming problem is to write the state transition equation, the so-called state transition, that is, to derive future steps through some previous steps.

The state transition equation is generally written as dp(i) = a series of dp(j) calculations, of which j < i .

Among them i and dp(i) is very important. Generally, dp(i) directly represents the answer to the question, and i has skills. For example, the Fibonacci sequence, dp(i) is the final result, and i represents the subscript. Since the Fibonacci sequence directly tells you the state transition equation f(x) = f(x-1) + f(x-2) , then there is no need for derivation at all.

For complex issues, it is difficult to define i and how to derive the next state from the previous state. If you do too many questions, you will have an experience. If you don't have it, it is difficult to explain it even if you don't have it, so let's just look at the example later.

First give a simplest example of dynamic programming-climbing stairs to illustrate the problem.

Stair climbing problem

Climbing stairs is a simple question, the topic is as follows:

Suppose you are climbing stairs. You need n to reach the top of the building. You can climb 1 or 2 steps each time. How many different ways do you have to climb to the top of a building? (Given n is a positive integer)

First of all, dp(i) is the answer to the question (the solution routine, dp(i) is the answer in most cases, so the idea of solving the problem will be the most simplified), that is, the number of ways i i , then 060bd84bb43ae7 naturally has to climb to the first step.

Let us first see whether there is optimal substructure ? Because it can only be i steps for step 060bd84bb43b14 depends on the number of steps ahead. can only climb 1 or 2 steps at a time. Therefore, step i can only be from 060bd84bb43b16 or i-1 i-2 a step climb to the , so the first i a step climb law is i-1 and i-2 total climb and law. So obviously there is an optimal substructure, and even the state transition equation is ready to come out.

Look at whether there duplicate sub-problems , in fact, stairs and columns similar to Fibonacci, the final state transition equation is the same, it is clear that there is a problem repeats. Of course, it is easy to analyze intuitively that the climbing method of 10 steps includes the climbing method of 8 and 9 steps, and the climbing method of 9 steps includes the 8 steps, so there is a duplication sub-problem.

Finally, see if no aftereffect ? Since choosing to climb 1 or 2 steps at a time will not affect the total number of steps, it will not affect the number of steps you can climb next time, so there is no aftereffect. If you have climbed 2 steps, because you are too tired, you can only climb 1 step next time, it is considered after-effect. Or as long as you have climbed 3 times and 2 steps in total, you will give up climbing stairs because you are too tired and go straight downstairs to rest. Then the problem ends early, which is also of aftereffect.

So the state transition equation of climbing stairs is:

  • dp(i) = dp(i-1) + dp(i-2)
  • dp(1) = 1
  • dp(2) = 2

Note that because the general state transition equations cannot be applied to the first and second steps, special enumeration is required. This kind of enumeration idea in the code is actually recursive end condition , that is, as a function dp(i) cannot be infinitely recursive, when the i is 1 or 2, the enumeration result is directly returned (for this question). So when writing recursion, be sure to write the recursion termination condition first.

Then we consider that for the first step, there is only one way to climb. There is no dispute about this. For the second step, you can step up directly in two steps, or you can take two steps, so there are two climbing methods, which are also easy to understand, so this problem is solved here.

Regarding the code part, just write about this topic, and the following topic will not write the code if there is no special reason:

function dp(i: number) {
  switch (i) {
    case 1:
      return 1;
    case 2:
      return 2;
    default:
      return dp(i - 1) + dp(i - 2);
  }
}

return dp(n);

Of course, writing this repetitive calculation of the sub-structure, so we don’t perform 060bd84bb43c21 stupidly every time (because this calculates a lot of repetitive sub-problems), we need to use the cache to dp(i - 1)

const cache: number[] = [];

function dp(i: number) {
  switch (i) {
    case 1:
      cache[i] = 1;
      break;
    case 2:
      cache[i] = 2;
      break;
    default:
      cache[i] = cache[i - 1] + cache[i - 2];
  }

  return cache[i];
}

// 既然用了缓存,最好子底向上递归,这样前面的缓存才能优先算出来
for (let i = 1; i <= n; i++) {
  dp(i);
}

return cache[n];

Of course, this is just a simple one-dimensional linear cache, and more advanced cache modes include rolling cache . We observed that this question cache space overhead is O(n) , but each time only a value on the cache twice, so the calculation to dp(4) time, cache[1] can be thrown away, or that we can use to scroll the cache, so cache[3] cache[1] the space of 060bd84bb43c63, then the overall space complexity can be reduced to O(1) , the specific method is:

const cache: [number, number] = [];

function dp(i: number) {
  switch (i) {
    case 1:
      cache[i % 2] = 1;
      break;
    case 2:
      cache[i % 2] = 2;
      break;
    default:
      cache[i % 2] = cache[(i - 1) % 2] + cache[(i - 2) % 2];
  }

  return cache[i % 2];
}

for (let i = 1; i <= n; i++) {
  dp(i);
}

return cache[n % 2];

By taking the surplus, the cache will always alternately occupy cache[0] and cache[1] to maximize the use of space. Of course, for this question, because the first two state transition equations are used continuously, it can be optimized this way. If you encounter the state transition equations that use all the previous caches, you cannot use the rolling cache solution. However, there are more advanced multi-dimensional caching, which will be mentioned later.

Next, let’s look at an advanced topic, the maximum sub-order sum.

Maximal subsequence sum

The maximum subsequence sum is a simple question, the topic is as follows:

Given an integer array nums , find a continuous sub-array with the largest sum (the sub-array contains at least one element), and return the largest sum.

First of all, according to the stair climbing routine, dp(i) represents the maximum sum. Since there may be negative numbers in the integer array, the more the majority is added, the larger the sum is not necessarily larger.

Then look i , for an array of problems, most i are representative of i string end position, then dp(i) it means to the first i string position and ending with the largest.

You might think to i end, it can only be [0-i] value range, then [j-i] string range is not to be ignored? Actually otherwise, [j-i] is the maximum sum, it will also be included in dp(i) , because our state transition equation can choose not to connect dp(i-1) .

Now start to solve the problem: First, the problem is the continuous sub-array of the largest sum. Generally, the continuous sub-array is relatively simple, because for dp(i) , it is either connected to the front or disconnected from the front, so the state transition equation is:

  • dp(i) = dp(i-1) + nums[i] if dp(i-1) > 0 .
  • dp(i) = nums[i] if dp(i-1) <= 0 .

How to understand? That is, the i state can be directly derived from the i-1 state. Since dp(i) refers to the maximum sum ending i dp(i-1) is the maximum sum ending with the i-1 dp(i-1) has already been counted as the maximum sum at the end of the 060bd84bb43dcb string. Come out, then how to derive dp(i)

Because the string is continuous, dp(i) either dp(i-1) + nums[i] , or it is directly nums[i] , so which one you choose depends on whether the preceding dp(i-1) is a positive number, because i ends with nums[i] , no matter whether it nums[i] , Must be brought. So it is easy to know that dp(i-1) is positive, it will be connected, otherwise it will not be connected.

Well, after such a detailed explanation, I believe you have fully understood the problem-solving routines of dynamic programming. I will not be so long-winded in the way of explaining the following topics!

What if this question is more complicated and not continuous? Let us look at the longest increasing subsequence problem.

Longest increasing subsequence

The longest increasing subsequence is a middle-level question, the title is as follows:

Give you an integer array nums and find the length of the longest strictly increasing subsequence.

A subsequence is a sequence derived from an array, deleting (or not deleting) elements in the array without changing the order of the remaining elements. For example, [3,6,2,7] is a subsequence of the array [0,3,1,6,2,2,7] .

In fact, the previous intensive reading "DOM diff Longest Ascending " 160bd84bb43eab has analyzed this problem in detail, including a better greedy solution, but this time we are still focusing on the dynamic programming method.

The difference between this question and the previous one is that it is increasing first, and discontinuous second.

According to the routine, dp(i) means the length of the longest ascending subsequence ending i dp(i) come from the previous derivation?

Because it is not continuous, you can't just look at dp(i-1) , because the nums[i] and dp(j) ( 0 <= j < i ) may reach the maximum length, so you need to traverse all j and try the combination of the maximum length.

So the state transition equation is:

dp[i] = max(dp[j]) + 1 , where 0<=j<i and num[j]<num[i] .

The emergence of this question indicates the emergence of a more complex state transition equation, that is, the i is not simply i-1 , but from all previous dp(j) , of which 0<=j<i .

In addition, there are derivation variants, that is, deriving according to dp(dp(i)) , that is, the function is contained in the function. This kind of problem is relatively more difficult because it deepens a layer of thinking about the brain circuit. Let's look at a topic like this: the longest valid parenthesis.

Longest valid parenthesis

The longest valid parenthesis is a difficult question, the topic is as follows:

Give you a '(' and ')' , find out the length of the longest valid (correctly formatted and continuous) bracket substring.

The reason why this question is difficult is because of the nested thinking in the state transition equation.

dp(i) as the answer according to the routine, that is, the longest effective bracket length in the string ending i Can you see it? In general string questions, i is defined at the end of the string subscript, and there are few definitions at the beginning or other definition behaviors. Of course, this is not the case for non-string issues, which will be discussed later.

Let's continue the problem. If s[i] is ( , then it is impossible to form a valid bracket, because the rightmost side must not be closed, so consider the scenario where s[i] is )

If s[i-1] is ( , it constitutes ...() , and the last two are self-contained legal closures, so just look at the previous one, dp(i-2) , so the state transition equation for this scenario is:

dp(i) = dp(i-2) + 2

What if s[i-1] is ) ? It constitutes the ...)) , then only i-1 is legally closed, and this legal closed segment must be closed before ( and the i item to form the longest effective bracket length at this time, so the state transition equation for this scenario is:

dp(i) = dp(i-1) + dp(i - dp(i-1) - 2) + 2 , you can understand it with the following figure:

<img width=300 src="https://img.alicdn.com/imgextra/i1/O1CN016tRvXm1o4p8U1Plfk_!!6000000005172-2-tps-1088-378.png">

As you can see, dp(i-1) is the length of the second horizontal line, and then if the red brackets match, the length is +2, and finally don’t forget to bring the leftmost if there is a matching match, this is dp(i - dp(i-1) - 2) , so add it together This is the maximum length of the parentheses in this scenario.

At this point, the depth of one-dimensional dynamic programming problem has basically been explored. Before entering the problem of multi-dimensional dynamic programming, there is also a type of one-dimensional dynamic programming problem, which is not difficult to express, and there is no such complicated nested DP as this problem, but thinking The complexity is extremely high, you must not stare at the whole process. If the complexity is too high, you need to fully recognize the meaning of the calculated part of dp(ix) and think highly abstract.

Fence coloring

Fence coloring is a difficult problem, the topic is as follows:

There are k colors and a n fence posts, each fence post can be colored with one of the colors.

You need to color all fence posts, and ensure that the adjacent fence posts same color at most two consecutive Then, return the number of all valid painting schemes.

This question k and n are very huge, the conventional violence solution and even the ordinary DP will time out. The meaning of choosing i is also very important. Here, i represent the use of several colors or several fences? It is easier to choose a fence, because the fence is the main body of the color. In this way, dp(i) represents all the coloring i 9 fences before coloring 060bd84bb440f.

First look at the recursive termination condition. Since a maximum of two consecutive same color, so dp(0) with dp(1) are k with k*k , since each barrier just brush color, free combination. Then dp(2) , the illegal situation is that the three fences are all the same color, so use all the possibilities to subtract the illegality. The illegal scene is only in k , so the result is k*k*k - k .

So considering the general situation, how dp(i) coloring schemes are there for 060bd84bb4413b? There is too much to think about the situation directly, we divide the situation into two, consider the two cases of the same color and different colors of i and i-1

If the i and i-1 are the same, then in order to be legal, i-1 must not be the i-2 , otherwise there will be three of the same color. In this case, no matter what color i-2 i-1 and i can only take one less color. color is i-2 color, so [i-1,i] this interval has k-1 fetch color scheme, front dp(i-2) species take color schemes, the final number is multiplied scheme: dp(i-2) * (k-1) .

There is actually a dynamic thinking behind this, that is, the k-1 each scene is a different color combination, but no matter dp(i-2) is, there must be k-1 two fences. Although the color value of the color combination is different, the color The number of combinations is constant, so it can be calculated uniformly. Understanding this is very important.

If the i and i-1 are different, then the i item has only k-1 , which is also dynamic, because it can never be the same color as i-1 Finally, multiply the dp(i-1) , which is the total number of schemes: dp(i-1) * (k-1) .

So the final total number of plans is the sum of the two, that is, dp(i) = dp(i-2) * (k-1) + dp(i-1) * (k-1) .

This question differs from that change too much, a fence to take any color can affect the color of the back fence to be taken, at first glance that there is a aftereffect of the subject, can not be solved with dynamic programming . But in fact, although there is after-effect, but if a reasonable disassembly is carried out, the total possibility of the rear fence k-1 is unchanged, so considering the total number of possibilities, it is with no after-effect, so we stand in the plan It is possible to solve this problem only by abstract thinking on the total number.

Next, we will introduce multi-dimensional dynamic programming, starting from two dimensions. Two-dimensional dynamic programming is to use two variables to express DP, namely dp(i,j) , which generally appears in two-dimensional array scenes. Of course, there are also some relationships between two arrays, which are also two-dimensional dynamic programming. In order to continue to discuss the string problem, I chose the two-dimensional dynamic programming example of the string problem, and the problem of edit distance is illustrated.

Edit distance

Edit distance is a difficult question, the topic is as follows:

Given the two words word1 and word2 , please calculate the minimum number of operations used to word1 to word2

You can perform the following three operations on a word:

  • Insert a character
  • Delete a character
  • Replace a character

As long as it is a string question, basically i means the string ending with the i item, but this question has two word strings. In order to consider any matching scenario, i j respectively. word1 minimum number of operations when 060bd84bb44357 and word2

Then for dp(i,j) consider whether word1[i] and word2[j] are the same, and finally through double recursion, first recurse i , and then recurse j within the recursion, and the answer comes out.

Suppose the same as the last character, i.e. word1[i] === word2[j] time, since the last character on the same do not change, so the number of operations is equivalent to taking into account the previous character , i.e. dp(i,j) = dp(i-1,j-1)

Assume different last character, then last step There are three modes available:

  1. Suppose it is replacement, that is, dp(i,j) = dp(i-1,j-1) + 1 , because it takes only one step to replace the last character and has nothing to do with the previous character, so the previous minimum number of operations is directly added.
  2. Assumed to be inserted, namely word1 insert a character becomes word2 , as long as the conversion to this point again +1 insert on the line, converting this step due to the insertion of a line, so word1 than word2 less a word, the other are the same, to To change to this step, it is necessary to dp(i,j-1) the transformation of dp(i,j) = dp(i,j-1) + 1 , so 060bd84bb443fa. .
  3. The assumption is deleted, word1 delete a character becomes word2 , empathy, to be dp(i-1,j) after changing one more step removed so dp(i,j) = dp(i-1,j) + 1 .

Since the title takes the least number of operations, the three cases can be the smallest, that is, dp(i,j) = min(dp(i-1,j-1), dp(i,j-1), dp(i-1,j)) + 1 .

So after considering whether the last character is the same, the combined state transition equation is the final answer.

Let's consider the termination condition, that is, when i or j is -1, because the state transition equations i and j constantly decreasing, they will definitely decrease to 0 or -1, because 0 is a string and there is a character. For example, consider -1 is convenient when the string is empty, so we consider -1 as the boundary condition.

When i is -1, that is, word1 is empty. At this word2 . Obviously, only inserting j is the minimum number of operations, so at this time dp(i,j) = j ; in the same way, when j is -1, that is, word2 is empty. i times need to be deleted, so the number of operations is i , so dp(i,j) = i .

Non-string issues

Speaking of this, I believe you are already comfortable with the issue of string verbs, let's look at the issue of verbs in non-string scenarios. There are three classic motion rules for non-string scenes. The first is the minimum distance of the rectangular path, or the maximum benefit; the second is the knapsack problem and its variants; the third is the housebreaking problem.

These problems are solved in the same way, but dp(i) is slightly different. For example, for the rectangle problem, dp(i,j) means the smallest path when walking to the i,j grid; for the backpack problem, dp(i,j) means that when the i item of 060bd84bb444e is installed, the backpack is still The maximum price is when the space j dp(i) robbery, 060bd84bb444e3 represents the maximum profit when i

Because of the length of the issue, I will not introduce it in detail here, but will briefly explain the rectangular problem and the house-robbing problem.

For the rectangle problem, the state transition equation focuses on how the last state is transferred. Generally, the rectangle can only move to the right or down. There may be some obstacles on the road. We need to make branch judgments, and then choose the one that best meets the problem. The required route can be used as the current transfer equation of dp(i)

Regarding the problem of house robbery, since adjacent houses cannot be robbed at the same time, for dp(i) , either for looting i-1 without looting the i , or looting i-2 i , the maximum value of the two final states can be used, namely dp(i) = max(dp(i-1), dp(i-2) + coins[i]) .

to sum up

The core of dynamic programming is divided into three steps. First, define the state clearly, that dp(i) is; then define the state transition equation, this step requires some thinking skills; finally think about verifying the correctness, that is, try to prove that the state transition equation you wrote is correct Yes, in this process, the state transition should be not repeated and not missed, and all the situations have been covered.

The most classic problem of dynamic programming is the backpack problem. Due to space reasons, a separate article may be published next time.

The discussion address is: Intensive Reading "Algorithm-Dynamic Programming" · Issue #327 · dt-fe/weekly

If you want to participate in the discussion, please click here , there are new topics every week, weekends or Mondays. Front-end intensive reading-to help you filter reliable content.

Follow front-end intensive reading WeChat public

<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">

Copyright notice: Freely reproduced-non-commercial-non-derivative-keep the signature ( Creative Commons 3.0 License )

黄子毅
7k 声望9.5k 粉丝