Loading docs/basic/bucket-sort.md +2 −3 Original line number Diff line number Diff line Loading @@ -8,4 +8,3 @@ 算法流程就是记录每一个数出现了多少次,然后从小到大依次输出。 一般考虑的是某一范围内的整数,但是计数排序也可以和[离散化](/misc/discrete)一起使用,来对浮点数、大整数进行排序。 docs/basic/construction.md +11 −11 Original line number Diff line number Diff line Loading @@ -74,7 +74,7 @@ $$ #### Problem [AtCoder Grand Contest 032 B](<https://atcoder.jp/contests/agc032/tasks/agc032_b>) [AtCoder Grand Contest 032 B](https://atcoder.jp/contests/agc032/tasks/agc032_b) #### Solution Loading @@ -94,7 +94,7 @@ $$ #### Problem [BZOJ4971:[Lydsy1708月赛]记忆中的背包](<https://www.lydsy.com/JudgeOnline/problem.php?id=4971>) [BZOJ4971:\[Lydsy1708 月赛\] 记忆中的背包](https://www.lydsy.com/JudgeOnline/problem.php?id=4971) #### Solution Loading docs/basic/divide-and-conquer.md +64 −78 Original line number Diff line number Diff line Loading @@ -14,7 +14,7 @@ 1. 如何给一堆数字排序?答:分成两半,先排左半边再排右半边,最后合并就行了,至于怎么排左边和右边,请重新阅读这句话。 2. 孙悟空身上有多少根毛?答:一根毛加剩下的毛。 3. 你今年几岁? 答:去年的岁数加一岁,1999 年我出生。 3. 你今年几岁?答:去年的岁数加一岁,1999 年我出生。 递归代码最重要的两个特征:结束条件和自我调用。自我调用是在解决子问题,而结束条件定义了最简子问题的答案。 Loading @@ -25,8 +25,6 @@ int func(传入数值) { } ``` 其实仔细想想, **递归运用最成功的是什么?我认为是数学归纳法。** 我们高中都学过数学归纳法,使用场景大概是:我们推不出来某个求和公式,但是我们试了几个比较小的数,似乎发现了一点规律,然后编了一个公式,看起来应该是正确答案。但是数学是很严谨的,你哪怕穷举了一万个数都是正确的,但是第一万零一个数正确吗?这就要数学归纳法发挥神威了,可以假设我们编的这个公式在第 k 个数时成立,如果证明在第 k + 1 时也成立,那么我们编的这个公式就是正确的。 那么数学归纳法和递归有什么联系?我们刚才说了,递归代码必须要有结束条件,如果没有的话就会进入无穷无尽的自我调用,直到内存耗尽。而数学证明的难度在于,你可以尝试有穷种情况,但是难以将你的结论延伸到无穷大。这里就可以看出联系了——无穷。 Loading Loading @@ -58,7 +56,6 @@ void sort(Comparable[] a, int lo, int hi) { sort(a, mid + 1, hi); merge(a, lo, mid, hi); } ``` 看起来简洁漂亮是一方面,关键是 **可解释性很强** :把左半边排序,把右半边排序,最后合并两边。而非递归版本看起来不知所云,充斥着各种难以理解的边界计算细节,特别容易出 bug 且难以调试,人生苦短,我更倾向于递归版本。 Loading @@ -79,8 +76,6 @@ public int size(Node head) { } ``` ### 写递归的技巧 我的一点心得是: **明白一个函数的作用并相信它能完成这个任务,千万不要试图跳进细节。** 千万不要跳进这个函数里面企图探究更多细节,否则就会陷入无穷的细节无法自拔,人脑能压几个栈啊。 Loading @@ -100,18 +95,14 @@ void traverse(TreeNode* root) { ```cpp void traverse(TreeNode* root) { if (root == nullptr) return; for (child : root->children) traverse(child); for (child : root->children) traverse(child); } ``` 至于遍历的什么前、中、后序,那都是显而易见的,对于 N 叉树,显然没有中序遍历。 以下 **详解 LeetCode 的一道题来说明** :给一棵二叉树,和一个目标值,节点上的值有正有负,返回树中和等于目标值的路径条数,让你编写 pathSum 函数: ``` /* 来源于 LeetCode PathSum III: https://leetcode.com/problems/path-sum-iii/ */ root = [10,5,-3,3,2,null,11,3,-2,null,1], sum = 8 Loading @@ -128,7 +119,6 @@ Return 3. The paths that sum to 8 are: 1. 5 -> 3 2. 5 -> 2 -> 1 3. -3 -> 11 ``` ```cpp /* 看不懂没关系,底下有更详细的分析版本,这里突出体现递归的简洁优美 */ Loading @@ -138,7 +128,8 @@ int pathSum(TreeNode root, int sum) { } int count(TreeNode node, int sum) { if (node == null) return 0; return (node.val == sum) + count(node.left, sum - node.val) + count(node.right, sum - node.val); return (node.val == sum) + count(node.left, sum - node.val) + count(node.right, sum - node.val); } ``` Loading @@ -158,7 +149,8 @@ int pathSum(TreeNode root, int sum) { if (root == null) return 0; int pathImLeading = count(root, sum); // 自己为开头的路径数 int leftPathSum = pathSum(root.left, sum); // 左边路径总数(相信他能算出来) int rightPathSum = pathSum(root.right, sum); // 右边路径总数(相信他能算出来) int rightPathSum = pathSum(root.right, sum); // 右边路径总数(相信他能算出来) return leftPathSum + rightPathSum + pathImLeading; } int count(TreeNode node, int sum) { Loading @@ -179,16 +171,12 @@ int count(TreeNode node, int sum) { LeetCode 有递归专题练习,[点这里去做题](https://leetcode.com/explore/learn/card/recursion-i/) ### 递归优化 比较 naive 的递归实现可能递归次数太多,容易超时。 怎么优化呢?详见[搜索优化](/search/optimization)和[记忆化搜索](/dp/memo/)。 ## 分治算法 **归并排序** ,典型的分治算法;分治,典型的递归结构。 Loading @@ -214,6 +202,4 @@ void merge_sort(一个数组) { LeetCode 上有分治算法的专项练习,[点这里去做题](https://leetcode.com/tag/divide-and-conquer/) 本文主要贡献者:[fudonglai](https://github.com/fudonglai) docs/basic/file-operation.md +23 −26 Original line number Diff line number Diff line Loading @@ -69,8 +69,7 @@ fclose(stdin); fclose(stdout); ``` !!! 注 `printf/scanf/cin/cout`等函数默认使用 `stdin/stdout`,将 `stdin/stdout`重定向后,这些函数将输入 / 输出到被定向的文件 !!! 注 `printf/scanf/cin/cout` 等函数默认使用 `stdin/stdout` ,将 `stdin/stdout` 重定向后,这些函数将输入/输出到被定向的文件 ### 模板 Loading Loading @@ -123,8 +122,6 @@ fclose(stdin); fclose(stdout); ``` ## C++ 的 `ifstream/ofstream` 文件输入输出流 ### 使用方法 Loading docs/basic/greedy.md +5 −6 Original line number Diff line number Diff line Loading @@ -23,7 +23,7 @@ 用排序法常见的情况是输入一个包含几个(一般一到两个)权值的数组,通过排序然后遍历模拟计算的方法求出最优值。 有些题的排序方法非常显然,如 [Luogu P1209 [USACO1.3]修理牛棚 Barn Repair](https://www.luogu.org/problemnew/show/P1209) 就是将输入数组差分后排序模拟求值。 有些题的排序方法非常显然,如[Luogu P1209 \[USACO1.3\] 修理牛棚 Barn Repair](https://www.luogu.org/problemnew/show/P1209)就是将输入数组差分后排序模拟求值。 然而有些时候很难直接一下子看出排序方法,比如[NOIP 2012 国王游戏](https://vijos.org/p/1779)就很容易凭直觉而错误地以 $a$ 或 $b$ 为关键字排序,过样例之后提交就发现 WA 了 QAQ。一个~~众所周知的~~常见办法就是尝试交换数组相邻的两个元素来 **推导** 出正确的排序方法。我们假设这题输入的俩个数用一个结构体来保存 Loading Loading @@ -89,7 +89,6 @@ struct uv { ~~看上去是不是很简单呢(这题高精度卡常……)~~ 如果看懂了就可以尝试下一道类似的题:[Luogu P2123 皇后游戏](https://www.luogu.org/problemnew/show/P2123) ## 后悔法 ??? note " 例题[Luogu P2949\[USACO09OPEN\]工作调度 Work Scheduling](https://www.luogu.org/problemnew/show/P2949)" Loading Loading
docs/basic/bucket-sort.md +2 −3 Original line number Diff line number Diff line Loading @@ -8,4 +8,3 @@ 算法流程就是记录每一个数出现了多少次,然后从小到大依次输出。 一般考虑的是某一范围内的整数,但是计数排序也可以和[离散化](/misc/discrete)一起使用,来对浮点数、大整数进行排序。
docs/basic/construction.md +11 −11 Original line number Diff line number Diff line Loading @@ -74,7 +74,7 @@ $$ #### Problem [AtCoder Grand Contest 032 B](<https://atcoder.jp/contests/agc032/tasks/agc032_b>) [AtCoder Grand Contest 032 B](https://atcoder.jp/contests/agc032/tasks/agc032_b) #### Solution Loading @@ -94,7 +94,7 @@ $$ #### Problem [BZOJ4971:[Lydsy1708月赛]记忆中的背包](<https://www.lydsy.com/JudgeOnline/problem.php?id=4971>) [BZOJ4971:\[Lydsy1708 月赛\] 记忆中的背包](https://www.lydsy.com/JudgeOnline/problem.php?id=4971) #### Solution Loading
docs/basic/divide-and-conquer.md +64 −78 Original line number Diff line number Diff line Loading @@ -14,7 +14,7 @@ 1. 如何给一堆数字排序?答:分成两半,先排左半边再排右半边,最后合并就行了,至于怎么排左边和右边,请重新阅读这句话。 2. 孙悟空身上有多少根毛?答:一根毛加剩下的毛。 3. 你今年几岁? 答:去年的岁数加一岁,1999 年我出生。 3. 你今年几岁?答:去年的岁数加一岁,1999 年我出生。 递归代码最重要的两个特征:结束条件和自我调用。自我调用是在解决子问题,而结束条件定义了最简子问题的答案。 Loading @@ -25,8 +25,6 @@ int func(传入数值) { } ``` 其实仔细想想, **递归运用最成功的是什么?我认为是数学归纳法。** 我们高中都学过数学归纳法,使用场景大概是:我们推不出来某个求和公式,但是我们试了几个比较小的数,似乎发现了一点规律,然后编了一个公式,看起来应该是正确答案。但是数学是很严谨的,你哪怕穷举了一万个数都是正确的,但是第一万零一个数正确吗?这就要数学归纳法发挥神威了,可以假设我们编的这个公式在第 k 个数时成立,如果证明在第 k + 1 时也成立,那么我们编的这个公式就是正确的。 那么数学归纳法和递归有什么联系?我们刚才说了,递归代码必须要有结束条件,如果没有的话就会进入无穷无尽的自我调用,直到内存耗尽。而数学证明的难度在于,你可以尝试有穷种情况,但是难以将你的结论延伸到无穷大。这里就可以看出联系了——无穷。 Loading Loading @@ -58,7 +56,6 @@ void sort(Comparable[] a, int lo, int hi) { sort(a, mid + 1, hi); merge(a, lo, mid, hi); } ``` 看起来简洁漂亮是一方面,关键是 **可解释性很强** :把左半边排序,把右半边排序,最后合并两边。而非递归版本看起来不知所云,充斥着各种难以理解的边界计算细节,特别容易出 bug 且难以调试,人生苦短,我更倾向于递归版本。 Loading @@ -79,8 +76,6 @@ public int size(Node head) { } ``` ### 写递归的技巧 我的一点心得是: **明白一个函数的作用并相信它能完成这个任务,千万不要试图跳进细节。** 千万不要跳进这个函数里面企图探究更多细节,否则就会陷入无穷的细节无法自拔,人脑能压几个栈啊。 Loading @@ -100,18 +95,14 @@ void traverse(TreeNode* root) { ```cpp void traverse(TreeNode* root) { if (root == nullptr) return; for (child : root->children) traverse(child); for (child : root->children) traverse(child); } ``` 至于遍历的什么前、中、后序,那都是显而易见的,对于 N 叉树,显然没有中序遍历。 以下 **详解 LeetCode 的一道题来说明** :给一棵二叉树,和一个目标值,节点上的值有正有负,返回树中和等于目标值的路径条数,让你编写 pathSum 函数: ``` /* 来源于 LeetCode PathSum III: https://leetcode.com/problems/path-sum-iii/ */ root = [10,5,-3,3,2,null,11,3,-2,null,1], sum = 8 Loading @@ -128,7 +119,6 @@ Return 3. The paths that sum to 8 are: 1. 5 -> 3 2. 5 -> 2 -> 1 3. -3 -> 11 ``` ```cpp /* 看不懂没关系,底下有更详细的分析版本,这里突出体现递归的简洁优美 */ Loading @@ -138,7 +128,8 @@ int pathSum(TreeNode root, int sum) { } int count(TreeNode node, int sum) { if (node == null) return 0; return (node.val == sum) + count(node.left, sum - node.val) + count(node.right, sum - node.val); return (node.val == sum) + count(node.left, sum - node.val) + count(node.right, sum - node.val); } ``` Loading @@ -158,7 +149,8 @@ int pathSum(TreeNode root, int sum) { if (root == null) return 0; int pathImLeading = count(root, sum); // 自己为开头的路径数 int leftPathSum = pathSum(root.left, sum); // 左边路径总数(相信他能算出来) int rightPathSum = pathSum(root.right, sum); // 右边路径总数(相信他能算出来) int rightPathSum = pathSum(root.right, sum); // 右边路径总数(相信他能算出来) return leftPathSum + rightPathSum + pathImLeading; } int count(TreeNode node, int sum) { Loading @@ -179,16 +171,12 @@ int count(TreeNode node, int sum) { LeetCode 有递归专题练习,[点这里去做题](https://leetcode.com/explore/learn/card/recursion-i/) ### 递归优化 比较 naive 的递归实现可能递归次数太多,容易超时。 怎么优化呢?详见[搜索优化](/search/optimization)和[记忆化搜索](/dp/memo/)。 ## 分治算法 **归并排序** ,典型的分治算法;分治,典型的递归结构。 Loading @@ -214,6 +202,4 @@ void merge_sort(一个数组) { LeetCode 上有分治算法的专项练习,[点这里去做题](https://leetcode.com/tag/divide-and-conquer/) 本文主要贡献者:[fudonglai](https://github.com/fudonglai)
docs/basic/file-operation.md +23 −26 Original line number Diff line number Diff line Loading @@ -69,8 +69,7 @@ fclose(stdin); fclose(stdout); ``` !!! 注 `printf/scanf/cin/cout`等函数默认使用 `stdin/stdout`,将 `stdin/stdout`重定向后,这些函数将输入 / 输出到被定向的文件 !!! 注 `printf/scanf/cin/cout` 等函数默认使用 `stdin/stdout` ,将 `stdin/stdout` 重定向后,这些函数将输入/输出到被定向的文件 ### 模板 Loading Loading @@ -123,8 +122,6 @@ fclose(stdin); fclose(stdout); ``` ## C++ 的 `ifstream/ofstream` 文件输入输出流 ### 使用方法 Loading
docs/basic/greedy.md +5 −6 Original line number Diff line number Diff line Loading @@ -23,7 +23,7 @@ 用排序法常见的情况是输入一个包含几个(一般一到两个)权值的数组,通过排序然后遍历模拟计算的方法求出最优值。 有些题的排序方法非常显然,如 [Luogu P1209 [USACO1.3]修理牛棚 Barn Repair](https://www.luogu.org/problemnew/show/P1209) 就是将输入数组差分后排序模拟求值。 有些题的排序方法非常显然,如[Luogu P1209 \[USACO1.3\] 修理牛棚 Barn Repair](https://www.luogu.org/problemnew/show/P1209)就是将输入数组差分后排序模拟求值。 然而有些时候很难直接一下子看出排序方法,比如[NOIP 2012 国王游戏](https://vijos.org/p/1779)就很容易凭直觉而错误地以 $a$ 或 $b$ 为关键字排序,过样例之后提交就发现 WA 了 QAQ。一个~~众所周知的~~常见办法就是尝试交换数组相邻的两个元素来 **推导** 出正确的排序方法。我们假设这题输入的俩个数用一个结构体来保存 Loading Loading @@ -89,7 +89,6 @@ struct uv { ~~看上去是不是很简单呢(这题高精度卡常……)~~ 如果看懂了就可以尝试下一道类似的题:[Luogu P2123 皇后游戏](https://www.luogu.org/problemnew/show/P2123) ## 后悔法 ??? note " 例题[Luogu P2949\[USACO09OPEN\]工作调度 Work Scheduling](https://www.luogu.org/problemnew/show/P2949)" Loading