├── .gitignore ├── README.md ├── media └── BallPlacementProblem.jpg ├── 专题 ├── 位运算找出数组中唯一的元素.md ├── 区间问题.md ├── 格雷码与二进制码.md ├── 正整数拆分.md └── 求[a,b]区间中 0~9 出现的次数.md ├── 动态规划 ├── 区间动态规划.md ├── 线性动态规划.md └── 背包问题.md ├── 图 ├── README.md ├── 图的遍历.md ├── 差分约束.md ├── 拓扑排序.md ├── 无向图的割点和割边.md ├── 最小生成树.md ├── 最短路径问题.md ├── 有向图的强连通分量问题.md ├── 树上问题.md ├── 欧拉图.md └── 网络流问题 │ ├── README.md │ ├── 最大流问题 │ └── 使用BFS的Edmonds-Karp算法.cpp │ └── 最小费用最大流问题 │ └── 使用Bellman-Ford的Edmonds-Karp算法.cpp ├── 基础算法 ├── 二分查找.md ├── 二维前缀和与二维差分.md ├── 划分算法.md ├── 子集生成和全排列.md ├── 快读.cpp ├── 排序算法.md ├── 整数取整.md ├── 日期处理.cpp └── 树 │ ├── 二叉树.md │ └── 多叉树.md ├── 字符串 ├── KMP算法.cpp ├── 分割字符串.cpp ├── 字典树Trie.md └── 最长回文子串.md ├── 数学 ├── README.md ├── 乘法逆元.md ├── 位运算.md ├── 分数.md ├── 快速幂.md ├── 欧几里得算法.md ├── 欧拉函数.md ├── 素数.md ├── 组合数.md ├── 计算时间复杂度的小 Trick.md ├── 进制转换.md └── 高精度整数运算.md ├── 数据结构 ├── 区间信息维护 │ ├── ST表.md │ ├── 树状数组.md │ └── 线段树.md ├── 堆 │ ├── README.md │ └── 配对堆.md ├── 平衡树 │ └── AVL树.cpp └── 并查集.md └── 计算几何 └── 距离.md /.gitignore: -------------------------------------------------------------------------------- 1 | /.history 2 | /.vscode 3 | /test 4 | # Created by https://www.toptal.com/developers/gitignore/api/c++ 5 | # Edit at https://www.toptal.com/developers/gitignore?templates=c++ 6 | 7 | ### C++ ### 8 | # Prerequisites 9 | *.d 10 | 11 | # Compiled Object files 12 | *.slo 13 | *.lo 14 | *.o 15 | *.obj 16 | 17 | # Precompiled Headers 18 | *.gch 19 | *.pch 20 | 21 | # Linker files 22 | *.ilk 23 | 24 | # Debugger Files 25 | *.pdb 26 | 27 | # Compiled Dynamic libraries 28 | *.so 29 | *.dylib 30 | *.dll 31 | 32 | # Fortran module files 33 | *.mod 34 | *.smod 35 | 36 | # Compiled Static libraries 37 | *.lai 38 | *.la 39 | *.a 40 | *.lib 41 | 42 | # Executables 43 | *.exe 44 | *.out 45 | *.app 46 | 47 | # End of https://www.toptal.com/developers/gitignore/api/c++ 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ACM、OI、OJ、PAT、CSP 题目常用代码模板 2 | 3 | ## 前言 4 | 5 | 本仓库主要提供 ACM、OI、OJ、PAT、CSP 题目中常见算法和数据结构的实现,它们都以**基于 C++11 语法的 C++接口**的形式呈现。如果有问题或者感觉我的代码中有 bug,可以随时提 issue。 6 | 7 | 注意,本仓库不负责讲解任何相关知识点,有关知识点的讲解和汇总可参考[OI wiki](https://oi-wiki.org/)。目前代码仓库还在完善中,部分模板代码还没有完全更新,之后还会添加一批新的模板代码,敬请期待! 8 | 9 | ## 默认代码 10 | 11 | 1. `gg`:表示`long long`类型。本代码仓库中所有整型数据均使用`long long`类型存储,之所以不用 `int` 是因为 `int` 很容易造成数据溢出错误。为了编码方便,可以为`long long`定义一个类型别名`gg`,代码可以是`using gg = long long`。本代码仓库中所有代码均默认导入了这行定义类型别名的代码,即**本代码仓库中所有的`gg`均表示`long long`类型**。 12 | 2. `MAX`:表示问题数据规模的上限。本仓库的代码经常会开一个非常大的全局数组,长度即为`MAX`。在使用这样的代码时,你需要在定义这个数组之前定义并初始化`MAX`变量,代码可以是`constexpr gg MAX = 1e6 + 5`。 13 | 3. `INF`:表示一个正无穷大的正整数。在使用含有`INF`的代码时,你需要在定义这个数组之前定义并初始化`INF`变量,代码可以是`constexpr gg INF = 2e18`。相应地,负无穷小可以用`-INF`表示。 14 | 4. `ni`、`mi`等:表示输入序列所包含的元素个数。例如,`ni`变量代表输入一维数据的总数,`ni`、`mi`分别表示输入二维数据两个维度上的总数,等等。后缀`i`是`input`的缩写,表明该变量是由输入指定的,以此与普通的`n`、`m`等变量相区分。 15 | 16 | 因此,运行本仓库代码前的默认依赖定义如下: 17 | 18 | ```cpp 19 | #include 20 | using namespace std; 21 | using gg = long long; 22 | constexpr gg MAX = 1e6 + 5; 23 | constexpr gg mod = 1e9 + 7; 24 | constexpr gg INF = 2e18; 25 | constexpr double thre = 1e-7; //用以判断两个浮点数是否相等的阈值 26 | gg ti, ni, mi, ki, di, pi, xi, yi; 27 | ``` 28 | 29 | ## 相关链接 30 | 31 | 有关知识点的讲解和汇总可参考[OI wiki](https://oi-wiki.org/),有关练习题目可参考[一个动态更新的洛谷综合题单](https://studyingfather.com/archives/841)。本仓库主要参考了这两个网站的内容,在此对 OI wiki 和综合题单的撰写者和维护者表示诚挚的谢意。 32 | 33 | ## 致谢 34 | 35 | 感谢[CitySkylines](https://github.com/CitySkylines)对仓库代码中的 bug 的提醒。 36 | -------------------------------------------------------------------------------- /media/BallPlacementProblem.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richenyunqi/code-templates/2c0a4ce8d016de4b4ecabedb1eca76adf8b05857/media/BallPlacementProblem.jpg -------------------------------------------------------------------------------- /专题/位运算找出数组中唯一的元素.md: -------------------------------------------------------------------------------- 1 | # 找出数组中唯一的元素 2 | 3 | ## 问题描述 4 | 5 | 给定一个整数数组,一个元素出现`p`次,其余元素出现`k`次。找到出现`p`次的元素( $k\gt 1,p\ge 1,p\ mod\ k\not ={0}$ )。 6 | 7 | ## 思路 8 | 9 | 设需要`m`个计数器 $x_m,x_{m-1},\cdots,x_2,x_1$ ,`m`满足 $m\ge \log_2 k$ 。如果 $2^m\gt k$ ,则说明需要一个`mask`变量,`mask`的计算由`k`的二进制位决定,假设`k=0b101`,则`mask=~(x3&~x2&x1)`;假设`k=0b1100`,则`mask=~(x4&x3&~x2&~x1)`。最后的结果由`p`的二进制位决定,假设`p=0b01`,则结果是`x1`;假设`p=0b10`,则结果是`x2`;假设`p=0b110`,则结果是`x3`或者`x2`。 10 | 11 | ## C++代码 12 | 13 | ```cpp 14 | gg nums[MAX]; 15 | gg findNumber() { 16 | gg xm = 0, ..., x2 = 0, x1 = 0, mask = 0; 17 | for (gg i : nums) { 18 | xm ^= x_{m-1}... & x1 & i; 19 | ... 20 | x2 ^= x1 & i; 21 | x1 ^= i; 22 | mask = ~(xm & ... & x2 & x1); //按要求计算mask 23 | xm &= mask; 24 | ... 25 | x2 &= mask; 26 | x1 &= mask; 27 | } 28 | return xi; //按要求返回一个x变量 29 | } 30 | ``` 31 | -------------------------------------------------------------------------------- /专题/区间问题.md: -------------------------------------------------------------------------------- 1 | # 区间覆盖问题 2 | 3 | 这里用`array`类型存储一个区间,区间起点存储在下标为 0 的位置,区间终点存储在下标为 1 的位置。 4 | 5 | ## 区间完全覆盖问题 6 | 7 | ### 问题描述 8 | 9 | 给定一个区间 $[0,m]$ ,再给出 $n$ 条区间 $[a_i,b_i]$ ,求最少使用多少条区间可以将整个区间完全覆盖。 10 | 11 | ### 贪心策略 12 | 13 | 1. 将每一个区间按照左端点升序排序,如果左端点相同,按右端点升序排序; 14 | 2. 设置一个变量`right`表示已经覆盖的区域的右端点,在左端点小于等于`right`的区间中,每次选择右端点最大且大于`right`的区间。 15 | 16 | 算法的时间复杂度为 $O(n\log n)$ ,需要先排序。 17 | 18 | ### C++代码 19 | 20 | ```cpp 21 | //只覆盖区间内的所有整点 22 | gg inter[MAX][2]; 23 | gg rangeFullCoverage(gg m) { 24 | sort(begin(inter), end(inter)); 25 | gg right = -1, ans = 0, i = 0; 26 | while (right < m) { 27 | gg maxr = 0; 28 | for (; i < inter.size() and inter[i][0] <= right + 1; ++i) { 29 | maxr = max(maxr, inter[i][1]); 30 | } 31 | if (right == maxr) { //如果给出的区间不能完全覆盖[0,m],返回-1 32 | return -1; 33 | } 34 | right = maxr; 35 | ++ans; 36 | } 37 | return ans; 38 | } 39 | ``` 40 | 41 | ```cpp 42 | //覆盖整个区间 43 | gg inter[MAX][2]; 44 | gg rangeFullCoverage(gg m) { 45 | sort(begin(inter), end(inter)); 46 | gg right = 0, ans = 0, i = 0; 47 | while (right < m) { 48 | gg maxr = 0; 49 | for (; i < inter.size() and inter[i][0] <= right; ++i) { 50 | maxr = max(maxr, inter[i][1]); 51 | } 52 | if (right == maxr) { //如果给出的区间不能完全覆盖[0,m],返回-1 53 | return -1; 54 | } 55 | right = maxr; 56 | ++ans; 57 | } 58 | return ans; 59 | } 60 | ``` 61 | 62 | ## 最大不相交问题 63 | 64 | ### 问题描述 65 | 66 | 给出 $n$ 条区间 $[a_i,b_i]$ ,从中选取尽量多的区间,使得这些区间两两没有公共点。 67 | 68 | ### 贪心策略 69 | 70 | 1. 将每一个区间按照右端点升序排序,如果右端点相同,按左端点升序排序; 71 | 2. 设置一个变量`right`表示已经覆盖的区域的右端点,如果当前区间左端点小于`right`,就选取该区间;否则用同样的方式尝试选取其它区间。 72 | 73 | 算法的时间复杂度为 $O(n\log n)$ ,需要先排序。 74 | 75 | ### C++代码 76 | 77 | ```cpp 78 | //覆盖整个区间 79 | gg inter[MAX][2]; 80 | gg rangeDisjoint() { 81 | sort(begin(inter), end(inter), 82 | [](const array& a, const array& b) { 83 | return a[1] != b[1] ? a[1] < b[1] : a[0] < b[0]; 84 | }); 85 | gg ans = 0, right = -1; 86 | for (gg i = 0; i < inter.size(); ++i) { 87 | if (right < inter[i][0]) { 88 | ++ans; 89 | right = inter[i][1]; 90 | } 91 | } 92 | return ans; 93 | } 94 | ``` 95 | 96 | ## 区间选点问题 97 | 98 | ### 问题描述 99 | 100 | 给出 $n$ 条区间 $[a_i,b_i]$ ,要求选取尽量少的点,使得每个区间内都至少有一个点(不同区间内含的点可以是同一个)。 101 | 102 | ### 贪心策略 103 | 104 | 1. 将每一个区间按照左端点升序排序,左端点相同的按右端点升序排序; 105 | 2. 从第一个区间右端点开始往后找,如果下一个区间的左端点大于当前已选区间的右端点,说明要新开一个点,计数器加 1,同时更新右区间能覆盖的最远距离;如果下一个区间右端点小于当前已选区间的右端点,说明共享的区间范围缩短了,那么就更新区间右端点为下一个区间右端点,重复以上操作,直至筛选完所有区间。 106 | 107 | 算法的时间复杂度为 $O(n\log n)$ ,需要先排序。 108 | 109 | ### C++代码 110 | 111 | ```cpp 112 | //区间选点 113 | gg inter[MAX][2]; 114 | gg rangeSelectPoint() { 115 | sort(begin(inter), end(inter)); 116 | gg ans = 0, right = -1; 117 | for (gg i = 0; i < inter.size(); ++i) { 118 | if (right < inter[i][0]) { 119 | ++ans; 120 | } 121 | right = min(inter[i][1], right); 122 | } 123 | return ans; 124 | } 125 | ``` 126 | -------------------------------------------------------------------------------- /专题/格雷码与二进制码.md: -------------------------------------------------------------------------------- 1 | # 格雷码与二进制码 2 | 3 | 在一组数的编码中,若任意两个相邻的代码只有一位二进制数不同,则称这种编码为格雷码(Gray Code)。3 位格雷码如下: 4 | 5 | | 十进制 | 二进制 | 格雷码 | 6 | | ------ | ------ | ------ | 7 | | 0 | 000 | 000 | 8 | | 1 | 001 | 001 | 9 | | 2 | 010 | 011 | 10 | | 3 | 011 | 010 | 11 | | 4 | 100 | 110 | 12 | | 5 | 101 | 111 | 13 | | 6 | 110 | 101 | 14 | | 7 | 111 | 100 | 15 | 16 | 设 $G$ 、 $B$ 为对应的格雷码与二进制码表示,从低位向高位由 0 开始编号,就像下面这样: 17 | | | 2 | 1 | 0 | 18 | | --- | --- | --- | --- | 19 | | $G$ | 0 | 1 | 1 | 20 | | $B$ | 0 | 1 | 0 | 21 | 22 | ## 递归生成格雷码 23 | 24 | 1. 1 位格雷码为 0; 25 | 2. $n+1$ 位格雷码的集合 = $n$ 位格雷码集合(顺序)加前缀 0 + $n$ 位格雷码集合(逆序)加前缀 1。 26 | 27 | ## n 位二进制码转 n 位格雷码 28 | 29 | ### 公式 30 | 31 | $$G=(B>\gt 1)\ xor\ B$$ 32 | $$G_{n-1}=B_{n-1},G_i=B_i\oplus B_{i+1}(0\le i\lt n-1)$$ 33 | 34 | ### C++代码 35 | 36 | ```cpp 37 | gg getGrayFromBinary(gg n) { return n >> 1 ^ n; } 38 | ``` 39 | 40 | ## n 位格雷码转 n 位二进制码 41 | 42 | ### 公式 43 | 44 | $$B=(G>>0)\oplus (G>>1) \oplus \cdots \oplus (G>\gt n)$$ 45 | $$B_{n-1}=G_{n-1},B_i=G_i ㊀ B_{i+1}(0\le i\lt n-1)$$ 46 | 47 | ### C++代码 48 | 49 | ```cpp 50 | gg getBinaryFromGray(gg n) { 51 | gg ans = 0; 52 | while (n != 0) { 53 | ans ^= n; 54 | n /= 2; 55 | } 56 | return ans; 57 | } 58 | ``` 59 | -------------------------------------------------------------------------------- /专题/正整数拆分.md: -------------------------------------------------------------------------------- 1 | # 正整数拆分 2 | 3 | ## 问题描述 4 | 5 | 模板题:[343. 整数拆分 - 力扣](https://leetcode.cn/problems/integer-break/description/) 6 | 7 | 给定一个正整数 n,将其拆分为至少多个正整数的和,并使这些整数的乘积最大化,返回你可以获得的最大乘积。 8 | 9 | ## C++ 代码 10 | 11 | 算法的时间复杂度为 $O(1)$ 12 | 13 | ```cpp 14 | //如果最少分解成2个正整数 15 | gg integerBreak2(gg n, gg mod) { 16 | if (n <= 3) { 17 | return n - 1; 18 | } 19 | gg a = n / 3, b = n % 3; 20 | if (b == 0) { 21 | return powMod(3, a, mod); 22 | } else if (b == 1) { 23 | return (powMod(3, a - 1, mod) * 4) % mod; 24 | } else { 25 | return (powMod(3, a, mod) * 2) % mod; 26 | } 27 | } 28 | //如果最少可以分解成1个正整数 29 | gg integerBreak1(gg n, gg mod) { 30 | if (n == 1) { 31 | return 1; 32 | } 33 | gg a = n / 3, b = n % 3; 34 | if (b == 0) { 35 | return powMod(3, a, mod); 36 | } else if (b == 1) { 37 | return (powMod(3, a - 1, mod) * 4) % mod; 38 | } else { 39 | return (powMod(3, a, mod) * 2) % mod; 40 | } 41 | } 42 | ``` 43 | -------------------------------------------------------------------------------- /专题/求[a,b]区间中 0~9 出现的次数.md: -------------------------------------------------------------------------------- 1 | # 求[a,b]区间中 0~9 出现的次数 2 | 3 | ## 问题描述 4 | 5 | 模板题:[P2602 [ZJOI2010] 数字计数 - 洛谷](https://www.luogu.com.cn/problem/P2602) 6 | 7 | 给定两个正整数 a 和 b,求在 $[a,b]$ 中的所有整数中,0~9 每个数码各出现了多少次。 8 | 9 | ## 思路 10 | 11 | 先计算 $[1,n]$ 区间中 0~9 每个数码出现的次数 $C_n$ ,借鉴前缀和的思想,利用 $C_b-C_{a-1}$ 求出 $[a,b]$ 中 0~9 每个数码出现的总次数。 12 | 13 | 算法的时间复杂度为 $O(10m)$ ,其中 $m$ 为 $n$ 的位数。 14 | 15 | ## C++ 代码 16 | 17 | ```cpp 18 | //计算[1,n]区间内0~9各个数字出现的次数 19 | vector countDigitsUpTo(gg n) { 20 | vector ans(10); 21 | if (n <= 0) { 22 | return ans; 23 | } 24 | gg left = n / 10, cur = n % 10, right = 0, p = 1; 25 | while (left > 0 || cur > 0) { 26 | for (gg digit = 0; digit < 10; ++digit) { 27 | ans[digit] += (left - 1) * p; 28 | if (cur > digit) { 29 | ans[digit] += p; 30 | } else if (cur == digit) { 31 | ans[digit] += (p == 1) ? 1 : (right + 1); 32 | } 33 | if (digit > 0) { 34 | ans[digit] += p; 35 | } 36 | } 37 | right += cur * p; 38 | cur = left % 10; 39 | left /= 10; 40 | p *= 10; 41 | } 42 | return ans; 43 | } 44 | vector countDigitsInRange(gg a, gg b) { 45 | if (a < b) { 46 | swap(a, b); 47 | } 48 | auto ans1 = countDigitsUpTo(a), ans2 = countDigitsUpTo(b - 1); 49 | for (gg i = 0; i < ans1.size(); i++) { 50 | ans1[i] -= ans2[i]; 51 | } 52 | return ans1; 53 | } 54 | ``` 55 | -------------------------------------------------------------------------------- /动态规划/区间动态规划.md: -------------------------------------------------------------------------------- 1 | # 区间动态规划 2 | 3 | ## 合并石子问题 4 | 5 | ### 问题描述 6 | 7 | n 堆石子排成一排,每次只能选相邻的 2 堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。求将 n 堆石子合并成 1 堆的最大得分。 8 | 9 | ### 思路 10 | 11 | 1. 状态含义:设 $dp[left][right]$ 表示将 $[left,right]$ 之间的石子合并成一堆的最大得分。 12 | 2. 边界条件: $dp[i][i]=0$ 13 | 3. 状态转移方程: $dp[left][right]=max(dp[left][right],dp[left][i]+dp[i+1][right]+\sum_{j=left}^{right} a_j$ ,其中 $left\le i\lt right$ , $\sum_{j=left}^{right} a_j$ 可用前缀和以 $O(1)$ 时间复杂度求解。 14 | 4. 算法的时间复杂度为 $O(n^3)$ 15 | 5. C++代码: 16 | 17 | ```cpp 18 | gg intervalDP(){ 19 | for (gg len = 2; len <= ni; ++len) { //枚举区间长度 20 | for (gg left = 1; left <= ni - len + 1; ++left) { //枚举左端点 21 | gg right = left + len - 1; //右端点 22 | for (gg i = left; i < right; ++i) { 23 | dp[left][right] = max(dp[left][right], dp[left][i] + dp[i + 1][right] + sum[right] - sum[left - 1]); 24 | } 25 | } 26 | } 27 | return dp[1][ni]; 28 | } 29 | ``` 30 | -------------------------------------------------------------------------------- /动态规划/线性动态规划.md: -------------------------------------------------------------------------------- 1 | # 线性动态规划 2 | 3 | 本文数组下标均从 1 开始。 4 | 5 | ## 最长上升/不下降子序列(LIS)问题 6 | 7 | 1. 状态含义:设 $dp[i]$ 表示以长度为`i`的上升/不下降子序列中末尾元素的最小值。 8 | 2. 边界条件: $dp[i]=\infty$ 9 | 3. 算法的时间复杂度为 $O(n\log n)$ 10 | 4. C++代码: 11 | 12 | ```cpp 13 | //最长上升子序列 14 | gg dp[MAX]; 15 | gg LIS(vector& nums) { 16 | fill(begin(dp) + 1, begin(dp) + ni + 1, INF); 17 | for (gg i : nums) { 18 | *lower_bound(begin(dp) + 1, begin(dp) + ni + 1, i) = i; 19 | } 20 | return lower_bound(begin(dp) + 1, begin(dp) + ni + 1, INF) - begin(dp) - 1; 21 | } 22 | ``` 23 | 24 | ```cpp 25 | //最长不下降子序列 26 | gg dp[MAX]; 27 | gg LIS(vector& nums) { 28 | fill(begin(dp) + 1, begin(dp) + ni + 1, INF); 29 | for (gg i : nums) { 30 | *upper_bound(begin(dp) + 1, begin(dp) + ni + 1, i) = i; 31 | } 32 | return lower_bound(begin(dp) + 1, begin(dp) + ni + 1, INF) - begin(dp) - 1; 33 | } 34 | ``` 35 | 36 | ## 最长公共子序列(LCS)问题 37 | 38 | 1. 思路:记录下 a 中所有数字的下标,将 b 中数字转换成对应在 a 中的下标,求转换后 b 数组的最长上升子序列。 39 | 2. 算法的时间复杂度为 $O(n\log m)$ 40 | 3. C++代码: 41 | 42 | ```cpp 43 | gg dp[MAX]; 44 | gg longestCommonSubsequence(vector& a, vector& b) { 45 | map> m; 46 | fill(begin(dp) + 1, begin(dp) + ni + 1, INF); 47 | for (gg i = 1; i <= ni; ++i) { 48 | m[a[i]].push_back(i); 49 | } 50 | for (gg i = 1; i <= mi; ++i) { 51 | if (m.count(b[i])) { 52 | for (auto j = m[b[i]].rbegin(); j != m[b[i]].rend(); ++j) { 53 | *lower_bound(begin(dp) + 1, begin(dp) + ni + 1, *j) = *j; 54 | } 55 | } 56 | } 57 | return lower_bound(begin(dp) + 1, begin(dp) + ni + 1, INF) - begin(dp) - 1; 58 | } 59 | ``` 60 | 61 | ## 数的划分问题 62 | 63 | ### 将正整数 n 划分成若干个正整数,这些正整数之和恰好等于 n,求划分方法数 64 | 65 | 划分方法数为 $2^{n-1}$ 66 | 67 | ### 将正整数 n 划分成不超过 m 个正整数,这些正整数之和恰好等于 n,求划分方法数 68 | 69 | 1. 状态含义: $dp[i][j]$ 表示将 正整数 i 划分成 j 个正整数的划分总数。 70 | 2. 边界条件: $dp[i][0]=0,dp[0][0]=1$ 71 | 3. 状态转移方程: 72 | 1. 如果不考虑顺序,即 1+1+2 和 1+2+1 是相同的划分方法, $dp[i][j]=dp[i-j][j]+dp[i][j-1]$ 73 | 2. 如果考虑顺序,即 1+1+2 和 1+2+1 是不同的划分方法, $dp[i][j]=\sum_{k=0}^{i} dp[i-k][j-1]=dp[i-1][j]+dp[i][j-1]$ 74 | 4. 算法的时间复杂度为 $O(nm)$ 75 | 76 | ### 将正整数 n 划分成恰好 m 个正整数,这些正整数之和恰好等于 n,求划分方法数 77 | 78 | 1. 状态含义: $dp[i][j]$ 表示将 正整数 i 划分成 恰好 j 个正整数的划分总数。 79 | 2. 边界条件: $dp[i][0]=0,dp[0][0]=1$ 80 | 3. 状态转移方程: 81 | 1. 如果不考虑顺序,即 1+1+2 和 1+2+1 是相同的划分方法, $dp[i][j]=dp[i-j][j]+dp[i-1][j-1]$ 82 | 2. 如果考虑顺序,即 1+1+2 和 1+2+1 是不同的划分方法, $dp[i][j]=\sum_{k=1}^{i} dp[i-k][j-1]=dp[i-1][j]+dp[i-1][j-1]$ 83 | 4. 算法的时间复杂度为 $O(nm)$ 84 | -------------------------------------------------------------------------------- /动态规划/背包问题.md: -------------------------------------------------------------------------------- 1 | # 背包问题 2 | 3 | ## 0-1 背包问题 4 | 5 | ### 问题描述 6 | 7 | 有 n 种重量和价值分别为 $w_i,v_i$ 的物品。从这些物品中挑选出总重量不超过 $w$ 的物品,求所有挑选方案中价值总和的最大值(物品下标为 1 到 n)。每种物品最多只能选一件。 8 | 9 | ### 从后向前递推 10 | 11 | 1. 状态含义:设 $dp[i][j]$ 表示在第 $i,i+1,\dots,n$ 物品中任选一些总重量不超过为 $j$ 的物品价值总和的最大值。 12 | 2. 边界条件: $dp[n+1][j]=0$ 13 | 3. 状态转移方程: 14 | 1. $j\lt w_i,dp[i][j]=dp[i+1][j]$ 15 | 2. $j\ge w_i,dp[i][j]=\max(dp[i+1][j],dp[i+1][j-w_i]+v_i)$ 16 | 4. 算法时间复杂度均为 $O(nw)$ 17 | 5. C++代码: 18 | 19 | ```cpp 20 | gg backpack01(){ 21 | for (gg i = ni; i >= 1; --i) { 22 | for (gg j = 0; j <= w; ++j) { 23 | dp[i][j] = dp[i + 1][j]; 24 | if (j >= wi[i]) { 25 | dp[i][j] = max(dp[i][j], dp[i + 1][j - wi[i]] + vi[i]); 26 | } 27 | } 28 | } 29 | return dp[1][w]; 30 | } 31 | ``` 32 | 33 | ### 从前向后递推 34 | 35 | 1. 状态含义:设 $dp[i][j]$ 表示在第 $1,\dots,i$ 这 $i$ 个物品中任选一些总重量不超过为 $j$ 的物品价值总和的最大值。 36 | 2. 边界条件: $dp[0][j]=0$ 37 | 3. 状态转移方程: 38 | 1. $j\lt w_i,dp[i][j]=dp[i-1][j]$ 39 | 2. $j\ge w_i,dp[i][j]=\max(dp[i-1][j],dp[i-1][j-w_i]+v_i)$ 40 | 4. 算法时间复杂度均为 $O(nw)$ 41 | 5. C++代码: 42 | 43 | ```cpp 44 | gg backpack01() { 45 | for (gg i = 1; i <= ni; ++i) { 46 | for (gg j = 0; j <= w; ++j) { 47 | dp[i][j] = dp[i - 1][j]; 48 | if (j >= wi[i]) 49 | dp[i][j] = max(dp[i][j], dp[i - 1][j - wi[i]] + vi[i]); 50 | } 51 | } 52 | return dp[ni][w]; 53 | } 54 | ``` 55 | 56 | ### 一维滚动数组 57 | 58 | 算法时间复杂度均为 $O(nw)$ 59 | 60 | ```cpp 61 | gg backpack01() { 62 | for (gg i = 1; i <= ni; ++i) { 63 | for (gg j = w; j >= wi[i]; --j) { 64 | dp[j] = max(dp[j], dp[j - wi[i]] + vi[i]); 65 | } 66 | } 67 | return dp[w]; 68 | } 69 | ``` 70 | 71 | ### 当总重量 w 过大时,更换 DP 对象 72 | 73 | 1. 状态含义:设 $dp[i][j]$ 表示在第 $1,\dots,i$ 这 $i+1$ 个物品中价值总和达到 $j$ 时的最小重量。 74 | 2. 注意 dp 数组第二维长度要等于物品价值总和+1 75 | 3. 边界条件: $dp[0][0]=0,dp[0][j]=\infty$ 76 | 4. 状态转移方程: 77 | 1. $j\lt v_i,dp[i][j]=dp[i-1][j]$ 78 | 2. $j\ge v_i,dp[i][j]=\min(dp[i-1][j],dp[i-1][j-v_i]+w_i)$ 79 | 5. 算法时间复杂度均为 $O(n\sum_{i=0}^{n}v_i)$ 80 | 6. C++代码: 81 | 82 | ```cpp 83 | gg backpack01() { 84 | gg v = accumulate(vi + 1, vi + n + 1, 0ll); 85 | fill(begin(dp[0]), begin(dp[0]) + v + 1, INF); 86 | dp[0][0] = 0; 87 | for (gg i = 1; i <= ni; ++i) { 88 | for (gg j = 0; j <= v; ++j) { 89 | dp[i][j] = dp[i - 1][j]; 90 | if (j >= vi[i]) { 91 | dp[i][j] = min(dp[i][j], dp[i - 1][j - vi[i]] + wi[i]); 92 | } 93 | } 94 | } 95 | for (gg j = v; j >= 0; --j) 96 | if (dp[ni][j] <= w) 97 | return j; 98 | } 99 | ``` 100 | 101 | ## 完全背包问题 102 | 103 | ### 问题描述 104 | 105 | 有 n 种重量和价值分别为 $w_i,v_i$ 的物品。从这些物品中挑选出总重量不超过 $w$ 的物品,求所有挑选方案中价值总和的最大值(物品下标为 1 到 n)。每种物品可以挑选任意多件。 106 | 107 | ### 从后向前递推 108 | 109 | 1. 状态含义:设 $dp[i][j]$ 表示在第 $i,i+1,\dots,n$ 物品中任选一些总重量不超过为 $j$ 的物品价值总和的最大值。 110 | 2. 边界条件: $dp[n+1][j]=0$ 111 | 3. 状态转移方程: $dp[i][j]=\max(dp[i+1][j-k\times w_i]+k\times v_i|k\ge 0)$ 112 | 4. 算法的时间复杂度为 $O(nw^2)$ 113 | 5. C++代码: 114 | 115 | ```cpp 116 | gg backpackComplete(){ 117 | for (gg i = ni; i >= 1; --i) { 118 | for (gg j = 0; j <= w; ++j) { 119 | for (gg k = 0; k * wi[i] <= j; ++k) { 120 | dp[i][j] = max(dp[i][j], dp[i + 1][j - k * wi[i]] + k * vi[i]); 121 | } 122 | } 123 | } 124 | return dp[1][w]; 125 | } 126 | ``` 127 | 128 | ### 从前向后递推 129 | 130 | 1. 状态含义:设 $dp[i][j]$ 表示在第 $1,\dots,i$ 这 $i$ 个物品中任选一些总重量不超过为 $j$ 的物品价值总和的最大值。 131 | 2. 边界条件: $dp[0][j]=0$ 132 | 3. 状态转移方程: $dp[i][j]=\max(dp[i-1][j-k\times w_i]+k\times v_i|0\le k)$ 133 | 4. 算法的时间复杂度为 $O(nw^2)$ 134 | 5. C++代码: 135 | 136 | ```cpp 137 | gg backpackComplete() { 138 | for (gg i = 1; i <= ni; ++i) { 139 | for (gg j = 0; j <= w; ++j) { 140 | for (gg k = 0; k * wi[i] <= j; ++k) { 141 | dp[i][j] = max(dp[i][j], dp[i - 1][j - k * wi[i]] + k * vi[i]); 142 | } 143 | } 144 | } 145 | return dp[ni][w]; 146 | } 147 | ``` 148 | 149 | ### 一维滚动数组 150 | 151 | 算法的时间复杂度为 $O(nw)$ ,与 0-1 背包问题的一维滚动数组差别仅在于 j 循环的方向。 152 | 153 | ```cpp 154 | gg backpackComplete() { 155 | for (gg i = 1; i <= ni; ++i) { 156 | for (gg j = wi[i]; j <= w; ++j) { 157 | dp[j] = max(dp[j], dp[j - wi[i]] + vi[i]); 158 | } 159 | } 160 | return dp[w]; 161 | } 162 | ``` 163 | 164 | ## 分组背包问题 165 | 166 | ### 问题描述 167 | 168 | 有 n 种重量和价值分别为 $w_i,v_i$ 且组号为 $g_i$ 的物品。同一组的物品互相冲突,不能放在一起。从这些物品中挑选出总重量不超过 $w$ 的物品,求所有挑选方案中价值总和的最大值(物品下标为 1 到 n)。每种物品可以挑选任意多件。 169 | 170 | ### 思路 171 | 172 | 这是 0-1 背包问题的变形、因为背包 dp 需要背包空间大小循环完一遍才能表示把一个物体放进去了,而在循环背包空间的过程中枚举物品,就相当于只放一种物品,因此我们可以在循环背包空间的过程外部枚举所有组,在循环背包空间的过程内部枚举同一组的所有物品,然后套用 0-1 背包的模板即可。算法的时间复杂度为 $O(nw)$ 。 173 | 174 | ```cpp 175 | unordered_map> groups; //存储组号和对应组的所有物品索引 176 | gg backpackGroup(){ 177 | for (auto& g : groups) { 178 | for (gg j = w; j >= 0; --j) { 179 | for (gg i : g.second) { 180 | if (j >= wi[i]) { 181 | dp[j] = max(dp[j], dp[j - wi[i]] + vi[i]); 182 | } 183 | } 184 | } 185 | } 186 | } 187 | ``` 188 | 189 | ## 树状+背包 DP 问题 190 | 191 | ### 问题描述 192 | 193 | 模板题:[P2014 [CTSC1997]选课 - 洛谷](https://www.luogu.com.cn/problem/P2014) 194 | 195 | 现在有 N 门功课,每门课有个学分,每门课有一门或没有直接先修课。一个学生要从这些课程里选择 M 门课程学习,问他能获得的最大学分是多少? 196 | 197 | ### 思路 198 | 199 | 我们以 $dp(root,i,j)$ 表示以 $root$ 号点为根的子树中,已经遍历了 $root$ 号点的前 $i$ 棵子树,选了 $j$ 门课程的最大学分。状态转移的过程结合了树形 DP 和背包 DP 的特点,我们枚举 $root$ 点的每个子结点 $v$ ,同时枚举以 $v$ 为根的子树选了几门课程,将子树的结果合并到 $root$ 上。 200 | 201 | 记点 $x$ 的儿子个数为 $s_x$ ,以 $x$ 为根的子树大小为 $size_x$ ,很容易写出下面的转移方程: 202 | 203 | $$dp(root,i,j)=\mathop{max} \limits_{1<=j<=max(m,size_{root}),0\le k\lt max(size_v+1,j)} dp(root,i-1,j-k)+dp(v,s_v,k)$$ 204 | 205 | $dp$ 的第二维可以很轻松地用滚动数组的方式省略掉,注意这时需要倒序枚举 $j$ 的值。 206 | 207 | 时间复杂度为 $O(nm)$ 。注意上面转移方程中的几个限制条件,这些限制条件确保了一些无意义的状态不会被访问到。如果没有这些限制条件,时间复杂度会升高到 $O(nm^2)$ 。 208 | 209 | ```cpp 210 | vector tree[MAX]; 211 | gg dfs(gg root) { 212 | gg s = 1; 213 | for (gg i : tree[root]) { 214 | gg t = dfs(i); 215 | s += t; 216 | for (gg j = min(mi, s); j >= 1; --j) { 217 | for (gg k = 0; k < min(j, t + 1); ++k) { 218 | dp[root][j] = max(dp[root][j], dp[i][k] + dp[root][j - k]); 219 | } 220 | } 221 | } 222 | return s; 223 | } 224 | ``` 225 | -------------------------------------------------------------------------------- /图/README.md: -------------------------------------------------------------------------------- 1 | 假设图中有`n`个结点,`m`条边,结点由`1~n`编号,默认按邻接表存储图。 2 | -------------------------------------------------------------------------------- /图/图的遍历.md: -------------------------------------------------------------------------------- 1 | # 图的遍历 2 | 3 | 算法的时间复杂度为 $O(n+m)$ ,其中 $n$ 为点的个数, $m$ 为边的个数。 4 | 5 | ```cpp 6 | vector graph[MAX]; 7 | bool visit[MAX]; 8 | //图的深度优先遍历 9 | void dfs(gg v) { 10 | visit[v] = true; 11 | cout << v << ' '; //访问v 12 | for (gg i : graph[v]) { 13 | if (not visit[i]) { 14 | dfs(i); 15 | } 16 | } 17 | } 18 | //图的广度优先遍历 19 | bool inQueue[MAX]; 20 | void bfs(gg v) { 21 | queue q; 22 | q.push(v); 23 | inQueue[v] = true; 24 | while (not q.empty()) { 25 | v = q.front(); 26 | q.pop(); 27 | cout << v << ' '; //访问顶点v 28 | for (gg i : graph[v]) { 29 | if (not inQueue[i]) { 30 | q.push(i); 31 | inQueue[i] = true; 32 | } 33 | } 34 | } 35 | } 36 | ``` 37 | -------------------------------------------------------------------------------- /图/差分约束.md: -------------------------------------------------------------------------------- 1 | # 差分约束 2 | 3 | 差分约束系统 是一种特殊的 $n$ 元一次不等式组,它包含 $n$ 个变量 $x_1,x_2,\cdots,x_n$ 以及 $m$ 个约束条件,每个约束条件是由两个其中的变量做差构成的,形如 $x_i-x_j\le c_k$ ,其中 $1<=i,j<=n,i\not ={j}, 1\lt =k\le m$ 并且 $c_k$ 是常数(可以是非负数,也可以是负数)。我们要解决的问题是:求一组解使得所有的约束条件得到满足,否则判断出无解。 4 | 5 | 如果要求差分方程的最大解,用最短路径;如果要求求差分方程的最小解,用最长路径。 6 | 7 | ## 求最大解 8 | 9 | 将方程 $x_i-x_j\le c_k$ 变形为 $x_i\le x_j+c_k$ ,从结点 $j$ 向结点 $i$ 连一条长度为 $c_k$ 的有向边。再建立一个源点,向各结点 $i$ 连长度为 $d_i$ 有向边,利用 Bellman-Ford 算法即可求解,如果图中存在负环,则无解;否则 $x_i=dis[i]$ 即为该差分约束系统在 $x_i\le d_i$ 条件下的最大解。 10 | 11 | ## 求最小解 12 | 13 | 将方程 $x_i-x_j\le c_k$ 变形为 $x_j\ge x_i-c_k$ ,从结点 $i$ 向结点 $j$ 连一条长度为 $-c_k$ 的有向边。再建立一个源点,向各结点 $i$ 连长度为 $d_i$ 有向边,利用 Bellman-Ford 算法即可求解,注意由于是求解最长路,需要将该算法进行一些修改: 14 | 15 | 1. $dis[i]$ 的初始值要从 $\infty$ 变为 $-\infty$ ; 16 | 2. $dis[i]=min(dis[i],dis[j]+\lt i,j\gt )$ 的转化式要变为 $dis[i]=max(dis[i],dis[j]+\lt i,j\gt )$ 。 17 | 18 | 如果图中存在负环,则无解;否则 $x_i=dis[i]$ 即为该差分约束系统在 $x_i\ge d_i$ 条件下的最小解。 19 | -------------------------------------------------------------------------------- /图/拓扑排序.md: -------------------------------------------------------------------------------- 1 | # 拓扑排序 2 | 3 | ## Kahn 算法 4 | 5 | 算法的时间复杂度为 $O(n+m)$ 6 | 7 | ```cpp 8 | //拓扑排序,返回值表示图中是否有圈 9 | //拓扑排序结束后,top中存放拓扑序列,degree存储各节点的入度 10 | vector graph[MAX]; 11 | gg degree[MAX]; 12 | vector top; 13 | bool topSort() { 14 | queue q; //储存入度为0的顶点 15 | for (gg i = 1; i <= ni; ++i) { //将入度为零的顶点放入队列中 16 | if (degree[i] == 0) { 17 | q.push(i); 18 | } 19 | } 20 | while (not q.empty()) { 21 | gg p = q.front(); 22 | q.pop(); 23 | top.push_back(p); 24 | for (gg i : graph[p]) { //遍历该结点的邻接顶点 25 | if (--degree[i] == 0) { //减少邻接顶点的入度,如果入度为零 26 | q.push(i); //压入队列 27 | } 28 | } 29 | } 30 | return top.size() == ni; 31 | } 32 | ``` 33 | 34 | ## dfs 算法 35 | 36 | 算法的时间复杂度为 $O(n+m)$ 37 | 38 | ```cpp 39 | vector graph[MAX]; 40 | gg visit[MAX]; 41 | vector top; 42 | bool dfs(gg v) { 43 | visit[v] = -1; 44 | for (gg i : graph[v]) { 45 | if (visit[i] < 0 or (visit[i] == 0 and not dfs(i))) { 46 | return false; 47 | } 48 | } 49 | visit[v] = 1; 50 | top.push_back(v); 51 | return true; 52 | } 53 | bool topSort() { 54 | for (gg i = 1; i <= ni; ++i) { 55 | if (visit[i] == 0 and not dfs(i)) { 56 | return false; 57 | } 58 | } 59 | reverse(begin(top), end(top)); 60 | return true; 61 | } 62 | ``` 63 | -------------------------------------------------------------------------------- /图/无向图的割点和割边.md: -------------------------------------------------------------------------------- 1 | # 无向图的割点和割边 2 | 3 | 对于一个无向图,如果把一个点删除后这个图的极大连通分量数增加了,那么这个点就是这个图的割点(又称割顶)。 4 | 5 | 对于一个无向图,如果删掉一条边后图中的极大连通分量数增加了,则称这条边为割边或者桥。 6 | 7 | ## Tarjan 算法求割点 8 | 9 | 算法的时间复杂度为 $O(n+m)$ ,其中 $n$ 为点的个数, $m$ 为边的个数。 10 | 11 | ```cpp 12 | vector graph[MAX]; 13 | // dfn表示每个结点的搜索次序 14 | gg dfn[MAX], low[MAX]; 15 | bool visit[MAX], ans[MAX]; // ans标记该结点是否为割点 16 | gg cnt = 0; // cnt表示搜索次序 17 | void dfs(gg v, gg fa) { 18 | visit[v] = true; 19 | low[v] = dfn[v] = ++cnt; 20 | gg child = 0; 21 | for (gg i : graph[v]) { 22 | if (not visit[i]) { 23 | ++child; 24 | dfs(i, v); 25 | low[v] = min(low[v], low[i]); 26 | if (fa != v and low[i] >= dfn[v] and not ans[v]) { 27 | ans[v] = true; 28 | } 29 | } else if (i != fa) { 30 | low[v] = min(low[v], dfn[i]); 31 | } 32 | } 33 | if (fa == v and child >= 2 and not ans[v]) { 34 | ans[v] = true; 35 | } 36 | } 37 | void tarjan() { 38 | for (gg i = 1; i <= ni; ++i) { 39 | if (not visit[i]) { 40 | cnt = 0; 41 | dfs(i, i); 42 | } 43 | } 44 | } 45 | ``` 46 | 47 | ## Tarjan 算法求桥 48 | 49 | 算法的时间复杂度为 $O(n+m)$ ,其中 $n$ 为点的个数, $m$ 为边的个数。 50 | 51 | ```cpp 52 | vector graph[MAX]; 53 | // dfn表示每个结点的搜索次序 54 | gg dfn[MAX], low[MAX], father[MAX], sz[MAX]; 55 | bool ans[MAX]; // 如果ans[i]为true,则表示(father[x],x)为割边 56 | gg cnt = 0; // cnt表示搜索次序 57 | gg cnt_bridge = 0; // cnt_bridge表示割边数 58 | void dfs(gg v, gg fa) { 59 | father[v] = fa; 60 | low[v] = dfn[v] = ++cnt; 61 | sz[v] = 1; 62 | for (gg i : graph[v]) { 63 | if (dfn[i] == 0) { 64 | dfs(i, v); 65 | low[v] = min(low[v], low[i]); 66 | sz[v] += sz[i]; 67 | if (low[i] > dfn[v]) { 68 | ans[i] = true; 69 | // sz[i]表示目前还未添加{v,i}这条边时i所在连通块的结点数,可以在这里添加一些计算连通块结点数的逻辑 70 | ++cnt_bridge; 71 | } 72 | } else if (dfn[i] < dfn[v] and i != fa) { 73 | low[v] = min(low[v], dfn[i]); 74 | } 75 | } 76 | } 77 | void tarjan() { 78 | for (gg i = 1; i <= ni; ++i) { 79 | if (dfn[i] == 0) { 80 | cnt = 0; 81 | dfs(i, i); 82 | } 83 | } 84 | } 85 | ``` 86 | -------------------------------------------------------------------------------- /图/最小生成树.md: -------------------------------------------------------------------------------- 1 | # 最小生成树 2 | 3 | ## Kruskal 算法 4 | 5 | 时间复杂度为 $O(m\log m)$ 。 6 | 7 | ```cpp 8 | struct Edge { //边的类,存储两个端点u,v和边的权值cost 9 | gg u, v, cost; 10 | Edge(gg up, gg vp, gg cp) : u(up), v(vp), cost(cp) {} 11 | }; 12 | vector edges; //存储所有的边 13 | gg ufs[MAX]; //并查集 14 | gg findRoot(gg x) { return ufs[x] == x ? x : ufs[x] = findRoot(ufs[x]); } 15 | //返回的first成员表示最小生成树的权值之和,second成员表示图是否连通 16 | pair Kruskal() { 17 | iota(begin(ufs), end(ufs), 0); //初始化并查集 18 | gg ans = 0, num = 0; 19 | sort(begin(edges), end(edges), 20 | [](const Edge& e1, const Edge& e2) { return e1.cost < e2.cost; }); 21 | for (auto& e : edges) { 22 | gg ua = findRoot(e.u), ub = findRoot(e.v); 23 | if (ua != ub) { 24 | ans += e.cost; 25 | ufs[ua] = ub; 26 | } 27 | } 28 | for (gg i = 1; i <= ni; ++i) { 29 | if (ufs[i] == i) { 30 | ++num; 31 | } 32 | } 33 | return {ans, num == 1}; 34 | } 35 | ``` 36 | 37 | ## Prim 算法 38 | 39 | 如果使用二叉堆等不支持 $O(1)$ 的 decrease-key 的堆,堆优化的 Prim 算法复杂度就不优于 Kruskal 算法,常数也比 Kruskal 大,时间复杂度为 $O((n+m)\log n)$ 。 40 | 41 | ```cpp 42 | struct Edge { 43 | gg to, cost; 44 | Edge(gg t, gg c) : to(t), cost(c) {} 45 | }; 46 | using agg2 = array; 47 | vector graph[MAX]; 48 | bool visit[MAX]; 49 | //返回的first成员表示最小生成树的权值之和,second成员表示图是否连通 50 | pair Prim(gg s) { 51 | gg num = 0, ans = 0; 52 | priority_queue, greater> pq; 53 | pq.push({0, s}); 54 | while (num < ni and not pq.empty()) { 55 | auto p = pq.top(); 56 | pq.pop(); 57 | if (not visit[p[1]]) { 58 | ans += p[0]; 59 | visit[p[1]] = true; 60 | ++num; 61 | for (auto& e : graph[p[1]]) { 62 | if (not visit[e.to]) { 63 | pq.push({e.cost, e.to}); 64 | } 65 | } 66 | } 67 | } 68 | return {ans, num == ni}; 69 | } 70 | ``` 71 | -------------------------------------------------------------------------------- /图/最短路径问题.md: -------------------------------------------------------------------------------- 1 | # 最短路径问题 2 | 3 | ## 问题类型和解决算法一览 4 | 5 | | 算法 | 解决的问题 | 图的特点 | 时间复杂度 | 6 | | ------------ | -------------------- | --------------------------------- | --------------------------------------- | 7 | | BFS | 单源最短路 | 无权图 | $O(n+m)$ | 8 | | 双端队列 BFS | 单源最短路 | 0-1 图(图中边的权值只有 0 和 1) | $O(n+m)$ | 9 | | Dijkstra | 单源最短路 | 非负权图 | $O(m\log m)$ (以 priority_queue 实现) | 10 | | Bellman-Ford | 单源最短路 | 任意图(可以判断负环是否存在) | $O(nm)$ | 11 | | Floyd | 每对结点之间的最短路 | 无负环图 | $O(n^3)$ | 12 | | Johnson | 每对结点之间的最短路 | 无负环图 | $O(nm\log m)$ | 13 | 14 | ## 针对无权图单源最短路的 BFS 算法 15 | 16 | ```cpp 17 | vector graph[MAX]; 18 | gg dis[MAX]; 19 | void bfs(gg v) { 20 | fill(begin(dis), end(dis), INF); 21 | queue q; 22 | q.push(v); 23 | dis[v] = 0; 24 | while (not q.empty()) { 25 | v = q.front(); 26 | q.pop(); 27 | for (gg i : graph[v]) { 28 | if (dis[i] > dis[v] + 1) { 29 | q.push(i); 30 | dis[i] = dis[v] + 1; 31 | } 32 | } 33 | } 34 | } 35 | ``` 36 | 37 | ## 针对 0-1 图单源最短路的双端队列 BFS 算法 38 | 39 | ```cpp 40 | struct Edge { 41 | gg to, cost; 42 | Edge(gg t, gg c) : to(t), cost(c) {} 43 | }; 44 | vector graph[MAX]; 45 | bool inQueue[MAX]; 46 | gg dis[MAX]; 47 | void bfs(gg v) { 48 | fill(begin(dis), end(dis), INF); 49 | deque dq; 50 | dq.push_back(v); 51 | inQueue[v] = true; 52 | dis[0] = 0; 53 | while (not dq.empty()) { 54 | v = dq.front(); 55 | dq.pop_front(); 56 | for (auto& e : graph[v]) { 57 | if (not inQueue[e.to]) { 58 | e.cost == 0 ? dq.push_front(e.to) : dq.push_back(e.to); 59 | dis[e.to] = dis[v] + e.cost; 60 | inQueue[e.to] = true; 61 | } 62 | } 63 | } 64 | } 65 | ``` 66 | 67 | ## 针对正权图单源最短路的 Dijkstra 算法 68 | 69 | ```cpp 70 | using agg2 = array; 71 | struct Edge { 72 | gg to, cost; 73 | Edge(gg t, gg c) : to(t), cost(c) {} 74 | }; 75 | vector graph[MAX]; 76 | gg dis[MAX]; 77 | void Dijkstra(gg s) { 78 | fill(begin(dis), end(dis), INF); 79 | priority_queue, greater> pq; 80 | dis[s] = 0; 81 | pq.push({0, s}); 82 | while (!pq.empty()) { 83 | auto p = pq.top(); 84 | pq.pop(); 85 | if (dis[p[1]] != p[0]) { 86 | continue; 87 | } 88 | for (auto& e : graph[p[1]]) { 89 | if (dis[e.to] > p[0] + e.cost) { 90 | dis[e.to] = p[0] + e.cost; 91 | pq.push({dis[e.to], e.to}); 92 | } 93 | } 94 | } 95 | } 96 | ``` 97 | 98 | ## 针对含负权图单源最短路的 Bellman-Ford 算法 99 | 100 | ```cpp 101 | struct Edge { 102 | gg to, cost; 103 | Edge(gg t, gg c) : to(t), cost(c) {} 104 | }; 105 | vector graph[MAX]; 106 | gg dis[MAX], num[MAX]; // num[i]表示结点i入队了多少次,用来检测是否有负环 107 | bool inQueue[MAX]; 108 | bool bellmanFord(gg s) { //返回false代表s能够到达一个负环 109 | fill(begin(dis), end(dis), INF); 110 | queue q; 111 | dis[s] = 0; 112 | inQueue[s] = true; 113 | q.push(s); 114 | while (!q.empty()) { 115 | gg u = q.front(); 116 | q.pop(); 117 | inQueue[u] = false; 118 | for (auto& e : graph[u]) { 119 | if (dis[u] < INF and dis[e.to] > dis[u] + e.cost) { 120 | dis[e.to] = dis[u] + e.cost; 121 | if (not inQueue[e.to]) { 122 | q.push(e.to); 123 | inQueue[e.to] = true; 124 | if (++num[e.to] > ni) { //入队次数超过图的结点数,代表到达了一个负环 125 | return false; 126 | } 127 | } 128 | } 129 | } 130 | } 131 | return true; 132 | } 133 | ``` 134 | 135 | ## 针对任意图所有结点对之间最短路的 Floyd 算法 136 | 137 | ```cpp 138 | /*图用邻接矩阵存储,dis存储到达所有结点对之间的最短路径长度,初始状态下,dis的值应为: 139 | 若为有权图,dis[i][i]=0,有权的即为权值,其余均为无穷大; 140 | 若为无权图,若i和j之间有边,dis[i][j]=1;其余均为0*/ 141 | gg dis[MAX][MAX]; 142 | void floyd() { 143 | for (gg k = 1; k <= ni; ++k) { 144 | for (gg i = 1; i <= ni; ++i) { 145 | for (gg j = 1; j <= ni; ++j) { 146 | dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]); //有权图用这条语句 147 | dis[i][j] = dis[i][j] or (dis[i][k] and dis[k][j]); //无权图用这条语句 148 | } 149 | } 150 | } 151 | } 152 | ``` 153 | 154 | ## 针对任意图所有结点对之间最短路的 Johnson 算法 155 | 156 | ```cpp 157 | using agg2 = array; 158 | struct Edge { 159 | gg to, cost; 160 | Edge(gg t, gg c) : to(t), cost(c) {} 161 | }; 162 | vector graph[MAX]; 163 | gg dis[MAX], num[MAX], ans[MAX][MAX]; 164 | bool inQueue[MAX]; 165 | bool bellmanFord(gg s) { //返回false代表s能够到达一个负环 166 | fill(begin(dis), end(dis), INF); 167 | queue q; 168 | dis[s] = 0; 169 | inQueue[s] = true; 170 | q.push(s); 171 | while (!q.empty()) { 172 | gg u = q.front(); 173 | q.pop(); 174 | inQueue[u] = false; 175 | for (auto& e : graph[u]) { 176 | if (dis[u] < INF and dis[e.to] > dis[u] + e.cost) { 177 | dis[e.to] = dis[u] + e.cost; 178 | if (not inQueue[e.to]) { 179 | q.push(e.to); 180 | inQueue[e.to] = true; 181 | if (++num[e.to] > ni) { 182 | return false; 183 | } 184 | } 185 | } 186 | } 187 | } 188 | return true; 189 | } 190 | void Dijkstra(gg s, gg* d) { 191 | priority_queue, greater> pq; 192 | d[s] = 0; 193 | pq.push({0, s}); 194 | while (!pq.empty()) { 195 | auto p = pq.top(); 196 | pq.pop(); 197 | if (d[p[1]] != p[0]) { 198 | continue; 199 | } 200 | for (auto& e : graph[p[1]]) { 201 | if (d[e.to] > p[0] + e.cost + dis[p[1]] - dis[e.to]) { 202 | d[e.to] = p[0] + e.cost + dis[p[1]] - dis[e.to]; 203 | pq.push({d[e.to], e.to}); 204 | } 205 | } 206 | } 207 | } 208 | bool johnson() { //返回false代表图中存在负环 209 | for (gg i = 1; i <= ni; ++i) { 210 | for (gg j = 1; j <= ni; ++j) { 211 | ans[i][j] = INF; 212 | } 213 | } 214 | //新建一个虚拟节点0,从这个点向其他所有点连一条边权为0的边。 215 | for (gg i = 1; i <= ni; ++i) { 216 | graph[0].push_back({i, 0}); 217 | } 218 | if (not bellmanFord(0)) { 219 | return false; 220 | } 221 | for (gg i = 1; i <= ni; ++i) { 222 | Dijkstra(i, ans[i]); 223 | } 224 | for (gg i = 1; i <= ni; ++i) { 225 | for (gg j = 1; j <= ni; ++j) { 226 | ans[i][j] -= dis[i] - dis[j]; 227 | } 228 | } 229 | return true; 230 | } 231 | ``` 232 | -------------------------------------------------------------------------------- /图/有向图的强连通分量问题.md: -------------------------------------------------------------------------------- 1 | # 有向图的强连通分量问题 2 | 3 | ## Tarjan 算法 4 | 5 | 算法的时间复杂度为 $O(n+m)$ ,其中 $n$ 为点的个数, $m$ 为边的个数。 6 | 7 | ```cpp 8 | vector graph[MAX]; 9 | // dfn表示每个结点的搜索次序,scc表示每个结点所属的强连通分量编号,sz表示每个强连通分量所含结点个数 10 | gg dfn[MAX], low[MAX], scc[MAX], sz[MAX]; 11 | bool visit[MAX], inStack[MAX]; 12 | gg cnt = 0, sccCnt = 0; // cnt表示搜索次序,scc表示强连通分量编号 13 | stack st; 14 | void dfs(gg v) { 15 | visit[v] = true; 16 | low[v] = dfn[v] = ++cnt; 17 | st.push(v); 18 | inStack[v] = true; 19 | for (gg i : graph[v]) { 20 | if (not visit[i]) { 21 | dfs(i); 22 | low[v] = min(low[v], low[i]); 23 | } else if (inStack[i]) { 24 | low[v] = min(low[v], dfn[i]); 25 | } 26 | } 27 | if (dfn[v] == low[v]) { 28 | ++sccCnt; 29 | while (true) { 30 | gg u = st.top(); 31 | scc[u] = sccCnt; 32 | sz[sccCnt]++; 33 | st.pop(); 34 | inStack[u] = false; 35 | if (u == v) { 36 | break; 37 | } 38 | } 39 | } 40 | } 41 | void tarjan() { 42 | for (gg i = 1; i <= ni; ++i) { 43 | if (not visit[i]) { 44 | dfs(i); 45 | } 46 | } 47 | } 48 | ``` 49 | 50 | ## Kosaraju 算法 51 | 52 | 算法的时间复杂度为 $O(n+m)$ ,其中 $n$ 为点的个数, $m$ 为边的个数。 53 | 54 | ```cpp 55 | // graph2为反向图 56 | vector graph[MAX], graph2[MAX]; 57 | // dfn表示每个结点的搜索次序,scc表示每个结点所属的强连通分量编号,sz表示每个强连通分量所含结点个数 58 | gg scc[MAX], sz[MAX]; 59 | bool visit[MAX]; 60 | vector post; 61 | gg sccCnt = 0; 62 | void dfs1(gg v) { 63 | visit[v] = true; 64 | for (gg i : graph[v]) { 65 | if (not visit[i]) { 66 | dfs1(i); 67 | } 68 | } 69 | post.push_back(v); 70 | } 71 | void dfs2(gg v) { 72 | scc[v] = sccCnt; 73 | ++sz[sccCnt]; 74 | for (gg i : graph2[v]) { 75 | if (scc[i] == 0) { 76 | dfs2(i); 77 | } 78 | } 79 | } 80 | void kosaraju() { 81 | for (gg i = 1; i <= ni; ++i) { 82 | if (not visit[i]) { 83 | dfs1(i); 84 | } 85 | } 86 | reverse(begin(post), end(post)); 87 | for (gg i : post) { 88 | if (scc[i] == 0) { 89 | ++sccCnt; 90 | dfs2(i); 91 | } 92 | } 93 | } 94 | ``` 95 | 96 | ## Garbow 算法 97 | 98 | 算法的时间复杂度为 $O(n+m)$ ,其中 $n$ 为点的个数, $m$ 为边的个数。 99 | 100 | ```cpp 101 | vector graph[MAX]; 102 | // scc表示每个结点所属的强连通分量编号,sz表示每个强连通分量所含结点个数 103 | gg low[MAX], scc[MAX], sz[MAX]; 104 | gg cnt = 0, sccCnt = 0; // cnt表示搜索次序,scc表示强连通分量编号 105 | stack st1, st2; 106 | void dfs(gg v) { 107 | st1.push(v); 108 | st2.push(v); 109 | low[v] = ++cnt; 110 | for (gg i : graph[v]) { 111 | if (low[i] == 0) { 112 | dfs(i); 113 | } else if (scc[i] == 0) { 114 | while (low[st2.top()] > low[i]) { 115 | st2.pop(); 116 | } 117 | } 118 | } 119 | if (st2.top() == v) { 120 | st2.pop(); 121 | sccCnt++; 122 | while (true) { 123 | gg p = st1.top(); 124 | ++sz[sccCnt]; 125 | scc[p] == sccCnt; 126 | st1.pop(); 127 | if (p == v) { 128 | break; 129 | } 130 | } 131 | } 132 | } 133 | void garbow() { 134 | for (gg i = 1; i <= ni; ++i) { 135 | if (low[i] == 0) { 136 | dfs(i); 137 | } 138 | } 139 | } 140 | ``` 141 | -------------------------------------------------------------------------------- /图/树上问题.md: -------------------------------------------------------------------------------- 1 | # 树上问题 2 | 3 | ## 树直径 4 | 5 | 无环连通图中所有最短路径的最大值即为`树直径`,可以用两次 DFS 或者树形 DP 的方法在 $O(n)$ 时间求出树的直径。 6 | 7 | ```cpp 8 | struct Edge { 9 | gg to, cost; 10 | Edge(gg t, gg c) : to(t), cost(c) {} 11 | }; 12 | vector tree[MAX]; 13 | gg dis[MAX]; 14 | void dfs(gg v, gg fa) { 15 | for (auto& e : tree[v]) { 16 | if (e.to != fa) { 17 | dis[e.to] = dis[v] + e.cost; 18 | dfs(e.to, v); 19 | } 20 | } 21 | } 22 | void findTreeDiameter() { 23 | dis[1] = 0; 24 | dfs(1, 0); 25 | gg r = max_element(begin(dis) + 1, begin(dis) + ni + 1) - begin(dis); 26 | dis[r] = 0; 27 | dfs(r, 0); 28 | } 29 | ``` 30 | 31 | ## 最近公共祖先 32 | 33 | ### 倍增算法 34 | 35 | 倍增算法的预处理时间复杂度为 $O(n\log n)$ ,单次查询时间复杂度为 $O(\log n)$ 。 36 | 37 | ```cpp 38 | constexpr gg Max2 = 20; 39 | vector tree[MAX]; 40 | gg depth[MAX], father[MAX][Max2]; 41 | void dfs(gg v, gg fa) { 42 | father[v][0] = fa; 43 | depth[v] = depth[fa] + 1; 44 | for (gg i = 1; i < Max2; ++i) { 45 | father[v][i] = father[father[v][i - 1]][i - 1]; 46 | } 47 | for (gg i : tree[v]) { 48 | if (i != fa) { 49 | dfs(i, v); 50 | } 51 | } 52 | } 53 | gg LCA(gg x, gg y) { 54 | if (depth[x] > depth[y]) { 55 | swap(x, y); 56 | } 57 | gg d = depth[y] - depth[x]; 58 | for (gg i = 0; d > 0; ++i, d >>= 1) { 59 | if (d & 1) { 60 | y = father[y][i]; 61 | } 62 | } 63 | if (x == y) { 64 | return x; 65 | } 66 | for (gg i = Max2 - 1; i >= 0 and x != y; --i) { 67 | if (father[x][i] != father[y][i]) { 68 | x = father[x][i]; 69 | y = father[y][i]; 70 | } 71 | } 72 | return father[x][0]; 73 | } 74 | ``` 75 | 76 | ### Tarjan 算法 77 | 78 | Tarjan 算法是一种离线算法,需要使用并查集记录某个结点的祖先结点。预处理时间复杂度为 $O(n)$ ,处理`m`次查询的时间复杂度为 $O(n+m)$ 。 79 | 80 | ```cpp 81 | struct UFS { 82 | gg ufs[MAX]; 83 | //初始化并查集 84 | UFS() { 85 | iota(begin(ufs), end(ufs), 0); 86 | } 87 | //查找结点所在树的根结点并进行路径压缩 88 | gg findRoot(gg x) { return ufs[x] == x ? x : ufs[x] = findRoot(ufs[x]); } 89 | //合并两个结点所在集合 90 | void unionSets(gg a, gg b) { ufs[findRoot(a)] = findRoot(b); } 91 | }; 92 | UFS ufs; 93 | vector tree[MAX], queryEdge[MAX]; 94 | bool visit[MAX]; 95 | map, gg> ans; 96 | vector> query; 97 | void tarjan(gg v, gg fa) { 98 | visit[v] = true; 99 | for (gg i : tree[v]) { 100 | if (i != fa) { 101 | tarjan(i, v); 102 | ufs.unionSets(i, v); 103 | } 104 | } 105 | for (gg i : queryEdge[v]) { 106 | if (visit[i]) { 107 | ans[{i, v}] = ufs.findRoot(i); 108 | ans[{v, i}] = ufs.findRoot(i); 109 | } 110 | } 111 | } 112 | ``` 113 | -------------------------------------------------------------------------------- /图/欧拉图.md: -------------------------------------------------------------------------------- 1 | # 欧拉图 2 | 3 | ## 定义 4 | 5 | 1. 通过图中所有边恰好一次且行遍所有顶点的通路称为欧拉通路。 6 | 2. 通过图中所有边恰好一次且行遍所有顶点的回路称为欧拉回路。 7 | 3. 具有欧拉回路的无向图或有向图称为欧拉图。 8 | 4. 具有欧拉通路但不具有欧拉回路的无向图或有向图称为半欧拉图。 9 | 10 | ## 判别法 11 | 12 | 1. 一个无向图是欧拉图,当且仅当图是连通的且没有奇度顶点。一个无向图是半欧拉图当且仅当图是连通的且图中恰有 0 个或 2 个奇度顶点,且必须从其中一个奇度顶点出发,另一个奇度顶点终止。 13 | 2. 一个无向图是欧拉图当且仅当图的所有顶点属于同一个强连通分量且每个顶点的入度和出度相同。一个有向图是半欧拉图当且仅当 14 | 1. 如果将图中的所有有向边退化为无向边时,那么图的所有顶点属于同一个连通分量。 15 | 2. 最多只有一个顶点的入度比出度小 1,最多只有一个顶点的出度比入度小 1,且必须入度比出度小 1 的结点出发,入度比出度大 1 的结点终止。 16 | 3. 所有其他顶点的入度和出度相同。 17 | 18 | ## DFS 求欧拉回路或欧拉路 19 | 20 | 算法的时间复杂度为 $O(n+m)$ ,其中 $n$ 为点的个数, $m$ 为边的个数。 21 | 22 | ```cpp 23 | struct Edge { 24 | gg from, to; 25 | bool visit; 26 | Edge(gg f, gg t, bool v = false) : from(f), to(t), visit(v) {} 27 | }; 28 | vector edges; 29 | vector graph[MAX]; //注意graph中存储的是边在edges中的下标 30 | vector ans; //存储欧拉序列 31 | //注意dfs之后,ans数组必须进行一次翻转才是正确的欧拉序列 32 | void dfs(gg v) { 33 | for (gg i : graph[v]) { 34 | if (not edges[i].visit) { 35 | edges[i].visit = true; 36 | dfs(v == edges[i].from ? edges[i].to : edges[i].from); 37 | } 38 | } 39 | ans.push_back(v); 40 | } 41 | ``` 42 | -------------------------------------------------------------------------------- /图/网络流问题/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richenyunqi/code-templates/2c0a4ce8d016de4b4ecabedb1eca76adf8b05857/图/网络流问题/README.md -------------------------------------------------------------------------------- /图/网络流问题/最大流问题/使用BFS的Edmonds-Karp算法.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richenyunqi/code-templates/2c0a4ce8d016de4b4ecabedb1eca76adf8b05857/图/网络流问题/最大流问题/使用BFS的Edmonds-Karp算法.cpp -------------------------------------------------------------------------------- /图/网络流问题/最小费用最大流问题/使用Bellman-Ford的Edmonds-Karp算法.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richenyunqi/code-templates/2c0a4ce8d016de4b4ecabedb1eca76adf8b05857/图/网络流问题/最小费用最大流问题/使用Bellman-Ford的Edmonds-Karp算法.cpp -------------------------------------------------------------------------------- /基础算法/二分查找.md: -------------------------------------------------------------------------------- 1 | ## 二分查找 2 | 3 | 二分查找算法的时间复杂度为 $O(\log n)$ 4 | 5 | ## 在升序序列中二分查找某数 x 的位置 6 | 7 | ```cpp 8 | //在升序序列中二分查找某数 x 的位置,二分区间为 [left,right],如果不存在,返回-1 9 | gg v[MAX]; 10 | gg binarySearch(gg left, gg right, gg x) { 11 | while (left <= right) { 12 | gg mid = (left + right) / 2; 13 | if (v[mid] == x) 14 | return mid; 15 | else if (v[mid] < x) 16 | left = mid + 1; 17 | else 18 | right = mid - 1; 19 | } 20 | return -1; 21 | } 22 | ``` 23 | 24 | ## 在非降序序列中二分查找第一个大于等于 x 的位置 25 | 26 | ```cpp 27 | //在非降序序列中二分查找第一个大于等于x的位置,二分区间为[left, right] 28 | //如果不存在这样的元素,返回-1 29 | gg v[MAX]; 30 | gg lowerBound(gg left, gg right, gg x) { 31 | while (left < right) { 32 | gg mid = (left + right) / 2; 33 | if (v[mid] >= x) 34 | right = mid; 35 | else 36 | left = mid + 1; 37 | } 38 | return left > right or v[left] >= x ? -1: left; 39 | } 40 | 41 | ``` 42 | -------------------------------------------------------------------------------- /基础算法/二维前缀和与二维差分.md: -------------------------------------------------------------------------------- 1 | # 二维前缀和 2 | 3 | ## 问题描述 4 | 5 | 给出一个 $m\times n$ 的矩阵 a 以及四个整数 $r_1,c_1,r_2,c_2(1\le r_1 \le r_2 \lt =m,1\le c_1 \le c_2 \le n)$ ,求 $\sum_{i=r_1}^{r_2}\sum_{j=c_1}^{c_2} a[i][j]$ 的值。注意,这里的下标都从 1 开始。 6 | 7 | ## 简析 8 | 9 | 定义一个矩阵 preSum 存储 a 的二维前缀和,即令 $preSum[i][j]$ 表示 $\sum_{r=1}^i\sum_{c=1}^j mat[i][j]$ 的值,其中, $1<=i<=m,0\lt =j\le n$ 。可以用递推公式 $preSum[i][j] = preSum[i - 1][j] + preSum[i][j - 1] - preSum[i - 1][j - 1] + a[i][j]$ 对 $preSum[i][j]$ 进行计算。建立二维前缀和数组的时间复杂度为 $O(mn)$ 。 10 | 11 | 那么,题目要求的解为 $preSum[r2][c2] - preSum[r1 - 1][c2] - preSum[r2][c1 - 1] + preSum[r1 - 1][c1 - 1]$ ,建立好二维前缀和数组后,每次查询的时间复杂度为 $O(1)$ 。 12 | 13 | ## C++代码 14 | 15 | ```cpp 16 | gg a[MAX][MAX], preSum[MAX][MAX]; 17 | //计算前缀和 18 | void getPrefixSum(gg m, gg n) { 19 | for (gg i = 1; i <= m; ++i) { 20 | for (gg j = 1; j <= n; ++j) { 21 | preSum[i][j] = preSum[i - 1][j] + preSum[i][j - 1] - preSum[i - 1][j - 1] + a[i][j]; 22 | } 23 | } 24 | } 25 | //计算将左上角为(r1,c1),右下角为(r2,c2)的矩阵的和 26 | gg getSum(gg r1, gg c1, gg r2, gg c2) { 27 | return preSum[r2][c2] - preSum[r1 - 1][c2] - preSum[r2][c1 - 1] + preSum[r1 - 1][c1 - 1]; 28 | } 29 | ``` 30 | 31 | # 二维差分 32 | 33 | ## 问题描述 34 | 35 | 给出一个 $m\times n$ 的零矩阵 a 以及多个修改操作,每个修改操作将以左上角坐标 $(r_1,c_1)$ ,右下角坐标 $(r_2,c_2)$ 的矩阵中所有的值都增加 $v$ , $(1\le r_1 \le r_2 \lt =m, 1\le c_1 \le c_2 \le n)$ ,求所有修改操作结束后矩阵 a 的值。注意,这里的下标都从 1 开始。 36 | 37 | ## 简析 38 | 39 | 利用差分的思想进行修改操作,然后利用前缀和的思想查询。能够保证每次修改操作的时间复杂度为 $O(1)$ ,查询操作的时间复杂度为 $O(mn)$ 。 40 | 41 | ## C++代码 42 | 43 | ```cpp 44 | gg a[MAX][MAX], preSum[MAX][MAX]; 45 | //计算差分,将左上角为(r1,c1),右下角为(r2,c2)的矩阵的值都加上v 46 | void update(gg r1, gg c1, gg r2, gg c2, gg v) { 47 | preSum[r1][c1] += v; 48 | preSum[r1][c2 + 1] -= v; 49 | preSum[r2 + 1][c1] -= v; 50 | preSum[r2 + 1][c2 + 1] += v; 51 | } 52 | //计算矩阵 53 | void getResult(gg m, gg n) { 54 | for (gg i = 1; i <= m; ++i) { 55 | for (gg j = 1; j <= n; ++j) { 56 | a[i][j] = preSum[i][j] + a[i][j - 1] + a[i - 1][j] - a[i - 1][j - 1]; 57 | } 58 | } 59 | } 60 | ``` 61 | -------------------------------------------------------------------------------- /基础算法/划分算法.md: -------------------------------------------------------------------------------- 1 | # 划分算法 2 | 3 | 假设划分元为$pivot$,这里提供的划分算法,会将数组 nums 分成 $\lt pivot$ 、 $== pivot$ 、 $\gt pivot$ 三部分,返回一个 array,表示 $== pivot$ 的部分的首元素索引、尾元素的下一个元素的索引。时间复杂度为 $O(n)$ 。 4 | 5 | ```cpp 6 | gg nums[MAX]; 7 | array partition(gg pivot) { 8 | gg i = 0, j = 0, k = nums.size() - 1; 9 | while (i <= k) 10 | if (nums[i] > pivot) 11 | swap(nums[i], nums[k--]); 12 | else if (nums[i] < pivot) 13 | swap(nums[i++], nums[j++]); 14 | else 15 | ++i; 16 | return {j, i}; 17 | } 18 | ``` 19 | -------------------------------------------------------------------------------- /基础算法/子集生成和全排列.md: -------------------------------------------------------------------------------- 1 | # 子集生成和全排列 2 | 3 | ## 子集生成 4 | 5 | 使用`vector>`类型的`ans`变量存储最终产生的所有子集。 6 | 7 | ### 无重复元素 8 | 9 | #### 递归构造法 10 | 11 | ```cpp 12 | vector> ans; 13 | vector temp; 14 | void sub(vector& nums, gg p) { 15 | ans.push_back(temp); 16 | for (gg i = p; i < nums.size(); ++i) { 17 | temp.push_back(nums[i]); 18 | sub(nums, i + 1); 19 | temp.pop_back(); 20 | } 21 | } 22 | vector> subsets(vector& nums) { 23 | sub(nums, 0); 24 | return ans; 25 | } 26 | ``` 27 | 28 | #### 位向量法 29 | 30 | ```cpp 31 | vector> ans; 32 | void sub(vector& nums, vector& f, gg p) { 33 | if (p == nums.size()) { 34 | ans.push_back({}); 35 | for (gg i = 0; i < f.size(); ++i) { 36 | if (f[i]) { 37 | ans.back().push_back(nums[i]); 38 | } 39 | } 40 | return; 41 | } 42 | f[p] = true; 43 | sub(nums, f, p + 1); 44 | f[p] = false; 45 | sub(nums, f, p + 1); 46 | } 47 | vector> subsets(vector& nums) { 48 | vector f(nums.size()); 49 | sub(nums, f, 0); 50 | return ans; 51 | } 52 | ``` 53 | 54 | #### 迭代构造法 55 | 56 | ```cpp 57 | vector> subsets(vector& nums) { 58 | vector> ans{{}}; 59 | for (gg i = 0; i < nums.size(); ++i) { 60 | gg s = ans.size(); 61 | for (gg j = 0; j < s; ++j) { 62 | ans.push_back(ans[j]); 63 | ans.back().push_back(nums[i]); 64 | } 65 | } 66 | return ans; 67 | } 68 | ``` 69 | 70 | #### 二进制法 71 | 72 | ```cpp 73 | vector> subsets(vector& nums) { 74 | gg n = nums.size(); 75 | vector> ans(1 << n); 76 | for (gg i = 0; i < (1 << n); ++i) { 77 | for (gg j = 0; j < n; ++j) { 78 | if ((i >> j) & 1) { 79 | ans[i].push_back(nums[j]); 80 | } 81 | } 82 | } 83 | return ans; 84 | } 85 | ``` 86 | 87 | ### 有重复元素 88 | 89 | #### 递归构造法 90 | 91 | ```cpp 92 | vector> ans; 93 | vector temp; 94 | void sub(vector& nums, gg p) { 95 | ans.push_back(temp); 96 | for (gg i = p; i < nums.size(); ++i) { 97 | if (i > p and nums[i] == nums[i - 1]) { 98 | continue; 99 | } 100 | temp.push_back(nums[i]); 101 | sub(nums, i + 1); 102 | temp.pop_back(); 103 | } 104 | } 105 | vector> subsetsWithDup(vector& nums) { 106 | sort(begin(nums), end(nums)); 107 | sub(nums, 0); 108 | return ans; 109 | } 110 | ``` 111 | 112 | #### 位向量法 113 | 114 | ```cpp 115 | vector> ans; 116 | void sub(vector& nums, vector& f, gg p) { 117 | if (p == nums.size()) { 118 | ans.push_back({}); 119 | for (gg i = 0; i < f.size(); ++i) { 120 | if (f[i]) { 121 | ans.back().push_back(nums[i]); 122 | } 123 | } 124 | return; 125 | } 126 | gg j = find_if(begin(nums) + p, end(nums), 127 | [&nums, p](gg a) { return a != nums[p]; }) - 128 | begin(nums); 129 | for (gg i = p; i < j; ++i) { 130 | f[i] = true; 131 | sub(nums, f, j); 132 | } 133 | for (gg i = p; i < j; ++i) { 134 | f[i] = false; 135 | } 136 | sub(nums, f, j); 137 | } 138 | vector> subsetsWithDup(vector& nums) { 139 | vector f(nums.size()); 140 | sort(begin(nums), end(nums)); 141 | sub(nums, f, 0); 142 | return ans; 143 | } 144 | ``` 145 | 146 | #### 迭代构造法 147 | 148 | ```cpp 149 | vector> subsetsWithDup(vector& nums) { 150 | vector> ans{{}}; 151 | sort(begin(nums), end(nums)); 152 | gg s = 1; 153 | for (gg i = 0; i < nums.size(); ++i) { 154 | gg k = ans.size(); 155 | if (i == 0 or nums[i] != nums[i - 1]) { 156 | s = k; 157 | } 158 | for (gg j = k - s; j < k; ++j) { 159 | ans.push_back(ans[j]); 160 | ans.back().push_back(nums[i]); 161 | } 162 | } 163 | return ans; 164 | } 165 | ``` 166 | 167 | ## 全排列 168 | 169 | 使用`vector>`类型的`ans`变量存储最终产生的所有排列。 170 | 171 | ### 无重复元素 172 | 173 | ```cpp 174 | //如果要求字典序输出全排列,调用之前请先对nums排序 175 | vector> ans; 176 | void per(vector& nums, gg p) { 177 | if (p == nums.size()) { 178 | ans.push_back(nums); 179 | return; 180 | } 181 | for (gg i = p; i < nums.size(); ++i) { 182 | swap(nums[i], nums[p]); 183 | per(nums, p + 1); 184 | swap(nums[i], nums[p]); 185 | } 186 | } 187 | vector> permute(vector& nums) { 188 | per(nums, 0); 189 | return ans; 190 | } 191 | ``` 192 | 193 | ### 有重复元素 194 | 195 | ```cpp 196 | vector> ans; 197 | void per(vector nums, gg p) { 198 | if (p == nums.size()) { 199 | ans.push_back(nums); 200 | return; 201 | } 202 | for (gg i = p; i < nums.size(); ++i) { 203 | if (i == p or nums[i] != nums[p]) { 204 | swap(nums[i], nums[p]); 205 | per(nums, p + 1); 206 | } 207 | } 208 | } 209 | vector> permuteUnique(vector& nums) { 210 | sort(begin(nums), end(nums)); 211 | per(nums, 0); 212 | return ans; 213 | } 214 | ``` -------------------------------------------------------------------------------- /基础算法/快读.cpp: -------------------------------------------------------------------------------- 1 | inline gg read() { 2 | gg x = 0, f = 1; 3 | char ch = cin.get(); 4 | while (!isdigit(ch)) { 5 | if (ch == '-') 6 | f = -1; 7 | ch = cin.get(); 8 | } 9 | while (isdigit(ch)) { 10 | x = x * 10 + ch - 48; 11 | ch = cin.get(); 12 | } 13 | return x * f; 14 | } -------------------------------------------------------------------------------- /基础算法/排序算法.md: -------------------------------------------------------------------------------- 1 | # 内部排序算法 2 | 3 | 可以使用[洛谷题目 P1177 【模板】快速排序](https://www.luogu.com.cn/problem/P1177)验证自己编写的算法是否正确(时间复杂度过高的排序算法会 TLE)。 4 | 5 | ## 算法分类 6 | 7 | 排序算法可以分为以下 5 类: 8 | 9 | 1. 插入类排序:直接插入排序、希尔排序 10 | 2. 选择类排序:简单选择排序、堆排序 11 | 3. 交换类排序:冒泡排序、快速排序 12 | 4. 归并类排序:归并排序 13 | 5. 基数类排序:基数排序 14 | 15 | **注:本博客编写的算法均是针对`vector`类型数据进行从小到大排序** 16 | 17 | ## 八大内部排序算法的 C++代码 18 | 19 | ### 直接插入排序 20 | 21 | ```cpp 22 | void InsertSort(vector& v) { 23 | for (gg i = 0; i < v.size(); ++i) { 24 | gg t = v[i], j; 25 | for (j = i; j >= 1 and v[j - 1] > t; --j) { 26 | v[j] = v[j - 1]; 27 | } 28 | v[j] = t; 29 | } 30 | } 31 | ``` 32 | 33 | ### 希尔排序 34 | 35 | 直接插入排序就是增量为 1 时的希尔排序,二者的代码非常相似,可类比记忆。 36 | 以下代码采取希尔提出的选取增量的方法,即每次增量分别为: 37 | $$\lfloor n/2 \rfloor、\lfloor n/4 \rfloor、··· ···、\lfloor n/2^k \rfloor、··· ···、2、1$$ 38 | 39 | ```cpp 40 | void ShellSort(vector& v) { 41 | for (gg inc = v.size() / 2; inc >= 1; inc /= 2) { 42 | for (gg i = inc; i < v.size(); ++i) { 43 | gg t = v[i], j; 44 | for (j = i; j >= inc and v[j - inc] > t; j -= inc) { 45 | v[j] = v[j - inc]; 46 | } 47 | v[j] = t; 48 | } 49 | } 50 | } 51 | ``` 52 | 53 | ### 简单选择排序 54 | 55 | ```cpp 56 | void SelectSort(vector& v) { 57 | for (gg i = 0; i < v.size(); ++i) { 58 | gg k = i; 59 | for (gg j = i + 1; j < v.size(); ++j) { 60 | if (v[j] < v[k]) { 61 | k = j; 62 | } 63 | } 64 | gg t = v[k]; 65 | v[k] = v[i]; 66 | v[i] = t; 67 | } 68 | } 69 | ``` 70 | 71 | ### 堆排序 72 | 73 | 以下代码建立的是大根堆 74 | 75 | ```cpp 76 | void Down(vector& v, gg n, gg p) { 77 | gg t = v[p]; 78 | while (2 * p + 1 < n) { 79 | gg child = 2 * p + 1; 80 | if (child + 1 < n and v[child] < v[child + 1]) { 81 | ++child; 82 | } 83 | if (t < v[child]) { 84 | v[p] = v[child]; 85 | p = child; 86 | } else { 87 | break; 88 | } 89 | } 90 | v[p] = t; 91 | } 92 | void HeapSort(vector& v) { 93 | for (gg i = v.size() / 2; i >= 0; --i) { 94 | Down(v, v.size(), i); 95 | } 96 | for (gg i = v.size() - 1; i > 0; --i) { 97 | gg t = v[i]; 98 | v[i] = v[0]; 99 | v[0] = t; 100 | Down(v, i, 0); 101 | } 102 | } 103 | ``` 104 | 105 | ### 冒泡排序 106 | 107 | 以下代码以**一趟排序过程中如果没有发生关键字交换**作为冒泡排序结束的标志。 108 | 109 | ```cpp 110 | void BubbleSort(vector& v) { 111 | for (gg i = 0; i < v.size(); ++i) { 112 | bool flag = true; //标记是否有关键字交换 113 | for (gg j = 1; j < v.size() - i; ++j) { 114 | if (v[j] < v[j - 1]) { 115 | flag = false; 116 | gg t = v[j]; 117 | v[j] = v[j - 1]; 118 | v[j - 1] = t; 119 | } 120 | } 121 | if (flag) { 122 | break; 123 | } 124 | } 125 | } 126 | ``` 127 | 128 | ### 快速排序 129 | 130 | 以下代码选择每个序列的第一个数字作为划分元素 131 | 132 | ```cpp 133 | void QSort(vector& v, gg left, gg right) { 134 | if (left >= right) 135 | return; 136 | gg pivot = v[left], i = left, j = right; 137 | while (i < j) { 138 | while (i < j and v[j] >= pivot) { 139 | --j; 140 | } 141 | if (i < j) { 142 | v[i++] = v[j]; 143 | } 144 | while (i < j and v[i] <= pivot) { 145 | ++i; 146 | } 147 | if (i < j) { 148 | v[j--] = v[i]; 149 | } 150 | } 151 | v[i] = pivot; 152 | QSort(v, left, i - 1); 153 | QSort(v, i + 1, right); 154 | } 155 | void QuickSort(vector& v) { QSort(v, 0, v.size() - 1); } 156 | ``` 157 | 158 | ### 归并排序 159 | 160 | ```cpp 161 | void Merge(vector& v, vector& a, gg left, gg mid, gg right) { 162 | for (gg i = left, j = mid + 1, k = left; k <= right; ++k) { 163 | if (i > mid) { 164 | a[k] = v[j++]; 165 | } else if (j > right) { 166 | a[k] = v[i++]; 167 | } else if (v[i] < v[j]) { 168 | a[k] = v[i++]; 169 | } else { 170 | a[k] = v[j++]; 171 | } 172 | } 173 | for (gg i = left; i <= right; ++i) { //将合并后的有序序列拷贝回原数组 174 | v[i] = a[i]; 175 | } 176 | } 177 | void MSort(vector& v, vector& a, gg left, gg right) { 178 | if (left >= right) { //待排序序列中只有0或1个关键字,不必排序,直接返回 179 | return; 180 | } 181 | gg mid = (left + right) / 2; //中间位置 182 | MSort(v, a, left, mid); //递归排序左半部分 183 | MSort(v, a, mid + 1, right); //递归排序右半部分 184 | Merge(v, a, left, mid, right); //合并两个有序序列 185 | } 186 | void MergeSort(vector& v) { 187 | vector a(v.size()); //合并两个有序序列时的输出序列 188 | MSort(v, a, 0, v.size() - 1); 189 | } 190 | ``` 191 | 192 | ### 基数排序 193 | 194 | 以下代码假设待排序序列以 10 为基数,且使用了 stl 中的队列 queue 作为桶 195 | 196 | ```cpp 197 | //返回数组中最大值的位数,即基数排序的趟数 198 | gg getNum(vector& v, gg radix = 10) { 199 | gg t = v[0]; 200 | for (gg i = 1; i < v.size(); ++i) { 201 | if (v[i] > t) { 202 | t = v[i]; 203 | } 204 | } 205 | gg i = 0; 206 | for (i = 0; t != 0; t /= radix) { 207 | ++i; 208 | } 209 | return i; 210 | } 211 | void RadixSort(vector& v, gg radix = 10) { 212 | gg num = getNum(v); 213 | queue q[radix]; 214 | for (gg i = 0, factor = 1; i < num; ++i, factor *= radix) { 215 | for (gg j = 0; j < v.size(); ++j) { 216 | q[v[j] / factor % radix].push(v[j]); 217 | } 218 | for (gg j = 0, k = 0; j < radix; ++j) 219 | while (!q[j].empty()) { 220 | v[k++] = q[j].front(); 221 | q[j].pop(); 222 | } 223 | } 224 | } 225 | ``` 226 | -------------------------------------------------------------------------------- /基础算法/整数取整.md: -------------------------------------------------------------------------------- 1 | # 整数取整 2 | 3 | 这里提供的整数取整方法可以对负数进行取整。 4 | 5 | ## 求 $\lceil \frac{n}{m} \rceil$ 6 | 7 | ```cpp 8 | gg up(gg n, gg m) { return n >= 0 ? (n + m - 1) / m : n / m; } 9 | ``` 10 | 11 | ## 求 $\lfloor \frac{n}{m} \rfloor$ 12 | 13 | ```cpp 14 | gg down(gg n, gg m) { return n >= 0 ? n / m : (n - m + 1) / m; } 15 | ``` 16 | -------------------------------------------------------------------------------- /基础算法/日期处理.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richenyunqi/code-templates/2c0a4ce8d016de4b4ecabedb1eca76adf8b05857/基础算法/日期处理.cpp -------------------------------------------------------------------------------- /基础算法/树/二叉树.md: -------------------------------------------------------------------------------- 1 | # 二叉树 2 | 3 | ## 结点类定义 4 | 5 | ```cpp 6 | struct BTNode { 7 | gg val; 8 | BTNode *left, *right; 9 | BTNode(gg v, BTNode* l = nullptr, BTNode* r = nullptr) : val(v) {} 10 | }; 11 | ``` 12 | 13 | ## 深度优先遍历(DFS)的递归写法 14 | 15 | 算法的时间复杂度为 $O(n)$ 16 | 17 | ```cpp 18 | //二叉树的先根遍历 19 | void preOrder(BTNode* root) { 20 | if (not root) //是空树直接返回 21 | return; 22 | cout << root->val << ' '; //访问根结点 23 | preOrder(root->left); //递归遍历左子树 24 | preOrder(root->right); //递归遍历右子树 25 | } 26 | //二叉树的中根遍历 27 | void inOrder(BTNode* root) { 28 | if (not root) //是空树直接返回 29 | return; 30 | inOrder(root->left); //递归遍历左子树 31 | cout << root->val << ' '; //访问根结点 32 | inOrder(root->right); //递归遍历右子树 33 | } 34 | //二叉树的先根遍历 35 | void postOrder(BTNode* root) { 36 | if (not root) //是空树直接返回 37 | return; 38 | postOrder(root->left); //递归遍历左子树 39 | postOrder(root->right); //递归遍历右子树 40 | cout << root->val << ' '; //访问根结点 41 | } 42 | ``` 43 | 44 | ## 深度优先遍历(DFS)的非递归写法 45 | 46 | 算法的时间复杂度为 $O(n)$ 47 | 48 | ```cpp 49 | //二叉树的先根遍历 50 | void preOrder(BTNode* root) { 51 | stack st; 52 | while (root or not st.empty()) { 53 | while (root) { 54 | cout << root->val << " "; 55 | st.push(root); 56 | root = root->left; 57 | } 58 | root = st.top(); 59 | st.pop(); 60 | root = root->right; 61 | } 62 | } 63 | //二叉树的中根遍历 64 | //注意中根遍历和先根遍历代码的差别仅仅在于ans.push_back(root->val);的位置 65 | void inOrder(BTNode* root) { 66 | stack st; 67 | while (root or not st.empty()) { 68 | while (root) { 69 | st.push(root); 70 | root = root->left; 71 | } 72 | root = st.top(); 73 | st.pop(); 74 | cout << root->val << " "; 75 | root = root->right; 76 | } 77 | } 78 | //二叉树的后根遍历 79 | //注意后根遍历和先根遍历代码的差别在于先将右子树入栈,再将左子树入栈,最后进行一次翻转 80 | void postOrder(BTNode* root) { 81 | vector ans; 82 | stack st; 83 | while (root or not st.empty()) { 84 | while (root) { 85 | ans.push_back(root->val); 86 | st.push(root); 87 | root = root->right; 88 | } 89 | root = st.top(); 90 | st.pop(); 91 | root = root->left; 92 | } 93 | reverse(begin(ans), end(ans)); 94 | for(gg i: ans){ 95 | cout << i << " "; 96 | } 97 | } 98 | ``` 99 | 100 | ## 广度优先遍历(BFS) 101 | 102 | 算法的时间复杂度为 $O(n)$ 103 | 104 | ```cpp 105 | //二叉树的层次遍历 106 | void levelOrder(BTNode* root) { 107 | queue q; 108 | q.push(root); 109 | while (not q.empty()) { 110 | gg s = q.size(); 111 | while (s--) { 112 | auto t = q.front(); 113 | q.pop(); 114 | cout << t->val << (s == 0 ? '\n' : ' '); 115 | if (t->left) 116 | q.push(t->left); 117 | if (t->right) 118 | q.push(t->right); 119 | } 120 | } 121 | } 122 | ``` 123 | 124 | ## 判断二叉树是否为完全二叉树 125 | 126 | 算法的时间复杂度为 $O(n)$ 127 | 128 | ```cpp 129 | bool isCompleteTree(BTNode* root) { 130 | queue> q; // pair的second成员存储结点编号 131 | q.push({root, 1}); 132 | for (gg i = 1; !q.empty(); ++i) { 133 | auto t = q.front(); 134 | q.pop(); 135 | if (i != t.second) 136 | return false; 137 | if (t.first->left != nullptr) 138 | q.push({t.first->left, t.second * 2}); 139 | if (t.first->right != nullptr) 140 | q.push({t.first->right, t.second * 2 + 1}); 141 | } 142 | return true; 143 | } 144 | ``` 145 | 146 | ## 二叉查找树的相关操作 147 | 148 | 插入、查找操作时间复杂度均为 $O(h)$ ,其中 h 为树的高 149 | 150 | ```cpp 151 | //向二叉查找树中插入元素x 152 | void insertElement(BTNode*& root, gg x) { 153 | if (not root) { //根结点为空,新建一个结点 154 | root = new BTNode(x); 155 | } else if (x <= root->val) { //向左子树中插入 156 | insertElement(root->left, x); 157 | } else { //向右子树中插入 158 | insertElement(root->right, x); 159 | } 160 | } 161 | //在二叉查找树中查找元素x的位置,查找失败则返回空指针 162 | BTNode* findElement(BTNode* root, gg x) { 163 | if (not root or root->val == x) { //查找失败或查找成功 164 | return root; 165 | } else if (x <= root->val) { //向左子树中查找 166 | return findElement(root->left, x); 167 | } else { //向右子树中查找 168 | return findElement(root->right, x); 169 | } 170 | } 171 | ``` 172 | 173 | ## 根据遍历序列创建二叉树问题 174 | 175 | 注意,**这里的代码模板针对的二叉树,结点数据域的值都是唯一的,不会出现重复** 176 | 177 | ### 由中根序列和先根序列创建二叉树 178 | 179 | 算法的时间复杂度为 $O(n)$ 180 | 181 | ```cpp 182 | // pre为先根序列,in为中根序列,r为根结点在pre中的下标 183 | //[left, right]为当前创建的树的中根序列区间 184 | BTNode* buildTree(vector& pre, vector& in, gg r, gg left, gg right) { 185 | if (left > right) //序列为空,返回空指针 186 | return nullptr; 187 | //查找根结点在中根序列中的位置 188 | gg i = find(begin(in), end(in), pre[r]) - begin(in); 189 | auto root = new BTNode(pre[r]); //创建根结点 190 | root->left = buildTree(pre, in, r + 1, left, i - 1); //创建左子树 191 | root->right = buildTree(pre, in, r + 1 + i - left, i + 1, right); //创建右子树 192 | return root; //返回根结点 193 | } 194 | ``` 195 | 196 | ### 由中根序列和先根序列得到后根序列 197 | 198 | 算法的时间复杂度为 $O(n)$ 199 | 200 | ```cpp 201 | // pre为先根序列,in为中根序列,r为根结点在pre中的下标 202 | //[left, right]为当前创建的树的中根序列区间 203 | void getPostFromPreIn(vector& pre, vector& in, gg r, gg left, gg right) { 204 | if (left > right) //序列为空,直接返回 205 | return; 206 | //查找根结点在中根序列中的位置 207 | gg i = find(begin(in), end(in), pre[r]) - begin(in); 208 | getPostFromPreIn(pre, in, r + 1, left, i - 1); //递归遍历左子树 209 | getPostFromPreIn(pre, in, r + 1 + i - left, i + 1, right); //递归遍历右子树 210 | cout << pre[r] << ' '; //输出后根序列 211 | } 212 | ``` 213 | 214 | ### 由先根序列创建二叉查找树 215 | 216 | 算法的时间复杂度为 $O(n)$ 217 | 218 | ```cpp 219 | // pre为先根序列,[left, right]为当前创建的树的先根序列区间 220 | BTNode* buildBST(vector& pre, gg left, gg right) { 221 | if (left > right) //序列为空,返回空指针 222 | return nullptr; 223 | //查找右子树根结点在先根序列中的位置 224 | gg i = find_if(begin(pre) + left, begin(pre) + right + 1, 225 | [&pre, left](gg a) { return a > pre[left]; }) - begin(pre); 226 | auto root = new BTNode(pre[left]); //创建根结点 227 | root->left = buildBST(pre, left + 1, i - 1); //创建左子树 228 | root->right = buildBST(pre, i, right); //创建右子树 229 | return root; //返回根结点 230 | } 231 | ``` 232 | 233 | ### 由二叉查找树的先根序列得到后根序列 234 | 235 | 算法的时间复杂度为 $O(n)$ 236 | 237 | ```cpp 238 | // pre为先根序列,[left, right]为当前创建的树的先根序列区间 239 | void getPostFromBSTPre(vector& pre, gg left, gg right) { 240 | if (left > right) //序列为空,直接返回 241 | return; 242 | //查找右子树根结点在先根序列中的位置 243 | gg i = find_if(begin(pre) + left, begin(pre) + right + 1, 244 | [&pre, left](gg a) { return a > pre[left]; }) - begin(pre); 245 | getPostFromBSTPre(pre, left + 1, i - 1); //递归遍历左子树 246 | getPostFromBSTPre(pre, i, right); //递归遍历右子树 247 | cout << pre[left] << ' '; 248 | } 249 | ``` 250 | 251 | ## 最近公共祖先(LCA)问题 252 | 253 | 这里的代码模板针对的二叉树和要查找的指针 p、q,满足以下约定: 254 | 255 | 1. 二叉树中结点数据域的值都是唯一的,不会出现重复; 256 | 2. p 指针和 q 指针指向的结点一定在树中; 257 | 3. 非空结点与空结点的最近公共祖先为该非空结点。 258 | 259 | ### 普通二叉树 260 | 261 | ```cpp 262 | //在普通二叉树中查找p和q的最近公共祖先 263 | BTNode* LCA(BTNode* root, BTNode* p, BTNode* q) { 264 | if (not root or root == p or root == q) 265 | return root; 266 | BTNode *left = LCA(root->left, p, q), *right = LCA(root->right, p, q); 267 | return not left ? right : not right ? left : root; 268 | } 269 | ``` 270 | 271 | ### 二叉查找树 272 | 273 | ```cpp 274 | //在二叉查找树中查找p和q的最近公共祖先 275 | BTNode* LCA(BTNode* root, BTNode* p, BTNode* q) { 276 | return (root->val - p->val) * (root->val - q->val) <= 0 ? 277 | root : LCA(p->val < root->val ? root->left : root->right, p, q); 278 | } 279 | ``` 280 | -------------------------------------------------------------------------------- /基础算法/树/多叉树.md: -------------------------------------------------------------------------------- 1 | # 多叉树 2 | 3 | ## 结点类定义 4 | 5 | ```cpp 6 | struct TreeNode { 7 | gg val; 8 | vector child; 9 | TreeNode(gg v) : val(v) {} 10 | }; 11 | ``` 12 | 13 | ## 深度优先遍历(DFS) 14 | 15 | 算法的时间复杂度为 $O(n)$ 16 | 17 | ```cpp 18 | //树的先根遍历 19 | void preOrder(TreeNode* root) { 20 | if (not root) //是空树直接返回 21 | return; 22 | cout << root->val << ' '; //访问根结点 23 | for (auto i : root->child) //递归遍历所有子树 24 | preOrder(i); 25 | } 26 | //树的后根遍历 27 | void postOrder(TreeNode* root) { 28 | if (not root) //是空树直接返回 29 | return; 30 | for (auto i : root->child) //递归遍历所有子树 31 | postOrder(i); 32 | cout << root->val << ' '; //访问根结点 33 | } 34 | ``` 35 | 36 | ## 广度优先遍历(BFS) 37 | 38 | 算法的时间复杂度为 $O(n)$ 39 | 40 | ```cpp 41 | //树的层次遍历 42 | void levelOrder(TreeNode* root) { 43 | queue q; 44 | q.push(root); 45 | while (not q.empty()) { 46 | gg s = q.size(); 47 | while (s--) { 48 | auto t = q.front(); 49 | q.pop(); 50 | cout << t->val << (s == 0 ? '\n' : ' '); 51 | for (auto i : t->child) 52 | q.push(i); 53 | } 54 | } 55 | } 56 | ``` 57 | -------------------------------------------------------------------------------- /字符串/KMP算法.cpp: -------------------------------------------------------------------------------- 1 | void getNext(string pattern,LL*next){ 2 | LL j=-1; 3 | next[0]=-1; 4 | for(LL i=1;i split(const string& s, string c = " ") { 2 | vector ans; 3 | for (gg i = 0, j = 0; i < s.size(); i = j + 1) { 4 | j = s.find(c, i); 5 | if (j == string::npos) 6 | j = s.size(); 7 | ans.push_back(s.substr(i, j - i)); 8 | } 9 | return ans; 10 | } -------------------------------------------------------------------------------- /字符串/字典树Trie.md: -------------------------------------------------------------------------------- 1 | # Trie 2 | 3 | ## 字符串 Trie 4 | 5 | ```cpp 6 | class Trie { 7 | public: 8 | Trie() : root(new TrieNode()) {} 9 | //插入一个单词 10 | void insert(const string& word) { 11 | auto i = root; 12 | for (char c : word) { 13 | if (not i->children.count(c)) { 14 | i->children[c] = new TrieNode(); 15 | } 16 | i = i->children[c]; 17 | } 18 | ++i->count; 19 | } 20 | //查找一个单词 21 | bool search(const string& word) { 22 | auto i = root; 23 | for (char c : word) { 24 | if (not i->children.count(c)) { 25 | return false; 26 | } 27 | i = i->children[c]; 28 | } 29 | return i->count > 0; //如果是查找前缀把这条语句换成return true;就可以了 30 | } 31 | 32 | private: 33 | struct TrieNode { 34 | gg count = 0; //该结点代表的单词个数,count>0表示这是一个单词结点 35 | map children; 36 | }; 37 | TrieNode* root; 38 | }; 39 | ``` 40 | 41 | ## 01-Trie 42 | 43 | ```cpp 44 | template 45 | class Trie { 46 | public: 47 | Trie() : root(new TrieNode()) {} 48 | //插入一个不超过bits位的非负整数 49 | void insert(gg n) { 50 | const bitset& bin(n); 51 | auto i = root; 52 | for (gg j = bits - 1; j >= 0; --j) { 53 | if (not i->children[bin[j]]) { 54 | i->children[bin[j]] = new TrieNode(); 55 | } 56 | i = i->children[bin[j]]; 57 | } 58 | } 59 | void remove(gg n) { 60 | const bitset& bin(n); 61 | root = dfs(root, bin, bits - 1); 62 | } 63 | //与 n 异或的最大值 64 | gg search(gg n) { 65 | const bitset& bin(n); 66 | auto i = root; 67 | gg ans = 0; 68 | for (gg j = bits - 1; j >= 0; --j) { 69 | gg k = bin[j] ? 0 : 1; 70 | if (i->children[k]) { 71 | ans = ans * 2 + 1; 72 | i = i->children[k]; 73 | } else { 74 | ans <<= 1; 75 | i = i->children[k ^ 1]; 76 | } 77 | } 78 | return ans; 79 | } 80 | 81 | private: 82 | struct TrieNode { 83 | TrieNode* children[2]{}; 84 | }; 85 | TrieNode* root; 86 | TrieNode* dfs(TrieNode* r, const bitset& bin, int p) { 87 | if (p < 0) { 88 | return nullptr; 89 | } 90 | r->children[bin[p]] = dfs(r->children[bin[p]], bin, p - 1); 91 | if (not r->children[0] and not r->children[1]) { 92 | return nullptr; 93 | } else { 94 | return r; 95 | } 96 | } 97 | }; 98 | ``` 99 | -------------------------------------------------------------------------------- /字符串/最长回文子串.md: -------------------------------------------------------------------------------- 1 | # 最长回文子串 2 | 3 | ## 问题描述 4 | 5 | 给你一个字符串 `s`,找到 `s` 中最长的回文子串。如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。 6 | 7 | ## 思路 8 | 9 | 可用 Manacher 算法求解,时间复杂度为 $O(n)$ 。 10 | 11 | ```cpp 12 | string longestPalindrome(string& s) { 13 | string news = "$#"; 14 | for (gg i = 0; i < s.size(); ++i) 15 | news = news + s[i] + "#"; 16 | vector p(news.size(), 1); 17 | gg x = 0, xr = 0; 18 | for (gg i = 1; i < news.size(); ++i) { 19 | if (i < xr) 20 | p[i] = min(xr - i, p[2 * x - i]); 21 | while (i + p[i] < news.size() && news[i - p[i]] == news[i + p[i]]) 22 | ++p[i]; 23 | if (xr < i + p[i]) { 24 | x = i; 25 | xr = i + p[i]; 26 | } 27 | } 28 | gg k = max_element(begin(p) + 1, end(p)) - begin(p); 29 | return s.substr((k - p[k]) / 2, p[k] - 1); 30 | } 31 | ``` 32 | -------------------------------------------------------------------------------- /数学/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richenyunqi/code-templates/2c0a4ce8d016de4b4ecabedb1eca76adf8b05857/数学/README.md -------------------------------------------------------------------------------- /数学/乘法逆元.md: -------------------------------------------------------------------------------- 1 | # 乘法逆元 2 | 3 | 如果一个线性同余方程 $ax\equiv 1(mod\ n)$ ,则 $x$ 称为 $a\ mod\ n$ 的逆元,记作 $a^{-1}$ 。 4 | 5 | ## 扩展欧几里得算法求逆元 6 | 7 | 若 $gcd(a,n)\not ={1}$ ,则方程无解。所以如果方程有解的话,就可以通过[扩展欧几里得算法](./欧几里得算法.md)求逆元。 8 | 9 | ## 快速幂法求逆元 10 | 11 | 根据费马小定理,若 $n$ 为素数,且 $gcd(a,n)=1$ , $a\ mod\ n$ 的逆元可以计算为 $a^{n-2}$ ,可通过[快速幂](./快速幂.md)求解。 12 | 13 | ## 求 $1,2,\cdots,n$ 中每个数关于 $p$ 的逆元 14 | 15 | 算法时间复杂度为 $O(n)$ 。 16 | 17 | ```cpp 18 | gg inv[MAX]; 19 | void invEle(gg n, gg p) { 20 | inv[1] = 1; 21 | for (gg i = 2; i <= n; ++i) { 22 | inv[i] = (p - p / i) * inv[p % i] % p; 23 | } 24 | } 25 | ``` 26 | 27 | ## 求任意 $n$ 个数关于 $p$ 的逆元 28 | 29 | 首先计算 n 个数的前缀积,记为 $s_i$ ,然后利用快速幂或扩展欧几里得算法计算 $s_n$ 的逆元,记作 $sv_n$ 。因为 $sv_n$ 是 n 个数的积的逆元,所以当我们把它乘上 $a_n$ 时,就会和 $a_n$ 的逆元抵消,于是就得到了 $a_1$ 到 $a_n$ 的积逆元,记为 $sv_{n-1}$ 。可以依次计算出所有的 $sv_i$ ,于是 $a_i^{-1}$ 就可以用 $s_{i-1}\cdot sv_i$ 求得。算法时间复杂度为 $O(n+\log\ p)$ 30 | 31 | ```cpp 32 | gg inv[MAX], s[MAX], a[MAX], sv[MAX]; 33 | void invEle(gg n, gg p) { 34 | s[0] = 1; 35 | for (gg i = 1; i <= n; ++i) { 36 | s[i] = s[i - 1] * a[i] % p; 37 | } 38 | //也可以用扩展欧几里得来求逆元,视个人喜好而定. 39 | sv[n] = powMod(s[n], p - 2, p); 40 | for (gg i = n; i >= 1; --i) { 41 | sv[i - 1] = sv[i] * a[i] % p; 42 | } 43 | for (gg i = 1; i <= n; ++i) { 44 | inv[i] = sv[i] * s[i - 1] % p; 45 | } 46 | } 47 | ``` 48 | -------------------------------------------------------------------------------- /数学/位运算.md: -------------------------------------------------------------------------------- 1 | # 位运算 2 | 3 | ## 基础的位运算 4 | 5 | 经常会将一个状态压缩成一个整数,这时掌握如何更改这个整数的二进制位是非常有必要的。 6 | 7 | ```cpp 8 | num |= 1 << n; //将num的第n位二进制位变为1 9 | num &= ~(1 << n); //将num的第n位二进制位变为0 10 | num ^= 1 << n; //将num的第n位二进制位翻转 11 | ``` 12 | 13 | 可以这样枚举一个二进制数 n 的所有为 1 的二进制位的组合: 14 | 15 | ```cpp 16 | for (gg t = n; t != 0; t = (t - 1) & n){ 17 | //对状态t进行操作 18 | } 19 | ``` 20 | 21 | ## 使用异或、位与运算做加法 22 | 23 | ```cpp 24 | gg plusANDXOR(gg a, gg b) { 25 | while (b != 0) { 26 | gg c = (unsigned gg)(a & b) << 1; 27 | a ^= b; 28 | b = c; 29 | } 30 | return a; 31 | } 32 | ``` 33 | -------------------------------------------------------------------------------- /数学/分数.md: -------------------------------------------------------------------------------- 1 | # 分数 2 | 3 | 我们用假分数的形式表示一个分数,这种形式下,一个分数只包含分子和分母两个整数,而且它还可以表示大于 1 的分数。可以使用`array`来表示这样一个假分数。由于`array`类型名过长,我们可以使用为它取一个类型别名,即`using F=array`(F 是分数的英文 fraction 的首字母)。为了表述和编码方便,如果我们有这样一个分数对象 f,我们针对这样一种表示方法约定以下规则: 4 | 5 | 1. 分数对象 f 应该是 F 类型,其中 f[0] 表示分子,f[1] 表示分母; 6 | 2. 分母 f[1] 永远都是正整数,如果 f 的值为负,那么让分子 f[0] 为负; 7 | 3. 分子 f[1] 和分母 f[1] 应该一直是互质的,即两者除了 1 以外没有其他公约数; 8 | 4. 如果 f 的值为 0,则令 f[0]=0,f[1]=1。 9 | 10 | 本模板给出的读入程序针对的是按 a/b 的格式给出分数形式,其中分子和分母全是整型范围内的整数,分母不为 0。 11 | 12 | 本模板输出的分数形式满足以下要求: 13 | 14 | 1. 如果分数的值是一个整数,只输出整数部分; 15 | 2. 如果分数的值大于 1,按带分数`k a/b`形式输出,k 为整数部分,a/b 为约分后的分数部分,如果有负号,只出现在整数部分; 16 | 3. 如果分数的值小于 1,按带分数`a/b`形式输出,a/b 为约分后的分数部分,如果有负号,只出现在分子前; 17 | 4. 如果分数的值为负,则会在分数前后输出圆括号`()`。 18 | 19 | 注意,进行分数除法时,你应该保证传入这个函数的除数不为 0。 20 | 21 | ```cpp 22 | using F = array; 23 | //分数的输入,针对的是按 a/b 的格式给出分数形式,分母不为 0 24 | F input() { 25 | F f; 26 | char c; //吸收'/'符号 27 | cin >> f[0] >> c >> f[1]; 28 | return f; 29 | } 30 | //分数的化简 31 | void simplify(F& f) { 32 | if (f[0] == 0) { //如果分子 f[0] 为 0,则令 f[1]=1 33 | f[1] = 1; 34 | return; 35 | } 36 | if (f[1] < 0) { //如果分母 f[1] 为负,将分子 f[0] 和分母 f[1] 都取相反数 37 | f[1] = -f[1]; 38 | f[0] = -f[0]; 39 | } 40 | gg d = gcd(abs(f[0]), abs(f[1])); //求出分子 f[0] 和分母 f[1] 绝对值的最大公约数 41 | f[0] /= d; 42 | f[1] /= d; 43 | } 44 | //分数的加法 45 | F Plus(const F& f1, const F& f2) { 46 | F f; 47 | f[0] = f1[0] * f2[1] + f2[0] * f1[1]; 48 | f[1] = f1[1] * f2[1]; 49 | simplify(f); 50 | return f; 51 | } 52 | //分数的减法 53 | F Sub(const F& f1, const F& f2) { 54 | F f; 55 | f[0] = f1[0] * f2[1] - f2[0] * f1[1]; 56 | f[1] = f1[1] * f2[1]; 57 | simplify(f); 58 | return f; 59 | } 60 | //分数的乘法 61 | F Multiply(const F& f1, const F& f2) { 62 | F f; 63 | f[0] = f1[0] * f2[0]; 64 | f[1] = f1[1] * f2[1]; 65 | simplify(f); 66 | return f; 67 | } 68 | //分数的除法 69 | F Div(const F& f1, const F& f2) { 70 | F f; 71 | f[0] = f1[0] * f2[1]; 72 | f[1] = f1[1] * f2[0]; 73 | simplify(f); 74 | return f; 75 | } 76 | //分数输出 77 | void output(const F& f) { 78 | if (f[0] < 0) 79 | cout << '('; 80 | if (f[1] == 1) { 81 | cout << f[0]; 82 | } else if (abs(f[0]) < f[1]) { 83 | cout << f[0] << "/" << f[1]; 84 | } else 85 | cout << f[0] / f[1] << " " << abs(f[0]) % f[1] << "/" << f[1]; 86 | if (f[0] < 0) 87 | cout << ')'; 88 | } 89 | ``` -------------------------------------------------------------------------------- /数学/快速幂.md: -------------------------------------------------------------------------------- 1 | # 快速幂 2 | 3 | ## 快速幂 4 | 5 | 算法的时间复杂度为 $O(\log n)$ ,其中 $n$ 为幂数。 6 | 7 | ```cpp 8 | gg binpow(gg a, gg b) { 9 | gg ans = 1; 10 | while (b > 0) { 11 | if (b & 1) 12 | ans = ans * a; 13 | a = a * a; 14 | b >>= 1; 15 | } 16 | return ans; 17 | } 18 | ``` 19 | 20 | ## 快速幂取模 21 | 22 | 算法的时间复杂度为 $O(\log n)$ ,其中 $n$ 为幂数。 23 | 24 | ```cpp 25 | //返回a^b%m 26 | gg powMod(gg a, gg b, gg m) { 27 | if (m == 1) { 28 | return 0; 29 | } 30 | a %= m; 31 | gg ans = 1; 32 | while (b > 0) { 33 | if (b & 1) 34 | ans = ans * a % m; 35 | a = a * a % m; 36 | b >>= 1; 37 | } 38 | return ans; 39 | } 40 | ``` 41 | 42 | ## 高精度快速幂 43 | 44 | ```cpp 45 | //返回a^b%mod,b是高精度整数 46 | gg superPow(gg a, string& b, gg mod) { 47 | a %= mod; 48 | if (b.empty()) 49 | return 1; 50 | gg k = b.back(); 51 | b.pop_back(); 52 | return powMod(superPow(a, b, mod), 10, mod) * powMod(a, k, mod) % mod; 53 | } 54 | ``` 55 | -------------------------------------------------------------------------------- /数学/欧几里得算法.md: -------------------------------------------------------------------------------- 1 | # 欧几里得算法 2 | 3 | 1. 欧几里得算法可以求解 a,b 两数的最大公约数,时间复杂度为 $O(\log(max(a,b)))$ ,递归实现一般不会栈溢出。用欧几里得算法去求斐波那契数列相邻两项的最大公约数,该算法会达到最坏复杂度。 4 | 2. 如果要求多个数的最大公约数(最小公倍数),当我们算出两个数的最大公约数(最小公倍数)之后,将它放入序列中对后面的数继续求解即可。 5 | 3. a,b,c 为任意整数,扩展欧几里得算法可以求解方程 $ax+by=gcd(a,b)$ 的一组整数解。 6 | 1. 调用扩展欧几里得算法时,需保证 a,b 为正整数。 7 | 2. 若方程 $ax+by=c$ 的一组整数解为 $(x_0,y_0)$ ,,则它的任意整数解都可以写成 $(x_0+kb', y_0-ka')$ ,其中 $a'=a/gcd(a,b)$ , $b'=b/gcd(a,b)$ ,k 取任意整数。 8 | 3. 若 $g=gcd(a,b)$ ,方程 $ax+by=g$ 的一组解是 $(x_0,y_0)$ ,则当 c 是 g 的倍数时, $ax+by=c$ 的一组解是 $(cx_0/g, cy_0/g)$ ;当 c 不是 g 的倍数时无整数解。 9 | 10 | ## 通过欧几里得算法计算两个正整数的最大公约数 11 | 12 | ```cpp 13 | gg gcd(gg a, gg b) { return b == 0 ? a : gcd(b, a % b); } 14 | ``` 15 | 16 | ## 扩展欧几里得算法求解方程 ax+by=gcd(a,b)的一组整数解,并返回 gcd(a,b)的值 17 | 18 | ```cpp 19 | //返回{gcd(a,b),x,y},x,y是方程ax+by=gcd(a,b)的一组解 20 | array extendGcd(gg a, gg b) { 21 | if (b == 0) { 22 | return {a, 1, 0}; 23 | } 24 | auto d = extendGcd(b, a % b); 25 | return {d[0], d[2], d[1] - (a / b) * d[2]}; 26 | } 27 | //返回方程ax+by=c的解中x的最小非负整数值 28 | gg getMinX(gg a, gg b, gg c) { 29 | auto d = extendGcd(a, b); 30 | return (c / d[0] * d[1] % (b / d[0]) + b / d[0]) % (b / d[0]); 31 | } 32 | ``` 33 | -------------------------------------------------------------------------------- /数学/欧拉函数.md: -------------------------------------------------------------------------------- 1 | # 欧拉函数 2 | 3 | 欧拉函数 $\varphi (n)$ 的值是小于等于 n 且与 n 互质的正整数个数。注意,1 与任何正整数都互质,所以 $\varphi (1)=1$ 。 4 | 5 | ## 欧拉函数的性质 6 | 7 | 1. 欧拉函数是积性函数,即如果有 $gcd(a,b)=1$ ,则 $\varphi (ab)=\varphi (a)\cdot \varphi (b)$ 。 8 | 2. $n=\sum_{d|n} \varphi (d)$ 9 | 3. 由唯一分解定理,若 $n=\prod_{i=1}^s p_i ^{k_i}$ ,其中 $p_i$ 是质数,则 $\varphi (n)=n \cdot \prod _{i=1}^s \frac {p_i-1} {p_i}$ 。特别地, $\varphi (p^k)=p^k-p^{k-1}$ 。 10 | 11 | ## 求 n 的欧拉函数值 12 | 13 | 算法的时间复杂度为 $O(\sqrt n)$ 。 14 | 15 | ```cpp 16 | gg euler_phi(gg n) { 17 | gg m = (gg)sqrt(n + 0.5), ans = n; 18 | for (gg i = 2; i <= m; i++) 19 | if (n % i == 0) { 20 | ans = ans / i * (i - 1); 21 | while (n % i == 0) 22 | n /= i; 23 | } 24 | if (n > 1) 25 | ans = ans / n * (n - 1); 26 | return ans; 27 | } 28 | ``` 29 | 30 | ## 求 1~n 的欧拉函数值 31 | 32 | 算法的时间复杂度为 $O(n \log \log n)$ 。 33 | 34 | ```cpp 35 | gg phi[MAX]; 36 | void phi_table(gg n) { 37 | for (gg i = 2; i <= ni; i++) { 38 | phi[i] = 0; 39 | } 40 | phi[1] = 1; 41 | for (gg i = 2; i <= ni; i++) { 42 | if (phi[i] == 0) { 43 | for (gg j = i; j <= ni; j += i) { 44 | if (phi[j] == 0) 45 | phi[j] = j; 46 | phi[j] = phi[j] / i * (i - 1); 47 | } 48 | } 49 | } 50 | } 51 | ``` 52 | -------------------------------------------------------------------------------- /数学/素数.md: -------------------------------------------------------------------------------- 1 | # 素数 2 | 3 | ## 用暴力方法判断 n 是否为素数 4 | 5 | 算法的时间复杂度为 $O(\sqrt n)$ 6 | 7 | ```cpp 8 | bool isPrime(gg n) { 9 | if (n < 2) { 10 | return false; 11 | } 12 | for (gg i = 2; i * i <= n; ++i) { 13 | if (n % i == 0) { 14 | return false; 15 | } 16 | } 17 | return true; 18 | } 19 | ``` 20 | 21 | ## 利用埃氏筛法求解 [2,n) 以内的所有数字的一个质因子 22 | 23 | 算法的时间复杂度为 $O(n\log \log n)$ 24 | 25 | ```cpp 26 | // prime[i]表示i的一个质因子,若prime[i]==0,表示i是一个素数 27 | gg prime[MAX]; 28 | void getPrime(gg n) { 29 | for (gg i = 2; i * i <= n; ++i) { 30 | if (prime[i] == 0) { 31 | for (gg j = i + i; j < n; j += i) { 32 | prime[j] = i; 33 | } 34 | } 35 | } 36 | } 37 | ``` 38 | -------------------------------------------------------------------------------- /数学/组合数.md: -------------------------------------------------------------------------------- 1 | # 组合数 2 | 3 | ## 求 $C_n^k$ 4 | 5 | ```cpp 6 | gg C[MAX]; 7 | void Cvalue(gg n){ 8 | C[0] = 1; 9 | for (gg i = 1; i <= n; ++i){ 10 | C[i] = C[i - 1] * (n - i + 1) % mod * powMod(i, mod - 2, mod) % mod; 11 | } 12 | } 13 | ``` 14 | 15 | ## 求 1~n 内所有的 $C_n^k$ 的值 16 | 17 | ```cpp 18 | gg C[MAX][MAX]; 19 | void Cvalue(gg n){ 20 | for (gg i = 0; i <= n; ++i) { 21 | C[i][0] = 1; 22 | for (gg j = 1; j <= i; ++j){ 23 | C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % mod; 24 | } 25 | } 26 | } 27 | ``` 28 | 29 | ## 小球放盒子问题 30 | 31 | 放球问题是指把 n 个球放到 m 个盒子里的方案数,可分成 16 个子问题。不同情况总结见下表[^1]。 32 | 33 | ![放球问题 16 个子问题总结](../media/BallPlacementProblem.jpg) 34 | 35 | [^1]: https://baike.baidu.com/item/%E6%94%BE%E7%90%83%E9%97%AE%E9%A2%98/12740706 36 | -------------------------------------------------------------------------------- /数学/计算时间复杂度的小 Trick.md: -------------------------------------------------------------------------------- 1 | # 计算时间复杂度的小 Trick 2 | 3 | 记录一些计算时间复杂度时可能会用到的一些小 Trick。 4 | 5 | 1. 在 $[1,10^6]$ 中,一个数最多有 240 个约数。另外当 $n$ 不是特别大时,约数个数的估计公式为 $\sqrt[3]{n}$ 。 6 | 2. 给定一个数组,以某个右端点为结尾的所有子数组中,其中不同的 `或`/`与`/`lcm`/`gcd` 的值至多只有 $\log n$ 个。 7 | -------------------------------------------------------------------------------- /数学/进制转换.md: -------------------------------------------------------------------------------- 1 | # 进制转换 2 | 3 | ## 用除基取余法将十进制数转换成 R 进制数 4 | 5 | 算法的时间复杂度为 $O(\log_{R}n)$ 6 | 7 | ```cpp 8 | // n 为要转换的十进制数,R 为要转换的进制 9 | //返回用 vector存储的转换成的 R 进制数 10 | vector decToR(gg n, gg R) { 11 | vector ans; //存储 R 进制数 12 | do { 13 | ans.push_back(n % R); //取余 14 | n /= R; //除基 15 | } while (n != 0); // n==0 时跳出循环 16 | reverse(begin(ans), end(ans)); //翻转整个数组 17 | return ans; 18 | } 19 | ``` 20 | 21 | ## 将 R 进制数转换成十进制数 22 | 23 | 算法的时间复杂度为 $O(n)$ 24 | 25 | ```cpp 26 | // R 为转换的进制,r 为要存储在 vector中的转换的 R 进制数,n为r的元素个数 27 | //返回对应的十进制数 28 | gg rToDec(const vector& r, gg R) { 29 | gg d = 0; 30 | for (gg i : r) 31 | d = d * R + i; 32 | return d; 33 | } 34 | ``` 35 | -------------------------------------------------------------------------------- /数学/高精度整数运算.md: -------------------------------------------------------------------------------- 1 | # 高精度整数四则运算 2 | 3 | 一般情况下,如果要用到高精度运算,最好使用 python 语言编写程序,因为 python 语言本身的整数类型就自带高精度运算。 4 | 5 | ## 高精度与高精度的加法 6 | 7 | ```cpp 8 | //默认输入的a和b均为非负整数 9 | string Plus(const string& a, const string& b) { 10 | string ans; 11 | gg carry = 0; //进位 12 | for (gg i = a.size() - 1, j = b.size() - 1; i >= 0 || j >= 0 || carry != 0; --i, --j) { 13 | gg p1 = i >= 0 ? a[i] - '0' : 0, p2 = j >= 0 ? b[j] - '0' : 0; 14 | gg k = p1 + p2 + carry; 15 | ans.push_back(k % 10 + '0'); 16 | carry = k / 10; 17 | } 18 | reverse(begin(ans), end(ans)); //要进行翻转 19 | return ans; 20 | } 21 | ``` 22 | 23 | ## 高精度与高精度的减法 24 | 25 | ```cpp 26 | //默认输入的a和b均为非负整数,且a>=b 27 | string Sub(string a, const string& b) { 28 | string ans; 29 | for (gg i = a.size() - 1, j = b.size() - 1; i >= 0 || j >= 0; --i, --j) { 30 | gg p1 = i >= 0 ? a[i] - '0' : 0, p2 = j >= 0 ? b[j] - '0' : 0; 31 | if (p1 < p2) { //不够减,要借位 32 | a[i - 1]--; 33 | p1 += 10; 34 | } 35 | gg k = p1 - p2; 36 | ans.push_back(k % 10 + '0'); 37 | } 38 | ans.erase(ans.find_last_not_of('0') + 1, ans.size()); 39 | reverse(begin(ans), end(ans)); //要进行翻转 40 | return ans.empty() ? "0" : ans; 41 | } 42 | ``` 43 | 44 | ## 高精度与高精度的乘法 45 | 46 | ```cpp 47 | //默认a和b均为非负整数 48 | string Multiply(const string& a, const string& b) { 49 | gg m = a.size(), n = b.size(); 50 | string ans(n + m, '0'); //最终乘积最多有n+m位 51 | for (gg i = m - 1; i >= 0; --i) { 52 | for (gg j = n - 1; j >= 0; --j) { 53 | gg k = (a[i] - '0') * (b[j] - '0'); 54 | gg t = k + (ans[i + j + 1] - '0'); 55 | ans[i + j] += t / 10; //向乘积的i+j位进位 56 | ans[i + j + 1] = t % 10 + '0'; //填充乘积的i+j+1位 57 | } 58 | } 59 | ans.erase(0, ans.find_first_not_of("0")); //删除前导0 60 | return ans.empty() ? "0" : ans; 61 | } 62 | ``` 63 | 64 | ## 高精度与低精度的除法 65 | 66 | ```cpp 67 | //默认a为非负整数,b为正整数 68 | pair DivMod(const string& a, gg b) { 69 | string ans; //商 70 | gg mod = 0; //余数 71 | for (char c : a) { 72 | mod = c - '0' + mod * 10; 73 | ans.push_back(mod / b + '0'); 74 | mod %= b; 75 | } 76 | ans.erase(0, ans.find_first_not_of('0')); //删除多余的前导0 77 | return {ans.empty() ? "0" : ans, mod}; 78 | } 79 | ``` 80 | 81 | ## 高精度与低精度的取模运算 82 | 83 | ```cpp 84 | //默认a为非负整数,b为正整数,返回a%b 85 | gg Mod(const string& a, gg b) { 86 | gg ans = 0; 87 | for (char c: a){ 88 | ans = ((ans * 10 + c - '0') % m); 89 | } 90 | return ans; 91 | } 92 | ``` 93 | -------------------------------------------------------------------------------- /数据结构/区间信息维护/ST表.md: -------------------------------------------------------------------------------- 1 | # ST 表 2 | 3 | ## 概述 4 | 5 | ST 表全称为`Sparse-Table`,是用于解决`可重复贡献问题`的数据结构。`可重复贡献问题`是指对于运算 $opt$ ,满足 $x\ opt\ x=x$ ,则对应的区间询问就是一个可重复贡献问题。例如,最大值有 $max(x,x)=x$ ,gcd 有 $gcd(x,x)=x$ ,所以 RMQ(RMQ 是英文 `Range Maximum/Minimum Query`的缩写,表示区间最大/最小值。) 和区间 GCD 就是一个`可重复贡献问题`。像区间和就不具有这个性质,如果求区间和的时候采用的预处理区间重叠了,则会导致重叠部分被计算两次。另外, $opt$ 还必须满足结合律才能使用 ST 表求解。 6 | 7 | ## 模板题 8 | 9 | 题目大意:给定 n 个数,有 m 个询问,对于每个询问,你需要回答区间 $[l,r]$ 中的最大值。 10 | 11 | ## 分析 12 | 13 | ST 表基于 `倍增` 思想,可以做到 $\Theta (n\log n)$ 预处理, $O(1)$ 回答每个询问。但是不支持修改操作。注意,查询的下标需从 1 开始。 14 | 15 | 1. 预处理:令 $f(i,j)$ 表示区间 $[i,i+2^j-1]$ 的最大值,则 $f(i,0)=a_i$ 。根据定义式,第二维就相当于倍增的时候“跳了 $2^j-1$ 步”,依据倍增的思路,写出状态转移方程: $f(i,j)=max(f(i,j-1),f(i+2^{j-1},j-1))$ 。 16 | 2. 查询:假设每个查询 $[l,r]$ ,则 $f(l,r)=max(f(l,s),f(r-2^s+1,s))$ ,其中 $s=\lfloor \log_2 (r-l+1) \rfloor$ 。 17 | 18 | ## C++代码 19 | 20 | ```cpp 21 | template 22 | struct Union_Op { 23 | //将下面的函数替换成你需要的逻辑(这里假设取最大值),默认将这个类传递给ST类作Union_Operation类型参数 24 | T operator()(const T& a, const T& b) const { return max(a, b); } 25 | }; 26 | template , gg n2 = 20> 27 | class ST { 28 | public: 29 | ST(gg len, T* A, const T& default_value) : 30 | n(len), st(len + 5, vector(n2, default_value)), union_op(Union_Operation()) { 31 | STinit(A); 32 | } 33 | //求[l,r]区间执行union_op后的值 34 | gg STquery(gg l, gg r) { 35 | gg s = log2(r - l + 1); 36 | return union_op(st[l][s], st[r - (1 << s) + 1][s]); 37 | } 38 | 39 | private: 40 | gg n; //记录输入序列的长度 41 | vector> st; 42 | Union_Operation union_op; 43 | void STinit(T* A) { 44 | for (gg i = 1; i <= n; ++i) { 45 | st[i][0] = A[i]; 46 | } 47 | for (gg j = 1; j <= n2; ++j) { 48 | for (gg i = 1; i + (1 << j) - 1 <= n; ++i) { 49 | st[i][j] = union_op(st[i][j - 1], st[i + (1 << (j - 1))][j - 1]); 50 | } 51 | } 52 | } 53 | }; 54 | ``` 55 | -------------------------------------------------------------------------------- /数据结构/区间信息维护/树状数组.md: -------------------------------------------------------------------------------- 1 | # 树状数组 2 | 3 | ## 概述 4 | 5 | 树状数组又称二叉索引树,简称 BIT,是算法竞赛中常用的用来维护区间信息的数据结构。树状数组中,下标均从 1 开始。getSum 和 update 操作时间复杂度均为 $O(\log n)$ 。 6 | 7 | ## lowbit 8 | 9 | 在计算机中,一个补码表示的整数 x 变成其相反数-x 的过程相当于把 x 的二进制的每一位都取反, 然后末位加 1,而这等价于**直接把 x 的二进制最右边的 1 左边的每一位都取反**。因此`lowbits(x)=x&(-x)`表示就是把 x 的二进制最右边的 1 左边的每一位都置为 0 得到的结果,它的值是能整除 x 的最大 2 的幂次数。 10 | 11 | ## 一维树状数组 12 | 13 | ### 单点修改,区间查询 14 | 15 | ```cpp 16 | class BIT { 17 | public: 18 | BIT(gg len) : c(len + 5) {} 19 | //实现将A[x] + v的功能 20 | void update(gg x, gg v) { 21 | for (gg i = x; i < c.size(); i += lowbit(i)) { 22 | c[i] += v; 23 | } 24 | } 25 | //求A[1]~A[x]之和 26 | gg getSum(gg x) { 27 | gg sum = 0; 28 | for (gg i = x; i > 0; i -= lowbit(i)) { 29 | sum += c[i]; 30 | } 31 | return sum; 32 | } 33 | 34 | private: 35 | vector c; 36 | inline gg lowbit(gg x) { return x & (-x); }; 37 | }; 38 | ``` 39 | 40 | ### 区间修改,单点查询 41 | 42 | update 和 getSum 函数与`单点修改,区间查询`完全一致,但是需要使用差分建树和查询,下面的代码在`单点修改,区间查询`的基础上进行了进一步的封装。 43 | 44 | ```cpp 45 | class BIT { 46 | public: 47 | BIT(gg len) : c(len + 5) {} 48 | //利用差分建树 49 | BIT(vector& v) : c(v.size() + 5) { 50 | for (gg i = 1; i < v.size(); ++i) { 51 | update(i, v[i] - v[i - 1]); 52 | } 53 | } 54 | //将区间[l,r]都加上v 55 | void realUpdate(gg l, gg r, gg v) { 56 | update(l, v); 57 | update(r + 1, -v); 58 | } 59 | //求A[x] 60 | gg getSum(gg x) { 61 | gg sum = 0; 62 | for (gg i = x; i > 0; i -= lowbit(i)) { 63 | sum += c[i]; 64 | } 65 | return sum; 66 | } 67 | 68 | private: 69 | vector c; 70 | inline gg lowbit(gg x) { return x & (-x); }; 71 | void update(gg x, gg v) { 72 | for (gg i = x; i < c.size(); i += lowbit(i)) { 73 | c[i] += v; 74 | } 75 | } 76 | }; 77 | ``` 78 | 79 | ### 区间修改,区间查询 80 | 81 | ```cpp 82 | class BIT { 83 | public: 84 | BIT(gg len) : sum1(len + 5), sum2(len + 5) {} 85 | //利用差分建树(如果序列有初始值) 86 | BIT(vector& v) : sum1(v.size() + 5), sum2(v.size() + 5) { 87 | for (gg i = 1; i < v.size(); ++i) { 88 | update(i, v[i] - v[i - 1]); 89 | } 90 | } 91 | //将区间[l,r]都加上v 92 | void realUpdate(gg l, gg r, gg v) { 93 | update(l, v); 94 | update(r + 1, -v); 95 | } 96 | //求区间[l,r]的和 97 | gg getRealSum(gg l, gg r) { return getSum(r) - getSum(l - 1); } 98 | 99 | private: 100 | vector sum1, sum2; 101 | inline gg lowbit(gg x) { return x & (-x); }; 102 | void update(gg x, gg v) { 103 | for (gg i = x; i < sum1.size(); i += lowbit(i)) { 104 | sum1[i] += v; 105 | sum2[i] += v * (x - 1); 106 | } 107 | } 108 | //求前缀和 109 | gg getSum(gg x) { 110 | gg ans = 0; 111 | for (gg i = x; i > 0; i -= lowbit(i)) { 112 | ans += x * sum1[i] - sum2[i]; 113 | } 114 | return ans; 115 | } 116 | }; 117 | ``` 118 | 119 | ## 二维树状数组 120 | 121 | ### 单点修改,区间查询 122 | 123 | ```cpp 124 | class BIT { 125 | public: 126 | BIT(gg len1, gg len2) : sum(len1 + 5, vector(len2 + 5)) {} 127 | //将A[x][y]的值增加v 128 | void update(gg x, gg y, gg v) { 129 | for (gg i = x; i < sum.size(); i += lowbit(i)) { 130 | for (gg j = y; j < sum[i].size(); j += lowbit(j)) { 131 | sum[i][j] += v; 132 | } 133 | } 134 | } 135 | // 求左上角为(a,b),右下角为(c,d)的矩阵的和 136 | gg getRealSum(gg a, gg b, gg c, gg d) { 137 | return getSum(c, d) - getSum(c, b - 1) - getSum(a - 1, d) + 138 | getSum(a - 1, b - 1); 139 | } 140 | 141 | private: 142 | vector> sum; 143 | inline gg lowbit(gg x) { return x & (-x); }; 144 | // 求维护左上角为(1,1),右下角为(x,y)的矩阵的和 145 | gg getSum(gg x, gg y) { 146 | gg ans = 0; 147 | for (gg i = x; i > 0; i -= lowbit(i)) { 148 | for (gg j = y; j > 0; j -= lowbit(j)) { 149 | ans += sum[i][j]; 150 | } 151 | } 152 | return ans; 153 | } 154 | }; 155 | ``` 156 | 157 | ### 区间修改,单点查询 158 | 159 | ```cpp 160 | class BIT { 161 | public: 162 | BIT(gg len1, gg len2) : sum(len1 + 5, vector(len2 + 5)) {} 163 | //利用差分建树(如果序列有初始值) 164 | BIT(vector>& v) : sum(v.size() + 5, vector(v[0].size() + 5)) { 165 | for (gg i = 1; i < v.size(); ++i) { 166 | for (gg j = 1; j < v[0].size(); ++j) { 167 | update(i, j, 168 | v[i][j] - v[i][j - 1] - v[i - 1][j] + v[i - 1][j - 1]); 169 | } 170 | } 171 | } 172 | //将左上角为(a,b),右下角为(c,d)的矩阵的值都加上v 173 | void realUpdate(gg a, gg b, gg c, gg d, gg v) { 174 | update(a, b, v); 175 | update(a, d + 1, -v); 176 | update(c + 1, b, -v); 177 | update(c + 1, d + 1, v); 178 | } 179 | // 求A[x][y] 180 | gg getSum(gg x, gg y) { 181 | gg ans = 0; 182 | for (gg i = x; i > 0; i -= lowbit(i)) { 183 | for (gg j = y; j > 0; j -= lowbit(j)) { 184 | ans += sum[i][j]; 185 | } 186 | } 187 | return ans; 188 | } 189 | 190 | private: 191 | vector> sum; 192 | inline gg lowbit(gg x) { return x & (-x); }; 193 | void update(gg x, gg y, gg v) { 194 | for (gg i = x; i < sum.size(); i += lowbit(i)) { 195 | for (gg j = y; j < sum[0].size(); j += lowbit(j)) { 196 | sum[i][j] += v; 197 | } 198 | } 199 | } 200 | }; 201 | ``` 202 | 203 | ### 区间修改,区间查询 204 | 205 | ```cpp 206 | class BIT { 207 | public: 208 | BIT(gg len1, gg len2) : 209 | sum1(len1 + 5, vector(len2 + 5)), sum2(len1 + 5, vector(len2 + 5)), 210 | sum3(len1 + 5, vector(len2 + 5)), sum4(len1 + 5, vector(len2 + 5)) {} 211 | //左上角为(a,b)右下角为(c,d)的矩阵全部加上x 212 | void realUpdate(gg a, gg b, gg c, gg d, gg x) { 213 | update(a, b, x); 214 | update(a, d + 1, -x); 215 | update(c + 1, b, -x); 216 | update(c + 1, d + 1, x); 217 | } 218 | //查询左上角为(a,b)右下角为(c,d)的矩阵和 219 | gg getRealSum(gg a, gg b, gg c, gg d) { 220 | return getSum(c, d) - getSum(a - 1, d) - getSum(c, b - 1) + 221 | getSum(a - 1, b - 1); 222 | } 223 | private: 224 | vector> sum1, sum2, sum3, sum4; 225 | inline gg lowbit(gg x) { return x & (-x); }; 226 | void update(gg x, gg y, gg val) { 227 | for (gg i = x; i < sum1.size(); i += lowbit(i)) { 228 | for (gg j = y; j < sum1[0].size(); j += lowbit(j)) { 229 | sum1[i][j] += val; 230 | sum2[i][j] += val * x; 231 | sum3[i][j] += val * y; 232 | sum4[i][j] += val * x * y; 233 | } 234 | } 235 | } 236 | //查询左上角为(1,1)右下角为(x,y)的矩阵和 237 | gg getSum(gg x, gg y) { 238 | gg ret = 0; 239 | for (gg i = x; i > 0; i -= lowbit(i)) { 240 | for (gg j = y; j > 0; j -= lowbit(j)) { 241 | ret += (x + 1) * (y + 1) * sum1[i][j]; 242 | ret -= (y + 1) * sum2[i][j] + (x + 1) * sum3[i][j]; 243 | ret += sum4[i][j]; 244 | } 245 | } 246 | return ret; 247 | } 248 | }; 249 | ``` 250 | -------------------------------------------------------------------------------- /数据结构/区间信息维护/线段树.md: -------------------------------------------------------------------------------- 1 | # 线段树 2 | 3 | 线段树是算法竞赛中常用的用来维护区间信息的数据结构。线段树可以在 $O(\log n)$ 的时间复杂度内实现单点修改、区间修改、区间查询(区间求和,求区间最大值,求区间最小值)等操作。线段树维护的信息,需要满足可加性,即能以可以接受的速度合并信息和修改信息,包括在使用懒惰标记时,标记也要满足可加性(例如取模就不满足可加性,两个取模操作就不能合并在一起做)。注意,线段树和输入序列下标均需从 1 开始。 4 | 5 | ## 单点修改,区间查询 6 | 7 | ```cpp 8 | template 9 | struct Update_Op { 10 | //将下面的函数替换成你需要的逻辑(这里假设取和),默认将这个类传递给SegmentTree类作Update_Operation类型参数 11 | T operator()(const T& a, const T& b) const { return a + b; } 12 | }; 13 | template 14 | struct Union_Op { 15 | //将下面的函数替换成你需要的逻辑(这里假设取和),默认将这个类传递给SegmentTree类作Union_Operation类型参数 16 | T operator()(const T& a, const T& b) const { return a + b; } 17 | }; 18 | template , typename Union_Operation = Union_Op> 19 | class SegmentTree { 20 | public: 21 | struct TreeNode { 22 | T val; 23 | TreeNode *left, *right; 24 | TreeNode(const T& _val, TreeNode* _left = nullptr, TreeNode* _right = nullptr) : 25 | val(_val), left(_left), right(_right) {} 26 | }; 27 | // A表示原始的输入序列,如果没有这样的原始序列,用default_value对线段树中的值进行初始化 28 | SegmentTree(gg len, const T& _default_value, T* A = nullptr) : 29 | n(len), default_value(_default_value), update_op(Update_Operation()), union_op(Union_Operation()) { 30 | root = init(1, n, A); 31 | } 32 | // update_op(A[p],v) 33 | void realUpdate(gg p, const T& v) { update(root, 1, n, p, v); } 34 | //查询A的[left,right]区间执行union_op后的值 35 | T realGetResult(gg left, gg right) { return getResult(root, 1, n, left, right); } 36 | 37 | private: 38 | TreeNode* root; //根结点 39 | gg n; //记录输入序列的长度 40 | T default_value; 41 | Update_Operation update_op; 42 | Union_Operation union_op; 43 | TreeNode* init(gg left, gg right, T* A) { 44 | if (left == right) { 45 | return A ? new TreeNode(A[left]) : new TreeNode(default_value); 46 | } 47 | TreeNode* root = new TreeNode(default_value); 48 | gg mid = (left + right) / 2; 49 | root->left = init(left, mid, A); 50 | root->right = init(mid + 1, right, A); 51 | root->val = union_op(root->left->val, root->right->val); 52 | return root; 53 | } 54 | // update_op(A[p],v),[l,r]表示当前root结点包含的区间 55 | void update(TreeNode* root, gg l, gg r, gg p, const T& v) { 56 | if (l == r) { 57 | root->val = update_op(root->val, v); 58 | return; 59 | } 60 | gg m = (l + r) / 2; 61 | if (p <= m) { 62 | update(root->left, l, m, p, v); 63 | } else { 64 | update(root->right, m + 1, r, p, v); 65 | } 66 | root->val = union_op(root->left->val, root->right->val); 67 | } 68 | //查询A的[left,right]区间执行union_op后的值,[l,r]表示当前root结点包含的区间 69 | T getResult(TreeNode* root, gg l, gg r, gg left, gg right) { 70 | if (left <= l and r <= right) { 71 | return root->val; 72 | } 73 | gg m = (l + r) / 2; 74 | T ans; 75 | bool flag = false; //标记ans是否被赋过值 76 | if (left <= m) { 77 | ans = getResult(root->left, l, m, left, min(m, right)); 78 | flag = true; 79 | } 80 | if (right > m) { 81 | auto res = getResult(root->right, m + 1, r, max(left, m + 1), right); 82 | if (flag) { 83 | ans = union_op(ans, res); 84 | } else { 85 | ans = res; 86 | } 87 | } 88 | return ans; 89 | } 90 | }; 91 | ``` 92 | 93 | ## 区间修改(只有加法),区间查询 94 | 95 | ```cpp 96 | template 97 | struct Update_Op { 98 | //将下面的函数替换成你需要的逻辑(这里假设取和),默认将这个类传递给SegmentTree类作Update_Operation类型参数 99 | T operator()(const T& a, const T& b) const { return a + b; } 100 | }; 101 | template 102 | struct Union_Op { 103 | //将下面的函数替换成你需要的逻辑(这里假设取和),默认将这个类传递给SegmentTree类作Union_Operation类型参数 104 | T operator()(const T& a, const T& b) const { return a + b; } 105 | }; 106 | template 107 | struct Lazy_Op { 108 | //将下面的函数替换成你需要的逻辑(这里假设取积),默认将这个类传递给SegmentTree类作Lazy_Operation类型参数 109 | T operator()(const T& a, const T& b) const { return a * b; } 110 | }; 111 | struct Lazy_Union_Op { 112 | //将下面的函数替换成你需要的逻辑(这里假设取和),默认将这个类传递给SegmentTree类作Lazy_Union_Operation类型参数 113 | T operator()(const T& a, const T& b) const { return a + b; } 114 | }; 115 | template , typename Union_Operation = Union_Op, 116 | typename Lazy_Operation = Lazy_Op, typename Lazy_Union_Operation = Lazy_Union_Op> 117 | class SegmentTree { 118 | public: 119 | struct TreeNode { 120 | T val, lazy; 121 | TreeNode *left, *right; 122 | TreeNode(const T& _val, const T& _lazy, TreeNode* _left = nullptr, TreeNode* _right = nullptr) : 123 | val(_val), lazy(_lazy), left(_left), right(_right) {} 124 | }; 125 | // A表示原始的输入序列,如果没有这样的原始序列,用default_value对线段树中的值进行初始化 126 | SegmentTree(gg len, const T& _sequence_default_value, const T& _lazy_default_value, T* A = nullptr) : 127 | n(len), sequence_default_value(_sequence_default_value), lazy_default_value(_lazy_default_value), 128 | update_op(Update_Operation()), union_op(Union_Operation()), lazy_op(Lazy_Operation()), 129 | lazy_union_op(Lazy_Union_Operation()) { 130 | root = init(1, n, A); 131 | } 132 | //将A的[left,right]区间的值都进行update_op操作 133 | void realUpdate(gg left, gg right, gg v) { update(root, 1, n, left, right, v); } 134 | //查询A的[left,right]区间执行union_op后的值 135 | T realGetResult(gg left, gg right) { return getResult(root, 1, n, left, right); } 136 | 137 | private: 138 | TreeNode* root; //根结点 139 | gg n; //记录输入序列的长度 140 | T sequence_default_value, lazy_default_value; 141 | Update_Operation update_op; 142 | Union_Operation union_op; 143 | Lazy_Operation lazy_op; 144 | Lazy_Union_Operation lazy_union_op; 145 | TreeNode* init(gg left, gg right, T* A) { 146 | if (left == right) { 147 | return A ? new TreeNode(A[left], lazy_default_value) : 148 | new TreeNode(sequence_default_value, lazy_default_value); 149 | } 150 | TreeNode* root = new TreeNode(sequence_default_value, lazy_default_value); 151 | gg mid = (left + right) / 2; 152 | root->left = init(left, mid, A); 153 | root->right = init(mid + 1, right, A); 154 | root->val = union_op(root->left->val, root->right->val); 155 | return root; 156 | } 157 | //下传懒惰标记 158 | void pushdown(TreeNode* root, gg l, gg r) { 159 | gg m = (l + r) / 2; 160 | if (root->lazy != lazy_default_value and l != r) { 161 | root->left->val = update_op(root->left->val, lazy_op(m - l + 1, root->lazy)); 162 | root->left->lazy = lazy_union_op(root->left->lazy, root->lazy); 163 | root->right->val = update_op(root->right->val, lazy_op(r - m, root->lazy)); 164 | root->right->lazy = lazy_union_op(root->right->lazy, root->lazy); 165 | root->lazy = lazy_default_value; 166 | } 167 | } 168 | // 将A的[left,right]区间的值都进行update_op操作,[l,r]表示当前root结点包含的区间 169 | void update(TreeNode* root, gg l, gg r, gg left, gg right, const T& v) { 170 | if (left <= l and r <= right) { 171 | root->val = update_op(root->val, lazy_op(r - l + 1, v)); 172 | root->lazy = lazy_union_op(root->lazy, v); 173 | return; 174 | } 175 | pushdown(root, l, r); 176 | gg m = (l + r) / 2; 177 | if (left <= m) { 178 | update(root->left, l, m, left, min(m, right), v); 179 | } 180 | if (right > m) { 181 | update(root->right, m + 1, r, max(left, m + 1), right, v); 182 | } 183 | root->val = union_op(root->left->val, root->right->val); 184 | } 185 | //查询A的[left,right]区间执行union_op后的值,[l,r]表示当前root结点包含的区间 186 | T getResult(TreeNode* root, gg l, gg r, gg left, gg right) { 187 | if (left <= l and r <= right) { 188 | return root->val; 189 | } 190 | pushdown(root, l, r); 191 | gg m = (l + r) / 2; 192 | T ans; 193 | bool flag = false; //标记ans是否被赋过值 194 | if (left <= m) { 195 | ans = getResult(root->left, l, m, left, min(m, right)); 196 | flag = true; 197 | } 198 | if (right > m) { 199 | auto res = getResult(root->right, m + 1, r, max(left, m + 1), right); 200 | if (flag) { 201 | ans = union_op(ans, res); 202 | } else { 203 | ans = res; 204 | } 205 | } 206 | return ans; 207 | } 208 | }; 209 | ``` 210 | 211 | ## 区间修改(加法和乘法),区间查询 212 | 213 | ```cpp 214 | template 215 | struct MulAdd { 216 | T operator()(const T& a, const T& b, const T& c) const { return (a * b % mod + c) % mod; } 217 | }; 218 | template 219 | struct Mul { 220 | T operator()(const T& a, const T& b) const { return a * b % mod; } 221 | }; 222 | template 223 | struct Add { 224 | T operator()(const T& a, const T& b) const { return (a + b) % mod; } 225 | }; 226 | template , typename Union_Operation = Add, 227 | typename Lazy_Add = Mul, typename Lazy_Union_Add = MulAdd, typename Lazy_Union_Mul = Mul> 228 | class SegmentTree { 229 | public: 230 | struct TreeNode { 231 | T val, lazy_add, lazy_mul; 232 | TreeNode *left, *right; 233 | TreeNode(const T& _val, const T& _lazy_mul, const T& _lazy_add, TreeNode* _left = nullptr, 234 | TreeNode* _right = nullptr) : 235 | val(_val), 236 | lazy_add(_lazy_add), lazy_mul(_lazy_mul), left(_left), right(_right) {} 237 | }; 238 | // A表示原始的输入序列,如果没有这样的原始序列,用default_value对线段树中的值进行初始化 239 | SegmentTree(gg len, const T& _sequence_default_value, const T& _lazy_mul_value, const T& _lazy_add_value, 240 | T* A = nullptr) : 241 | n(len), 242 | sequence_default_value(_sequence_default_value), lazy_add_value(_lazy_add_value), 243 | lazy_mul_value(_lazy_mul_value), update_op(Update_Operation()), union_op(Union_Operation()), 244 | lazy_add(Lazy_Add()), lazy_union_add(Lazy_Union_Add()), lazy_union_mul(Lazy_Union_Mul()) { 245 | root = init(1, n, A); 246 | } 247 | //将A的[left,right]区间的值都加上v 248 | void realAdd(gg left, gg right, const T& v) { update(root, 1, n, left, right, lazy_mul_value, v); } 249 | //将A的[left,right]区间的值都乘上v 250 | void realMul(gg left, gg right, const T& v) { update(root, 1, n, left, right, v, lazy_add_value); } 251 | //查询A的[left,right]区间执行union_op后的值 252 | T realGetResult(gg left, gg right) { return getResult(root, 1, n, left, right); } 253 | 254 | private: 255 | TreeNode* root; //根结点 256 | gg n; //记录输入序列的长度 257 | T sequence_default_value, lazy_add_value, lazy_mul_value; 258 | Update_Operation update_op; 259 | Union_Operation union_op; 260 | Lazy_Add lazy_add; 261 | Lazy_Union_Add lazy_union_add; 262 | Lazy_Union_Mul lazy_union_mul; 263 | TreeNode* init(gg left, gg right, T* A) { 264 | if (left == right) { 265 | return A ? new TreeNode(A[left], lazy_mul_value, lazy_add_value) : 266 | new TreeNode(sequence_default_value, lazy_mul_value, lazy_add_value); 267 | } 268 | TreeNode* root = new TreeNode(sequence_default_value, lazy_mul_value, lazy_add_value); 269 | gg mid = (left + right) / 2; 270 | root->left = init(left, mid, A); 271 | root->right = init(mid + 1, right, A); 272 | root->val = union_op(root->left->val, root->right->val); 273 | return root; 274 | } 275 | //下传懒惰标记 276 | void pushdown(TreeNode* root, gg l, gg r) { 277 | gg m = (l + r) / 2; 278 | if ((root->lazy_add != lazy_add_value or root->lazy_mul != lazy_mul_value) and l != r) { 279 | root->left->val = update_op(root->left->val, root->lazy_mul, lazy_add(m - l + 1, root->lazy_add)); 280 | root->left->lazy_add = lazy_union_add(root->left->lazy_add, root->lazy_mul, root->lazy_add); 281 | root->left->lazy_mul = lazy_union_mul(root->left->lazy_mul, root->lazy_mul); 282 | root->right->val = update_op(root->right->val, root->lazy_mul, lazy_add(r - m, root->lazy_add)); 283 | root->right->lazy_add = lazy_union_add(root->right->lazy_add, root->lazy_mul, root->lazy_add); 284 | root->right->lazy_mul = lazy_union_mul(root->right->lazy_mul, root->lazy_mul); 285 | root->lazy_add = lazy_add_value; 286 | root->lazy_mul = lazy_mul_value; 287 | } 288 | } 289 | // 将A的[left,right]区间的值都进行update_op操作,[l,r]表示当前root结点包含的区间 290 | void update(TreeNode* root, gg l, gg r, gg left, gg right, const T& mul, const T& add) { 291 | if (left <= l and r <= right) { 292 | root->val = update_op(root->val, mul, lazy_add(r - l + 1, add)); 293 | root->lazy_add = lazy_union_add(root->lazy_add, mul, add); 294 | root->lazy_mul = lazy_union_mul(root->lazy_mul, mul); 295 | return; 296 | } 297 | pushdown(root, l, r); 298 | gg m = (l + r) / 2; 299 | if (left <= m) { 300 | update(root->left, l, m, left, min(m, right), mul, add); 301 | } 302 | if (right > m) { 303 | update(root->right, m + 1, r, max(left, m + 1), right, mul, add); 304 | } 305 | root->val = union_op(root->left->val, root->right->val); 306 | } 307 | //查询A的[left,right]区间执行union_op后的值,[l,r]表示当前root结点包含的区间 308 | T getResult(TreeNode* root, gg l, gg r, gg left, gg right) { 309 | if (left <= l and r <= right) { 310 | return root->val; 311 | } 312 | pushdown(root, l, r); 313 | gg m = (l + r) / 2; 314 | T ans; 315 | bool flag = false; //标记ans是否被赋过值 316 | if (left <= m) { 317 | ans = getResult(root->left, l, m, left, min(m, right)); 318 | flag = true; 319 | } 320 | if (right > m) { 321 | auto res = getResult(root->right, m + 1, r, max(left, m + 1), right); 322 | if (flag) { 323 | ans = union_op(ans, res); 324 | } else { 325 | ans = res; 326 | } 327 | } 328 | return ans; 329 | } 330 | }; 331 | ``` 332 | -------------------------------------------------------------------------------- /数据结构/堆/README.md: -------------------------------------------------------------------------------- 1 | | 操作 | 二叉堆 | 左偏树 | 配对堆 | 2 | | :------------------------: | :---------: | :---------: | :---------: | 3 | | 插入(insert) | $O(\log n)$ | $O(\log n)$ | $O(1)$ | 4 | | 查询最小值(find-min) | $O(1)$ | $O(1)$ | $O(1)$ | 5 | | 删除最小值(delete-min) | $O(\log n)$ | $O(\log n)$ | $O(\log n)$ | 6 | | 合并(merge) | $O(n)$ | $O(\log n)$ | $O(1)$ | 7 | | 减小元素的值(decrease-key) | $O(\log n)$ | $O(\log n)$ | $o(\log n)$ | 8 | | 是否支持可持久化 | √ | √ | × | 9 | -------------------------------------------------------------------------------- /数据结构/堆/配对堆.md: -------------------------------------------------------------------------------- 1 | # 配对堆 2 | 3 | 以下给出的代码实现了小根配对堆,如果需要 decreaseKey 操作,配对堆结点类还需添加 prev 域,代码实现也会复杂一些。因此下面给出没实现和实现了 decreaseKey 操作的两份代码。 4 | 5 | ## 未实现 decreaseKey 操作的代码 6 | 7 | ```cpp 8 | struct Node { 9 | gg val; 10 | Node *child = nullptr, *sibling = nullptr; 11 | Node(gg v) : val(v) {} 12 | }; 13 | Node* findMin(Node* x) { return x; } 14 | Node* mergeTwoHeaps(Node* a, Node* b) { 15 | if (not a) { 16 | return b; 17 | } 18 | if (not b) { 19 | return a; 20 | } 21 | if (a->val > b->val) { //如果要实现大根配对堆,只需将这里的>改成< 22 | swap(a, b); 23 | } 24 | b->sibling = a->child; 25 | a->child = b; 26 | return a; 27 | } 28 | Node* mergeSiblings(Node* x) { 29 | if (not x or not x->sibling) { 30 | return x; 31 | } 32 | Node *a = x->sibling, *b = a->sibling; 33 | x->sibling = a->sibling = nullptr; 34 | return mergeTwoHeaps(mergeTwoHeaps(x, a), mergeSiblings(b)); 35 | } 36 | //如果先使用findMin,再使用deleteMin,务必不要将前面得到的findMin的返回值再合并到堆中,应该重建一个结点 37 | Node* deleteMin(Node* x) { return mergeSiblings(x->child); } 38 | ``` 39 | 40 | ## 实现了 decreaseKey 操作的代码 41 | 42 | ```cpp 43 | struct Node { 44 | gg val; 45 | Node *child = nullptr, *sibling = nullptr, *prev = nullptr; 46 | Node(gg v) : val(v) {} 47 | }; 48 | Node* findMin(Node* x) { return x; } 49 | Node* mergeTwoHeaps(Node* a, Node* b) { 50 | if (not a) { 51 | return b; 52 | } 53 | if (not b) { 54 | return a; 55 | } 56 | if (a->val < b->val) { //如果要实现大根配对堆,只需将这里的<改成> 57 | swap(a, b); 58 | } 59 | a->prev = nullptr; 60 | b->prev = a; 61 | b->sibling = a->child; 62 | if (a->child) { 63 | a->child->prev = b; 64 | } 65 | a->child = b; 66 | return a; 67 | } 68 | Node* mergeSiblings(Node* x) { 69 | if (not x) { 70 | return x; 71 | } 72 | x->prev = nullptr; 73 | if (not x->sibling) { 74 | return x; 75 | } 76 | Node *a = x->sibling, *b = a->sibling; 77 | x->sibling = a->sibling = nullptr; 78 | a->prev = nullptr; 79 | return mergeTwoHeaps(mergeTwoHeaps(x, a), mergeSiblings(b)); 80 | } 81 | //如果先使用findMin,再使用deleteMin,务必不要将前面得到的findMin的返回值再合并到堆中,应该重建一个结点 82 | Node* deleteMin(Node* x) { return mergeSiblings(x->child); } 83 | // root为堆的根,x为要操作的节点,v为新的权值,调用时需保证x->val>=v 84 | Node* decreaseKey(Node* root, Node* x, gg v) { 85 | x->val = v; 86 | if (not x->prev) 87 | return x; 88 | if (x->prev->child == x) { 89 | x->prev->child = x->sibling; 90 | } else { 91 | x->prev->sibling = x->sibling; 92 | } 93 | x->sibling->prev = x->prev; 94 | x->sibling = nullptr; 95 | x->prev = nullptr; 96 | return mergeTwoHeaps(root, x); 97 | } 98 | ``` 99 | -------------------------------------------------------------------------------- /数据结构/平衡树/AVL树.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richenyunqi/code-templates/2c0a4ce8d016de4b4ecabedb1eca76adf8b05857/数据结构/平衡树/AVL树.cpp -------------------------------------------------------------------------------- /数据结构/并查集.md: -------------------------------------------------------------------------------- 1 | ```cpp 2 | class UFS { 3 | public: 4 | //初始化并查集 5 | UFS(gg n) : ufs(n + 5) { iota(begin(ufs), end(ufs), 0); } 6 | //查找结点所在树的根结点并进行路径压缩 7 | gg findRoot(gg x) { return ufs[x] == x ? x : ufs[x] = findRoot(ufs[x]); } 8 | //合并两个结点所在集合,如果已在同一集合,返回false 9 | bool unionSets(gg a, gg b) { 10 | a = findRoot(a), b = findRoot(b); 11 | if (a == b) { 12 | return false; 13 | } 14 | ufs[a] = b; 15 | return true; 16 | } 17 | 18 | private: 19 | vector ufs; 20 | }; 21 | ``` 22 | -------------------------------------------------------------------------------- /计算几何/距离.md: -------------------------------------------------------------------------------- 1 | # 距离 2 | 3 | ## 欧氏距离 4 | 5 | 欧氏距离,一般也称作欧几里得距离。在平面直角坐标系中,设点 A,B 的坐标分别为 $A(x_1,y_1),B(x_2,y_2)$ ,则两点间的欧氏距离为: 6 | 7 | $$|AB|=\sqrt{(x_1-x_2)^2+(y_1-y_2)^2}$$ 8 | 9 | $n$ 维空间中,对于 $\vec{A}(x_{11},x_{12},\cdots,x_{1n}), \vec{B}(x_{21},x_{22},\cdots,x_{2n})$ ,欧氏距离的距离公式: 10 | 11 | $$|AB|=\sqrt{\sum_{i=1}^n(x_{1i}-x_{2i})^2}$$ 12 | 13 | ## 曼哈顿距离 14 | 15 | 在平面直角坐标系中,设点 A,B 的坐标分别为 $A(x_1,y_1),B(x_2,y_2)$ ,则两点间的曼哈顿距离为: 16 | 17 | $$d(A,B)=|x_1-x_2|+|y_1-y_2|$$ 18 | 19 | $n$ 维空间中,对于 $\vec{A}(x_{11},x_{12},\cdots,x_{1n}), \vec{B}(x_{21},x_{22},\cdots,x_{2n})$ ,曼哈顿距离的距离公式: 20 | 21 | $$d(A,B)=\sum_{i=1}^n |x_{1i}-x_{2i}|$$ 22 | 23 | ## 切比雪夫距离 24 | 25 | 在平面直角坐标系中,设点 A,B 的坐标分别为 $A(x_1,y_1),B(x_2,y_2)$ ,则两点间的曼哈顿距离为: 26 | 27 | $$d(A,B) = \max(|x_1 - x_2|, |y_1 - y_2|)$$ 28 | 29 | $n$ 维空间中,对于 $\vec{A}(x_{11},x_{12},\cdots,x_{1n}), \vec{B}(x_{21},x_{22},\cdots,x_{2n})$ ,曼哈顿距离的距离公式: 30 | 31 | $$d(A,B)=\max(|x_{1i}-x_{2i}|) (i\in [1,n])$$ 32 | 33 | ## 曼哈顿距离与切比雪夫距离的相互转化 34 | 35 | 在平面直角坐标系中,设点 A,B 的坐标分别为 $A(x_1,y_1),B(x_2,y_2)$ ,将两点间的曼哈顿距离的绝对值拆开,可以得到四个值, 36 | 37 | $$ 38 | \begin{aligned} 39 | d(A,B)&=|x_1 - x_2| + |y_1 - y_2|\\ 40 | &=\max\begin{Bmatrix} x_1 - x_2 + y_1 - y_2, x_1 - x_2 + y_2 - y_1,x_2 - x_1 + y_1 - y_2, x_2 - x_1 + y_2 - y_1\end{Bmatrix}\\ 41 | &= \max(|(x_1 + y_1) - (x_2 + y_2)|, |(x_1 - y_1) - (x_2 - y_2)|) 42 | \end{aligned} 43 | $$ 44 | 45 | 这就是 $(x_1 + y_1,x_1 - y_1), (x_2 + y_2,x_2 - y_2)$ 两点之间的切比雪夫距离。 46 | 47 | 同理, $A,B$ 两点的切比雪夫距离为: 48 | 49 | $$ 50 | \begin{aligned} 51 | d(A,B)&=\max\begin{Bmatrix} |x_1 - x_2|,|y_1 - y_2|\end{Bmatrix}\\ 52 | &=\max\begin{Bmatrix} \left|\dfrac{x_1 + y_1}{2}-\dfrac{x_2 + y_2}{2}\right|+\left|\dfrac{x_1 - y_1}{2}-\dfrac{x_2 - y_2}{2}\right|\end{Bmatrix} 53 | \end{aligned} 54 | $$ 55 | 56 | 而这就是 $(\dfrac{x_1 + y_1}{2},\dfrac{x_1 - y_1}{2}), (\dfrac{x_2 + y_2}{2},\dfrac{x_2 - y_2}{2})$ 两点之间的曼哈顿距离。 57 | 58 | ### 结论 59 | 60 | - 曼哈顿坐标系是通过切比雪夫坐标系旋转 $45^\circ$ 后,再缩小到原来的一半得到的。 61 | - 将一个点 $(x,y)$ 的坐标变为 $(x + y, x - y)$ 后,原坐标系中的曼哈顿距离等于新坐标系中的切比雪夫距离。 62 | - 将一个点 $(x,y)$ 的坐标变为 $(\dfrac{x + y}{2},\dfrac{x - y}{2})$ 后,原坐标系中的切比雪夫距离等于新坐标系中的曼哈顿距离。 63 | 64 | 碰到求切比雪夫距离或曼哈顿距离的题目时,我们往往可以相互转化来求解。两种距离在不同的题目中有不同的优缺点,应该灵活运用。 65 | 66 | ## 闵可夫斯基距离 67 | 68 | 我们定义 $n$ 维空间中两点 $X(x_1, x_2, \dots, x_n), Y(y_1, y_2, \dots, y_n)$ 之间的闵可夫斯基距离为: 69 | 70 | $$ 71 | D(X, Y) = \left(\sum_{i=1}^n \left\vert x_i - y_i \right\vert ^p\right)^{\frac{1}{p}}. 72 | $$ 73 | 74 | 特别的: 75 | 76 | 1. 当 $p=1$ 时, $D(X, Y) = \sum_{i=1}^n \left\vert x_i - y_i \right\vert$ 即为曼哈顿距离; 77 | 2. 当 $p=2$ 时, $D(X, Y) = \left(\sum_{i=1}^n (x_i - y_i)^2\right)^{1/2}$ 即为欧几里得距离; 78 | 3. 当 $p \to \infty$ 时, $D(X, Y) = \lim_{p \to \infty}\left(\sum_{i=1}^n \left\vert x_i - y_i \right\vert ^p\right) ^{1/p} = \max\limits_{i=1}^n \left\vert x_i - y_i \right\vert$ 即为切比雪夫距离。 79 | 80 | 注意:当 $p \ge 1$ 时,闵可夫斯基距离才是度量,具体证明参见 [Minkowski distance - Wikipedia](https://en.wikipedia.org/wiki/Minkowski_distance)。 81 | 82 | ## 汉明距离 83 | 84 | 汉明距离是两个字符串之间的距离,它表示两个长度相同的字符串对应位字符不同的数量。 85 | 86 | 我们可以简单的认为对两个串进行异或运算,结果为 1 的数量就是两个串的汉明距离。 87 | 88 | ## 模板题 89 | 90 | 1. [P5098 [USACO04OPEN] Cave Cows 3 - 洛谷](https://www.luogu.com.cn/problem/P5098) 91 | 2. [P3964 [TJOI2013] 松鼠聚会 - 洛谷](https://www.luogu.com.cn/problem/P3964) 92 | --------------------------------------------------------------------------------