Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

[pull] master from labuladong:master #93

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
pull merged 1 commit into AlgorithmAndLeetCode:master from labuladong:master
May 24, 2023
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
61 changes: 33 additions & 28 deletions 动态规划系列/状态压缩技巧.md
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,24 @@ tags: ['动态规划', '核心框架']
什么叫「和 `dp[i][j]` 相邻的状态」呢,比如前文 [最长回文子序列](https://labuladong.github.io/article/fname.html?fname=子序列问题模板) 中,最终的代码如下:

<!-- muliti_language -->
```cpp
int longestPalindromeSubseq(string s) {
int n = s.size();
// nxn 的 dp 数组全部初始化为 0
vector<vector<int>> dp(n, vector<int>(n, 0));
```java
int longestPalindromeSubseq(String s) {
int n = s.length();
// dp 数组全部初始化为 0
int[][] dp = new int[n][n];
// base case
for (int i = 0; i < n; i++)
for (int i = 0; i < n; i++) {
dp[i][i] = 1;
}
// 反着遍历保证正确的状态转移
for (int i = n - 2; i >= 0; i--) {
for (int i = n - 1; i >= 0; i--) {
for (int j = i + 1; j < n; j++) {
// 状态转移方程
if (s[i] == s[j])
if (s.charAt(i) == s.charAt(j)) {
dp[i][j] = dp[i + 1][j - 1] + 2;
else
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
} else {
dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
}
}
}
// 整个 s 的最长回文子串长度
Expand Down Expand Up @@ -93,7 +95,7 @@ for (int i = n - 2; i >= 0; i--) {
if (s[i] == s[j])
dp[j] = dp[j - 1] + 2;
else
dp[j] = max(dp[j], dp[j - 1]);
dp[j] = Math.max(dp[j], dp[j - 1]);
}
}
```
Expand All @@ -112,15 +114,15 @@ for (int i = n - 2; i >= 0; i--) {

那么问题已经解决了一大半了,只剩下二维 `dp` 数组中的 `dp[i+1][j-1]` 这个状态我们不能直接从一维 `dp` 数组中得到:

```cpp
```java
for (int i = n - 2; i >= 0; i--) {
for (int j = i + 1; j < n; j++) {
if (s[i] == s[j])
// dp[i][j] = dp[i+1][j-1] + 2;
dp[j] = ?? + 2;
else
// dp[i][j] = max(dp[i+1][j], dp[i][j-1]);
dp[j] = max(dp[j], dp[j - 1]);
dp[j] = Math.max(dp[j], dp[j - 1]);
}
}
```
Expand All @@ -131,7 +133,7 @@ for (int i = n - 2; i >= 0; i--) {

**那么如果我们想得到 `dp[i+1][j-1]`,就必须在它被覆盖之前用一个临时变量 `temp` 把它存起来,并把这个变量的值保留到计算 `dp[i][j]` 的时候**。为了达到这个目的,结合上图,我们可以这样写代码:

```cpp
```java
for (int i = n - 2; i >= 0; i--) {
// 存储 dp[i+1][j-1] 的变量
int pre = 0;
Expand All @@ -141,7 +143,7 @@ for (int i = n - 2; i >= 0; i--) {
// dp[i][j] = dp[i+1][j-1] + 2;
dp[j] = pre + 2;
else
dp[j] = max(dp[j], dp[j - 1]);
dp[j] = Math.max(dp[j], dp[j - 1]);
// 到下一轮循环,pre 就是 dp[i+1][j-1] 了
pre = temp;
}
Expand Down Expand Up @@ -173,12 +175,13 @@ for (int i = 5; i--) {
那么现在我们成功对状态转移方程进行了降维打击,算是最硬的的骨头啃掉了,但注意到我们还有 base case 要处理呀:

<!-- muliti_language -->
```cpp
```java
// dp 数组全部初始化为 0
vector<vector<int>> dp(n, vector<int>(n, 0));
int[][] dp = new int[n][n];
// base case
for (int i = 0; i < n; i++)
for (int i = 0; i < n; i++) {
dp[i][i] = 1;
}
```

如何把 base case 也打成一维呢?很简单,记住空间压缩就是投影,我们把 base case 投影到一维看看:
Expand All @@ -188,29 +191,31 @@ for (int i = 0; i < n; i++)
二维 `dp` 数组中的 base case 全都落入了一维 `dp` 数组,不存在冲突和覆盖,所以说我们直接这样写代码就行了:

<!-- muliti_language -->
```cpp
// 一维 dp 数组全部初始化为 1
vector<int> dp(n, 1);
```java
// base case:一维 dp 数组全部初始化为 1
int[] dp = new int[n];
Arrays.fill(dp, 1);
```

至此,我们把 base case 和状态转移方程都进行了降维,实际上已经写出完整代码了:

<!-- muliti_language -->
```cpp
int longestPalindromeSubseq(string s) {
int n = s.size();
// base case:一维 dp 数组全部初始化为 0
vector<int> dp(n, 1);
```java
int longestPalindromeSubseq(String s) {
int n = s.length();
// base case:一维 dp 数组全部初始化为 1
int[] dp = new int[n];
Arrays.fill(dp, 1);

for (int i = n - 2; i >= 0; i--) {
int pre = 0;
for (int j = i + 1; j < n; j++) {
int temp = dp[j];
// 状态转移方程
if (s[i] == s[j])
if (s.charAt(i) == s.charAt(j))
dp[j] = pre + 2;
else
dp[j] = max(dp[j], dp[j - 1]);
dp[j] = Math.max(dp[j], dp[j - 1]);
pre = temp;
}
}
Expand Down
2 changes: 1 addition & 1 deletion 动态规划系列/高楼扔鸡蛋问题.md
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ tags: ['动态规划']

这是力扣第 887 题「鸡蛋掉落」,我描述一下题目:

你面前有一栋从 1 到 `N` 共 `N` 层的楼,然后给你 `K` 个鸡蛋(`K` 至少为 1)。现在确定这栋楼存在楼层 `0 <= F <= N`,在这层楼将鸡蛋扔下去,鸡蛋**恰好没摔碎**(高于 `F` 的楼层都会碎,低于 `F` 的楼层都不会碎)。现在问你,**最坏**情况下,你**至少**要扔几次鸡蛋,才能**确定**这个楼层 `F` 呢?
你面前有一栋从 1 到 `N` 共 `N` 层的楼,然后给你 `K` 个鸡蛋(`K` 至少为 1)。现在确定这栋楼存在楼层 `0 <= F <= N`,在这层楼将鸡蛋扔下去,鸡蛋**恰好没摔碎**(高于 `F` 的楼层都会碎,低于 `F` 的楼层都不会碎,如果鸡蛋没有碎,可以捡回来继续扔)。现在问你,**最坏**情况下,你**至少**要扔几次鸡蛋,才能**确定**这个楼层 `F` 呢?

也就是让你找摔不碎鸡蛋的最高楼层 `F`,但什么叫「最坏情况」下「至少」要扔几次呢?我们分别举个例子就明白了。

Expand Down
4 changes: 2 additions & 2 deletions 数据结构系列/单调队列.md
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ public void push(int n) {
}
```

你可以想象,加入数字的大小代表人的体重,把前面体重不足的都压扁了,直到遇到更大的量级才停住。
你可以想象,加入数字的大小代表人的体重,体重大的会把前面体重不足的压扁,直到遇到更大的量级才停住。

![](https://labuladong.github.io/pictures/单调队列/3.png)

Expand Down Expand Up @@ -182,7 +182,7 @@ class MonotonicQueue {
}
```

之所以要判断 `data.getFirst() == n`,是因为我们想删除的队头元素 `n` 可能已经被「压扁」了,可能已经不存在了,所以这时候就不用删除了:
之所以要判断 `n == maxq.getFirst()`,是因为我们想删除的队头元素 `n` 可能已经被「压扁」了,可能已经不存在了,所以这时候就不用删除了:

![](https://labuladong.github.io/pictures/单调队列/2.png)

Expand Down
2 changes: 2 additions & 0 deletions 算法思维系列/BFS框架.md
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ int minDepth(TreeNode root) {
}
```

<visual slug='minimum-depth-of-binary-tree' />

这里注意这个 `while` 循环和 `for` 循环的配合,**`while` 循环控制一层一层往下走,`for` 循环利用 `sz` 变量控制从左到右遍历每一层二叉树节点**:

![](https://labuladong.github.io/pictures/dijkstra/1.jpeg)
Expand Down
24 changes: 23 additions & 1 deletion 算法思维系列/BFS解决滑动拼图.md
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,29 @@ int[][] neighbor = new int[][]{

观察上图就能发现,如果二维数组中的某个元素 `e` 在一维数组中的索引为 `i`,那么 `e` 的左右相邻元素在一维数组中的索引就是 `i - 1` 和 `i + 1`,而 `e` 的上下相邻元素在一维数组中的索引就是 `i - n` 和 `i + n`,其中 `n` 为二维数组的列数。

这样,对于 `m x n` 的二维数组,我们可以写一个函数来生成它的 `neighbor` 索引映射,篇幅所限,我这里就不写了。
这样,对于 `m x n` 的二维数组,我们可以写一个函数来生成它的 `neighbor` 索引映射:

```java
int[][] generateNeighborMapping(int m, int n) {
int[][] neighbor = new int[m * n][];
for (int i = 0; i < m * n; i++) {
List<Integer> neighbors = new ArrayList<>();

// 如果不是第一列,有左侧邻居
if (i % n != 0) neighbors.add(i - 1);
// 如果不是最后一列,有右侧邻居
if (i % n != n - 1) neighbors.add(i + 1);
// 如果不是第一行,有上方邻居
if (i - n >= 0) neighbors.add(i - n);
// 如果不是最后一行,有下方邻居
if (i + n < m * n) neighbors.add(i + n);

// Java 语言特性,将 List 类型转为 int[] 数组
neighbor[i] = neighbors.stream().mapToInt(Integer::intValue).toArray();
}
return neighbor;
}
```

至此,我们就把这个问题完全转化成标准的 BFS 问题了,借助前文 [BFS 算法框架](https://labuladong.github.io/article/fname.html?fname=BFS框架) 的代码框架,直接就可以套出解法代码了:

Expand Down
2 changes: 1 addition & 1 deletion 算法思维系列/UnionFind算法详解.md
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ class UF {

![](https://labuladong.github.io/pictures/unionfind/9.gif)

用语言描述就是,每次 while 循环都会把一对儿父子节点改到同一层,这样每次调用 `find` 函数向树根遍历的同时,顺手就将树高缩短了。
用语言描述就是,每次 while 循环都会让部分子节点向上移动,这样每次调用 `find` 函数向树根遍历的同时,顺手就将树高缩短了。

路径压缩的第二种写法是这样:

Expand Down
6 changes: 3 additions & 3 deletions 算法思维系列/双指针技巧.md
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,9 @@ ListNode deleteDuplicates(ListNode head) {

![](https://labuladong.github.io/pictures/数组去重/2.gif)

这里可能有读者会问,链表中那些重复的元素并没有被删掉,就让这些节点在链表上挂着,合适吗?

这就要探讨不同语言的特性了,像 Java/Python 这类带有垃圾回收的语言,可以帮我们自动找到并回收这些「悬空」的链表节点的内存,而像 C++ 这类语言没有自动垃圾回收的机制,确实需要我们编写代码时手动释放掉这些节点的内存。
> note:这里可能有读者会问,链表中那些重复的元素并没有被删掉,就让这些节点在链表上挂着,合适吗?
>
> 这就要探讨不同语言的特性了,像 Java/Python 这类带有垃圾回收的语言,可以帮我们自动找到并回收这些「悬空」的链表节点的内存,而像 C++ 这类语言没有自动垃圾回收的机制,确实需要我们编写代码时手动释放掉这些节点的内存。

不过话说回来,就算法思维的培养来说,我们只需要知道这种快慢指针技巧即可。

Expand Down
1 change: 1 addition & 0 deletions 算法思维系列/回溯算法详解修订版.md
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,7 @@ def backtrack(...):
- [Dijkstra 算法模板及应用](https://labuladong.github.io/article/fname.html?fname=dijkstra算法)
- [FloodFill算法详解及应用](https://labuladong.github.io/article/fname.html?fname=FloodFill算法详解及应用)
- [base case 和备忘录的初始值怎么定?](https://labuladong.github.io/article/fname.html?fname=备忘录等基础)
- [一文秒杀所有岛屿题目](https://labuladong.github.io/article/fname.html?fname=岛屿题目)
- [东哥带你刷二叉树(纲领篇)](https://labuladong.github.io/article/fname.html?fname=二叉树总结)
- [二分搜索怎么用?我和快手面试官进行了深度探讨](https://labuladong.github.io/article/fname.html?fname=二分分割子数组)
- [分治算法详解:运算优先级](https://labuladong.github.io/article/fname.html?fname=分治算法)
Expand Down
23 changes: 12 additions & 11 deletions 算法思维系列/差分技巧.md
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -34,27 +34,28 @@ tags: ['数组', '差分数组']
```java
class PrefixSum {
// 前缀和数组
private int[] prefix;
private int[] preSum;

/* 输入一个数组,构造前缀和 */
public PrefixSum(int[] nums) {
prefix = new int[nums.length + 1];
// preSum[0] = 0,便于计算累加和
preSum = new int[nums.length + 1];
// 计算 nums 的累加和
for (int i = 1; i < prefix.length; i++) {
prefix[i] = prefix[i - 1] + nums[i - 1];
for (int i = 1; i < preSum.length; i++) {
preSum[i] = preSum[i - 1] + nums[i - 1];
}
}

/* 查询闭区间 [i, j] 的累加和 */
public int query(int i, int j) {
return prefix[j + 1] - prefix[i];
/* 查询闭区间 [left, right] 的累加和 */
public int sumRange(int left, int right) {
return preSum[right + 1] - preSum[left];
}
}
```

![](https://labuladong.github.io/pictures/差分数组/1.jpeg)

`prefix[i]` 就代表着 `nums[0..i-1]` 所有元素的累加和,如果我们想求区间 `nums[i..j]` 的累加和,只要计算 `prefix[j+1] - prefix[i]` 即可,而不需要遍历整个区间求和。
`preSum[i]` 就代表着 `nums[0..i-1]` 所有元素的累加和,如果我们想求区间 `nums[i..j]` 的累加和,只要计算 `preSum[j+1] - preSum[i]` 即可,而不需要遍历整个区间求和。

本文讲一个和前缀和思想非常类似的算法技巧「差分数组」,**差分数组的主要适用场景是频繁对原始数组的某个区间的元素进行增减**。

Expand All @@ -64,7 +65,7 @@ class PrefixSum {

常规的思路很容易,你让我给区间 `nums[i..j]` 加上 `val`,那我就一个 for 循环给它们都加上呗,还能咋样?这种思路的时间复杂度是 O(N),由于这个场景下对 `nums` 的修改非常频繁,所以效率会很低下。

这里就需要差分数组的技巧,类似前缀和技巧构造的 `prefix` 数组,我们先对 `nums` 数组构造一个 `diff` 差分数组,**`diff[i]` 就是 `nums[i]` 和 `nums[i-1]` 之差**:
这里就需要差分数组的技巧,类似前缀和技巧构造的 `preSum` 数组,我们先对 `nums` 数组构造一个 `diff` 差分数组,**`diff[i]` 就是 `nums[i]` 和 `nums[i-1]` 之差**:

<!-- muliti_language -->
```java
Expand Down
2 changes: 1 addition & 1 deletion 高频面试系列/LRU算法.md
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ LRU 缓存淘汰算法就是一种常用策略。LRU 的全称是 Least Recently

![](https://labuladong.github.io/pictures/LRU算法/3.jpg)

现在你应该理解 LRU(Least Recently Used)策略了。当然还有其他缓存淘汰策略,比如不要按访问的时序来淘汰,而是按访问频率(LFU 策略)来淘汰等等,各有应用场景。本文讲解 LRU 算法策略。
现在你应该理解 LRU(Least Recently Used)策略了。当然还有其他缓存淘汰策略,比如不要按访问的时序来淘汰,而是按访问频率(LFU 策略)来淘汰等等,各有应用场景。本文讲解 LRU 算法策略,我会在 [LFU 算法详解](https://labuladong.github.io/article/fname.html?fname=LFU) 中讲解 LFU 算法

### 一、LRU 算法描述

Expand Down
2 changes: 2 additions & 0 deletions 高频面试系列/二分运用.md
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ tags: ['二分查找']

因为算法题一般都让你求最值,比如让你求吃香蕉的「最小速度」,让你求轮船的「最低运载能力」,求最值的过程,必然是搜索一个边界的过程,所以后面我们就详细分析一下这两种搜索边界的二分算法代码。

> note:注意,本文我写的都是左闭右开的二分搜索写法,如果你习惯两端都闭的写法,可以自行改写代码。

「搜索左侧边界」的二分搜索算法的具体代码实现如下:

<!-- muliti_language -->
Expand Down
2 changes: 1 addition & 1 deletion 高频面试系列/子集排列组合.md
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ List<List<Integer>> subsets(int[] nums)

为什么集合 `[2]` 只需要添加 `3`,而不添加前面的 `1` 呢?

因为集合中的元素不用考虑顺序,`[1,2,3]` 中 `2` 后面只有 `3`,如果你向前考虑 `1`,那么 `[2,1]` 会和之前已经生成的子集 `[1,2]` 重复。
因为集合中的元素不用考虑顺序,`[1,2,3]` 中 `2` 后面只有 `3`,如果你添加了前面的 `1`,那么 `[2,1]` 会和之前已经生成的子集 `[1,2]` 重复。

**换句话说,我们通过保证元素之间的相对顺序不变来防止出现重复的子集**。

Expand Down
6 changes: 4 additions & 2 deletions 高频面试系列/岛屿题目.md
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ void dfs(int[][] grid, int i, int j, boolean[][] visited) {

因为二维矩阵本质上是一幅「图」,所以遍历的过程中需要一个 `visited` 布尔数组防止走回头路,如果你能理解上面这段代码,那么搞定所有岛屿系列题目都很简单。

这里额外说一个处理二维数组的常用小技巧,你有时会看到使用「方向数组」来处理上下左右的遍历,和前文 [图遍历框架](https://labuladong.github.io/article/fname.html?fname=) 的代码很类似:
这里额外说一个处理二维数组的常用小技巧,你有时会看到使用「方向数组」来处理上下左右的遍历,和前文 [union-find 算法详解](https://labuladong.github.io/article/fname.html?fname=UnionFind算法详解) 的代码很类似:

<!-- muliti_language -->
```java
Expand Down Expand Up @@ -499,6 +499,8 @@ void dfs(int[][] grid, int i, int j, StringBuilder sb, int dir) {
}
```

> note:仔细看这个代码,在递归前做选择,在递归后撤销选择,它像不像 [回溯算法框架](https://labuladong.github.io/article/fname.html?fname=回溯算法详解修订版)?实际上它就是回溯算法,因为它关注的是「树枝」(岛屿的遍历顺序),而不是「节点」(岛屿的每个格子)。

`dir` 记录方向,`dfs` 函数递归结束后,`sb` 记录着整个遍历顺序。有了这个 `dfs` 函数就好办了,我们可以直接写出最后的解法代码:

<!-- muliti_language -->
Expand All @@ -523,7 +525,7 @@ int numDistinctIslands(int[][] grid) {
}
```

这样,这道题就解决了,至于为什么初始调用 `dfs` 函数时的 `dir` 参数可以随意写,这里涉及 DFS 和回溯算法的一个细微差别,前文 [图算法基础](https://labuladong.github.io/article/fname.html?fname=图) 有写,这里就不展开了
这样,这道题就解决了,至于为什么初始调用 `dfs` 函数时的 `dir` 参数可以随意写,因为这个 `dfs` 函数实际上是回溯算法,它关注的是「树枝」而不是「节点」,前文 [图算法基础](https://labuladong.github.io/article/fname.html?fname=图) 有写具体的区别,这里就不赘述了

以上就是全部岛屿系列题目的解题思路,也许前面的题目大部分人会做,但是最后两题还是比较巧妙的,希望本文对你有帮助。

Expand Down

AltStyle によって変換されたページ (->オリジナル) /