目录
- 位运算
- 初级数论(辗转相除/素数/快速幂)
- 离散化
- 二分判定
- 区间合并/区间贪心调度
- 前缀和/差分
- 树状数组
- 线段树
- 图论
- 字符串匹配
- Trie树
- 高精度
位运算
取最靠右的一位1
1 | int lowbit(int x) { |
二进制中1的个数
1 | while(x) { |
获取/设置右起第k位数
1 | // 获取第k位 |
初级数论
辗转相除法
最大公约数
时间复杂度 $O(\lg max(a,b))$
1 | int gcd(int a, int b) { |
最大公倍数
1 | int lcm(int a, int b) { |
扩展欧几里得算法
求x, y整数,使得ax + by = gcd(a, b),时间复杂度 $O(\lg max(a,b))$
裴蜀定理
有任意正整数a, b,gcd(a,b)= d,那么对于任意的整数x,y,ax+by都一定是d的倍数,特别地,一定存在整数x,y,使ax+by=d成立。
推论
a,b互素的充要条件是存在整数x,y使ax+by=1
1 | int exgcd(int a, int b, int &x, int &y) { |
素数
素数判定 / 试除法
试除法实现素数判定、约数枚举、整数分解的时间复杂度均为 $O(\sqrt n)$
1 | bool isPrime(int x) { |
约数枚举
1 | vector<int> divisor(int x) { |
整数分解
1 | map<int, int> prime_factor(int x) { |
素数筛法
埃氏筛法 时间复杂度 $O(n \lg n lg n)$
1 | int prime[N]; |
1 | vector<int> primes; |
快速幂
求$a^k\mod p$的值,反复平方法 时间复杂度 $O(\lg k)$
预处理出 $a^{2^0} \mod p$, $a^{2^1} \mod p$, $a^{2^2} \mod p$,…, $a^{2^{lgk}} \mod p$的值(反复平方k次),然后根据底数不变指数相加,将k拆分为若干个2的次幂之和,则可以根据k的二进制形式将预处理的值按需相乘
1 | typedef long long LL; |
离散化
待离散值排序、去重,然后二分求离散化对应坐标
单值离散化
1 | vector<int> all; |
线段坐标离散化
每个端点需要考虑其本身和前后两点,从而将线段压缩
1 | int compress(vector<int> &x1, vector<int> &x2, int w) { |
二分判定
给定N个数,将其分为X组,每组K = N / X个数且需要保证K个数互不重复,求出每组最大数和最小数间的差值,求能够实现的所有组该差值总和的最小值
1 | bool check(int n, int k, vector<int>& a, int mid) { |
区间问题
区间合并
按区间左端点排序
1 | void merge(vector<PII> & segs) { |
区间覆盖问题
求覆盖各区间的最少点数(每个区间至少包含一个点)
按区间右端点排序,选取最右点
1 | typedef pair<int, int> PII; |
区间调度问题/求不相交区间的最多区间数目
如果最多有x个不相交区间,那么就至少需要x个点覆盖所有区间,因为本题和上一题等价,只有区间边界可能略有不同
选取结束时间最早的活动 -> 按区间右端点排序,选取max_ed和下一个区间起始不相交的位置
求按照不相交区间分组的最少分组数目
按区间左端点排序,记录每个组的最右坐标,依次枚举每个区间能否加入各个组,如果不能则新开一组。实际上枚举每个区间是否有可加入的组只需要找右坐标最小的组即可。
1 | #define l first |
求覆盖指定线段的最少区间数目
按区间左端点排序,依次枚举选取满足左端点在线段左侧的右端点最大的区间
1 | sort(range, range + n); |
#####区间交集问题
两区间[x1, y1]和[x2, y2]不存在交集的条件是 y1 < x2 || y2 < x1
,反之,则交集区间是[max(x1, x2), min(y1, y2)]
前缀和/差分
一维前缀和
为便于计算,下标从1开始
s[0] = a[0] = 0
s[i] = s[i-1] + a[i];
求区间[l, r]元素和s[r] - s[l-1]
二维前缀和
s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + a[i][j]
求区间[(x1, y1), (x2, y2)]元素和s[x2][y2] - s[x1-1][y1] - s[x1][y1-1] + s[x1-1][y1-1]
一维差分
差分可看作前缀和的逆操作,可实现$O(1)$时间的区间修改和单点修改
1 | void modify(int l, int r, int c) { |
二维差分
1 | const int N = 1005; // 注意数组从1开始且设计N+1,N至少要大于等于2 |
差分+贪心
差分+前缀和
树状数组
树状数组是支持区间单点修改的前缀和
将上图所有区间从左至右按序排列,其区间长度的二进制表示为:
1,10,1, 100, 1, 10, 1, 1000
而图中区间标号对应的二进制表示为:
1,10,11,100,101,110,111,1000
用lowbit函数将区间标号映射为区间长度:区间长度即区间标号二进制表示从右往左出现第一个1以及这个1之后的那些0组成数的二进制对应的十进制的数。
区间标号映射区间长度
1 | int lowbit(int x) { |
初始化空间$O(n)$,注意下标从1到n
单点修改操作需要更新所有包含它的区间,时间复杂度$O(log n)$
1 | int tr[N]; |
区间查询需要查询分支包含的所有区间,时间复杂度$O(log n)$
1 | // a[1]...a[x] |
e.x. 计算右侧小于当前元素的个数
差分+树状数组
可实现区间修改、单点查询或区间修改、区间查询
二维树状数组
线段树
五类操作, 四倍空间,初始化复杂度$O(n)$,区间操作查询或更新复杂度均为$O(\lg n)$
pushdown操作用于区间修改时的懒标记(仅支持单点修改时不需要),在区间修改和查询需要分裂区间前调用
支持单点修改的区间最大值查询
1 | const int N = 200010; |
支持区间修改的区间和查询
1 | const int N = 100010; |
图论
邻接矩阵int G [maxv][maxv]
或<vector<vector<int> > G
邻接表vector<int> G[maxv]
struct edge {
int to;
int cost;
}
vector<edge> G[maxv]
BFS/DFS搜索
时间复杂度均为$O(V+ E)$
BFS优化:
多源BFS
矩阵各点到多个候选起点的最短距离
可假设一虚拟源点,将其与多个起点分别相连,则转换为单源BFS的最短距离,实际实现时只需要将多个起点在第一轮都加入队列即可
双端队列BFS
适用于不同边权的情况
BFS优化
双向广搜
A*
染色法判定二分图
1 | vector<int> G[maxv]; |
拓扑排序
拓扑图(可以拓扑排序的图) 等价于有向无环图DAG
将入度为0点入队,出队去边减去相关入度将入度为0点入队,队列元素即拓扑序,队列元素小于顶点数说明可能存在重边和自环,拓扑序列不存在
$O(V+E)$
1 | const int N = 100001; |
最短路问题
Bellman-Ford 允许负环
每条边松弛|V| -1次(最坏情况下每次循环只松弛了一条边)之后如果存在不满足三角不等式的结点v.d > u.d + w(u,v)说明存在负权重环
时间复杂度$O(VE)$
优化 - 拓扑排序后按序松弛
Dijkstra 不许负权重边
维护一个已求出最短路径节点的集合S,以v.d为key构造最小堆,每次选择V-S中的最小堆顶,将其加入S并松弛所有与其相邻的边。注意第一次执行循环extract-min得到的是源点s
优先队列实现时间复杂度$O(VE)$
Floyd 适用负权重边,不允许存在负权重环
时间复杂度$O(V^3)$
最小生成树
Kruskal算法:集合A是森林,按权重从低到高考察每条边,如果它将两棵不同的树连接起来就加入到森林A里并完成两棵树的合并
Prim算法:集合A是一棵树,每次加入连接集合A和A之外结点的所有边中权重最小的边
用并查集和优先队列分别实现,时间复杂度均为$O(ElgV)$
字符串匹配
暴力
时间复杂度$O(n-m+1)*m$
1 | for (int i = 1; i <= (n - m + 1); i++) { |
KMP
前缀函数 $\pi[q]$是能构成$P_q (即P[1…q])$真后缀的P的最长前缀长度
$\pi[q] = max(k : k < q 且 P_k \sqsupset P_q)$
next[i]表示以p[i]结尾的p的子串的前缀函数值,即next[i] = j 表示 p[1…j] == p[i - j + 1…i]
预处理阶段摊还分析,时间复杂度$\Theta(m)$,因为j最多++ m次,因此while循环最多执行m次,同理匹配阶段时间复杂度$\Theta(n)$
字符串下标从1开始,next[1] = 0
1 | // 待匹配串s,模式串p,下标从1开始 |
字符串下标从0开始,next[0] = -1
1 | ne[0] = -1; |
Trie 树
高效存储和查询字符串的集合
插入和查询时间复杂度$O(\log n)$,时间复杂度$O(n^2)$
1 | const int N = 100010; |
高精度
高精度加法
数据范围为数字位数而非数字本身,使用string读入,vector逆序存储便于进位
1 | vector<int> add(vector<int> &a, vector<int> &b) { |
高精度减法
负号的判定 cmp函数:依次判断长度和各个位置的数
减法进位的处理
1 | c.push_back((t + 10) % 10); |
先导0的去除,注意最后结果是0要保留一位0
while(c.size() > 1 && c.back() == 0) c.pop_back();
1 | bool cmp(vector<int> & a, vector<int> & b) { |
高精度乘法
高精度乘整数
1 | vector<int> mul(vector<int> &a, int b) { |
高精度乘高精度
1 | vector<int> mul(vector<int> &a, vector<int> &b) { |
高精度除法
高精度除整数
1 | vector<int> div(vector<int> &a, int b, int &r) { |