A Preliminary Study on Dynamic Programming

Table of contents

foreword

What is Dynamic Programming

Detailed Explanation of Verb Regulation Questions

recursion tree

recursive form

The difference between dynamic programming and greedy

The difference between dynamic programming and divide and conquer

Afterword


foreword

Recently, preparing for the Blue Bridge Cup, I just stumbled upon the topic of dynamic programming. Different from most of the previous algorithms, dynamic programming is an extremely ingenious algorithm idea. There is no fixed template and it is very changeable. It requires us to analyze specific problems. However, most articles on the Internet directly discuss the idea of ​​dynamic programming as soon as they come up, and do not write the derivation process. Therefore, this article mainly introduces and summarizes some simple dynamic programming methods and models, so as to facilitate the understanding of small partners who are new to dynamic programming.


What is Dynamic Programming

Dynamic programming (Dynamic Programming, hereinafter referred to as DP) is a set of methodology used to solve the optimal value problem. Unlike most algorithms we have come into contact with before, DP does not have a set of fixed templates, and we need to analyze specific problems.

Generally speaking, DP needs to decompose a complex problem to be solved into several sub-problems, and first solve the optimal solution of the sub-problems, so as to obtain the optimal solution of the original problem . It should be noted that these sub-problems are not independent, that is to say, there is a certain connection between each sub-problem, and DP will record the solution of each sub-problem that has been solved, so that when we encounter this sub-problem next time When there is a problem, you can directly use the previously recorded solution instead of repeated calculations (DP uses this method to improve efficiency, but this is not the core idea of ​​DP).

So how do we distinguish what kind of problems are suitable for solving with DP? Here we also introduce two concepts - [optimal substructure] and [overlapping subproblems].

If you look confused here, it doesn’t matter, it’s because we haven’t combined specific topics to understand it. You only need to remember these two concepts, and you will understand later.

Detailed Explanation of Verb Regulation Questions

In order to facilitate everyone's understanding of dynamic programming, let's talk about a classic DP problem - Fibonacci (Fibonacci) sequence.

The Fibonacci sequence is defined as: F_{0} = 1, F_{1} = 1, ... , F_{n} = F{n - 1} + F{n - 2} (n \geq 2).

recursion tree

We have mentioned the recursive writing method in the concept of tree . For the Fibonacci sequence, its recursive writing method is as follows:

int fibonacci(int n)
{
    if(n == 0 || n == 1)return 1;
    else return fibonacci(n - 1) + fibonacci(n - 2);
}

Combined with the code, we will find that return fibonacci(n - 1) + fibonacci(n - 2); when the expression is executed, the program always creates two "branches". This structure is the same as the binary tree we are familiar with. We Abstract it as a recursive tree .

The figure below is the recursion tree when n == 5.

In fact, when we observe this recursive tree, we will find a problem: when n == 5, F(5) = F(4) + F(3), and when n==4, F(4 ) = F(3) + F(2), at this time the expression of F(3) will be calculated twice, and the same is true for the following F(2).

The time complexity at this time is O(n^2), and if n is very large, we cannot afford it.

In order to avoid repeated calculations, we can create an array dp to record the results that have already appeared.

int dp[n + 1];
int fibonacci(int n)
{
    if(n == 0 || n == 1)return 1;
    if(dp[n] != 0)return dp[n];    //如果dp不是0,说明已经计算过了该结果,直接返回
    else
    {
        dp[n] = fibonacci(n - 1) + fibonacci(n - 2);
        return dp[n];
    }
}

In this way, we reduced the complexity from O(n^2) to O(n), which is a linear level.

This is the overlapping subproblem:

  • If a problem can be decomposed into several sub-problems, and these sub-problems will appear repeatedly, then the problem is said to have overlapping sub-problems.

recursive form

Now there is such a question:

Every winter, the Weiming Lake of Peking University is a good place for skating. The sports team of Peking University prepared a lot of skates, but there were too many people. After work was over every afternoon, there was often no pair of skates left.

Every morning, there will be a long queue at the shoe rental window, assuming that there are m people who return shoes, and n people who need to rent shoes. The question now is how many ways these people can line up to avoid the embarrassing situation where the sports group has no skates to rent. (Two people with the same needs (such as both renting shoes or returning shoes) exchange positions are the same arrangement)

According to the requirements of the topic: this question needs to solve the optimal solution of a sorting queue. We can choose to use the simulation method. Of course, if we read the question carefully, we will find that this question is actually the same as the Fibonacci sequence. the same.

Now we simplify the problem to: there are m people returning shoes and n people borrowing shoes, and inserting n people into m people's queue so that the number of people who borrow shoes is never greater than the number of people who return shoes There are several methods.

For another example, assuming m == 3, n == 2, then there are a total of 5 ways to solve the above problem.

It looks like you're doing math, but it's not. We can use the exhaustive method to list all possible sorts, and then filter out those that do not meet the conditions.

This is actually the same idea as the simulation, we only care about the attributes of the team leader. That is to say, for the queue, there is only the situation of [return] or [borrow]. If the person at the head of the team came to return the shoes, then the number of people in the shoe returning queue must be reduced by one; It is necessary to make a judgment at the time, if it is not enough to borrow, then it must have failed, right?

Then we can use recursive writing to enumerate its sorting every time. The code is as follows:

int func(int n, int m)
{
    if(n > m)return 0;    //不够借的情况
    if(n == 0)return 1;    //要借的人全部借完的情况
    return func(n - 1, m) + func(n, m - 1);
}

Similarly, we can get its recursive tree:

We can still prune through the method above to optimize the complexity.

However, if we try to exhaustively enumerate all paths, then when n is very large, the recursion tree we get will also be very large (generally speaking, when the recursion depth exceeds 20, it is considered bad ), which is also unacceptable to us.

For the above considerations, we might as well observe this recursive tree, and we can find that when the leaves of the tree are always n==0 or n<m, its value is 1 or 0 at this time, and looking from bottom to top, we find The value of the father of the leaf is always and only obtained by the sum of the values ​​​​of its two subtrees, assuming n = m = 1, that is, dp[1][1] = d[1][0] + dp [0][1].

From this, it can be concluded that when n==0, dp[i][0] = 1; and when n > m, dp[i][j] = 0 (i < j); thus Its recurrence relation is:

                ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​dp[i][j] = dp[i - 1][j] + dp[i][j - 1];

We call dp[i][j] the state of the problem, and the above formula is called the state transition equation, which transfers the state dp[i][j] to dp[i - 1][j] + dp[ i][j - 1]. And dp[i][0] is called the boundary.

Generally speaking, DP is always from the bottom (boundary) upward, and diffuses to the entire dp array through the state transition equation (obviously, recursion is derived from top to bottom).

Write the dynamic programming code according to this idea:

  for(int i = 1; i <= m; i++)
    {
        dp[i][0] = 1;
        for(int j = 1; j <= n; j++)
        {
            if(i < j)dp[i][j] = 0;
            else dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
        }
    }
    cout << dp[m][n] << endl;

The above process describes the optimal substructure:

  • A problem is said to have an optimal substructure if the optimal solution of a problem can be efficiently constructed from the optimal solutions of its subproblems;

The optimal substructure ensures that the optimal solution of the original problem in dynamic programming can be derived from the optimal solution of the subproblem. Therefore, a problem must have overlapping subproblems and optimal substructure to be solved using dynamic programming regions.

In fact, how to derive the state and state transition equations is the core of dynamic programming and also its difficulty.


Finally, let's talk about the difference between dynamic programming, greed, and divide and conquer.

The difference between dynamic programming and greedy

Greedy and DP both require that the original problem must have an optimal substructure. However, the difference is that the idea of ​​greed is top-down, and the optimal solution to the current problem is directly selected through decision-making, and the sub-problems that are not selected are discarded. In other words, greedy always chooses on the basis of the previous step.

However, dynamic programming problems, whether top-down or bottom-up, are always generalized from the boundary to the whole world, that is to say, DP will always consider all sub-problems, even if a sub-problem is currently ignored, but Due to the nature of overlapping subproblems, it will be considered again later.

The number tower problem as shown in the figure below:

If greedy is used, then the maximum value obtained is 5>8>12>10>5 == 40, which is obviously wrong, because if we use dynamic programming, then the maximum value obtained is 5>3 >16>11>9==44.

(The number of towers problem is also one of the classic dynamic programming problem models, friends can try to write its code)

The difference between dynamic programming and divide and conquer

Both divide and conquer and DP decompose the problem into sub-problems, and then combine the sub-problems to obtain the solution of the original problem. The difference, however, is that the subproblems of divide and conquer are not overlapping.

The subproblems of dynamic programming overlap.

As we mentioned before, the merge sort and quick sort algorithms use divide and conquer, processing the left and right sequences separately and then combining their results. There is no overlapping sub-problem in the process. At the same time, the problem solved by divide and conquer is not necessarily optimal, but DP must be.

Afterword

I hope that after reading this article, you can have a preliminary understanding of the problem of dynamic programming. At the same time, the best way to improve the ability of the algorithm is to write more questions. You can find DP questions on various platforms to do it. As the questions we practice become more and more More, the understanding of DP will become more and more in-depth.

Guess you like

Origin blog.csdn.net/ZER00000001/article/details/127788226