algorithm
算法板子
算法汇总
算法
建图
三种建图方式
1 |
|
图最短路
从某一基点到其他点的最短路径
Dijkstra算法
普通堆
1 |
|
反向索引堆
1 |
|
求最短奇偶路径
该思想可拓展为某一个节点有n种状态,故每条边增加一个变量为路的数量,一般设置为t = 1,而跑DJ算法时,可以在优先队列后面再加一个属性表示状态type,type = (type + t) % n。
当再增加一个属性表示节点的类别,相同节点间可以以某个代价相互传送时,则可以抽象为新增了一个祖先节点,该祖先与所有该类别的节点相互联通,节点到祖先的t=1,祖先到节点的t=0。
1 | // 假设边权为1,若不为1则w+1变成w+weight[ei] |
1 | /*迷宫有n个房间,通过m条单向道路连接,迷宫的起点是1号房间,终点是n号房间。每条道路都有一个守路人,需要支付对应数量的金币才能通过,这个迷宫还存在着两个神秘的规则: |
Floyd算法
1 |
|
SPFA
(Shortest Path Faster Algorithm)算法
SPFA
算法是对 Bellman-Ford 算法的一种改进,主要用于在含有负权边的图中求最短路径。它利用队列来维护“待更新”的节点,从而提高更新效率。
根据最短路径理论,在没有负权回路的图中,从起点到任一节点的最短路径最多只需要经过 n-1
条边。所以若某节点的路径更新次数超过 n-1
,就能确定有负权回路存在。
visited
:标记当前节点是否在队列中,防止重复入队
1 |
|
最小生成树
树上的一条无回路的连通所有节点的一条权值最小的路径
Kruskal算法
1 | // Kruskal 算法:利用并查集求最小生成树 |
Prim算法
1 | // Prim 算法 |
二叉树
二叉树的序列化与反序列化及先中序构造
1 |
|
利用先序和中序构造二叉树
1 | //先序和中序构造二叉树 |
二叉树中LCA算法
1 | //LCA问题 |
树状数组
一维
单点修改区间查询
1 |
|
区间修改单点查询
在单点基础上,利用一维差分,最后求从0到i的累加和即为i点的值
1 |
|
范围修改范围查询
1 |
|
二维
单点修改范围查询
1 |
|
范围查询范围修改
1 |
|
线段树
范围增加范围查询累加和
1 | // https://www.luogu.com.cn/problem/P3372 |
有优先级调整的多范围修改任务
1 | // https://www.luogu.com.cn/problem/P1253 |
离散化
离散化之后要注意间隙,如覆盖问题,要在离散化之后的两个差值不为一i的数据中间添加一个数
区间最值和历史最值
1 | // https://www.luogu.com.cn/problem/P6242 |
结合摩尔投票
经典摩尔投票,求数组中出现次数大于$\frac{n}{2}$d 数
1 | class Solution { |
结合线段树求区间海王数
1 | // https://leetcode.cn/problems/online-majority-element-in-subarray/ |
求一个数组中某一个区间出现了某个数的次数
参考上面摩尔投票结合线段树的例子,将一个数组按照数的大小和出现顺序升序排序,然后用不同逻辑的二分查找得到相对位置
稀疏表ST
**Sparse Table(稀疏表)**是一种高效的数据结构,主要用于 静态数组的区间最值查询(RMQ)。
- 适用于:查询频繁,修改不需要或很少
- 查询时间复杂度:O(1)
- 预处理时间复杂度:O(n log n)
支持的运算
Sparse Table 支持 幂等操作(idempotent operations):
min()
✅max()
✅gcd()
✅lcm()
❌(不满足幂等性)sum()
❌(不满足幂等性)
核心构建流程(以最大值为例)
- 定义
st[i][j]
表示从 下标 i 开始,长度为 2^j 的区间最大值
- 初始化
1 | for (int i = 1; i <= n; i++) { |
- 状态转移
1 | for (int j = 1; (1 << j) <= n; j++) { |
预处理 log2 表
为什么要提前预处理 log2[]
?
查询时需计算区间长度对应的 log2(r - l + 1)
,预处理可避免浮点误差和时间浪费
1 | log2[0] = -1; |
区间查询(以最大值为例)
1 | int k = log2[r - l + 1]; |
这个查询方式的核心是使用两个长度为 2^k
的区间覆盖整个 [l, r]
区间。
Luogu P4155:环形区间覆盖跳跃优化
**目标:**给定一系列区间,在环上查找覆盖长度为 m
所需的最少跳跃次数(倍增思想)
关键用法:
st[i][j]
表示从第i
个区间开始,跳2^j
步可覆盖到的位置
构建:
1 | st[i][0] = rightmost reachable from i |
查询:
1 | for (int j = logn; j >= 0; j--) { |
利用 Sparse Table 进行跳跃优化(倍增思想)
每次跳跃代表一段区间,用于计算覆盖长度最少的路径
模板封装(最值查询)
1 | const int N = 1e5 + 5, LOG = 20; |
LCA
最近祖先问题,用于快速查询树中两个节点的最近公共祖先。
采用deep数组和st表
deep数组用于记录深度,st表用于快速查询父亲及以上节点
1 | // https://www.luogu.com.cn/problem/P3379 |
Tarjan
利用并查集来一次dfs就构建好lca信息
1 | // https://www.luogu.com.cn/problem/P3379 |
树的重心
如果在树中选择某个节点并删除,这棵树将分为若干棵子树,使所有子树最大值最小的节点被称为整个树的重心。
性质:
- 树的重心如果不唯一,则至多有两个,且这两个重心相邻。
- 以树的重心为根时,所有子树的大小都不超过整棵树大小的一半。
- 树中所有点到某个点的距离和中,到重心的距离和是最小的;如果有两个重心,那么到它们的距离和一样。
- 把两棵树通过一条边相连得到一棵新的树,那么新的树的重心在连接原来两棵树的重心的路径上。
- 在一棵树上添加或删除一个叶子,那么它的重心最多只移动一条边的距离。
1 |
|
当节点有值时,重心的依据是节点权而不是边权。
树的直径
树上任意两点之间最常的简单路径为树的直径,一棵树可以有多条直径,长度相等
两次DFS
适用与无负边树
首先从任意节点 y 开始进行第一次 DFS,到达距离其最远的节点,记为 z,然后再从z 开始做第二次 DFS,到达距离z最远的节点,记为z’,则 $\delta(z,z’)$ 即为树的直径。
显然,如果第一次 DFS 到达的节点z是直径的一端,那么第二次 DFS 到达的节点z’一定是直径的一端。我们只需证明在任意情况下,z必为直径的一端。
定理:在一棵树上,从任意节点y开始进行一次 DFS,到达的距离其最远的节点z必为直径的一端。
1 | int n; |
树形DP
可用于有负边树
我们记录当1为树的根时,每个节点作为子树的根向下,所能延伸的最长路径长度$d_{1}$与次长路径(与最长路径无公共边)$d_{2}$,那么直径就是对于每一个点,该点的$d_{1}+d_{2}$能取到的最大值
1 | vector<int> head1(n + 1, 0), next1(n << 1 | 1), to1(n << 1 | 1), weight(n << 1 | 1); |
并查集
对数据进行分类
1 | // 递归找祖先 |
常见二分
加速查询
1 |
|
三分
1 | // F视具体情况 |
博弈论(SG
定理)
所有相同子游戏最终sg
异或结果为0则先手必败,不为零则先手必胜,sg
为从某一节点开始,到最大限度往前便利后的第一个没有出现的自然数。
1 |
|
乘积最大子数组
保留乘积最小的结果以准备触底反弹
1 | vector<int> v1; |
栈
单调栈
求某一侧第一个 大于或者小于其的数
1 |
|
最大频率栈
1 |
|
堆
动态堆寻找中位数
1 |
|
大小根堆
1 |
|
扫描线
利用扫描思想平移某一个轴时,加工另一个轴的信息来一边扫过去就能解决问题
1 | // https://www.luogu.com.cn/problem/P5490 |
排序
归并分治
分而治之
1 |
|
随机快排 & 选择第 k 小/大 的数
1 |
|
阶乘与逆元
乘以逆元等于除以某一个数再取模
1 |
|
线性递推逆元
1 | // p 必须为质数 |
矩阵快速幂
加速矩阵计算
1 |
|
扩展欧几里得算法
得到某一个ax + by = d的各种参数,d为最大公约数,x和y为贝祖系数(裴蜀定理的算法实现)
1 |
|
Stein GCD算法
Stein算法是一种计算两个非负整数的最大公约数的算法。它比传统的欧几里得算法在某些情况下更高效,
因为它避免了使用除法操作。该算法利用了以下几条规则:
GCD(0, b) = b:如果一个数是0,另一个数就是最大公约数。
GCD(a, 0) = a:同上,如果一个数是0,另一个数就是最大公约数。
如果a和b都是偶数,那么GCD(a, b) = 2 * GCD(a/2, b/2)。
如果a是偶数且b是奇数,那么GCD(a, b) = GCD(a/2, b)。
如果a是奇数且b是偶数,那么GCD(a, b) = GCD(a, b/2)。
如果a和b都是奇数,并且a >= b,那么GCD(a, b) = GCD((a - b)/2, b)。
如果a和b都是奇数,并且a < b,那么GCD(a, b) = GCD((b - a)/2, a)。
通过不断应用这些规则,最终可以将两个数变为0,从而找到它们的最大公约数。
1 | unsigned int steinGCD(unsigned int a, unsigned int b) { |
Least Recently Used (LRU)算法
1 |
|
二进制位
位图
判断某一个数是否存在
1 |
|
位运算实现四则运算
加速运算
1 |
|
位运算骚操作
1 |
|
Brain Kernighan算法
异或技巧
1 |
|
链表基本操作
返回两个无环链表相交的第一个节点
1 |
|
每k个节点一组翻转链表
1 | //每k个节点一组翻转链表 |
复制带随机指针的链表
1 | //复制带随机指针的链表 |
判断是否是回文结构(快慢指针)
1 | //判断链表是否是回文结构(快慢指针) |
返回第一个入环节点
1 | //返回链表的第一个入环节点 |
稳定排序
1 | //在链表上排序,时间复杂度O(n*logn),额外空间复杂度(1),排序有稳定性 |
合并n个有序链表
1 |
|
莫队算法
离线加速处理查询
1 |
|
KMP算法
字符匹配匹配加速,加速的点在于匹配失败后不一定直接返回起点重新匹配,而是跳到最大前缀处,如abab......
匹配失败时,会跳到2位置而不是0位置
1 |
|
manacher算法
以某一节点为中心向两端扩展的最大回文串
1 |
|
Z函数
对于一个长度为n的字符串s,定义函数z[i]表示s和s[i, n - 1]的最长公共前缀(LCP)的长度则z被成为s的Z函数
1 |
|
前缀树
1 |
|
AC自动机
1 |
|
字符串哈希
1 |
|
异或哈希
题目大概描述:给定一个数字序列a,请你找出最长的连续子区间,使得这个子区间内所有数字的乘积是一个完全平方数。如果存在多个长度相同的子区间,选择最靠左的一个。
完全平方数可以差分为质因数乘积,且所有质因数的个数均为偶数。
故把每一个数的值改为落单质因数的哈希值,如果某一个区间的异或值为0,就说明该区间的质因数都是偶数,说明乘积是一个完全平方数,利用前缀和思想可以加速这一查找过程(当一个状态第一次出现时,记录位置,以后每次出现都说明中间与第一次出现的位置的间隔为一个完全平方数)。
1 | using ull = unsigned long long; |
质数/素数
质数判别
1 |
|
线性质数筛
1 | // 质数筛 |
Pick定理
S = i + b / 2 - 1
S
:图形的面积(可以是实数);
i
:图形内部的格点数;
b
:图形边界上的格点数(包括顶点);
给定两个整数点(x1, y1), (y1, y2)
, 两点间(包括端点)的格点数为gcd(|x2 - x1|, |y2 - y1|) + 1
则求某区域内的格点数的方法为i = S − 2 / b + 1
差分
对区间进行快速加法操作
一维
1 | // 若有原数组则要分开处理,最后加上原数组或者进行以下处理 |
二维
1 | // 和原数组分开处理,最后加上原数组,或者进行以下处理 |
序列/数组问题
最长公共子序列
递归/递推+路径
1 |
|
最长递增子序列
两个序列包含完全相同的数字使,可以将一个数组的顺序映射到另一个数组上,这时其最长公共子序列变成求最长递增子序列问题(或者反向求最长递减子序列)
普通版
1 |
|
字典序最小版
1 |
|
累加和不大于k的最长子数组
1 |
|
DP
数位DP
1 | // 数位dp https://www.luogu.com.cn/problem/P2602 |
树形DP
1 |
|
Fliegel–Van Flandern 算法
这个算法可以将从公元前4713年1月1日(即儒略日JD=0)起计算的**儒略日数(JD)**转换为公历日期(年、月、日),并自动判断是在儒略历还是格里历中(1582年10月15日为切换点)。
- JD ≥ 2299161:采用格里历(即1582年10月15日之后)
- JD < 2299161:采用儒略历
1 | // Fliegel–Van Flandern 算法 |
逆波兰式
逆波兰表达式(Reverse Polish Notation,简称 RPN
),也称为后缀表达式,是一种将运算符放在操作数之后的数学表达方式。
1 |
|
约瑟夫环
约瑟夫环问题描述的是一群人围成一圈,每隔固定人数(即第 k 个人)被淘汰,直到最后剩下一个人。最后存活的人的位置可以通过下面的递推式来求解:
假设只有 1 个人时,存活的位置为 0(注意:这里用 0 表示编号,从 0 开始);当有 i 个人时,可以利用 i-1 个人的结果递推出 i 个人的情况。
递推公式为:
f(i) = (f(i−1) + k ) mod i
其中,f(i)表示 i 个人时最后存活者的编号。
i = 1 时,f[i]显然等于0
1 | int josephus(int n, int k) { |
离散化
1 |
|
N皇后问题
位运算版本
每次将状态进行左移和右移一位模拟对角线移动,而且利用位运算快速得到此行能够放置的位置
1 |
|
容器
set
基于红黑树实现的自动排序、无重复元素的关联容器
1 |
|
插入/删除/查找都是O(log n)
ordered_set
解释各个部分的作用
代码部分 | 功能 | 说明 |
---|---|---|
s.insert(5) |
插入元素 | ordered_set 自动去重 |
s.erase(7) |
删除元素 | 需先检查元素是否存在 |
find_by_order(k) |
获取第 k 小元素 | 0-based ,需先检查是否为空 |
order_of_key(10) |
获取小于某个值的元素数量 | O(log n) 复杂度 |
find(5) |
查找元素 | 直接返回迭代器 |
s.size() |
获取集合大小 | O(1) 复杂度 |
s.clear() |
清空集合 | 直接清空 |
时间复杂度
- 插入:
O(log n)
- 删除:
O(log n)
- 查找:
O(log n)
- 获取第 k 小元素:
O(log n)
- 获取小于 x 的元素数量:
O(log n)
1 |
|
string
数值与字符串转化
函数签名 | 作用说明 |
---|---|
to_string(val) |
把 val 转换成 string |
stoi(s, p, b) |
把字符串 s 从下标 p 开始转换成 b 进制的 int |
stol(s, p, b) |
转换成 long |
stoul(s, p, b) |
转换成 unsigned long |
stoll(s, p, b) |
转换成 long long |
stoull(s, p, b) |
转换成 unsigned long long |
stof(s, p, b) |
转换成 float |
stod(s, p, b) |
转换成 double |
stold(s, p, b) |
转换成 long double |
p
和 b
可省,默认为十进制
字符串输入与分割
getline(cin, s)
stringstream s(ss)
1 | string s1; |
改变大小写
1 | transform(起点, 终点, 输出位置, 处理函数) |
子串substr
substr(st, len)
len
省略时表示到en
查找
find
1 |
|
rfind
1 | //rfind是找最后一个出现的匹配字符串 |
find_...of
1 | find_first_of(args) // **查找args中任何一个字符第一次出现的位置** |
插入insert
1 |
|
tips
重载运算符号(比较)
1 | // 结构体 |
求最大的最小或者最小的最大问题
大概率是用二分来查询最合适的答案,具体算法看题
圆周率π
1 | const double pi = acos(-1); |
求容器最大值
1 | *max_element(vec.begin(), vec.end()); |
全排列
1 | std::string s = "aba"; |
求二维平面上的三角形面积
给定三个顶点
1 | S = abs(x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)) / 2; |
给定三条边
1 | p = (a + b + c) / 2; |
求三维空间中两条直线最短距离
L1
经过A(x1, y1, z1)
, B(x2, y2, z2)
L2
经过 C(x3, y3, z3)
, D(x4, y4, z4)
L1
的方向向量d1 = B - A
, L2
的方向向量d2 = D - C
n = d1 * d2
如果n为0,则两直线平行
任选L1
上的点A和L2
上的点C,计算AC = C - A
不平行时,两直线最短距离d = |AC .* n| / |n|
平行时,d = |AC * d1| / |d1|
不平行情况
$$
若 \vec{d_1} \times \vec{d_2} \neq 0,则最短距离公式为
$$
$$
d = \frac{\left| (C - A) \cdot \bigl(\vec{d_1} \times \vec{d_2}\bigr) \right|}{|\vec{d_1} \times \vec{d_2}|}.
$$
其中:
$$
(C - A) 表示向量,
\cdot 为点乘,
\times 为叉乘。
$$
$$
该公式的几何意义是:\vec{d_1} \times \vec{d_2} 垂直于两条直线,(C - A) 在此垂直方向上的投影长度即为两条直线的最短距离。
$$
平行情况
$$
若 \vec{d_1} \times \vec{d_2} = 0,则说明两条直线平行或重合。此时最短距离可以由下式给出:
$$
$$
d = \frac{|(C - A) \times \vec{d_1}|}{|\vec{d_1}|}.
$$
如果出现退化情况(例如 A=B 或 C=D),需要进一步做特殊处理。
1 |
|
GCD数量级
一个数组的GCD前缀不同种类是log级别的,可通过此题进行进一步了解和体会2024ICPCKunming
点到直线的距离公式
设:
$$
P(x_0,,y_0)
$$
为平面上一点,
直线 L 的方程为
$$
A x + B y + C = 0.
$$
则P到直线L的距离d为
$$
d = \frac{|,A x_0 + B y_0 + C,|}{\sqrt{A^2 + B^2}}.
$$
随机数
1 | srand((unsigned)time(NULL)); |
完全平方数
一个数为完全平方数当且仅当该分解后该数所有质因数个数均为偶数
随机哈希编码
1 | mt19937_64 rng(chrono::steady_clock::now().time_since_epoch().count()); |
批量操作
1 | int a[5]; |