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

Commit ff455ff

Browse files
update content
1 parent df7b1ed commit ff455ff

File tree

5 files changed

+214
-7
lines changed

5 files changed

+214
-7
lines changed

‎动态规划系列/动态规划之博弈问题.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -119,20 +119,24 @@ for 0 <= i < n:
119119
dp[i][j].fir = max(piles[i] + dp[i+1][j].sec, piles[j] + dp[i][j-1].sec)
120120
dp[i][j].fir = max( 选择最左边的石头堆 , 选择最右边的石头堆 )
121121
# 解释:我作为先手,面对 piles[i...j] 时,有两种选择:
122-
# 要么我选择最左边的那一堆石头,然后面对 piles[i+1...j]
123-
# 但是此时轮到对方,相当于我变成了后手;
124-
# 要么我选择最右边的那一堆石头,然后面对 piles[i...j-1]
125-
# 但是此时轮到对方,相当于我变成了后手。
122+
123+
# 要么我选择最左边的那一堆石头 piles[i],局面变成了 piles[i+1...j],
124+
# 然后轮到对方选了,我变成了后手,此时我作为后手的最优得分是 dp[i+1][j].sec
125+
126+
# 要么我选择最右边的那一堆石头 piles[j],局面变成了 piles[i...j-1]
127+
# 然后轮到对方选了,我变成了后手,此时我作为后手的最优得分是 dp[i][j-1].sec
126128

127129
if 先手选择左边:
128130
dp[i][j].sec = dp[i+1][j].fir
129131
if 先手选择右边:
130132
dp[i][j].sec = dp[i][j-1].fir
131133
# 解释:我作为后手,要等先手先选择,有两种情况:
134+
132135
# 如果先手选择了最左边那堆,给我剩下了 piles[i+1...j]
133-
# 此时轮到我,我变成了先手;
136+
# 此时轮到我,我变成了先手,此时的最优得分是 dp[i+1][j].fir
137+
134138
# 如果先手选择了最右边那堆,给我剩下了 piles[i...j-1]
135-
# 此时轮到我,我变成了先手
139+
# 此时轮到我,我变成了先手,此时的最优得分是 dp[i][j-1].fir
136140
```
137141

138142
根据 dp 数组的定义,我们也可以找出 **base case**,也就是最简单的情况:

‎数据结构系列/二叉树总结.md

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,204 @@ class Solution {
566566

567567
![](https://labuladong.github.io/pictures/二叉树收官/plugin2.png)
568568

569+
### 以树的视角看动归/回溯/DFS算法的区别和联系
570+
571+
前文我说动态规划/回溯算法就是二叉树算法两种不同思路的表现形式,相信能看到这里的读者应该也认可了我这个观点。但有细心的读者经常问我:东哥,你的理解思路让我豁然开朗,但你好像一直没讲过 DFS 算法?
572+
573+
其实我在 [一文秒杀所有岛屿题目](https://labuladong.github.io/article/fname.html?fname=岛屿题目) 中就是用的 DFS 算法,但我确实没有单独用一篇文章讲 DFS 算法,**因为 DFS 算法和回溯算法非常类似,只是在细节上有所区别**
574+
575+
这个细节上的差别是什么呢?其实就是「做选择」和「撤销选择」到底在 for 循环外面还是里面的区别,DFS 算法在外面,回溯算法在里面。
576+
577+
为什么有这个区别?还是要结合着二叉树理解。这一部分我就把回溯算法、DFS 算法、动态规划三种经典的算法思想,以及它们和二叉树算法的联系和区别,用一句话来说明:
578+
579+
**动归/DFS/回溯算法都可以看做二叉树问题的扩展,只是它们的关注点不同**:
580+
581+
- **动态规划算法属于分解问题的思路,它的关注点在整棵「子树」**
582+
- **回溯算法属于遍历的思路,它的关注点在节点间的「树枝」**
583+
- **DFS 算法属于遍历的思路,它的关注点在单个「节点」**
584+
585+
怎么理解?我分别举三个例子你就懂了:
586+
587+
**第一个例子**,给你一棵二叉树,请你用分解问题的思路写一个 `count` 函数,计算这棵二叉树共有多少个节点。代码很简单,上文都写过了:
588+
589+
<!-- muliti_language -->
590+
```java
591+
// 定义:输入一棵二叉树,返回这棵二叉树的节点总数
592+
int count(TreeNode root) {
593+
if (root == null) {
594+
return 0;
595+
}
596+
// 我这个节点关心的是我的两个子树的节点总数分别是多少
597+
int leftCount = count(root.left);
598+
int rightCount = count(root.right);
599+
// 后序位置,左右子树节点数加上自己就是整棵树的节点数
600+
return leftCount + rightCount + 1;
601+
}
602+
```
603+
604+
**你看,这就是动态规划分解问题的思路,它的着眼点永远是结构相同的整个子问题,类比到二叉树上就是「子树」**
605+
606+
你再看看具体的动态规划问题,比如 [动态规划框架套路详解](https://labuladong.github.io/article/fname.html?fname=动态规划详解进阶) 中举的斐波那契的例子,我们的关注点在一棵棵子树的返回值上:
607+
608+
<!-- muliti_language -->
609+
```java
610+
int fib(int N) {
611+
if (N == 1 || N == 2) return 1;
612+
return fib(N - 1) + fib(N - 2);
613+
}
614+
```
615+
616+
![](https://labuladong.github.io/pictures/动态规划详解进阶/2.jpg)
617+
618+
**第二个例子**,给你一棵二叉树,请你用遍历的思路写一个 `traverse` 函数,打印出遍历这棵二叉树的过程,你看下代码:
619+
620+
<!-- muliti_language -->
621+
```java
622+
void traverse(TreeNode root) {
623+
if (root == null) return;
624+
printf("从节点 %s 进入节点 %s", root, root.left);
625+
traverse(root.left);
626+
printf("从节点 %s 回到节点 %s", root.left, root);
627+
628+
printf("从节点 %s 进入节点 %s", root, root.right);
629+
traverse(root.right);
630+
printf("从节点 %s 回到节点 %s", root.right, root);
631+
}
632+
```
633+
634+
不难理解吧,好的,我们现在从二叉树进阶成多叉树,代码也是类似的:
635+
636+
<!-- muliti_language -->
637+
```java
638+
// 多叉树节点
639+
class Node {
640+
int val;
641+
Node[] children;
642+
}
643+
644+
void traverse(Node root) {
645+
if (root == null) return;
646+
for (Node child : root.children) {
647+
printf("从节点 %s 进入节点 %s", root, child);
648+
traverse(child);
649+
printf("从节点 %s 回到节点 %s", child, root);
650+
}
651+
}
652+
```
653+
654+
这个多叉树的遍历框架就可以延伸出 [回溯算法框架套路详解](https://labuladong.github.io/article/fname.html?fname=回溯算法详解修订版) 中的回溯算法框架:
655+
656+
```java
657+
void backtrack(...) {
658+
for (int i = 0; i < ...; i++) {
659+
// 做选择
660+
...
661+
662+
// 进入下一层决策树
663+
backtrack(...);
664+
665+
// 撤销刚才做的选择
666+
...
667+
}
668+
}
669+
```
670+
671+
**你看,这就是回溯算法遍历的思路,它的着眼点永远是在节点之间移动的过程,类比到二叉树上就是「树枝」**
672+
673+
你再看看具体的回溯算法问题,比如 [回溯算法秒杀排列组合子集的九种题型](https://labuladong.github.io/article/fname.html?fname=子集排列组合) 中讲到的全排列,我们的关注点在一条条树枝上:
674+
675+
```java
676+
// 回溯算法核心部分代码
677+
void backtrack(int[] nums) {
678+
// 回溯算法框架
679+
for (int i = 0; i < nums.length; i++) {
680+
// 做选择
681+
used[i] = true;
682+
track.addLast(nums[i]);
683+
684+
// 进入下一层回溯树
685+
backtrack(nums);
686+
687+
// 取消选择
688+
track.removeLast();
689+
used[i] = false;
690+
}
691+
}
692+
```
693+
694+
![](https://labuladong.github.io/pictures/排列组合/2.jpeg)
695+
696+
**第三个例子**,我给你一棵二叉树,请你写一个 `traverse` 函数,把这棵二叉树上的每个节点的值都加一。很简单吧,代码如下:
697+
698+
<!-- muliti_language -->
699+
```java
700+
void traverse(TreeNode root) {
701+
if (root == null) return;
702+
// 遍历过的每个节点的值加一
703+
root.val++;
704+
traverse(root.left);
705+
traverse(root.right);
706+
}
707+
```
708+
709+
**你看,这就是 DFS 算法遍历的思路,它的着眼点永远是在单一的节点上,类比到二叉树上就是处理每个「节点」**
710+
711+
你再看看具体的 DFS 算法问题,比如 [一文秒杀所有岛屿题目](https://labuladong.github.io/article/fname.html?fname=岛屿题目) 中讲的前几道题,我们的关注点是 `grid` 数组的每个格子(节点),我们要对遍历过的格子进行一些处理,所以我说是用 DFS 算法解决这几道题的:
712+
713+
```java
714+
// DFS 算法核心逻辑
715+
void dfs(int[][] grid, int i, int j) {
716+
int m = grid.length, n = grid[0].length;
717+
if (i < 0 || j < 0 || i >= m || j >= n) {
718+
return;
719+
}
720+
if (grid[i][j] == 0) {
721+
return;
722+
}
723+
// 遍历过的每个格子标记为 0
724+
grid[i][j] = 0;
725+
dfs(grid, i + 1, j);
726+
dfs(grid, i, j + 1);
727+
dfs(grid, i - 1, j);
728+
dfs(grid, i, j - 1);
729+
}
730+
```
731+
732+
![](https://labuladong.github.io/pictures/岛屿/5.jpg)
733+
734+
好,请你仔细品一下上面三个简单的例子,是不是像我说的:动态规划关注整棵「子树」,回溯算法关注节点间的「树枝」,DFS 算法关注单个「节点」。
735+
736+
有了这些铺垫,你就很容易理解为什么回溯算法和 DFS 算法代码中「做选择」和「撤销选择」的位置不同了,看下面两段代码:
737+
738+
<!-- muliti_language -->
739+
```java
740+
// DFS 算法把「做选择」「撤销选择」的逻辑放在 for 循环外面
741+
void dfs(Node root) {
742+
if (root == null) return;
743+
// 做选择
744+
print("我已经进入节点 %s 啦", root)
745+
for (Node child : root.children) {
746+
dfs(child);
747+
}
748+
// 撤销选择
749+
print("我将要离开节点 %s 啦", root)
750+
}
751+
752+
// 回溯算法把「做选择」「撤销选择」的逻辑放在 for 循环里面
753+
void backtrack(Node root) {
754+
if (root == null) return;
755+
for (Node child : root.children) {
756+
// 做选择
757+
print("我站在节点 %s 到节点 %s 的树枝上", root, child)
758+
backtrack(child);
759+
// 撤销选择
760+
print("我站在节点 %s 到节点 %s 的树枝上", child, root)
761+
}
762+
}
763+
```
764+
765+
看到了吧,你回溯算法必须把「做选择」和「撤销选择」的逻辑放在 for 循环里面,否则怎么拿到「树枝」的两个端点?
766+
569767
### 层序遍历
570768

571769
二叉树题型主要是用来培养递归思维的,而层序遍历属于迭代遍历,也比较简单,这里就过一下代码框架吧:
@@ -726,6 +924,7 @@ class Solution {
726924
- [后序遍历的妙用](https://labuladong.github.io/article/fname.html?fname=后序遍历)
727925
- [回溯算法秒杀所有排列/组合/子集问题](https://labuladong.github.io/article/fname.html?fname=子集排列组合)
728926
- [回溯算法解题套路框架](https://labuladong.github.io/article/fname.html?fname=回溯算法详解修订版)
927+
- [图论基础及遍历算法](https://labuladong.github.io/article/fname.html?fname=图)
729928
- [在插件中解锁二叉树专属题解](https://labuladong.github.io/article/fname.html?fname=解锁tree插件)
730929
- [归并排序详解及应用](https://labuladong.github.io/article/fname.html?fname=归并排序)
731930
- [我的刷题心得](https://labuladong.github.io/article/fname.html?fname=算法心得)

‎数据结构系列/图.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,9 @@ void traverse(Graph graph, int s) {
206206

207207
另外,你应该注意到了,这个 `onPath` 数组的操作很像前文 [回溯算法核心套路](https://labuladong.github.io/article/fname.html?fname=回溯算法详解修订版) 中做「做选择」和「撤销选择」,区别在于位置:回溯算法的「做选择」和「撤销选择」在 for 循环里面,而对 `onPath` 数组的操作在 for 循环外面。
208208

209-
为什么有这个区别呢?这就是前文 [回溯算法核心套路](https://labuladong.github.io/article/fname.html?fname=回溯算法详解修订版) 中讲到的回溯算法和 DFS 算法的区别所在:回溯算法关注的不是节点,而是树枝。不信你看前文画的回溯树,我们需要在「树枝」上做选择和撤销选择:
209+
为什么有这个区别呢?这就是前文 [东哥带你刷二叉树(纲领篇)](https://labuladong.github.io/article/fname.html?fname=二叉树总结) 中讲到的回溯算法和 DFS 算法的区别所在:回溯算法关注的不是节点,而是树枝。如果没印象了,强烈建议重新阅读前文。
210+
211+
对于回溯算法,我们需要在「树枝」上做选择和撤销选择:
210212

211213
![](https://labuladong.github.io/pictures/backtracking/5.jpg)
212214

‎高频面试系列/子集排列组合.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -985,6 +985,7 @@ void backtrack(int[] nums) {
985985
<details>
986986
<summary><strong>引用本文的文章</strong></summary>
987987

988+
- [东哥带你刷二叉树(纲领篇)](https://labuladong.github.io/article/fname.html?fname=二叉树总结)
988989
- [分治算法详解:运算优先级](https://labuladong.github.io/article/fname.html?fname=分治算法)
989990
- [动态规划和回溯算法的思维转换](https://labuladong.github.io/article/fname.html?fname=单词拼接)
990991
- [回溯算法解题套路框架](https://labuladong.github.io/article/fname.html?fname=回溯算法详解修订版)

‎高频面试系列/岛屿题目.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,7 @@ class Solution {
550550
<summary><strong>引用本文的文章</strong></summary>
551551

552552
- [FloodFill算法详解及应用](https://labuladong.github.io/article/fname.html?fname=FloodFill算法详解及应用)
553+
- [东哥带你刷二叉树(纲领篇)](https://labuladong.github.io/article/fname.html?fname=二叉树总结)
553554
- [并查集(Union-Find)算法](https://labuladong.github.io/article/fname.html?fname=UnionFind算法详解)
554555

555556
</details><hr>

0 commit comments

Comments
(0)

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