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

Leetcode | 第一节:动态规划(上)

作者头像
学弱猹
发布2021-08-10 11:19:41
6090
发布2021-08-10 11:19:41
举报

大家好!

相信读者你绝对想不到,我们居然出了一个Leetcode的系列。

从去年7月到现在,我已经在北京的互联网公司呆了整整一年的时间。这中间经历过各种各样的酸甜苦辣,自己为了面试刷题的过程(从杉数到滴滴——未入门算法工程师再找实习工作记),也会经常听到北美同学面试的时候所遇到的各种艰难。是的,只要是互联网公司,无论是国内还是国外,总是要考察很多leetcode的东西。而leetcode如何刷,刷多少,刷到什么程度,其实各个公司也各不相同。但是事实上,leetcode的本质考察点是算法与数据结构,而除去基本的算法与数据结构外,leetcode困难的地方在于熟练度+一些技巧。然而技巧毕竟是存量,不是增量,我们刷多了,自然就有经验。所以这一个系列,我们不面向easy的题目,而更多关注hard和medium+的高频题,并通过大量的leetcode原题,来刻画出互联网公司究竟会考察哪些实际算法与数据结构的知识,以达到复习《算法与数据结构》的效果。

不过Leetcode系列毕竟不是传统的数学课,没有什么大纲可参考,因此这一个系列完全是个人经验的总结,因此我们不会覆盖所有考察的方面,也很难保证适用于所有人。但是一定要明白的是,考察leetcode的目的,还是为了考察算法与数据结构,这也是编程和思考能力基础中的基础。至于技巧的部分,只要看得多,自然就会了,但千万不要为了细节而忽视了本质。

那么我们开始吧。

动态规划(上)

为了吸引阅读量,我们总需要一些标题党,动态规划(Dynamic Programming,DP)就是一个很好的标题党。动态规划究竟应该怎么考虑,我学到大学毕业都没学明白。因此第一篇文章,我们就盯着动态规划来看,看看它究竟应该怎么思考。

动态规划的优化原理

动态规划的优化原理一句话就能说清楚:寻找最优子结构,避免重复计算。初学者看这句话可能有点抽象,我们用斐波那契数列(Fibonacci)的例子来解释。

Problem 1: 考虑数列

\{a_n\}

,如果数列满足

a_0 = a_1 = 1

a_{n + 2} = a_{n + 1} + a_n

,则称其为斐波那契数列。设计算法计算斐波那契数列的通项。

计算数列通项的算法很明显,有了

a_0, a_1

,就可以通过公式计算出

a_2, a_3, \ldots

。一个常见的解决问题的方案是递归。简单来说假如设

f(n)

表示数列的第

n

a_n

,那么只需要写出这样的函数就可以

代码语言:javascript
复制
int f(int n){
  if(n == 0 || n == 1)return 1;
  return f(n - 1) + f(n - 2);
}

这个算法的问题在什么地方呢?这里我们假设要计算

f(4)

,那么我们可以通过一棵递归调用树来观察一下。

可以看出,数值

f(0), f(1), f(2), f(3)

等,其实在调用树中是被多次调用的。对于f(2)这种数值,因为它没有被事先赋值,所以多次调用就涉及到了多次的重复计算。那么效率自然相形见绌。

那么什么是动态规划呢?它严格来说可以理解为是一个运筹学的概念。但记住一句话:寻找最优子结构,在这里,就是确认

f(n)

与之前的函数的关系,并且观察,是否一个自变量更大的函数值,会被自变量更小的函数值来决定。而避免重复计算的意思,就是在计算的时候,努力避免已经计算好的值被重复再计算一次。因此在这里,我们其实可以考虑按照这个方式来完成代码。

代码语言:javascript
复制
# Assume that you want to compute f(N)

dp[0] = 1;
dp[1] = 1;
for(int i = 2; i <= N; i++){
  dp[i] = dp[i - 1] + dp[i - 2];
}
printf(dp[N]);

这里的核心计算逻辑dp[i] = dp[i - 1] + dp[i - 2]有个专门的说法,叫做状态转移方程。很明显,它就告诉了你,你定义的状态怎么走,才能转移到下一个状态

也就是说,这里的dp[i],其实就对应了上面的

f(i)

。可以看出,这个过程中,我们使用循环代替了递归,是因为循环走一遍就会结束,因此一个值计算完毕之后,不会“跳回去”再计算一遍

当然这里要注意的是,动态规划的建模需要考虑无后效性。简单来说,需要保证的是,你定义状态的时候,之后的状态只依赖于之前的状态。在这里,因为你可以让你的状态转移方程满足这个条件,所以无后效性是满足的。

所以总结一下动态规划类型的题目,大概可以分为这么几个步骤

  1. 定义状态(函数)和状态变量(自变量)
  2. 寻找状态转移方程
  3. 寻找循环方式
  4. 考虑边界情况,注意无后效性

考虑边界情况的意思是,注意给动态规划设置一个初始的迭代起点。比方说在这个数列中,必须要知道

a_0 = 1, a_1 = 1

,你才能继续计算,对吧

好的,接下来,我们来看一些常见的动态规划的模型题。

Problem 2: Leetcode 62 一个机器人位于一个 m x n 网格的左上角 。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角。 问总共有多少条不同的路径?

这是一个网格类型的问题,所以很明显,影响函数的自变量就是2个:一个行数,一个列数

现在我们假设说,dp[i][j]表示从左上角走到位置

(i, j)

,对应的方案数。那么这样的话,我们可以考虑的是,这个状态是如何转移的。更具体一点来说就是,从之前的状态,到之后的状态,这中间会经历什么?然后把所有的可能的转移都考虑到,取最优解,就是我们的答案。

对于这个题其实不难想,因为我们有要求说,机器人只有两个方向可以走。所以到达

(i, j)

前,机器人只能从上边,或者左边来。而从上边来的话,对应的是dp[i-1][j]的状态,而从左边来的话,对应的是dp[i][j-1]的状态。而这两个都有可能发生,所以到这个点的总的状态数,我们应该加在一起。因此,状态转移方程可以写为

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

边界条件非常简单,考虑dp[i][0], dp[0][j]就可以了。这个时候相当于只在边界上走,那么因为只有一种可能(一直往下/一直往右),所以全部赋值为1。有了初始条件之后,就相当于有了初始的推力,那么状态转移方程就像引擎一样,可以一直把所有的结果都计算出来。

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

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

最终只需要输出f[m-1][n-1]即可。

这里

i, j

的遍历顺序都是从小到大,这是因为两个变量对应的状态都是由比它更小的数决定的。也就是说,如果情况反过来,那么遍历顺序也有可能是从大到小

这里要注意的是,一般编程来说大家都会以0作为下标的开始。不过我们很多时候在题干意义上定义问题,会习惯以1作为下标开始。这有的时候会带来一些理解上的麻烦,也请大家多多谅解~

虽然这一个题算是dp的一个入门级别的题,但是事实上,它在空间上的优化点还是很明显的。我们可以看出,一个状态最多只会依赖到它”相邻的两个状态“。换句话说它没有办法关注到更远的状态了。我们用图来更形象的说一下。

对于红色的点来说,可以看出它只会受到“上一个”和“前一个”的制约,因此严格来说,如果到计算这一个点的状态值的时候,实际上是不需要考虑第一列的结果的。

因此,这个情况下,其实我们可以把它简化成

dp[i]= dp[i]+dp[i-1]

确实,不需要额外的状态维度来保存数据这一点,没人否认。但是这个式子又是怎么得到的呢?注意,空间省了,计算步骤是没变的,时间复杂度还是一样的。我们再画一张图看一下。

所以可以看出,需要修改的dp[i],对应的就是新的一行的对应列的元素,而dp[i-1]就是格子的左边。赋值等式右边的dp[i]对应的是旧的一行的对应列的元素。所以这两个式子的等价看似无厘头,其实画个图就很清楚了。

好的,代码怎么改相信读者也都比较清楚了,这里就不多说了。

Problem 3: Leetcode 300 给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。 子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

比方说如果输入[10,9,2,5,3,7,101,18],那么输出就是4,因为 [2,3,7,101]是答案。

这个其实就是大名鼎鼎的最长上升子序列(Longest Incresing Subsequence,LIS)的问题。它的参数非常好找,就是数组的元素个数。

定义dp[i]表示“前

i

个元素,且以第

i

个元素结尾,对应的最长上升子序列长度“。这里有一个技巧就是要强制要求且以第

i

个元素结尾,为什么?这是因为,我们要保证的是子序列是递增的,所以必须要有相邻元素的比较。那么上一个状态和这一个状态的”相邻元素的比较“从哪里来?其实也只能从最后一个元素来(仔细品一品这句话)。而且因为最长上升子序列一定会以某一个下标的元素结尾,所以不会有遗漏的情况,所以把所有的情况取最值,是一定可以得到最优解的

好的,那么我们看怎么比较?既然要有“相邻元素的比较”,那么对于dp[i]而言,之前任意一个dp[j], j < i是不是都有可能?所以只要我们的nums[j] < nums[i],我们就可以认为,第

i

个数字的前一个可能是第

j

个。那么相连起来,序列长度就要加一,所以状态转移方程为

dp[i] = max_{j < i, nums[j] < nums[i]} dp[j] + 1

初始的情况其实也不难,一开始其实可以假设没有任何的“拼接”发生。这相当于认为,序列长度为1。

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

代码语言:javascript
复制
for (int i = 1; i < nums.length; i++) {
  dp[i] = 1;
  for (int j = 0; j < i; j++) {
    if (nums[i] > nums[j]) {
      dp[i] = Math.max(dp[i], dp[j] + 1);
    }
  }
  maxans = Math.max(maxans, dp[i]);
}

这里的maxans就是答案。

Problem 4: Leetcode 322 给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。 你可以认为每种硬币的数量是无限的。

比方说如果我们有coins = [1, 2, 5], amount = 11,那么答案就是3,因为我们有11 = 5 + 5 + 1,所以3枚硬币就够了。

这一个问题实际上就是一个完全背包问题。说它是背包问题,是因为这个题中,每一个硬币都可以选择“要”或者“不要”,并且硬币数量是不受限的(也就是说如果每一种硬币只有1个,那就是一个0-1背包问题了)。那么这样的话,其实我们的递推思路也很简单,就是每一次,去看到底选哪一种硬币就可以了

我们设dp[i]表示“金额为

i

的情况下,最少需要的硬币数”。枚举每一类硬币,其实就可以找到对应的之前的状态。具体来说,如果我们这里选择面值为1的硬币,那么状态就回退到了dp[i-1]。面值为2的话,状态就回退到了dp[i-2]

好的,所以状态转移方程就是

dp[i] = \min_{j = 0, 1, \ldots, n, i >= c_j}dp[i - c_j] + 1

这里的

n

就是硬币的种数,

c_j

就是对应的硬币的面值。

那么,边界条件是什么呢?这里其实就是dp[0] = 0。但是同时,其他的位置的值,我们要设置为一个很大的数,表示“目前还没有找到方案,所以认为不可能完成”的意思。

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

代码语言:javascript
复制
vector<int> dp(amount + 1, Max);
dp[0] = 0;

for (int i = 1; i <= amount; ++i) {
  for (int j = 0; j < (int)coins.size(); ++j) {
    if (coins[j] <= i) {
      dp[i] = min(dp[i], dp[i - coins[j]] + 1);
    }
  }
}

这里的Max就是一个很大的值。最后的结果就是dp[amount]。如果这个数还是一个很大的值,那就说明不可能完成。根据题目要求,我们需要返回-1

有的人可能会问0-1背包问题的具体例子,这个对应的是Lintcode 125,或者NOIP的“采药”问题。感兴趣的朋友可以自己去看一看,因为思路比这里的完全背包问题要简单,我们就不多说了。

Problem 5: Leetcode 122 给定一个数组 prices ,其中 prices[i] 是一支给定股票第 i 天的价格。 设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。 注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

比方说如果股票每天的价格分别为[7,1,5,3,6,4],那么答案就是7,因为可以第2天买,第3天卖,第4天买,第5天卖。

从这个题开始,我们就开始介绍股票系列的问题。这些问题也是相对比较容易考到的高频问题,并且在思考难度上层层递进,值得我们多说两句。

首先,这个题容易想到的变量是股票的天数,毕竟每一天股票价格不一样。但是就这其实是不够的,因为可以看到,股票的买卖状态也会影响你的操作,所以需要刻画股票的买卖状态。因此这个问题,除了天数以外,还需要增加一个“买卖”的维度,分别对应0和1。

我们定义dp[i][0]为“第

i

天交易完后手里没有股票的最大利润”,dp[i][1]为“第

i

天交易完后手里有一支股票的最大利润”。那么这样的话,分开考虑,对于dp[i][0],它的前一天有可能没有股票,也有可能有股票。前一天没有,今天也没有,说明没有操作。前一天有,今天没了,说明卖掉了,会有收益。所以这样的话,状态转移方程就是

dp[i][0] = \max(dp[i-1][0], dp[i-1][1] + prices[i])

对于dp[i][1],分析方式是一致的。我们就直接给出结果了。

dp[i][1] = \max(dp[i-1][1], dp[i-1][0] - prices[i])

具体怎么来的,交给读者思考。

关于边界条件,其实就是在一开始的时候股票是什么样的。这里其实就是dp[0][0] = 0, dp[0][1] = -prices[0]。这是因为一开始,如果手上就有股票,说明买了。如果手上没有股票,那么收益为0。

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

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

最后只需要输出dp[n-1][0]就可以。这里的下标是从0开始的,所以n-1实际上就是第

n

天。

Problem 6: Leetcode 309 给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。 设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票): 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。

这个题相比较Problem 5,多了一个限制,就是冷冻期。但是考虑到冷冻期只会有1天,所以无非就是多了一个状态而已。也就是说相比较之前的“买卖”而言,还多了一个状态,这个状态就是“冷冻”。

我们设dp[i][0]表示持股,dp[i][1]表示没有持股,并且在冷冻期,dp[i][2]表示没有持股,并且不在冷冻期(完整的context在上一个题,我就不多写了……)。分这三种情况讨论,可以看出,如果是dp[i][0],那么上一天,要不持股,要不没持股,注意,必须要不在冷冻期内,所以我们可以写出

dp[i][0] = \max(dp[i-1][0], dp[i-1][2] -prices[i])

如果是dp[i][1],那么因为进入了冷冻期,所以只有可能是卖出了,所以这种情况就是

dp[i][1] = \max(dp[i-1][0] + prices[i])

类似的,对于dp[i][2],也可以得到结果为

dp[i][2] = \max(dp[i-1][1], dp[i-1][2])

最后还有个边界条件,这个不难思考,dp[0][0]= -prices[0], dp[0][2] = 0,但是还有一个问题是dp[0][1] = 0这个是为什么?这个其实从题干意义上,不容易判断。但是我们可以从方程上去看,取什么值是合适的

其实可以看到,这个初值可以影响到的就是dp[1][0], dp[1][2],对于dp[1][0],其实就应该是刚买股票,那么对应的结果就应该是-prices[i],所以这个初值是一定的。不放心的话,可以再看dp[1][2],而这个情况相当于没有股票,也没有冷冻期。那么正常情况下思考,它当然应该是和dp[0][2]相同才对(因为状态转移的另外一种情况,实际上是不存在的)。因此,这里应该赋值为0。当然,其实你赋值-1,严格来说也是符合逻辑的,但是结合dp[i][0]那个结果来看,就不一定了。

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

代码语言:javascript
复制
dp[0][0] = -prices[0];
for (int i = 1; i < n; ++i) {
  dp[i][0] = max(f[i - 1][0], f[i - 1][2] - prices[i]);
  f[i][1] = f[i - 1][0] + prices[i];
  f[i][2] = max(f[i - 1][1], f[i - 1][2]);
}

最终返回的就是dp[n-1][1], dp[n-1][2]中的更大的那个。

Problem 7: Leetcode 123 给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。 设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。 注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

这里的问题相比较之前来说,多了一个限制,就是最多两笔交易。但是本质上,这还是一个序列的问题,所以思考方式是一样的:从前往后思考。

这里刻画状态就不需要按照天数来刻画了(虽然也确实是影响因素),但是这样之后,”两笔消费“的这个信息就很难用了。所以要克服这个困局,必须要先考虑,怎么利用“两笔消费”的信息,而不是坚持使用之前的思路。而事实上,信息的更新,是可以通过时间的推进来做的。具体什么意思,大家后面就能看明白了。

我们设变量

x_1, x_2, x_3, x_4, x_5

对应五种状态,分别对应为

  1. 未进行过任何操作;
  2. 只进行过一次买操作;
  3. 进行了一次买操作和一次卖操作,即完成了一笔交易;
  4. 在完成了一笔交易的前提下,进行了第二次买操作;
  5. 完成了全部两笔交易。

第一个状态其实不用多说的,因为如果一直没有任何操作,那就肯定一直没有收益,所以很明显

x_1 = 0

剩下的四个状态,随着时间的推进,可能在每一天的结果不一样,所以要考虑状态转移。假如说

x_2', x_3', x_4', x_5'

对应的是第

i - 1

天的状态,那么对于第

i

天,我们分别考虑。

对于

x_2

,有两种可能,有可能保持状态不变,也有可能从0开始,买了一支股票。所以状态转移方程为

x_2 = \max(x_2', -prices[i])

对于

x_3

,可以考虑保持状态不变,也可以考虑说,在之前买过一次之后,把它卖出去。所以状态转移方程为

x_3= \max(x_3', x_2' + prices[i])

类似的我们有

x_4 = \max(x_4', x_3' - prices[i])
x_5 = \max(x_5', x_4' + prices[i])

最后,还有一个边界条件。这里对应的就是

x_2', x_3', x_4', x_5'

在第1天的情况。很明显,一开始持有股票,和不持有股票,对应的收益肯定会差一个prices[0]。所以事实上,持有的话,那么初始值就是-prices[0]。不持有的话,就是0

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

代码语言:javascript
复制
int x2 = -prices[0], x3 = 0;
int x4 = -prices[0], x5 = 0;
for (int i = 1; i < n; ++i) {
  x2 = max(x2, -prices[i]);
  x3 = max(x3, x2 + prices[i]);
  x4 = max(x4, x3 - prices[i]);
  x5 = max(x5, x4 + prices[i]);
}

最终只需要给出

x_5

的值就可以了。

可能有的人会问为什么不能给

x_3

?其实也是可以的,但是要注意的是,其实这里我们是允许同一天进行买和卖的操作的(虽然没有收益),所以这个算法中,

x_3

这个情况的最优解,应该在

x_5

中也有,所以其实就不需要再考虑

x_3

最后的结果是多少了。

Problem 8: Leetcode 188 给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。 设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。 注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

这个问题虽然看起来是Problem 7的升级版本,但是事实上,我们可以发现,同样的思路也可以运用到Problem 8中来。也就是说,之前我们通过“两次交易”的状态,设置了5个变量。合理推断,如果是

k

次交易,其实可以设置

2k +1

个变量。这样虽然可以做,但是很明显的一个问题在于,你不能事先知道

k

是多少,所以不如把它当成额外的第二维的参数,来思考这个问题。

buy[i][j]表示“

i

天,交易了

j

笔之后,手上有股票,得到的最大收益“。设sell[i][j]表示手上没股票的对应情况。那么这样的话,如何推导,其实我觉得思路一模一样,也没有多提的必要了。所以我们这里就直接放出核心代码了。

代码语言:javascript
复制
buy[0][0] = -prices[0];
sell[0][0] = 0;
for (int i = 1; i <= k; ++i) {
   buy[0][i] = -prices[0];
   sell[0][i] = 0;
}

for (int i = 1; i < n; ++i) {
  buy[i][0] = max(buy[i - 1][0], sell[i - 1][0] - prices[i]);
  for (int j = 1; j <= k; ++j) {
    buy[i][j] = max(buy[i - 1][j], sell[i - 1][j] - prices[i]);
    sell[i][j] = max(sell[i - 1][j], buy[i - 1][j - 1] + prices[i]);   
  }
}

最终只需要返回最后一天,sell数组的交易次数的不同情况即可。

Problem 9: Leetcode 376 如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。 例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。 相反,[1, 4, 7, 2, 5] 和 [1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。 给你一个整数数组 nums ,返回 nums 中作为 摆动序列 的 最长子序列的长度 。

这个问题其实在思路上已经没有什么创新点了。但是考虑到它也很高频,读者如果希望自己思考的话,可以先不要往下看……

按照动态规划的传统思路,如果是这种问题的话,那么自然是考虑,当时间不断推进的时候,状态会如何转移

很明显,如果要求是摆动序列,那么我们观察最后两个元素的时候,只会有两种情况:要不第一个元素比第二个元素大,要不就小。所以我们可以考虑,设up[i]表示“前

i

个元素中的某一个为结尾的,且最后两个元素是上升趋势的序列长度”,down[i]类似意思。注意这里没有要求序列中以第

i

个为结尾,是因为题目中没有出现“序列连续”的要求。但是有的题目中,我们考虑状态的时候是有这个要求的。

那么我们先看up[i]可以由什么状态转移过来。如果是up[i],说明我们在考虑最后两个元素是“上升趋势”的情况。那么这样的话,事实上,如果最近的两个数依然是上升趋势(nums[i] > nums[i-1]),那么要不可以不取这个数作为子序列的一部分,要不就取,分别对应up[i-1]down[i-1] + 1。但是如果最近的两个数不是上升趋势,那么就不能够取nums[i]了(想想为什么?),只能够考虑取up[i-1]。这个相当于用第

i-1

个数来取代第

i

个数的一个方案。

如果是down[i],那么趋势就是“下降趋势”了,这个可以做类似的讨论。

好的,那么这个题的状态转移方程就是

up[i] = \begin{cases}up[i-1] & nums[i] \le nums[i-1] \\ max(up[i-1], down[i-1] + 1) & nums[i] >nums[i-1]\end{cases}
down[i] = \begin{cases}down[i-1] & nums[i] \ge nums[i-1] \\ max(up[i-1] + 1, down[i-1] & nums[i] < nums[i-1] \end{cases}

至于边界条件,这个我想不是很难,我们直接给出,就是up[0] = down[0] = 1

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

代码语言:javascript
复制
up[0] = down[0] = 1;
for (int i = 1; i < n; i++) {
  if (nums[i] > nums[i - 1]) {
    up[i] = max(up[i - 1], down[i - 1] + 1);
    down[i] = down[i - 1];
  } else if (nums[i] < nums[i - 1]) {
    up[i] = up[i - 1];
    down[i] = max(up[i - 1] + 1, down[i - 1]);
  } else {
    up[i] = up[i - 1];
    down[i] = down[i - 1];
  }
}

最终就是看up[n-1], down[n-1]中的较大的那个。

当然了,这个题在空间上其实是可以优化的,因为我们发现,我们每一次都只会依赖前一个状态,不会依赖到两个状态以前的那些状态。因此,我们可以考虑只用两个变量up, down来完成我们的任务。这里代码大家可以自己去改,或者看官网题解,我们就不多说了。

小结

本节我们介绍了动态规划的基本做题方法,并举出了一些很常见的动态规划的基本题。这些题的做法各有不同,但是大的动态规划的解题思路是相同的。也就是说,我们需要合理的去观察各个状态,并尝试理解它们。刷题刷多了就熟了没错,但更重要的是了解做题的大的框架,再去记忆小的做题的tricks。只有两手抓,两手都硬,才能事半功倍。

下一节我们会介绍一些动态规划的更加困难的高频题。算是再让大家理解一些困难题题解的思路吧~

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

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

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

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

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