Commit ac2071c8 authored by ouuan's avatar ouuan
Browse files

Merge remote-tracking branch 'main/master' into graph-index

parents 9e63ea37 01fc6e15
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -137,3 +137,7 @@ python2 -m SimpleHTTPServer

<!-- <img src='https://i.loli.net/2018/12/07/5c0a6e4c31b30.png' alt='QVQNetWork' width=233> 
鸣谢 QVQNetwork 赞助的服务器。 -->

感谢 北大算协 和 Hulu 的支持!

![](https://assets.pcmag.com/media/images/560767-hulu.png?width=333&height=245)
+1 −0
Original line number Diff line number Diff line
@@ -26,6 +26,7 @@ $$
C++ 代码:

```cpp
// 假设数组的大小是n+1,冒泡排序从数组下标1开始
void bubble_sort(int *a, int n) {
  bool flag = true;
  while (flag) {
+8 −8
Original line number Diff line number Diff line
author: fudonglai
author: fudonglai, AngelKitty

首先简单阐述一下递归,分治算法,动态规划,贪心算法这几个东西的区别和联系,心里有个印象就好。

@@ -27,11 +27,11 @@ int func(传入数值) {
}
```

其实仔细想想, **递归运用最成功的是什么?我认为是数学归纳法。** 我们高中都学过数学归纳法,使用场景大概是:我们推不出来某个求和公式,但是我们试了几个比较小的数,似乎发现了一点规律,然后了一个公式,看起来应该是正确答案。但是数学是很严谨的,你哪怕穷举了一万个数都是正确的,但是第一万零一个数正确吗?这就要数学归纳法发挥神威了,可以假设我们的这个公式在第 k 个数时成立,如果证明在第 k + 1 时也成立,那么我们的这个公式就是正确的。
其实仔细想想, **递归运用最成功的是什么?我认为是数学归纳法。** 我们高中都学过数学归纳法,使用场景大概是:我们推不出来某个求和公式,但是我们试了几个比较小的数,似乎发现了一点规律,然后猜想了一个公式,看起来应该是正确答案。但是数学是很严谨的,你哪怕穷举了一万个数都是正确的,但是第一万零一个数正确吗?这就要数学归纳法发挥神威了,可以假设我们猜想的这个公式在第 k 个数时成立,如果证明在第 k + 1 时也成立,那么我们猜想的这个公式就是正确的。

那么数学归纳法和递归有什么联系?我们刚才说了,递归代码必须要有结束条件,如果没有的话就会进入无穷无尽的自我调用,直到内存耗尽。而数学证明的难度在于,你可以尝试有穷种情况,但是难以将你的结论延伸到无穷大。这里就可以看出联系了——无穷。

递归代码的精髓在于调用自去解决规模更小的子问题,直到到达结束条件;而数学归纳法之所以有用,就在于不断把我们的猜测向上加一,扩大结论的规模,没有结束条件,从而把结论延伸到无穷无尽,也就完成了猜测正确性的证明。
递归代码的精髓在于调用自去解决规模更小的子问题,直到到达结束条件;而数学归纳法之所以有用,就在于不断把我们的猜测向上加一,扩大结论的规模,没有结束条件,从而把结论延伸到无穷无尽,也就完成了猜测正确性的证明。

### 为什么要写递归

@@ -137,13 +137,13 @@ int count(TreeNode node, int sum) {

题目看起来很复杂吧,不过代码却极其简洁,这就是递归的魅力。我来简单总结这个问题的 **解决过程**

首先明确,递归求解树的问题必然是要遍历整棵树的,所以 **二叉树的遍历框架** (分别对左右孩子递归调用函数本身)必然要出现在主函数 pathSum 中。那么对于每个节点,们应该干什么呢?们应该看看,自己和脚底下的小弟们包含多少条符合条件的路径。好了,这道题就结束了。
首先明确,递归求解树的问题必然是要遍历整棵树的,所以 **二叉树的遍历框架** (分别对左右孩子递归调用函数本身)必然要出现在主函数 pathSum 中。那么对于每个节点,们应该干什么呢?们应该看看,自己和脚底下的小弟们包含多少条符合条件的路径。好了,这道题就结束了。

按照前面说的技巧,根据刚才的分析来定义清楚每个递归函数应该做的事:

PathSum 函数:给一个节点和一个目标值,返回以这个节点为根的树中,和为目标值的路径总数。
PathSum 函数:给一个节点和一个目标值,返回以这个节点为根的树中,和为目标值的路径总数。

count 函数:给一个节点和一个目标值,返回以这个节点为根的树中,能凑出几个以该节点为路径开头,和为目标值的路径总数。
count 函数:给一个节点和一个目标值,返回以这个节点为根的树中,能凑出几个以该节点为路径开头,和为目标值的路径总数。

```cpp
/* 有了以上铺垫,详细注释一下代码 */
@@ -167,7 +167,7 @@ int count(TreeNode node, int sum) {
}
```

还是那句话, **明白每个函数能做的事,并相信们能够完成。** 
还是那句话, **明白每个函数能做的事,并相信们能够完成。** 

总结下,PathSum 函数提供的二叉树遍历框架,在遍历中对每个节点调用 count 函数,看出先序遍历了吗(这道题什么序都是一样的);count 函数也是一个二叉树遍历,用于寻找以该节点开头的目标值路径。好好体会吧!

@@ -200,6 +200,6 @@ void merge_sort(一个数组) {
}
```

好了,这个算法也就这样了,完全没有任何难度。记住之前说的,相信函数的能力,传给半个数组,那么这半个数组就已经被排好了。而且你会发现这不就是个二叉树遍历模板吗?为什么是后序遍历?因为我们分治算法的套路是 **分解 -> 解决(触底)-> 合并(回溯)** 啊,先左右分解,再处理合并,回溯就是在退栈,就相当于后序遍历了。至于 `merge` 函数,参考两个有序链表的合并,简直一模一样。
好了,这个算法也就这样了,完全没有任何难度。记住之前说的,相信函数的能力,传给半个数组,那么这半个数组就已经被排好了。而且你会发现这不就是个二叉树遍历模板吗?为什么是后序遍历?因为我们分治算法的套路是 **分解 -> 解决(触底)-> 合并(回溯)** 啊,先左右分解,再处理合并,回溯就是在退栈,就相当于后序遍历了。至于 `merge` 函数,参考两个有序链表的合并,简直一模一样。

LeetCode 上有分治算法的专项练习, [点这里去做题](https://leetcode.com/tag/divide-and-conquer/) 
+1 −1
Original line number Diff line number Diff line
@@ -36,7 +36,7 @@ $$
还有一点需要注意的是,很容易写出这样的错误核心代码:

```cpp
for (int i = 1; i <= W; i++)
for (int i = 1; i <= n; i++)
  for (int l = 0; l <= W - w[i]; l++)
    f[l + w[i]] = max(f[l] + v[i], f[l + w[i]]);
// 由 f[i][l + w[i]] = max(max(f[i - 1][l + w[i]],f[i - 1][l] + w[i]),f[i][l +
+2 −0
Original line number Diff line number Diff line
@@ -25,3 +25,5 @@
用块状链表后除了单点修改是 $O(1)$ 外其他都是 $O(n^{\frac{1}{2}})$ 的。

 `ETT` 不支持换根操作。对于链(区间)修改,分为两种情况,一是贡献相同(如 $\operatorname{xor}$ ) 是可以的,二是贡献不同(如 $\operatorname{sum}$ ) 是不行的。现在的主流做法毕竟是 `LCT` ,所以这些操作比较多,在避开这种操作的情况下运用这种做法还是不错的。

注:标准的 ETT(用欧拉回路而不是 dfs 括号序实现)是支持换根操作的,但是实现较为复杂。
Loading