动态规划解题套路框架

您所在的位置:网站首页 labuladong公司 动态规划解题套路框架

动态规划解题套路框架

#动态规划解题套路框架 | 来源: 网络整理| 查看: 265

Info

数据结构精品课open in new window 和 递归算法专题课open in new window 限时附赠网站会员!

读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:

LeetCode力扣难度322. Coin Changeopen in new window322. 零钱兑换open in new window🟠509. Fibonacci Numberopen in new window509. 斐波那契数open in new window🟢-剑指 Offer II 103. 最少的硬币数目open in new window🟠

Tip

本文有视频版:动态规划框架套路详解open in new window。建议关注我的 B 站账号,我会用视频领读的方式带大家学习那些稍有难度的算法技巧。

这篇文章是我们公众号半年前一篇 200 多赞赏的成名之作 动态规划详解open in new window 的进阶版。由于账号迁移的原因,旧文无法被搜索到,所以我润色了本文,并添加了更多干货内容,希望本文成为解决动态规划的一部「指导方针」。

动态规划问题(Dynamic Programming)应该是很多读者头疼的,不过这类问题也是最具有技巧性,最有意思的。本书使用了整整一个章节专门来写这个算法,动态规划的重要性也可见一斑。

本文解决几个问题:

动态规划是什么?解决动态规划问题有什么技巧?如何学习动态规划?

刷题刷多了就会发现,算法技巧就那几个套路,我们后续的动态规划系列章节,都在使用本文的解题框架思维,如果你心里有数,就会轻松很多。所以本文放在第一章,来扒一扒动态规划的裤子,形成一套解决这类问题的思维框架,希望能够成为解决动态规划问题的一部指导方针。本文就来讲解该算法的基本套路框架,下面上干货。

首先,动态规划问题的一般形式就是求最值。动态规划其实是运筹学的一种最优化方法,只不过在计算机问题上应用比较多,比如说让你求最长递增子序列呀,最小编辑距离呀等等。

既然是要求最值,核心问题是什么呢?求解动态规划的核心问题是穷举。因为要求最值,肯定要把所有可行的答案穷举出来,然后在其中找最值呗。

动态规划这么简单,就是穷举就完事了?我看到的动态规划问题都很难啊!

首先,虽然动态规划的核心思想就是穷举求最值,但是问题可以千变万化,穷举所有可行解其实并不是一件容易的事,需要你熟练掌握递归思维,只有列出正确的「状态转移方程」,才能正确地穷举。而且,你需要判断算法问题是否具备「最优子结构」,是否能够通过子问题的最值得到原问题的最值。另外,动态规划问题存在「重叠子问题」,如果暴力穷举的话效率会很低,所以需要你使用「备忘录」或者「DP table」来优化穷举过程,避免不必要的计算。

以上提到的重叠子问题、最优子结构、状态转移方程就是动态规划三要素。具体什么意思等会会举例详解,但是在实际的算法问题中,写出状态转移方程是最困难的,这也就是为什么很多朋友觉得动态规划问题困难的原因,我来提供我总结的一个思维框架,辅助你思考状态转移方程:

明确 base case -> 明确「状态」-> 明确「选择」 -> 定义 dp 数组/函数的含义。

按上面的套路走,最后的解法代码就会是如下的框架:

# 自顶向下递归的动态规划 def dp(状态1, 状态2, ...): for 选择 in 所有可能的选择: # 此时的状态已经因为做了选择而改变 result = 求最值(result, dp(状态1, 状态2, ...)) return result # 自底向上迭代的动态规划 # 初始化 base case dp[0][0][...] = base case # 进行状态转移 for 状态1 in 状态1的所有取值: for 状态2 in 状态2的所有取值: for ... dp[状态1][状态2][...] = 求最值(选择1,选择2...)

下面通过斐波那契数列问题和凑零钱问题来详解动态规划的基本原理。前者主要是让你明白什么是重叠子问题(斐波那契数列没有求最值,所以严格来说不是动态规划问题),后者主要举集中于如何列出状态转移方程。

# 一、斐波那契数列

力扣第 509 题「斐波那契数open in new window」就是这个问题,请读者不要嫌弃这个例子简单,只有简单的例子才能让你把精力充分集中在算法背后的通用思想和技巧上,而不会被那些隐晦的细节问题搞的莫名其妙。想要困难的例子,接下来的动态规划系列里有的是。

1、暴力递归

斐波那契数列的数学形式就是递归的,写成代码就是这样:

int fib(int N) { if (N == 1 || N == 2) return 1; return fib(N - 1) + fib(N - 2); } // 注意:cpp 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。 // 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。 int fib(int N) { if (N == 1 || N == 2) return 1; return fib(N - 1) + fib(N - 2); } # 注意:python 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。 # 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。 def fib(N: int) -> int: if N == 1 or N == 2: return 1 return fib(N - 1) + fib(N - 2) // 注意:go 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。 // 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。 // fib 计算斐波那契数列的第 N 项 func fib(N int) int { if N == 1 || N == 2 { return 1 } return fib(N-1) + fib(N-2) } // 注意:javascript 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。 // 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。 var fib = function(N) { if (N === 1 || N === 2) return 1; return fib(N - 1) + fib(N - 2); };

这个不用多说了,学校老师讲递归的时候似乎都是拿这个举例。我们也知道这样写代码虽然简洁易懂,但是十分低效,低效在哪里?假设 n = 20,请画出递归树:

Tip

但凡遇到需要递归的问题,最好都画出递归树,这对你分析算法的复杂度,寻找算法低效的原因都有巨大帮助。

这个递归树怎么理解?就是说想要计算原问题 f(20),我就得先计算出子问题 f(19) 和 f(18),然后要计算 f(19),我就要先算出子问题 f(18) 和 f(17),以此类推。最后遇到 f(1) 或者 f(2) 的时候,结果已知,就能直接返回结果,递归树不再向下生长了。

递归算法的时间复杂度怎么计算?就是用子问题个数乘以解决一个子问题需要的时间。

首先计算子问题个数,即递归树中节点的总数。显然二叉树节点总数为指数级别,所以子问题个数为 O(2^n)。

然后计算解决一个子问题的时间,在本算法中,没有循环,只有 f(n - 1) + f(n - 2) 一个加法操作,时间为 O(1)。

所以,这个算法的时间复杂度为二者相乘,即 O(2^n),指数级别,爆炸。

观察递归树,很明显发现了算法低效的原因:存在大量重复计算,比如 f(18) 被计算了两次,而且你可以看到,以 f(18) 为根的这个递归树体量巨大,多算一遍,会耗费巨大的时间。更何况,还不止 f(18) 这一个节点被重复计算,所以这个算法及其低效。

这就是动态规划问题的第一个性质:重叠子问题。下面,我们想办法解决这个问题。

2、带备忘录的递归解法

明确了问题,其实就已经把问题解决了一半。即然耗时的原因是重复计算,那么我们可以造一个「备忘录」,每次算出某个子问题的答案后别急着返回,先记到「备忘录」里再返回;每次遇到一个子问题先去「备忘录」里查一查,如果发现之前已经解决过这个问题了,直接把答案拿出来用,不要再耗时去计算了。

一般使用一个数组充当这个「备忘录」,当然你也可以使用哈希表(字典),思想都是一样的。

int fib(int N) { // 备忘录全初始化为 0 int[] memo = new int[N + 1]; // 进行带备忘录的递归 return dp(memo, N); } // 带着备忘录进行递归 int dp(int[] memo, int n) { // base case if (n == 0 || n == 1) return n; // 已经计算过,不用再计算了 if (memo[n] != 0) return memo[n]; memo[n] = dp(memo, n - 1) + dp(memo, n - 2); return memo[n]; } // 注意:cpp 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。 // 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。 int fib(int N) { // 备忘录全初始化为 0 int memo[N + 1]; memset(memo, 0, sizeof(memo)); // 进行带备忘录的递归 return dp(memo, N); } // 带着备忘录进行递归 int dp(int memo[], int n) { // base case if (n == 0 || n == 1) return n; // 已经计算过,不用再计算了 if (memo[n] != 0) return memo[n]; memo[n] = dp(memo, n - 1) + dp(memo, n - 2); return memo[n]; } # 注意:python 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。 # 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。 def fib(N: int) -> int: # 备忘录全初始化为 0 memo = [0] * (N + 1) # 进行带备忘录的递归 return dp(memo, N) # 带着备忘录进行递归 def dp(memo: List[int], n: int) -> int: # base case if n == 0 or n == 1: return n # 已经计算过,不用再计算了 if memo[n] != 0: return memo[n] memo[n] = dp(memo, n - 1) + dp(memo, n - 2) return memo[n] // 注意:go 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。 // 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。 func fib(N int) int { // 备忘录全初始化为 0 memo := make([]int, N+1) // 进行带备忘录的递归 return dp(memo, N) } // 带着备忘录进行递归 func dp(memo []int, n int) int { // base case if n == 0 || n == 1 { return n } // 已经计算过,不用再计算了 if memo[n] != 0 { return memo[n] } memo[n] = dp(memo, n-1) + dp(memo, n-2) return memo[n] } // 注意:javascript 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。 // 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。 var fib = function(N) { // 备忘录全初始化为 0 let memo = new Array(N + 1).fill(0); // 进行带备忘录的递归 return dp(memo, N); }; // 带着备忘录进行递归 var dp = function(memo, n) { // base case if (n == 0 || n == 1) return n; // 已经计算过,不用再计算了 if (memo[n] != 0) return memo[n]; memo[n] = dp(memo, n - 1) + dp(memo, n - 2); return memo[n]; };

现在,画出递归树,你就知道「备忘录」到底做了什么。

实际上,带「备忘录」的递归算法,把一棵存在巨量冗余的递归树通过「剪枝」,改造成了一幅不存在冗余的递归图,极大减少了子问题(即递归图中节点)的个数。

递归算法的时间复杂度怎么计算?就是用子问题个数乘以解决一个子问题需要的时间。

子问题个数,即图中节点的总数,由于本算法不存在冗余计算,子问题就是 f(1), f(2), f(3) ... f(20),数量和输入规模 n = 20 成正比,所以子问题个数为 O(n)。

解决一个子问题的时间,同上,没有什么循环,时间为 O(1)。

所以,本算法的时间复杂度是 O(n),比起暴力算法,是降维打击。

至此,带备忘录的递归解法的效率已经和迭代的动态规划解法一样了。实际上,这种解法和常见的动态规划解法已经差不多了,只不过这种解法是「自顶向下」进行「递归」求解,我们更常见的动态规划代码是「自底向上」进行「递推」求解。

啥叫「自顶向下」?注意我们刚才画的递归树(或者说图),是从上向下延伸,都是从一个规模较大的原问题比如说 f(20),向下逐渐分解规模,直到 f(1) 和 f(2) 这两个 base case,然后逐层返回答案,这就叫「自顶向下」。

啥叫「自底向上」?反过来,我们直接从最底下、最简单、问题规模最小、已知结果的 f(1) 和 f(2)(base case)开始往上推,直到推到我们想要的答案 f(20)。这就是「递推」的思路,这也是动态规划一般都脱离了递归,而是由循环迭代完成计算的原因。

3、dp 数组的迭代(递推)解法

有了上一步「备忘录」的启发,我们可以把这个「备忘录」独立出来成为一张表,通常叫做 DP table,在这张表上完成「自底向上」的推算岂不美哉!

int fib(int N) { if (N == 0) return 0; int[] dp = new int[N + 1]; // base case dp[0] = 0; dp[1] = 1; // 状态转移 for (int i = 2; i int fib(int n) { if (n == 0 || n == 1) { // base case return n; } // 分别代表 dp[i - 1] 和 dp[i - 2] int dp_i_1 = 1, dp_i_2 = 0; for (int i = 2; i // coins 中是可选硬币面值,amount 是目标金额 int coinChange(int[] coins, int amount); // 注意:cpp 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。 // 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。 // coins 中是可选硬币面值,amount 是目标金额 int coinChange(vector& coins, int amount); # 注意:python 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。 # 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。 # coins 中是可选硬币面值,amount 是目标金额 def coinChange(coins: List[int], amount: int) -> int: // 注意:go 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。 // 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。 // coins 中是可选硬币面值,amount 是目标金额 func coinChange(coins []int, amount int) int {} // 注意:javascript 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。 // 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。 var coinChange = function(coins, amount) { // 函数体 };

比如说 k = 3,面值分别为 1,2,5,总金额 amount = 11。那么最少需要 3 枚硬币凑出,即 11 = 5 + 5 + 1。

你认为计算机应该如何解决这个问题?显然,就是把所有可能的凑硬币方法都穷举出来,然后找找看最少需要多少枚硬币。

1、暴力递归

首先,这个问题是动态规划问题,因为它具有「最优子结构」的。要符合「最优子结构」,子问题间必须互相独立。啥叫相互独立?你肯定不想看数学证明,我用一个直观的例子来讲解。

比如说,假设你考试,每门科目的成绩都是互相独立的。你的原问题是考出最高的总成绩,那么你的子问题就是要把语文考到最高,数学考到最高…… 为了每门课考到最高,你要把每门课相应的选择题分数拿到最高,填空题分数拿到最高…… 当然,最终就是你每门课都是满分,这就是最高的总成绩。

得到了正确的结果:最高的总成绩就是总分。因为这个过程符合最优子结构,「每门科目考到最高」这些子问题是互相独立,互不干扰的。

但是,如果加一个条件:你的语文成绩和数学成绩会互相制约,不能同时达到满分,数学分数高,语文分数就会降低,反之亦然。

这样的话,显然你能考到的最高总成绩就达不到总分了,按刚才那个思路就会得到错误的结果。因为「每门科目考到最高」的子问题并不独立,语文数学成绩户互相影响,无法同时最优,所以最优子结构被破坏。

回到凑零钱问题,为什么说它符合最优子结构呢?假设你有面值为 1, 2, 5 的硬币,你想求 amount = 11 时的最少硬币数(原问题),如果你知道凑出 amount = 10, 9, 6 的最少硬币数(子问题),你只需要把子问题的答案加一(再选一枚面值为 1, 2, 5 的硬币),求个最小值,就是原问题的答案。因为硬币的数量是没有限制的,所以子问题之间没有相互制,是互相独立的。

Tip

关于最优子结构的问题,后文 动态规划答疑篇 还会再举例探讨。

那么,既然知道了这是个动态规划问题,就要思考如何列出正确的状态转移方程?

1、确定 base case,这个很简单,显然目标金额 amount 为 0 时算法返回 0,因为不需要任何硬币就已经凑出目标金额了。

2、确定「状态」,也就是原问题和子问题中会变化的变量。由于硬币数量无限,硬币的面额也是题目给定的,只有目标金额会不断地向 base case 靠近,所以唯一的「状态」就是目标金额 amount。

3、确定「选择」,也就是导致「状态」产生变化的行为。目标金额为什么变化呢,因为你在选择硬币,你每选择一枚硬币,就相当于减少了目标金额。所以说所有硬币的面值,就是你的「选择」。

4、明确 dp 函数/数组的定义。我们这里讲的是自顶向下的解法,所以会有一个递归的 dp 函数,一般来说函数的参数就是状态转移中会变化的量,也就是上面说到的「状态」;函数的返回值就是题目要求我们计算的量。就本题来说,状态只有一个,即「目标金额」,题目要求我们计算凑出目标金额所需的最少硬币数量。

所以我们可以这样定义 dp 函数:dp(n) 表示,输入一个目标金额 n,返回凑出目标金额 n 所需的最少硬币数量。

搞清楚上面这几个关键点,解法的伪码就可以写出来了:

// 伪码框架 int coinChange(int[] coins, int amount) { // 题目要求的最终结果是 dp(amount) return dp(coins, amount) } // 定义:要凑出金额 n,至少要 dp(coins, n) 个硬币 int dp(int[] coins, int n) { // 做选择,选择需要硬币最少的那个结果 for (int coin : coins) { res = min(res, 1 + dp(coins, n - coin)) } return res } // 注意:cpp 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。 // 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。 int coinChange(vector& coins, int amount) { // 题目要求的最终结果是 dp(amount) return dp(coins, amount); } // 定义:要凑出金额 n,至少要 dp(coins, n) 个硬币 int dp(vector& coins, int n) { // 做选择,选择需要硬币最少的那个结果 int res = INT_MAX; for (const int coin : coins) { res = min(res, subProb + 1); } return res; } # 注意:python 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。 # 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。 # 伪码框架 def coinChange(coins: List[int], amount: int) -> int: # 题目要求的最终结果是 dp(amount) return dp(coins, amount) # 定义:要凑出金额 n,至少要 dp(coins, n) 个硬币 def dp(coins: List[int], n: int) -> int: # 做选择,选择需要硬币最少的那个结果 res = float('inf') # 初始化res为正无穷 for coin in coins: res = min(res, sub_problem + 1) return res // 注意:go 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。 // 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。 func coinChange(coins []int, amount int) int { // 题目要求的最终结果是 dp(amount) return dp(coins, amount) } // 定义:要凑出金额 n,至少要 dp(coins, n) 个硬币 func dp(coins []int, n int) int { // 初始化为最大值 res := math.MaxInt32 // 做选择,选择需要硬币最少的那个结果 for _, coin := range coins { res = min(res, 1+subProblem) } return res } // 注意:javascript 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。 // 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。 var coinChange = function(coins, amount) { return dp(coins, amount); }; var dp = function(coins, n) { let res = Infinity; for (let coin of coins) { res = Math.min(res, 1 + subproblem); // 选择硬币数最少的结果 } } return res; };

根据伪码,我们加上 base case 即可得到最终的答案。显然目标金额为 0 时,所需硬币数量为 0;当目标金额小于 0 时,无解,返回 -1:

int coinChange(int[] coins, int amount) { // 题目要求的最终结果是 dp(amount) return dp(coins, amount) } // 定义:要凑出金额 n,至少要 dp(coins, n) 个硬币 int dp(int[] coins, int amount) { // base case if (amount == 0) return 0; if (amount

至此,状态转移方程其实已经完成了,以上算法已经是暴力解法了,以上代码的数学形式就是状态转移方程:

至此,这个问题其实就解决了,只不过需要消除一下重叠子问题,比如 amount = 11, coins = {1,2,5} 时画出递归树看看:

递归算法的时间复杂度分析:子问题总数 x 解决每个子问题所需的时间。

子问题总数为递归树的节点个数,但算法会进行剪枝,剪枝的时机和题目给定的具体硬币面额有关,所以可以想象,这棵树生长的并不规则,确切算出树上有多少节点是比较困难的。对于这种情况,我们一般的做法是按照最坏的情况估算一个时间复杂度的上界。

假设目标金额为 n,给定的硬币个数为 k,那么递归树最坏情况下高度为 n(全用面额为 1 的硬币),然后再假设这是一棵满 k 叉树,则节点的总数在 k^n 这个数量级。

接下来看每个子问题的复杂度,由于每次递归包含一个 for 循环,复杂度为 O(k),相乘得到总时间复杂度为 O(k^n),指数级别。

2、带备忘录的递归

类似之前斐波那契数列的例子,只需要稍加修改,就可以通过备忘录消除子问题:

class Solution { int[] memo; int coinChange(int[] coins, int amount) { memo = new int[amount + 1]; // 备忘录初始化为一个不会被取到的特殊值,代表还未被计算 Arrays.fill(memo, -666); return dp(coins, amount); } int dp(int[] coins, int amount) { if (amount == 0) return 0; if (amount */ // 子问题无解则跳过 if (subProblem == -1) continue; // 在子问题中选择最优解,然后加一 res = Math.min(res, subProblem + 1); } // 把计算结果存入备忘录 memo[amount] = (res == Integer.MAX_VALUE) ? -1 : res; return memo[amount]; } } // 注意:cpp 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。 // 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。 class Solution { private: vector memo; /** * memo 数组记录了 amount 对应的最优解,-666 代表还未被计算。 * 因此,在计算 amount 对应的最优解时需要先查找 memo。 */ int dp(vector& coins, int amount){ if(amount == 0) return 0; // 如果 amount = 0,那么硬币数量为 0 if(amount int: memo = [-666] * (amount + 1) def dp(coins, amount): if amount == 0: return 0 if amount int coinChange(int[] coins, int amount) { int[] dp = new int[amount + 1]; // 数组大小为 amount + 1,初始值也为 amount + 1 Arrays.fill(dp, amount + 1); // base case dp[0] = 0; // 外层 for 循环在遍历所有状态的所有取值 for (int i = 0; i */ } } return (dp[amount] == amount + 1) ? -1 : dp[amount]; } // 注意:cpp 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。 // 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。 int coinChange(vector& coins, int amount) { vector dp(amount + 1, amount + 1); // 数组大小为 amount + 1,初始值也为 amount + 1 dp[0] = 0; // 外层 for 循环在遍历所有状态的所有取值 for (int i = 0; i Dijkstra 算法模板及应用 base case 和备忘录的初始值怎么定? 一个方法团灭 LeetCode 打家劫舍问题 一个方法团灭 LeetCode 股票买卖问题 东哥带你刷二叉树(纲领篇) 分治算法详解:运算优先级 动态规划和回溯算法的思维转换 动态规划帮我通关了《辐射4》 动态规划帮我通关了《魔塔》 动态规划穷举的两种视角 动态规划设计:最长递增子序列 如何运用贪心思想玩跳跃游戏 学习算法和刷题的框架思维 对动态规划进行降维打击 归并排序详解及应用 当老司机学会了贪心算法 我的刷题心得 旅游省钱大法:加权最短路径 最优子结构原理和 dp 数组遍历方向 本站简介 算法可视化功能简介(必读) 算法学习和心流体验 算法时空复杂度分析实用指南 算法笔试「骗分」套路 经典动态规划:0-1 背包问题 经典动态规划:博弈问题 经典动态规划:完全背包问题 经典动态规划:戳气球 经典动态规划:最长公共子序列 经典动态规划:编辑距离引用本文的题目

安装 我的 Chrome 刷题插件open in new window 点开下列题目可直接查看解题思路:

LeetCode力扣112. Path Sumopen in new window112. 路径总和open in new window115. Distinct Subsequencesopen in new window115. 不同的子序列open in new window139. Word Breakopen in new window139. 单词拆分open in new window1696. Jump Game VIopen in new window1696. 跳跃游戏 VIopen in new window221. Maximal Squareopen in new window221. 最大正方形open in new window240. Search a 2D Matrix IIopen in new window240. 搜索二维矩阵 IIopen in new window256. Paint Houseopen in new window🔒256. 粉刷房子open in new window🔒279. Perfect Squaresopen in new window279. 完全平方数open in new window343. Integer Breakopen in new window343. 整数拆分open in new window365. Water and Jug Problemopen in new window365. 水壶问题open in new window542. 01 Matrixopen in new window542. 01 矩阵open in new window576. Out of Boundary Pathsopen in new window576. 出界的路径数open in new window62. Unique Pathsopen in new window62. 不同路径open in new window63. Unique Paths IIopen in new window63. 不同路径 IIopen in new window70. Climbing Stairsopen in new window70. 爬楼梯open in new window91. Decode Waysopen in new window91. 解码方法open in new window-剑指 Offer 04. 二维数组中的查找open in new window-剑指 Offer 10- I. 斐波那契数列open in new window-剑指 Offer 10- II. 青蛙跳台阶问题open in new window-剑指 Offer 14- I. 剪绳子open in new window-剑指 Offer 46. 把数字翻译成字符串open in new window-剑指 Offer II 091. 粉刷房子open in new window-剑指 Offer II 097. 子序列的数目open in new window-剑指 Offer II 098. 路径的数目open in new window-剑指 Offer II 103. 最少的硬币数目open in new window

《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复「全家桶」可下载配套 PDF 和刷题全家桶:



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3