Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@ Gitee Pages 地址:https://labuladong.gitee.io/algo/
* [番外:用算法打败算法](https://labuladong.github.io/article/fname.html?fname=PDF中的算法)
* [数据结构精品课](https://labuladong.github.io/article/fname.html?fname=ds课程简介)
* [二叉树(递归)专题课](https://labuladong.github.io/article/fname.html?fname=tree课程简介)
* [14 天刷题打卡挑战](https://labuladong.github.io/article/fname.html?fname=打卡挑战简介)
* [学习本站所需的 Java 基础](https://labuladong.github.io/article/fname.html?fname=网站Java基础)

### [第零章、核心框架汇总](https://labuladong.github.io/algo/)
Expand Down
4 changes: 4 additions & 0 deletions 动态规划系列/动态规划之博弈问题.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,14 @@

函数签名如下:

<!-- muliti_language -->
```java
boolean PredictTheWinner(int[] nums);
```

那么如果有了一个计算先手和后手分差的 `stoneGame` 函数,这道题的解法就直接出来了:

<!-- muliti_language -->
```java
public boolean PredictTheWinner(int[] nums) {
// 先手的分数大于等于后手,则能赢
Expand Down Expand Up @@ -163,6 +165,7 @@ for (int i = n - 2; i >= 0; i--) {

如何实现这个 fir 和 sec 元组呢,你可以用 python,自带元组类型;或者使用 C++ 的 pair 容器;或者用一个三维数组 `dp[n][n][2]`,最后一个维度就相当于元组;或者我们自己写一个 Pair 类:

<!-- muliti_language -->
```java
class Pair {
int fir, sec;
Expand All @@ -175,6 +178,7 @@ class Pair {

然后直接把我们的状态转移方程翻译成代码即可,注意我们要倒着遍历数组:

<!-- muliti_language -->
```java
/* 返回游戏最后先手和后手的得分之差 */
int stoneGame(int[] piles) {
Expand Down
4 changes: 4 additions & 0 deletions 动态规划系列/动态规划之四键键盘.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@

函数签名如下:

<!-- muliti_language -->
```java
int maxA(int N);
```
Expand Down Expand Up @@ -84,6 +85,7 @@ dp(n - 2, a_num, a_num) # C-A C-C

这样可以看到问题的规模 `n` 在不断减小,肯定可以到达 `n = 0` 的 base case,所以这个思路是正确的:

<!-- muliti_language -->
```python
def maxA(N: int) -> int:

Expand All @@ -105,6 +107,7 @@ def maxA(N: int) -> int:

这个解法应该很好理解,因为语义明确。下面就继续走流程,用备忘录消除一下重叠子问题:

<!-- muliti_language -->
```python
def maxA(N: int) -> int:
# 备忘录
Expand Down Expand Up @@ -170,6 +173,7 @@ dp[i] = dp[i - 1] + 1;

**刚才说了,最优的操作序列一定是 `C-A C-C` 接着若干 `C-V`,所以我们用一个变量 `j` 作为若干 `C-V` 的起点**。那么 `j` 之前的 2 个操作就应该是 `C-A C-C` 了:

<!-- muliti_language -->
```java
public int maxA(int N) {
int[] dp = new int[N + 1];
Expand Down
2 changes: 2 additions & 0 deletions 动态规划系列/动态规划之正则表达.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@

函数签名如下:

<!-- muliti_language -->
```cpp
bool isMatch(string s, string p);
```
Expand All @@ -48,6 +49,7 @@ bool isMatch(string s, string p);

**如果不考虑 `*` 通配符,面对两个待匹配字符 `s[i]` 和 `p[j]`,我们唯一能做的就是看他俩是否匹配**:

<!-- muliti_language -->
```cpp
bool isMatch(string s, string p) {
int i = 0, j = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@

输入一个无序的整数数组,请你找到其中最长的严格递增子序列的长度,函数签名如下:

<!-- muliti_language -->
```java
int lengthOfLIS(int[] nums);
```
Expand Down Expand Up @@ -121,6 +122,7 @@ for (int i = 0; i < nums.length; i++) {

结合我们刚才说的 base case,下面我们看一下完整代码:

<!-- muliti_language -->
```java
int lengthOfLIS(int[] nums) {
// 定义:dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度
Expand Down Expand Up @@ -184,6 +186,7 @@ int lengthOfLIS(int[] nums) {

> tip:前文 [二分查找算法详解](https://labuladong.github.io/article/fname.html?fname=二分查找详解) 详细介绍了二分查找的细节及变体,这里就完美应用上了,如果没读过强烈建议阅读。

<!-- muliti_language -->
```java
int lengthOfLIS(int[] nums) {
int[] top = new int[nums.length];
Expand Down Expand Up @@ -257,6 +260,7 @@ int lengthOfLIS(int[] nums) {

下面看解法代码:

<!-- muliti_language -->
```java
// envelopes = [[w, h], [w, h]...]
public int maxEnvelopes(int[][] envelopes) {
Expand Down
66 changes: 39 additions & 27 deletions 动态规划系列/动态规划详解进阶.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ for 状态1 in 状态1的所有取值:

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

<!-- muliti_language -->
```java
int fib(int N) {
if (N == 1 || N == 2) return 1;
Expand Down Expand Up @@ -110,20 +111,22 @@ int fib(int N) {

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

<!-- muliti_language -->
```java
int fib(int N) {
// 备忘录全初始化为 0
int[] memo = new int[N + 1];
// 进行带备忘录的递归
return helper(memo, N);
return dp(memo, N);
}

int helper(int[] memo, int 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] = helper(memo, n - 1) + helper(memo, n - 2);
memo[n] = dp(memo, n - 1) + dp(memo, n - 2);
return memo[n];
}
```
Expand Down Expand Up @@ -154,6 +157,7 @@ int helper(int[] memo, int n) {

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

<!-- muliti_language -->
```java
int fib(int N) {
if (N == 0) return 0;
Expand Down Expand Up @@ -195,6 +199,7 @@ int fib(int N) {

所以,可以进一步优化,把空间复杂度降为 O(1)。这也就是我们最常见的计算斐波那契数的算法:

<!-- muliti_language -->
```java
int fib(int n) {
if (n == 0 || n == 1) {
Expand Down Expand Up @@ -226,6 +231,7 @@ int fib(int n) {

给你 `k` 种面值的硬币,面值分别为 `c1, c2 ... ck`,每种硬币的数量无限,再给一个总金额 `amount`,问你**最少**需要几枚硬币凑出这个金额,如果不可能凑出,算法返回 -1 。算法的函数签名如下:

<!-- muliti_language -->
```java
// coins 中是可选硬币面值,amount 是目标金额
int coinChange(int[] coins, int amount);
Expand Down Expand Up @@ -265,6 +271,7 @@ int coinChange(int[] coins, int amount);

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

<!-- muliti_language -->
```java
// 伪码框架
int coinChange(int[] coins, int amount) {
Expand All @@ -284,6 +291,7 @@ int dp(int[] coins, int n) {

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

<!-- muliti_language -->
```java
int coinChange(int[] coins, int amount) {
// 题目要求的最终结果是 dp(amount)
Expand Down Expand Up @@ -334,36 +342,39 @@ int dp(int[] coins, int amount) {

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

<!-- muliti_language -->
```java
int[] memo;
class Solution {
int[] memo;

int coinChange(int[] coins, int amount) {
memo = new int[amount + 1];
// 备忘录初始化为一个不会被取到的特殊值,代表还未被计算
Arrays.fill(memo, -666);
int coinChange(int[] coins, int amount) {
memo = new int[amount + 1];
// 备忘录初始化为一个不会被取到的特殊值,代表还未被计算
Arrays.fill(memo, -666);

return dp(coins, amount);
}
return dp(coins, amount);
}

int dp(int[] coins, int amount) {
if (amount == 0) return 0;
if (amount < 0) return -1;
// 查备忘录,防止重复计算
if (memo[amount] != -666)
return memo[amount];
int dp(int[] coins, int amount) {
if (amount == 0) return 0;
if (amount < 0) return -1;
// 查备忘录,防止重复计算
if (memo[amount] != -666)
return memo[amount];

int res = Integer.MAX_VALUE;
for (int coin : coins) {
// 计算子问题的结果
int subProblem = dp(coins, amount - coin);
// 子问题无解则跳过
if (subProblem == -1) continue;
// 在子问题中选择最优解,然后加一
res = Math.min(res, subProblem + 1);
int res = Integer.MAX_VALUE;
for (int coin : coins) {
// 计算子问题的结果
int subProblem = dp(coins, amount - coin);
// 子问题无解则跳过
if (subProblem == -1) continue;
// 在子问题中选择最优解,然后加一
res = Math.min(res, subProblem + 1);
}
// 把计算结果存入备忘录
memo[amount] = (res == Integer.MAX_VALUE) ? -1 : res;
return memo[amount];
}
// 把计算结果存入备忘录
memo[amount] = (res == Integer.MAX_VALUE) ? -1 : res;
return memo[amount];
}
```

Expand All @@ -377,6 +388,7 @@ int dp(int[] coins, int amount) {

根据我们文章开头给出的动态规划代码框架可以写出如下解法:

<!-- muliti_language -->
```java
int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1];
Expand Down
Loading