Contact us : Youdao technical team assistant : ydtech01 / Email : [ydtech@rd.netease.com]
When I learned the basics before, I also said that there is an ambiguous relationship between recursion and dynamic programming. It can be said that dynamic programming is a recursion in the decision-making process. Therefore, our brushing questions today will also involve some relatively simple dynamic programming topics, which can also help us deeply understand the recursive algorithm, and also lay the foundation for our in-depth study of dynamic programming algorithms and optimization of dynamic programming in the future. .
1. Pre-knowledge
Every recursive algorithm has a recursive formula, through which we can understand the recursive algorithm more clearly.
1.1 Fibonacci number sequence deed recurrence formula
Usage scenario : When the state of our bottom (upper) row can be derived from the information of the upper (lower) row only, and we are asked to solve the last (first) row, we don’t need to store the data of each row in an array , You can save space complexity by scrolling the array technique.
specifically implements : Assuming that we already know the data of the first row, and the data of the second row can be obtained through the first row of data through certain operations, then we only need an array to temporarily store two rows of data, and then we can The result of each row of data is calculated, and the two rows of data are continuously replaced by rolling, and finally the method of the last row of data is solved.
key point : derive and calculate the index of the current line and the next (upper) line. Since the array is scrolling, the index of our target row also changes at any time. The following is a general formula for obtaining the current row and the upper (lower) row:
current line : const curIdx = i% 2
Since our scrolling array has only 2 rows, the current index only needs to be modulo 2;
upper (lower) line : const preIdx = +!curIdx
Since the scrolling array has only two rows, the index is either 0 or 1, we can get the index of the upper (lower) row by negating the current row (note that in js, negating 1 is false, negating 0 Is true, so we convert the Boolean type to a numeric type through an implicit type conversion).
How do we understand this question? If we want to climb to the nth staircase, the previous step may be at n-1, or it may be at n-2
Practical use: will often be used when brushing questions later, see below for details
Two, brush the question dinner
2.1 LeetCode 120. Triangle minimum path sum
2.1.1 Problem-solving ideas
According to the recursive routine we will have before:
1. Define the recurrence state : In this problem, the path we take each step mainly depends on the current row number i and the current column number j. Therefore, the recurrence state of our problem should be Yes: dp[i, j]
2. Recurrence formula : After determining the recurrence state, we have to determine the recurrence formula. So, how can the data in the i-th row and j-th column be derived? First of all, according to the meaning of the question, we require the minimum path sum. If we go from the bottom to the top, then we can know that the data of the next row of i should be the minimum value of the legal path of the previous row i+1 plus the current path to the node value.
Therefore, we have the following formula:
// 第i行第j列数据的推导公式
dp[i, j] = min(dp[i+1, j], dp[i+1, j+1]) + val[i,j]
3. Analyze the boundary conditions : We need to initialize the known conditions of our topic into our recursive array as boundary conditions. In this question, the boundary condition is the data of the last row. We add the data of the last row to the scrolling array first, so that the total path sum can be continuously derived from the last row of data to find the minimum path.
4. Program implementation : We directly use the loop and rolling array technique to achieve.
2.1.2 Code Demo
function minimumTotal(triangle: number[][]): number {
const n = triangle.length;
// 递推公式(状态转义方程)以下为自底向上走的公式
// dp[i, j] = min(dp[i+1, j], dp[i+1, j+1]) + val[i,j]
// 由于i只跟i+1有关,因此,我们可以用滚动数组的技巧定义dp数组
let dp: number[][] = [];
for(let i=0;i<2;i++){
dp.push([]);
}
// 首先初始化最后一行的数值,由于使用了滚动数组的技巧,因此,我们最后一行的索引应该是(n-1)%2
for(let i=0;i<n;i++) {
dp[(n-1)%2][i] = triangle[n-1][i];
}
// 然后从倒数第二行开始求值
for(let i = n-2;i>=0;--i) {
// 由于使用了滚动数组,因此,当前行的下标为i%2,而下一行的下标则是当前行下标取反
let idx = i % 2;
let nextIdx = +!idx;
// 根据上面的公式计算出每一个位置上的值
for (let j=0; j <= i; j++) {
dp[idx][j] = Math.min(dp[nextIdx][j], dp[nextIdx][j + 1]) + triangle[i][j];
}
}
// 最终,三角形顶点的那个值就是我们要求的值
return dp[0][0];
};
2.2 LeetCode 119. Yanghui Triangle II
2.2.1 Problem solving ideas
This question is similar to the previous question. You can still deduce the value of the next line based on the previous line. Therefore, you still have to use the technique of scrolling the array. The analysis of the recursion state and the recursion formula is similar. You can try to derive it yourself.
The boundary condition of this question is that the first place of each row should be 1.
2.1.2 Code Demo
function getRow(rowIndex: number): number[] {
const res: number[][] = [];
// 初始化两行全部初始填充0的滚动数组
for(let i=0;i<2;i++)res.push(new Array(rowIndex+1).fill(0));
for(let i=0;i<=rowIndex;i++) {
// 计算滚动数组当前索引和上一行索引
let idx = i % 2;
let preIdx = +!idx;
res[idx][0] = 1;
// 计算每一行出第一位外其他位置的值
for(let j=1;j<=i;j++) {
res[idx][j] = res[preIdx][j-1] + res[preIdx][j];
}
}
// 滚动数组最后一行
return res[(rowIndex % 2)]
};
2.3 LeetCode 198. House Robbery
2.3.1 Problem-solving ideas
1. Recursive state analysis :
Since the maximum amount that can be stolen is required, assuming that the last one is n, then the maximum amount is directly related to whether we steal the last one. We need to classify and discuss:
a. 不偷最后一家:dp[n][0]其中,0代表不偷
b. 偷最后一家:dp[n][1]其中,1代表偷
2. Determine the recurrence formula :
Since the recurrence state is discussed in two situations, our recurrence formula should also be divided into two parts:
a. 不偷最后一家:由于不能连续偷相邻的两家,如果最后一家不偷,那么,我们倒数第二家就可以偷,因此,此时我们的最大收益就取决于偷倒数第二家的金额与不偷倒数第二家金额的最大值。即:dp[n][0] = max(dp[n-1][0], dp[n-1][1])
b. 偷最后一家:由于要偷最后一家,那么就不能偷倒数第二家,因此,这种情况的最大收益是不偷倒数第二家获得的收益加上偷最后一家带来的收益,即dp[n][1] = dp[n-1][0] + nums[n]
3. Determine the boundary conditions:
According to the meaning of the question, if we do not steal the first house, because none of the house is stolen, the income should be 0 at this time. If you steal the first house, then the proceeds will be the first house’s money. At this point, we have established the initial boundary conditions.
4. Program realization:
Since the current income of this question only depends on the income of the previous one, it is still implemented using the rolling array technique and loop.
2.3.2 Code Demo
function rob(nums: number[]): number {
const n = nums.length;
// 由于不能连续偷两家,因此,最大收益应该分为两种情况讨论:
// 1. dp[n][0] = max(dp[n-1][0], dp[n-1][1]) 即:不偷最后一家的最后收益,取决于我偷倒数第二家的收益与不偷倒数第二家的收益的最大值
// 2. dp[n][1] = dp[n-1][0] + nums[n] 即:如果投了最后一家,那么倒数第二家就不能偷,所以最大收益就等于不偷倒数第二家的收益加上偷最后一家获得的收益
const dp: number[][] = [];
for(let i=0;i<2;i++) dp.push([]);
// 初始化第0家的值
dp[0][0] = 0;// 不偷第一家时收益为0
dp[0][1] = nums[0];// 偷第一家时收益为第一家的钱
for(let i=1;i<n;i++) {
// 使用滚动数组技巧
let idx = i % 2;
let preIdx = +!idx;
dp[idx][0] = Math.max(dp[preIdx][0] , dp[preIdx][1]);
dp[idx][1] = dp[preIdx][0] + nums[i];
}
// 最终收益最大值时不偷最后一家和偷最后一家的最大值
return Math.max(dp[(n-1) % 2][0], dp[(n-1) % 2][1]);
};
2.4 LeetCode 152. Maximum product sub-array
2.4.1 Problem-solving ideas
1. Recursive state analysis: we require the product of the largest sub-array, then we can use dp[n] to represent the maximum value of the largest sub-array product whose last bit is n.
2. Recurrence formula: The is n. There are two possibilities. The first is to multiply the maximum product of n-1 by the current value of n, and the second is that n is not the same as the previous value. Multiply, self-opening a country, therefore, we should choose a maximum value in these two cases, so the recurrence formula should be: dp[n] = max(dp[n-1] * val[n], val[ n])
3. Boundary conditions: may contain negative numbers in the array, it is possible that the current value multiplied by the original maximum value becomes the minimum value, or the current value multiplied by the original minimum value becomes the maximum value. Therefore, we not only Need to record the maximum value before the current number, and also record the minimum value to facilitate the handling of negative numbers. At the beginning, because we require a product relationship, then our maximum and minimum values are initialized to 1.
4. Program implementation: Since the maximum product of the n-th item is only related to n-1, we can use the variable method to store the relationship, and there is no need to open up additional recursive array space.
2.4.2 Code Demo
function maxProduct(nums: number[]): number {
// 递推状态:dp[n]代表以n作为结尾的最长连续子数组的乘积
// 递推公式:dp[n] = max(dp[n-1] * val[n], val[n]),即这个有两种可能,一种是以n作为结尾,然后乘上之前的最大值,另一种是不乘之前的值,自己独立成为一个结果
// 我们应该从这两种方案中选择一个较大的值作为结果
// 由于当前值只跟上一个值有关,我们可以使用变量方式来替代递推数组,又因为数组中可能存在负数,有可能导致当前值乘上之前的最大值变成了最小值,因此,我们
// 还需要额外记录一下数组中的最小值
let res = Number.MIN_SAFE_INTEGER;
// 由于是乘积关系,因此,最小值和最大值初始化为1
let min = 1;
let max = 1;
for(let num of nums) {
// 由于num是小于0的,因此,如果我们依然乘以原先的最大值,那就可能反而变成最小值,因此当num小于0时,我们交换最大值和最小值,这样,num乘以原先的最小值,就可以得到较大值了
if(num < 0) [min, max] = [max, min];
// 计算最大值和最小值
min = Math.min(min * num, num);
max = Math.max(max * num, num);
// 最终结果在上一个结果和max间取最大值
res = Math.max(res, max);
}
return res;
};
2.5 LeetCode 322. Change Exchange
2.5.1 Problem-solving ideas
1. Recurrence state: depends on the denomination of the amount to be collected. Therefore, our recurrence state is: dp[n]
2. Recursive formula: If the denomination we want to collect is n, then the smallest number of coins we can collect should be the number of n-coin denomination coins plus the current coin number 1, and each time Just take the least amount. Therefore, our final recurrence formula should be: dp[n] = min(dp[i-coin] + 1), that is, money with a denomination of n requires us to take a minimum value among all the patchwork schemes, and each A patchwork solution should be the current denomination i minus the current coin denomination coin plus the current number of coins 1.
3. Boundary conditions: When the denomination is 0, we need 0 coins.
4. The program achieves : When we put together the denominations, there are some special circumstances that need to be considered, such as: if the current denomination to be put together is smaller than the denomination of the coin, then we cannot use the current coin to put together the target denomination. If i-coin If the denominations cannot be pieced together successfully, then we certainly cannot use the coin denominations to piece together the target denomination, because i-coin is a pre-condition.
2.5.2 Code Demo
function coinChange(coins: number[], amount: number): number {
// 我们需要计算出每一个面额所需要的硬币数量
let dp: number[] = new Array(amount+1);
// 初始时全部填充为-1代表不可达
dp.fill(-1);
// 拼凑0元需要0枚硬币
dp[0] = 0;
// 循环每一个面额
for(let i=1;i<=amount;i++) {
for(let coin of coins) {
// 如果当前要拼凑的面额比当前硬币还小,那就不能使用当前硬币
if(i < coin) continue;
// 如果没办法拼凑到dp[i-coin]的硬币,那么自然也不可能使用coin面额的硬币拼凑到dp[i]
if(dp[i - coin] === -1) continue;
// 如果当前的匹配方案没有使用过,或者使用当前匹配方案的结果比上一个匹配方案的结果大,说明我们找到了更小的拼凑方案,那么我们就把当前拼凑方案替换之前的拼凑方案
if(dp[i] === -1 || dp[i] > dp[i - coin] + 1) dp[i] = dp[i - coin]+1;
}
}
return dp[amount];
};
2.6 LeetCode 300. The longest increasing subsequence
2.6.1 Problem-solving ideas
Concept literacy:
a. Increasing subsequence:
You can select elements "jumping" in a complete sequence, and the next element must be no less than the previous element. The so-called "jumping" means that the element index does not need to be continuous, as in the following example:
`# 原始序列
1, 4, 2, 2, 3, 5, 0, 6
# 递增子序列
1, 2, 2, 3, 5, 6`
b. Strictly longest increasing subsequence:
Strictly increasing subsequence is based on the increasing subsequence with an additional restriction, that is, the next element cannot be equal to the previous element, but can only be greater than, as shown in the following example:
# 原始序列
1, 4, 2, 2, 3, 5, 0, 6
# 严格递增子序列
1, 2, 3, 5, 6
1. Recurrence state : Since the length of our longest incremental subsequence is related to which element I currently use as the last element, our recurrence state is: dp[n], which represents the end with position n The length of the longest increasing subsequence
2. Recursive formula : If we want to calculate the length of the longest increasing subsequence ending with the nth element, we must find the last element j of the longest legal longest increasing subsequence, and our first The length of the longest increasing subsequence with n elements at the end is the length of the last longest increasing subsequence plus one. We only need to find the maximum length of all the longest increasing subsequences that meet this condition to get the final result, namely :Dp[n] = max(dp[j] + 1) | j<n & val(j) <val(n)
3. Boundary condition : when n is 1, the length of the longest increasing subsequence is 1
4. Program implementation : Since our recursive state defines the length of the longest incremental subsequence ending with n, the default initial value of each item is 1, and at least the current number must be selected.
2.6.2 Code Demo
function lengthOfLIS(nums: number[]): number {
const dp: number[] = [];
let res: number = Number.MIN_SAFE_INTEGER;
for(let i=0;i<nums.length;i++) {
// 每一项都初始化为1,因为dp[i]代表以i位置作为结尾的最长递增子序列的长度,那我们最少都应该选择i这个数,长度就是1
dp[i] = 1;
// 在i位置之前找到满足nums[j] < num[i]的值
for(let j = 0; j < i;j++) {
if(nums[j] < nums[i]) dp[i] = Math.max(dp[i], dp[j] + 1);
}
// 到此,我们已经找到了一组最长递增子序列了,更新一下res
res = Math.max(res, dp[i]);
}
return res;
3. Conclusion
This is the end of today's questioning.
In fact, it’s not difficult to find from the above questions. Whether it is recursive algorithm or dynamic programming, there are certain routines to follow. Although this routine is not easy to learn, at least there is a clear learning direction. A seemingly complicated recursive or dynamic program can be analyzed and solved one by one through four general routines: recursive state definition, recursive formula (state transition equation) derivation, boundary condition establishment, and program realization.
Of course, the implementation of the above program, in order to make it easier to understand, does not use too many techniques for optimization, and is not necessarily optimal. If there is an opportunity in the future, I will share the optimization techniques of dynamic programming with you. Also welcome everyone to communicate with us~
-END-
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。