前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Leetcode | 第2节:动态规划(下)

Leetcode | 第2节:动态规划(下)

作者头像
学弱猹
发布2021-08-10 11:20:02
6750
发布2021-08-10 11:20:02
举报

大家好!欢迎回到动态规划的世界!

这一节的题目绝大部分都是选择的Leetcode中的hard(当然也有少部分的medium)。主要是挑选了一些之前看过的高频题。这些题目没有什么通用的解法,每一个题都需要仔细的去建模和考量,才能够写出对应的状态转移方程。读者可以尝试自己先思考,也可以通过解析摸一摸困难的动态规划题,可能会有哪些难点。

那么我们开始吧。

动态规划(下)

好的,接下来我们就用大量实际的真题,来看一下究竟如何解决动态规划类的问题。

Problem 2: Leetcode 416 给你一个 只包含正整数非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

一个例子是,如果nums = [1,5,11,5],那么答案是true,因为可以分为1, 5, 511两组。

这里要控制的参数有两个:元素和,和数组长度。所以可以考虑一个二维数组dp[i][j]。现在的问题可能就是,应该如何考虑

i, j

的状态转移方程

可以这么想,前

m

个数,要达到和为

n

,可能之前有哪些情况?首先很明显的是,数多的情况,肯定由数少的情况发展而来。所以大概率,

m

个数的情况,肯定由

m- 1, m- 2, \ldots

等情况发展而来了。

所以思考一下,假如现在你有

m

个数,并且元素和为

n

(相当于dp[m][n])。它如何由

m- 1

个数的情况变过来?首先你多了这一个数,你自然可以把它放在两个组中的任何一个。那么一定程度上,你可以认为,这个问题划归为了

在前

m

个数中,选择若干个放在一组,是否有可能和为

n

因为放在一个组认为是“选择”的话,那么放在另一个组就相当于“不选择”,正好对应了两种情况。这个本质上的思路就是0-1背包问题

这样的话,dp[i][j]的含义就不难想了。不失一般性我们设

i

是数组长度,而

j

是元素和,那么大体来看,我们应该要输出true or false,那么不妨假设,dp[i][j]表示“

i

个数,选择若干个,是否有可能和为

j

“?这个时候,如果我们认为数组长度是

m

,而元素和为

n

,那么要求的结果就是dp[m][target],这里的target是数组和的一半(想想为什么?),也就是说,如果数组和是奇数,或者最大的数已经超过一半,直接返回false就好,这个要想到,但容易被忽略。

我们知道了最后的结果,那么如何转移呢?上面已经说了,相当于有“选择”和“不选择”的两种方案。假如说前

m-1

个数已经决定好,那么实际上第

m

个数如果选择,对应的状态就倒退回了dp[m-1][n - nums[m]],这里的nums[m]表示第

m

个数的值。如果不选择,对应的状态就倒退回了dp[m-1][n]。这两个情况有一个是true,结果就应该是true,所以相当于取或运算的关系。

分析到这里,似乎没什么问题。但是要注意,n - nums[m]是有可能为负的。从题目含义上来分析,相当于说,如果你要求的数字和还没有超过第

m

个数,那么自然不可能去“选择”,因此这种情况下,状态只能倒退回dp[m-1][n]

所以总结一下,状态转移方程为

dp[i][j] = \begin{cases}dp[i - 1][j - nums[i]] | dp[i- 1][j] & j >= nums[i] \\ dp[i-1][j] & j < nums[i]\end{cases}

无后效性这个不难观察,这个我们不多说了。最后,我们看看边界情况。

  1. 不选择任何正整数,那么和为0,这是一定可以做到的。所以dp[i][0] = true
  2. 只有一个正整数可以选,对应的是dp[1][nums[1]] = true

好的,最后我们来把核心的代码粘贴过来。

代码语言:javascript
复制
# Ignored the boundary cases

for (int i = 1; i <= n; i++) {
  dp[i][0] = true;
}
dp[1][nums[1]] = true;
for (int i = 2; i <= n; i++) {
  int num = nums[i];
  for (int j = 1; j <= target; j++) {
    if (j >= num) {
      dp[i][j] = dp[i - 1][j] | dp[i - 1][j - num];
    } else {
      dp[i][j] = dp[i - 1][j];
    }
  }
}
return dp[n][target];

好的,这个题算分析的比较细了。之后的题目中,我们会适当减少对大流程的思考,而重点关注题目的一些技巧。

Problem 3: Leetcode 1458 给你两个数组 nums1 和 nums2 。 请你返回 nums1 和 nums2 中两个长度相同的 非空 子序列的最大点积。 数组的非空子序列是通过删除原数组中某些元素(可能一个也不删除)后剩余数字组成的序列,但不能改变数字间相对顺序。比方说,[2,3,5] 是 [1,2,3,4,5] 的一个子序列而 [1,5,3] 不是。

比方说,nums1 = [2,1,-2,5], nums2 = [3,0,-6],那么答案是18。因为可以从 nums1 中得到子序列 [2,-2] ,从 nums2 中得到子序列 [3,-6] 。它们的点积为18(你没忘记点积怎么计算,对吧?)。

这一个题的思路也比较直接。考虑dp[i][j]表示“nums1的前

i

个元素,和nums2的前

j

个元素,所能够构成的最大的内积“。所以又到了要考虑状态转移的时候了,很明显,数是一个一个堆起来的,内积也是一个一个对应乘积再求和的。所以可以考虑两个数组中的分别的最后一个元素。

很明显,这还是一个选择或者不选择的问题。两个元素一共可以有四种搭配,其实分别讨论一下就可以知道。

  1. 内积序列中选择nums1[i], nums2[j],那么这个时候除了要加上nums1[i]*nums2[j],还需要加上dp[i-1][j-1],相当于回退到了之前的情况。
  2. 只选择nums1[i],那么这个时候,其实对应的情况是dp[i][j-1],因为不考虑nums2[j],所以第二个数组考虑前
j

个元素,和考虑前

j - 1

个元素,其实是没区别的

  1. 只选择nums2[j],对应dp[i-1][j]
    1. 两个都不考虑,对应dp[i-1][j-1]

总结一下,我们的状态转移方程就是

dp[i][j] = max(dp[i-1][j-1] + nums1[i]*nums2[j], dp[i-1][j], dp[i][j-1], dp[i-1][j-1])

无后效性也是不难想到的(至少大部分题都不用担心这一点,有特例但是不多)。至于边界条件,只需要给定一个dp[1][1]作为初始解就可以了。

好的,我们放出核心代码。

代码语言:javascript
复制
for (int i = 1; i <= m; ++i) {
  for (int j = 1; j <= n; ++j) {
    int xij = nums1[i] * nums2[j];
    dp[i][j] = xij;
    if (i > 0) {
      dp[i][j] = max(dp[i][j], dp[i - 1][j]);
    }
    if (j > 0) {
      dp[i][j] = max(dp[i][j], dp[i][j - 1]);
    }
    if (i > 0 && j > 0) {
      dp[i][j] = max(dp[i][j], dp[i - 1][j - 1] + xij);
    }
  }
}

最后只需要输出dp[m][n]就可以了,这里的

m,n

分别对应两个数组的长度。

Problem 4: Leetcode 115 给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。 字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,"ACE" 是 "ABCDE" 的一个子序列,而 "AEC" 不是) 题目数据保证答案符合 32 位带符号整数范围。

一个简单的案例是,假如s = "rabbbit", t = "rabbit",那么一共其实有3种可能,大家容易发现,就是你这3个字母b选一个去掉就可以。

这个题就更抽象一点了,但是归根到底其实还是一种递推思想。即思考原序列和子序列在不断推进的时候,状态会发生什么样的变化

官方题解中,设dp[i][j]表示

s[i:]

的子序列中,

t[j:]

出现的个数。这里的

s[i:]

就表示从第

i

个字符到最后一个的这一段,是一种从后到前的推断。但实际上这个题完全也可以从前到后来分析,过程上没有太大的差别。

还是一样,我们考虑是否选择s[i]t[j],这种序列类的题目,很多都是这个思路,从前往后看,或从后往前看,目的就是,观察构成的新的原序列和子序列,是否是符合题目要求的。这就需要观察是否有s[i] == t[j]

如果满足,那么你可以选择让t[j]对应上s[i],那么这个时候的方案数就取决于dp[i+1][j+1]。但也可以说让t[j]并不对应上s[i],那么这就对应了dp[i+1][j]。所以这两种情况加在一起,就对应了dp[i][j]

如果这个条件不满足,那自然不可能选择这两个位置上的字符,因为这样就无法构成子序列的关系了。所以这样的话只有一种情况,就是dp[i+1][j]

总结一下,我们的状态转移方程就是

dp[i][j] = \begin{cases}dp[i+1][j+1] + dp[i+1][j] & s[i] = t[j] \\ dp[i+1][j] & s[i] \ne t[j]\end{cases}

由于这个我们是从后向前枚举的,所以观察初始状态的时候,应该是观察dp[i][n+1]的情况,这里

n

是子序列的长度。由于这个时候,相当于子序列是空序列(想想为什么?),所以应该答案就是1,也就是说

dp[i][n+1] = 1, i = 1, 2, m\ldots,

好的,我们来看看核心代码吧。

代码语言:javascript
复制
for (int i = 1; i <= m+1; i++) {
  dp[i][n+1] = 1;
}
for (int i = m; i >= 1; i--) {
  char sChar = s[i];
  for (int j = n; j >= 1; j--) {
    char tChar = t[j];
    if (sChar == tChar) {
      dp[i][j] = dp[i + 1][j + 1] + dp[i + 1][j];
    } else {
      dp[i][j] = dp[i + 1][j];
    }
  }
}

最后的结果就是dp[1][1]

不过这里有一个优化点,就是使用滚动数组。大家可以发现一个事情,就是第一个下标

i

不管由哪一个状态变过来,上一个状态的第一个下标都是

i + 1

。这就意味着其实你没有必要去存储这个维度,因此实际你只需要一个参数

j

就可以了。这样的代码相信也不难改,我们交给读者。

Problem 5: Leetcode 1143 给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。 一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。 例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

比方说如果text1 = "abcde", text2 = "ace" ,答案就是3,因为最长的就是"ace"

这个题其实就是最长公共子序列(Largest Common Subsequence,LCS)的问题,是一个面试一定要会的题目。思路和之前的题目完全一样,就是在序列不断推进的时候,观察状态如何做的转移。

现在还是一样,假设dp[i][j],表示**“原序列

s[0:i]

(包括

i

,下同),新序列

t[0:j]

,对应的公共子序列的最长长度**“。那么很明显,只有s[i] == t[j]才有可能考虑说对公共子序列的长度有增益,这个时候对应的结果是dp[i-1][j-1] + 1(多说两句,你当然也可以选择说,这两个字符都不选择,倒退到dp[i-1][j-1]这种情况。但很明显,这个结果不可能比之前那个大,所以没必要考虑这种可能性了)。否则的话,就只能取dp[i-1][j], dp[i][j-1]这两种情况中,较大的那个了。

所以状态转移方程为

dp[i][j] = \begin{cases}dp[i-1][j-1] + 1 & s[i] = t[j] \\ \max(dp[i-1][j], dp[i][j-1]) & s[i] \not = t[j]\end{cases}

边界情况其实不用多说,毕竟一开始的情况,数值肯定是0。所以核心代码如下

代码语言:javascript
复制
for (int i = 1; i <= m; i++) {
  char c1 = text1[i - 1];
  for (int j = 1; j <= n; j++) {
    char c2 = text2[j - 1];
    if (c1 == c2) {
      dp[i][j] = dp[i - 1][j - 1] + 1;
    } else {
      dp[i][j] = fmax(dp[i - 1][j], dp[i][j - 1]);
    }
  }
}

最后的答案就是dp[m][n]。这个题就当复习了。

Problem 6: Leetcode 312 有 n 个气球,编号为0 到 n - 1,每个气球上都标有一个数字,这些数字存在数组 nums 中。 现在要求你戳破所有的气球。戳破第 i 个气球,你可以获得 nums[i - 1] * nums[i] * nums[i + 1] 枚硬币。这里的 i - 1 和 i + 1 代表和 i 相邻的两个气球的序号。如果 i - 1或 i + 1 超出了数组的边界,那么就当它是一个数字为 1 的气球。 求所能获得硬币的最大数量。

比方说如果nums = [3, 1, 5, 8],那么答案就是167。但这个167怎么变过来的,我就不写了,你们看看原出处就好……

这一个题的思路其实不看确实想不到,但是考虑到它实在是hard中的一个高频题,我们把它当成了例子。

首先要注意的是,戳气球和它的反问题:放气球,其实是相同的。大家可以分一个气球在中间和在两边的情况来看(比方说放中间,下标为

i

,那就得到nums[i - 1] * nums[i] * nums[i + 1] 这么多硬币)。所以这个问题中,我们考虑放气球的方式似乎更好,可以从0开始不断的分布状态到最后的结果。不过这里其实你不这么做也可以,这只是单纯为了方便……

OK,那么这样的话,其实我们就不难思考了,设dp[i][j]表示“从第

i

到第

j

个气球,不包括第

i, j

个。可以得到的最多的硬币数“,那么假如说我们要计算dp[i][j],应该怎么考虑?事实上就是考虑说,第一次是在哪里放的气球?假如说在坐标

mid

下放的,那么这个时候,除去你得到的那个硬币数以外,还需要加上之前的状态已经得到过的硬币。这里就是dp[i][mid]dp[mid][j]。所以实际上,把这个坐标

mid

枚举一遍,就可以得到答案。

因此状态转移方程为

dp[i][j] = \max_{mid = i+1}^{j - 1} nums[i]*nums[mid]*nums[j] + dp[i][mid]+dp[mid][j], i < j- 1

好的,我们来看看代码

代码语言:javascript
复制
int solve(int left, int right) {
    if (left >= right - 1) {
        return 0;
    }
    if (rec[left][right] != -1) {
        return rec[left][right];
    }
    for (int i = left + 1; i < right; i++) {
        int sum = val[left] * val[i] * val[right];
        sum += solve(left, i) + solve(i, right);
        rec[left][right] = fmax(rec[left][right], sum);
    }
    return rec[left][right];
}

int maxCoins(int* nums, int numsSize) {
    memset(rec, -1, sizeof(rec));
    val[0] = val[numsSize + 1] = 1;
    for (int i = 1; i <= numsSize; i++) {
        val[i] = nums[i - 1];
    }

    return solve(0, numsSize + 1);
}

可以看出,这个时候我们返回的解就是dp[0][n + 1],这里的

n

就是气球的个数。

这个题可能有一个比较难想的点在于,这个

mid

为什么要定义为“第一次放入的气球的位置”。一个自然的想法当然是我们考虑“上一次”到“这一次”的状态,但这样问题也很明显,就是你放气球之后,同一个区间范围,同一组气球,却有了不同的下标。这种情况状态转移方程就写不出来了。而如果这么定义

mid

,无论你上一次放哪里,其实我都不影响我这个额外增益的计算。而最关键的点在于,这么设计之后,你依然可以枚举出所有的情况(毕竟你第一次在

(i, j)

这个范围内放的气球的位置,也不可能超过

(i, j)

对吧?),所以枚举完成之后,一定是可以找到最优解的。

还有一个细节是,我为什么要设置为区间为开区间,闭区间可不可以?左开右闭可不可以?当然也是可以的,一切以方便为主。但是要注意的是,其他的情况对应的状态转移方程会略有不同,这个我们交给读者思考。

Problem 7: Leetcode 72 给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。 你可以对一个单词进行如下三种操作: 插入一个字符 删除一个字符 替换一个字符

比方说word1 = "horse", word2 = "ros",那么对应的答案是3

这个题也是比较传统的编辑距离(Edit DIstance)的问题,也是hard中的高频题。这个题本身也是涉及NLP领域的一个基本算法。

这个题的难点在于,这里一共有6种操作,但需要经过多思考,把它化为3种情况。这3种情况分别为

给字符串A插入一个字符 给字符串B插入一个字符 给字符串A修改一个字符

剩下的3种,其实都和这三种等价了。比方说给字符串B插入一个字符,其实和给字符串A删除一个字符,作用是相同的。读者可以想想为什么,这本质上还是一个逆变换的思想。

那么好,这样的话,其实就可以设dp[i][j]表示word1的前

i

个和word2的前

j

个对应的编辑距离。那么一个好的消息是,不管是哪一种变换,都只会改变一个元素。所以按照序列问题朴素的思考方式,我们从前到后考虑一下

如果最近的一次是第一种(给字符串A插入一个字符),那么插入的字符就是第

i

个,这个时候相当于可以回退到dp[i-1][j]。但是因为多了一步操作数,所以结果要加1。

如果是第二种,类似,结果是dp[i][j-1] + 1。如果是第三种,就要看word1[i] = word2[j]是否成立了。如果成立,其实就不需要修改的操作。但不成立,就要修改,所以这一个其实要根据实际情况去看。

总结一下,状态转移方程就是

dp[i][j] = max(dp[i-1][j] + 1, dp[i][j- 1] + 1, dp[i-1][j-1] (word1[i] = word2[j]), dp[i-1][j-1] + 1 (word1[i] \not = word2[j]))

对于边界条件,这里就是考虑有一个字符串没有字符,另一个有的情况。比方说word1有,word2是空的,那么只需要把word1也删除干净就可以了。这里相当于对应dp[i][0] = i, dp[0][j] = j

这里多说几句,其实在我们从前到后推理的过程中,其实就已经把所有的字符串的字符的所有可能的插入,删除和修改情况都已经考虑到了。如果对状态转移方程有疑问,就想一下这一点是否make sense就可以了。

好的,我们看一下核心代码。

代码语言:javascript
复制
for (int i = 0; i < n + 1; i++) {
  dp[i][0] = i;
}
for (int j = 0; j < m + 1; j++) {
  dp[0][j] = j;
}

// 计算所有 DP 值
for (int i = 1; i < n + 1; i++) {
  for (int j = 1; j < m + 1; j++) {
    int left = dp[i - 1][j] + 1;
    int down = dp[i][j - 1] + 1;
    int left_down = dp[i - 1][j - 1];
    if (word1[i - 1] != word2[j - 1]) left_down += 1;
    dp[i][j] = min(left, min(down, left_down));
  }
}

这里的最后的返回结果就是dp[n][m],其中

n,m

分别对应word1和word2的长度。

Problem 8: Leetcode 1027 给定一个整数数组 A,返回 A 中最长等差子序列的长度。 回想一下,A 的子序列是列表 A[i_1], A[i_2], ..., A[i_k] 其中 0 <= i_1 < i_2 < ... < i_k <= A.length - 1。并且如果 B[i+1] - B[i] ( 0 <= i < B.length - 1) 的值都相同,那么序列 B 是等差的。

比方说如果输入是[3,6,9,12],那么输出就是4,因为整个大的数组就是最长的等差子数列,长度为4。

这个问题的麻烦点可能在于,你仅仅使用一个参数(就是数的个数)来描述是不够的,因为你的公差不同,你对应的最长等差子数列可能就不一样。所以这里用了第二个参数就是公差。方法也是非常朴素的,最长上升子序列的做法。

现在假设dp[i][diff]表示公差为

diff

,考虑前

i

个数,并且以第

i

个数为最后一个数,对应的最长的等差数列的长度(这里我们要求第

i

个数必须在等差数列中,也是为了递推方便,其实也是LIS里的trick)。那么这样的话,只要考虑之前的

j

个数(

j < i

),然后看从这个状态到

i

的状态,中间应该怎么去变。

这个不难,因为假如说从

j

i

,如果我们从“以第

j

个数为最后一个数“,去跳到”以第

i

个数为最后一个数“,那么实际上就相当于,最后的两个数是相邻的两个数。那么必须要求公差为它们俩的差(否则就不是等差数列了)。因此状态转移方程就可以得到为

dp[i][diff] = \max_{j = 0, 1, \ldots, i-1}(dp[i][diff], dp[j][diff] + 1), diff = nums[j]-nums[i]

这里有一个细节,就是**

diff

不一定是正数**。所以实际的代码中,会把第二维每一次得到的差去加一个很大的正数。但是很明显,正数加的越大,空间占用越大,所以实际要加多少,取决于题目的条件。比方说如果告诉你,公差不会低于-10000,那么你加10000其实就够了。

好的,我们来看一下核心代码。

代码语言:javascript
复制
for (int i = 0; i < N; i++) {
  for (int j = 0; j < i; j++) {
    auto diff = A[i] - A[j];
    int offset = 10000;
    diff += offset;
    dp[i][diff] = max(dp[i][diff], dp[j][diff] + 1);
    res = max(dp[i][diff], res);
  }
}

这里的最终的结果就是代码里的res,和其他题不太一样的是,这里你不知道最长的等差数列的公差是多少,也不知道最后一个元素是哪一个,所以你只能把所有情况都考虑一下,然后取个max了。

Problem 9: Leetcode 1335 你需要制定一份 d 天的工作计划表。工作之间存在依赖,要想执行第 i 项工作,你必须完成全部 j 项工作( 0 <= j < i)。 你每天 至少 需要完成一项任务。工作计划的总难度是这 d 天每一天的难度之和,而一天的工作难度是当天应该完成工作的最大难度。 给你一个整数数组 jobDifficulty 和一个整数 d,分别代表工作难度和需要计划的天数。第 i 项工作的难度是 jobDifficulty[i]。 返回整个工作计划的 最小难度 。如果无法制定工作计划,则返回 -1 。

比方说如果我们有jobDifficulty = [6,5,4,3,2,1], d = 2,那么实际上,答案就是7。因为你可以先做前5个工作(cost是6),再做最后一个(cost是1)。

这个问题本质上其实也是一个序列决策问题,所以还是只需要看,之前的决策怎么影响之后的决策,或者反过来

现在我们考虑设dp[i][j]表示“到达第

i

个工作,还需要完成

j

天的任务的时候,所可以达到的最小的难度“(你当然也可以反过来说,考虑“已经完成

j

天“任务,这个没有影响,只是状态转移方程会有些不同)。所以这样的话,我们不难发现的一点是,第

i

个工作之后的几个工作,它对应的工作难度可以计算,并且安排之后,相当于就只差

j-1

天的任务完成了。所以不难想,假如说我们安排一下完成2个任务(对应第

i, i+1

个),那么工作难度就需要增加

max(i, i +1)

,并且状态就变成了dp[i + 2][j-1]。类似的,不难想的是,你当然也可以安排一下完成后面的所有任务,这中间的每一个情况,都是一种可能的状态转移,所以状态转移方程就是

dp[i][j] = \min_{k = 1, 2, \ldots, n-i-j + 1}(dp[i + k][j- 1] + max(diff[i],\ldots, diff[ i + k-1])

这里注意

k

的最大值,这是因为我们有要求每一天必须至少完成一个任务

那么对于它的边界条件,其实不难想,就是“最后只剩下1天任务要完成”的情况。这个时候,最后的所有的任务其实你都需要做完,那么对应的工作难度就变成了最后几个任务中,难度最大的那个。比方说你还剩下[9, 8, 7]这3个任务,那么答案就是9

好的,我们给出这个题目的核心代码。

代码语言:javascript
复制
for (let i = LEN - 1, curMax = 0; i >= 0; --i) {
  jobDifficulty[i] > curMax && (curMax = jobDifficulty[i]);
  dp[i][1] = curMax;
}

for (int i = 2; i <= d; ++i) {
  for (int j = 0; j <= LEN - i; ++j) {
    int Max = 0;
    for (int k = j; k <= LEN - i; ++k) {
      jobDifficulty[k] > Max && (Max = jobDifficulty[k]);
      dp[j][i] = min(dp[j][i], dp[k + 1][i - 1] + Max);
    }
  }
}

最后只需要返回dp[0][d]就可以了。

小结

这一篇文章算是给这一个系列的难度试一试水。我们列举了一些相对来说比较高频,也比较困难的动态规划系列的题目。这些题目各有各的tricks,但是也并不是完全没有共通点。正如之前所说,刷题一定要抓住算法与数据结构的本质,而不是为了tricks而舍本逐末。而这一节中,我们已经可以看出,经典模型的一些思路和方法在这一篇文章中也有很多被使用。而同时我们也可以看出动态规划的关键步骤。而抓住这些步骤,了解基本模型之后,即使一些tricks相对比较难想到,其实也只需要强记就好,不会因为hard而耽误了medium和easy的通过率。

下一篇文章我们开始介绍其他的知识(具体要写啥,我目前还没有想好233),当然我必须承认动态规划还有相当多的难题没有写在笔记里,这一部分我们也会挑出一部分,放到后面的难题中,因为它们大部分都需要一些思维量,没有这两节的题目那么直接,因此可能会吓到大家hhh,还是循序渐进为好~

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-07-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 学弱猹的精品小屋 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 动态规划(下)
  • 小结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档