Is dynamic programming difficult? To be honest, I found it difficult, especially for beginners, when I first started dynamic programming, I was looking at the 0-1 backpack problem, and I was really confused. Later, I encountered the dynamic programming problem, read the answer, but is not to do, do not know how to start. If you are not familiar with recursion, I strongly recommend reading it: Why you can’t learn recursion, say goodbye to recursion, and tell me about my experience
For dynamic programming, many problems will be used in the spring and fall of dynamic programming, in a fit of rage, and then Leetcode continuously brush dozens of questions
And then all of a sudden, dynamic programming wasn’t that difficult, so today, I’m going to tell you how I did dynamic programming, and some of the tricks THAT I learned. I’m sure you’ll get something out of it
If you’re interested in dynamic programming, or if you know about dynamic programming, but you don’t know how to do it, then I suggest that you read this article in much the same way as the previous article on recursion, with lots of examples. If you can’t read it at one time, it is recommended to collect it. At the same time, don’t forget the quality of three.
So for beginners, I’m going to start with the easiest problem, and then I’m going to get more and more difficult, and then I’m going to show you how to optimize it. Because 80% of the dynamic gauges can be optimized. But I have to say, if you haven’t even heard of dynamic programming, this article is probably going to be stressful for you.
First, the three steps of dynamic programming
Dynamic programming is all about using history to avoid our double counting. And this history, we need some variables to store it, usually in a one dimensional array or a two dimensional array. So let’s talk about three very important steps for dynamic programming,
And if you don’t understand that, that’s fine, because there’s a lot of examples that you’ll get the idea. And the reason WHY I’m not going to do this with the example is because I don’t want you to get confused
Step 1: Define the meaning of the array elements. As mentioned above, we will use an array to hold the history array. Let’s say we use a one-dimensional array dp[]. It is very, very important to specify the meaning of the elements of your array. For example, what does your DP [I] stand for?
The second step: find the relationship between the array elements, I think dynamic programming, or a little similar to our high school learning induction, when we want to calculate dp[n], dp[n-1], DP [n-2]….. For example, dp[n] = dp[n-1] + dp[n-2], this is the relationship between the elements of the array. And this is the most difficult step, and I’ll do several types of problems later.
Those of you who have studied dynamic programming often hear about the optimal substructure, breaking down big problems into smaller problems, and when I said that, at the beginning, I was confused about the optimal substructure. I think you’ve heard a lot about this, so this time, I’m going to do it in a different way, not in terms of all the seeds, all the optimal substructures. So the big guy don’t spray me again nonsense, because I said, this is my usual routine to do the problem.
Step 3: Find the initial value. Dp [n] = dp[n-1] + dp[n-2], dp[n-1] = dp[n-2], dp[n-2] = dp[n-1], dp[n-2] = dp[n-1], dp[n-2] = dp[n-1], dp[n-2] = dp[n-1] = dp[2] + DP [1]. However, dp[2] and DP [1] cannot be decomposed any more, so we must be able to directly obtain the values of DP [2] and DP [1], and this is the so-called initial value.
Given the initial value, and given the relationship between the elements of the array, then we have the value of dp[n], and dp[n] is defined by you, you define what it is, and then we have solved the problem.
Don’t understand? Okay, so let’s do three or four examples, and I’m going to do it exactly this way.
Two, case detailed explanation
Case 1: Simple one-dimensional DP
A frog can jump up one or two steps at a time. Find out how many ways the frog can jump up an N step.
(1) Define the meaning of the array elements
Let’s define dp[I] as: there are dp[I] ways to jump up n steps. Let’s define dp[I] as: there are DP [I] ways to jump up I steps. So, if we can figure out dp[n], isn’t that what we’re looking for? So the first step is defined.
(2) Find the relationship between the elements of the array
Our goal is to ask dp[n], the problem of dynamic programming, as you’ve often heard, is to take a larger problem and divide it into smaller problems, and then derive a larger problem from the smaller problems. That is, dp[n] has size N, and smaller ones are n-1, n-2, n-3…. In other words, dp[n] must match dp[n-1], dp[n-2]…. There is some kind of relationship. We need to find out what their relationship is.
So the question is, how?
How to find this is the core, the hardest one, so we have to go back to the problem, to find the relationship, what is dp[n] equal to?
There are two ways for the frog to reach the NTH step, since there is a choice between jumping one step or two
One is to jump up from level n-1
One is to jump up at the n-2 level
Since we are counting all possible jumps, we have dp[n] = DP [n-1] + dp[n-2].
(3) Find out the initial conditions
When n = 1, dp[1] = dp[0] + dp[-1], and we are not allowed to have negative subscripts, so we must directly give the value of dp[1], which is equivalent to the initial value, obviously, dp[1] = 1. Similarly, dp[0] = 0. (because there are 0 steps, there must be 0 jump methods). Hence the initial value:
When n <= 1, dp[n] = n. When n <= 1, dp[n] = n.
All three steps are done, so let’s write the code, which will be commented in detail.
int f( int n ){
if(n <= 1)
return n;
// Create an array to hold historical data
int[] dp = new int[n+1];
// Give the initial value
dp[0] = 0;
dp[1] = 1;
// 通过关系式来计算出 dp[n]
for(int i = 2; i <= n; i++){
dp[i] = dp[i-1] + dp[-2];
}
// Return the final result
return dp[n];
}
Copy the code
(4) And initialization
Everyone first think, do you think, the above code is not a problem?
Is it a problem, or is it a mistake? The mistake is that I’m not rigorous enough in looking for the initial value, which I did on purpose, to tell you about the rigor of the initial value. For example, when n = 2, dp[2] = dp[1] + dp[0] = 1. This is obviously wrong, you can simulate, it should be dp[2] = 2.
In other words, when looking for the initial value, we must be careful not to miss it. Dp [2] is also an initial value, which cannot be calculated by formula. One might say, I can’t imagine what to do. This is easy to do, just do a few more problems.
Here are three different examples, and IN future articles, I will continue to follow this step and give you a few difficult and different types of questions. The following several examples, will not talk about the characteristics of detailed ha. In fact, the one dimensional array above can be optimized to make the space smaller, but we’re not going to do the optimization for now, and we’re not going to do the optimization for the next one.
Case 2: DP for a two-dimensional array
I’ve done dozens of DP algorithm problems, 80% of the problems, I would say, are based on two-dimensional arrays, so the following problems are mainly based on two-dimensional arrays, and of course some people might say, how do I know if I want to use one-dimensional or two-dimensional? That’s not a big problem. Let’s move on.
Problem description
A robot is located in the upper left corner of an m x N grid (the starting point is marked “Start” in the image below).
The robot can only move one step down or to the right at a time. The robot tries to reach the lower right corner of the grid (marked “Finish” in the image below).
How many different paths are there?
This is Problem 62 of Leetcode: leetcode-cn.com/problems/un…
Same old story, three steps.
Step 1: Define the meaning of the array elements
Since our goal is how many paths there are from the upper left corner to the lower right corner, we define the meaning of DP [I] [j] as: when the robot walks from the upper left corner to the position (I, j), there are dp[I] [J] paths. So, dp[m-1] [n-1] is our answer.
Note that the grid corresponds to a two-dimensional array, and the array is calculated from the index 0, so the lower right corner is (m-1, n-1), so dp[m-1] [n-1] is the answer we are looking for.
Step two: Find the relationship between the elements of the relational array
Imagine, how does the robot get to the position (I, j)? Since the robot can go down or to the right, there are two ways to get there
One way is to take a step from the position (I -1, j)
One is to take one step from the position (I, j-1)
Dp [I] [j] = dp[I -1] [j] + dp[I] [j-1]
Step 3. Find the initial value
Obviously, when dp[I] [j], if either I or j is 0, can we use the relation? The answer is no, because then I -1 or j -1 will become negative, and the array will be in trouble, so our initial value is to calculate all dp[0] [0… n-1] and all DP [0… m-1] [0]. Again, this is very easy to calculate, the equivalent of the top row and the left column of a computer graph. So the initial values are as follows:
Dp [0] [0… n – 1) = 1; // The same as the top row, the robot can only go left
Dp [0… m – 1] [0] = 1; // This is equivalent to the leftmost column. The robot can only go down
Lu code
All three steps are written, so let’s just look at the code
public static int uniquePaths(int m, int n) {
if (m <= 0 || n <= 0) {
return 0;
}
int[][] dp = new int[m][n]; //
/ / initialization
for(int i = 0; i < m; i++){
dp[i][0] = 1;
}
for(int i = 0; i < n; i++){
dp[0][i] = 1;
}
// derive dp[m-1][n-1]
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = dp[i-1][j] + dp[i][j-1]; }}return dp[m-1][n-1];
}
Copy the code
The space complexity of O(n*m) can be optimized to the space complexity of O(min(n, m)), but I won’t talk about it here
Case 3: A two-dimensional array DP
Write here, a little tired, but still have to write, so reading friends, you have to continue to read ah. The next one is not hard, it’s a bit harder than the one above, but it’s very similar
Problem description
Given an m x N grid containing non-negative integers, find a path from the top left to the bottom right that minimizes the sum of numbers on the path.
** Note: ** Can only move down or right one step at a time.
For example: Input: arr = [[1.3.1],
[1.5.1],
[4.2.1[] output:7Explanation: Because of the path1-3-1-1-1The sum of PI is the smallest.Copy the code
Similar to above, but with the optimal path sum, this is Problem 64 of LeetCode: leetcode-cn.com/problems/mi…
Or the same old, maybe some people are tired of watching, haha, but I still want to follow the steps to write, let those who do not understand the deeper understanding. Some of you may think that these questions are too easy, don’t panic, let’s start with the medium ones, and then I’ll give you some hard ones.
Step 1: Define the meaning of the array elements
Since our goal is to go from the upper left corner to the lower right corner, what is the minimum path sum? Then we define the meaning of DP [I] [j] as: when the robot goes from the upper left corner to the position (I, j), the lowest path sum is DP [I] [j]. So, dp[m-1] [n-1] is our answer.
Note that this grid is equivalent to a two-dimensional array, and the array is calculated from the index 0, so the position from the bottom Angle is (m-1, n-1), so dp[m-1] [n-1] is the answer we are going to go.
Step two: Find the relationship between the elements of the relational array
Imagine, how does the robot get to the position (I, j)? Since the robot can go down or to the right, there are two ways to get there
One way is to take a step from the position (I -1, j)
One is to take one step from the position (I, j-1)
But this time, instead of calculating all possible paths, we need to calculate which path sum is the smallest, so we need to choose one of these two ways, so that the value of DP [I] [j] is the smallest, obviously
dp[i] [j] = min(dp[i-1] [j], dp [I] [j -1]) + arr[i][j];// arr[I][j] represents the value of grid species
Copy the code
Step 3. Find the initial value
Obviously, when dp[I] [j], if either I or j is 0, can we use the relation? The answer is no, because then I -1 or j -1 will become negative, and the array will be in trouble, so our initial value is to calculate all dp[0] [0… n-1] and all DP [0… m-1] [0]. Again, this is very easy to calculate, the equivalent of the top row and the left column of a computer graph. So the initial values are as follows:
dp[0] [j] = arr[0] [j] + dp[0] [j-1]; // The same as the top row, the robot can only go left
dp[i] [0] = arr[i] [0] + dp[i] [0]; // This is equivalent to the leftmost column. The robot can only go down
The following code
public static int uniquePaths(int[][] arr) {
int m = arr.length;
int n = arr[0].length;
if (m <= 0 || n <= 0) {
return 0;
}
int[][] dp = new int[m][n]; //
/ / initialization
dp[0] [0] = arr[0] [0];
// Initialize the leftmost column
for(int i = 1; i < m; i++){
dp[i][0] = dp[i-1] [0] + arr[i][0];
}
// Initialize the top line
for(int i = 1; i < n; i++){
dp[0][i] = dp[0][i-1] + arr[0][i];
}
// derive dp[m-1][n-1]
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = Math.min(dp[i-1][j], dp[i][j-1]) + arr[i][j]; }}return dp[m-1][n-1];
}
Copy the code
The space complexity of O(n*m) can be optimized to the space complexity of O(min(n, m)), but I won’t talk about it here
Case 4: Edit distance
This question is a little more difficult than the above one. In leetcdoe, the position is hard. It looks like problem 72 in LeetCode.
Problem description
Given two words word1 and word2, calculate the minimum operand used to convert word1 to word2.
There are three things you can do with a word:
Insert a character remove a character replace a character
For example, enter word1 ="horse", word2 = "ros"Output:3Explanation: horse -> rorse (will'h'Replace with'r'Rorse -> rose (delete'r'Rose -> ros (delete'e')
Copy the code
answer
Again, follow the three steps above, and I can tell you here that 90% of the string problems can be solved with dynamic programming, and 90% of the string problems are solved with two-dimensional arrays.
Step 1: Define the meaning of the array elements
Because our purpose is to find the least operand used to convert word1 into Word2. Dp [I] [j] = dp[I] [j] = dp[I] [j]
Sometimes it’s not easy to figure out what arrays mean, so again, I’m going to give you a routine, and the rest is up to you to figure it out.
Step two: Find the relationship between the elements of the relational array
Now we have to find the relationship between dp[I] [j] elements, which is a little bit harder to find than the other problems, but no matter how hard it is to find, most of the time, Dp [I] [j] and dp [j], [I – 1] dp [1], [I] dp [I – 1] [1] certainly exist some kind of relationship. Because our goal is, ** from the small, by some manipulation, to the large. For this problem, we can perform three operations on word1
Insert a character remove a character replace a character
Since we are trying to minimize the number of operations, we are looking for the best operation. Then there is the following relation:
Dp [I] [j] = dp[i-1] [j-1] if word1[I] = word2 [j] = word2 [j] Don’t forget what dp[I] [J] means.
If word1[I] is not equal to word2 [j], then we must adjust it. The relationship between the three operations is as follows (note the difference between strings and characters) :
Word1 [I] [j] = dp[i-1] [j-1] + 1; word1[I] [j] = dp[i-1] [j-1] + 1;
Dp [I] [j] = dp[I] [j-1] + 1; dp[I] [j] = dp[I] [j-1] + 1;
Dp [I] [j] = dp[i-1] [j] + 1;
So we should choose an operation that minimizes the value of dp[I] [j]
Dp [I] [j] = min (dp [I – 1] [1], dp [1], [I] dp [[I – 1] [j]]) + 1;
So, our relationship follows,
Step 3. Find the initial value
Obviously, when dp[I] [j], if either I or j is 0, can we use the relation? The answer is no, because then I minus 1, or j minus 1, will become negative, and the array will be in trouble, so our initial value is to calculate all dp[0] [0… n] and all dp[0… m] [0]. And this is very easy to calculate, because when you have a string of length zero, and you convert to another string, you just keep inserting and deleting.
The following code
public int minDistance(String word1, String word2) {
int n1 = word1.length();
int n2 = word2.length();
int[][] dp = new int[n1 + 1][n2 + 1];
// Initial value of dp[0][0...n2]
for (int j = 1; j <= n2; j++)
dp[0][j] = dp[0][j - 1] + 1;
// dp[0...n1] Specifies the initial value of [0]
for (int i = 1; i <= n1; i++) dp[i][0] = dp[i - 1] [0] + 1;
// Dp [n1][n2]
for (int i = 1; i <= n1; i++) {
for (int j = 1; j <= n2; j++) {
Word1 [I] = word2[j] The ith character corresponds to I minus 1
if (word1.charAt(i - 1) == word2.charAt(j - 1)){
p[i][j] = dp[i - 1][j - 1];
}else {
dp[i][j] = Math.min(Math.min(dp[i - 1][j - 1], dp[i][j - 1]), dp[i - 1][j]) + 1; }}}return dp[n1][n2];
}
Copy the code
Finally, if you want to practice, you can go to LeetCode, choose the dynamic programming topic, and swipe it dozens of times in a row to make sure you’re never afraid of dynamic programming again. Of course, if it’s hard, we still have to go.
Leetcode dynamic planning direct: leetcode-cn.com/tag/dynamic…
3. How to optimize?
Two days ago, I wrote an 8000 sub article on dynamic programming to bid farewell to dynamic programming. I also wrote 40 dynamic programming algorithm problems, and summarized the routines of dynamic programming
This article more on my usual routine, to solve the problem. But because of the length is too long, for the four cases, no explanation optimization, this article is to explain, today how the optimization of dynamic programming, and a few days before the article the problem as an example of direct optimization, if seen advice look (see also go, I will directly give the problem and the code before optimization) : farewell to dynamic programming, and brush 40 dynamic metric algorithm problems, I summarized the routines of dynamic metric
Four, optimize the core: drawing! Draw pictures! drawing
Yes, 80% of the dynamic programming questions can be drawn, 80% of the questions can be drawn to know how to optimize at once, of course, DP also has some difficult questions, want to optimize is not so easy, but today I want to talk about, is not very difficult, and the most common, interview and written test most often the difficulty of the test.
Let’s talk about optimization directly through three problems, you will find, these problems, after optimization, the code is only slightly changed, you only need to know one or two, can say that 80% of the problem.
O(n*m) space complexity optimization to O(n)
Last time, the frog jumped the steps of the DP problem, which can optimize the space complexity of O(n) to O(1), I was going to start from this problem, but I thought, I want to learn the feeling of dp optimization is at least a little big guy, so I will not talk about it, I will start from the TWO-DIMENSIONAL array of DP.
Case 1: Maximum number of paths
Problem description
A robot is located in the upper left corner of an m x N grid (the starting point is marked “Start” in the image below).
The robot can only move one step down or to the right at a time. The robot tries to reach the lower right corner of the grid (marked “Finish” in the image below).
How many different paths are there?
This is Problem 62 of Leetcode: leetcode-cn.com/problems/un…
Dp [I] [j] = dp[i-1] [j] + dp[I] [j-1
Do not understand my previous article: farewell to dynamic programming, even brush 40 rules algorithm, I summarized the rules of the rules
public static int uniquePaths(int m, int n) {
if (m <= 0 || n <= 0) {
return 0;
}
int[][] dp = new int[m][n]; //
/ / initialization
for(int i = 0; i < m; i++){
dp[i][0] = 1;
}
for(int i = 0; i < n; i++){
dp[0][i] = 1;
}
// derive dp[m-1][n-1]
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = dp[i-1][j] + dp[i][j-1]; }}return dp[m-1][n-1];
}
Copy the code
The space complexity of this approach is O(n * m), so let’s show you how to optimize it to O(n).
Dp [I] [j] is a two-dimensional matrix. Let’s draw a graph of a two-dimensional matrix and initialize the matrix
According to the formula dp[I][j] = dp[i-1][j] + dp[I][j-1], when calculating the value of row I, we do not need to use the values of rows 1 to 2 except for row i-1, that is to say, Do we need to keep the values that we don’t need?
Answer: No, we only need to use a one-dimensional DP [] to hold a line of history. Then the value of dp[] is constantly updated during the process of the computer. It may not be easy for you to understand, but let me show you the process by hand.
1. Dp [0..n-1] is the value of the first row when the first row is initialized.
2. Update the value of dp[I] and discard the value of the first row.
For the sake of description, let’s use arr (I, j) to represent the values of row I and column J of our matrix. Let’s start at 0. That means we have row 0th.
Obviously, the value of matrix (1, 0) is equivalent to the previous initialization value, which is 1. And then at this point, the value of the matrix (0,0) doesn’t need to be stored anymore, because I don’t need it anymore.
(2), then continue to update the value of (1, 1), according to the previous formula (I, j) = (I -1, j) + (I, j-1). So 1,1 is equal to 0,1, plus 1,0, which is 2.
dp[i] = dp[i] + dp[i-1]
Dp [1] = dp[1] + dp[0] Since dp[I] originally held the values of the first row, it is now updated to hold the values of the second row.
dp[i] = dp[i-1] + dp[i]
Dp is equivalent to the dp before [I – 1] [j], [I – 1] dp [I] is equivalent to the dp before [I] [1].
So I kept filling in the last line according to this formula, and the result is as follows:
public static int uniquePaths(int m, int n) {
if (m <= 0 || n <= 0) {
return 0;
}
int[] dp = new int[n]; //
/ / initialization
for(int i = 0; i < n; i++){
dp[i] = 1;
}
Dp [I] = dp[I -1] + dp[I]
for (int i = 1; i < m; i++) {
// The initial value of row I and column 0
dp[0] = 1;
for (int j = 1; j < n; j++) {
dp[j] = dp[j-1] + dp[j]; }}return dp[n-1];
}
Copy the code
Case 2: Edit distance
Dp [I][j] depends on dp[i-1][j] and dp[I][j-1]. And there is also a situation is dp [I] [j] dependent on dp [j], [I – 1] dp [I – 1] [1] and dp [I] [1].
Problem description
Given two words word1 and word2, calculate the minimum operand used to convert word1 to word2.
There are three things you can do with a word:
Insert a character remove a character replace a character
For example, enter word1 ="horse", word2 = "ros"Output:3Explanation: horse -> rorse (will'h'Replace with'r'Rorse -> rose (delete'r'Rose -> ros (delete'e')
Copy the code
answer
Yesterday’s code is as follows. If you don’t understand it, please remember to read the previous article: Farewell to dynamic programming, and brush 40 dynamic programming algorithm problems. I summarized the routines of dynamic programming
public int minDistance(String word1, String word2) {
int n1 = word1.length();
int n2 = word2.length();
int[][] dp = new int[n1 + 1][n2 + 1];
// Initial value of dp[0][0...n2]
for (int j = 1; j <= n2; j++)
dp[0][j] = dp[0][j - 1] + 1;
// dp[0...n1] Specifies the initial value of [0]
for (int i = 1; i <= n1; i++) dp[i][0] = dp[i - 1] [0] + 1;
// Dp [n1][n2]
for (int i = 1; i <= n1; i++) {
for (int j = 1; j <= n2; j++) {
Word1 [I] = word2[j] The ith character corresponds to I minus 1
if (word1.charAt(i - 1) == word2.charAt(j - 1)){
p[i][j] = dp[i - 1][j - 1];
}else {
dp[i][j] = Math.min(Math.min(dp[i - 1][j - 1], dp[i][j - 1]), dp[i - 1][j]) + 1; }}}return dp[n1][n2];
}
Copy the code
The space complexity between no optimizations is O(n*m)
You can do it yourself, according to the above model, will you optimize?
(I, j-1) (I, j-1) (I, j-1) (I, j-1)
Well, let’s just go ahead and show you the picture, because the text is going around and around and it’s going to confuse you. When we want to calculate the values of (I,j) in the graph, in case 1, we need to use the values of (I -1, j) and (I, j-1). (See the color of the square in the picture)
So, in this case, we need an extra variable, pre, to hold the value of (I minus 1,j minus 1) at all times
dp[i][j] = min(dp[i-1][j] , dp[i-1][j-1] , dp[i][j-1]) + 1
Copy the code
It’s going to be one-dimensional
Dp [I] = min(dp[I -1], dp[I]) + 1.Copy the code
So, case 2 is actually not that different from case 1, except that it has an extra variable to keep temporarily. The final code looks like this (but for beginners, it’s not that easy to write)
The following code
public int minDistance(String word1, String word2) {
int n1 = word1.length();
int n2 = word2.length();
int[] dp = new int[n2 + 1];
// The initial value of dp[0...n2]
for (int j = 0; j <= n2; j++)
dp[j] = j;
// dp[j] = min(dp[j-1], pre, dp[j]) + 1
for (int i = 1; i <= n1; i++) {
int temp = dp[0];
// Equivalent to initialization
dp[0] = i;
for (int j = 1; j <= n2; j++) {
// pre = dp[i-1][j-1]
int pre = temp;
temp = dp[j];
Word1 [I] = word2[j] The ith character corresponds to I minus 1
if (word1.charAt(i - 1) == word2.charAt(j - 1)){
dp[j] = pre;
}else {
dp[j] = Math.min(Math.min(dp[j - 1], pre), dp[j]) + 1;
}
// Save the value to be discarded}}return dp[n2];
}
Copy the code
conclusion
These questions above, are basically not very difficult to get started, except for the last one is relatively difficult. And basically 80% of the 2-d matrix DP can be optimized to 1-D matrix DP just like the above method, the core is to draw them, see their value dependence, of course, there are a lot of other difficult optimizations, but most of the problems I run into are my type of optimizations. I’m going to show you how to get to the other ones, but today I’m going to talk about the most general optimization. Remember, draw a matrix diagram of DP in two dimensions, and then look at the value dependencies between the elements, and then it becomes clear how to optimize.
In future articles, I will follow this step and give you four or five hard dynamic programming questions, which will be put in the second tweet of each day. If you feel fruitful, don’t let go of three (like, thank, share), hee hee.
Have a harvest? Hope the old iron to a triple strike, to more people see this article
1, give me a thumbs up, can let more people see this article, by the way to inspire me, hee hee.
2, old iron people, pay attention to my original wechat public number “handsome play programming **”, focus on writing algorithm + basic knowledge of computer (computer network + operating system + database +Linux). Save it so you can see something after, don’t believe you hit me.
Finally, let’s take a look at github, where you can download hundreds of popular ebooks. Here are some screenshots