Day 1基础算法:
二分:
求解满足 \(x\) 条件的最小 \(y\) 值 \(\Rightarrow\) 二分一个答案 \(y\),判断 \(y\) 是否满足 \(x\) 条件
时间复杂度:log 二分答案,暴力地判断
标志:最小xx的最大值/最大xx的最小值
贪心:
思考顺序:
分治:
对于一个问题,把它分解成两个大小相等的子问题,通过log层解决
计算整个区间的答案 => 计算横跨两个区间的答案
单调栈和单调队列:
作用:维护一些单调的信息
“在单调栈上二分” \(\Rightarrow\) 在单调栈上弹出元素代替二分
位运算:
&
按位与:两位均为 1 则为 1,否则为 0
|
按位或:两位均为 0 则为 0,否则为 1
^
按位异或:相同则为 0,不同则为 1
深度优先搜索与广度优先搜索:
深搜:所有走出迷宫的方法,用栈实现
广搜:最快走出迷宫的方法,用队列实现
0-1bfs
:求一个图的单元最短路,边权只有 0 或 1,要求线性复杂度
和bfs
相似,走 1 的时候加入队尾,走 0 的时候加入队首(双端队列deque
)
Day 2 数学专题:
快速幂:
求 \(a^n\) 的值
原理:设 \(n=\sum_{i=1}^m2^{k_i}\),那么 \(a_n=a^{\sum_{i=1}^m2^{k_i}}=\prod_{i=1}^ma^{2^{k_i}}\)
实现:依次计算 \(a^{2^i}\) 的值,若 \(n\) 的二进制表示下有 \(2^i\) 这一项则乘上 \(a^{2^i}\)
int qpow(int a, int b) {
int res = 1;
while (b) {
if (b & 1) res = res * a % Mod;
a = a * a % Mod;
b >>= 1;
}
return res;
}
求最大公约数:
- 更相损减术:
1.如果两个数都为偶数,将他们都除以 2,否则执行下一步
2.用大数减去小数,将得到的数与小的数作为新的一对数,递归执行这两个过程
3.直到两个数都相等为止,此时第一步除掉的若干个 2 和余下的数的乘积就是最大公约数
证明:本质上是证明 \(\gcd(a,b)=\gcd(b,a-b)(a>b)\)
- 欧几里得算法
用取模运算代替更相减损术:
- \(b=0,\gcd(a,b)=a\)
- \(b\ne 0,\gcd(a,b)=\gcd(b,a\%b)\)
时间复杂度:\(O(\log n)\)
二元线性 Diophantus 方程
求方程 \(ax+by=c\) 的正整数解(\(a,b,c\)均为整数)。
性质:
- 当且仅当\(\gcd(a,b)|c\)时,方程有解
- 若\(a{x_0}+b{y_0}=c\),则原方程的解集为:(\(x=x_0+k*\tfrac{b}{\gcd(a,b)},y=y_0-k*\tfrac{b}{\gcd(a,b)}\))(\(k\) 为整数)
扩展欧几里得算法:
用于求解 \(ax+by=\gcd(a,b)\) 的一组特解。
设:\(ax_1+by_1=\gcd(a,b)\),\(bx_2+(a\bmod b)y_2=gcd(b,a\bmod b)\)
则:\(ax_1+by_1=bx_2+(a\bmod b)y_2\)
\(\Rightarrow ax_1+by_1=bx_2+(a-(\left\lfloor\dfrac{a}{b}\right\rfloor \times b))y_2\)
\(\Rightarrow ax_1+by_1=ay_2+b(x_2-\left\lfloor\dfrac{a}{b}\right\rfloor y_2)\)
\(\Rightarrow x_1=y_2,y_1=x_2-\left\lfloor\dfrac{a}{b}\right\rfloor y_2\)
将 \(x_2,y_2\)不断递归求解,直到\(\gcd\)为 0 递归 \(x=1,y=0\) 回去求解
void exgcd(int a, int b, int& x, int& y) {
if (b == 0) {
x = 1, y = 0;
return;
}
exgcd(b, a % b, y, x);
y −= a / b * x;
}
复杂度同求最大公约数。
乘法逆元:
如果 \(ax \equiv 1 \pmod b\),则称 \(x\) 为 \(a\bmod b\) 的乘法逆元。乘法逆元存在当且仅当 \(\gcd(a,b)=1\)
求解:
当模数 \(p\) 为素数时:
- 快速幂:
若 \(p\) 为素数,由费马小定理 \(a^{p-1}\equiv1\pmod p\) 推出 \(a^{p-2}\equiv\pmod p\),接下来用快速幂求解
当 \(p\) 不为素数时:
- 扩展欧几里得算法
\(ax \equiv 1 \pmod b \Rightarrow b|(ax-1)\),则存在整数 \(y\) 使得 \(y=\tfrac{ax-1}{b}\),得:\(ax-by=1\),用扩展欧几里得算法得出的 \(x\) 便是答案。
线性求逆元:
- 求 1 到 \(n\) 的关于 \(p\) 的逆元:
若在求解 \(i^{-1}\) 时任意 \(j^{-1}(0<j<i)\) 均以求得,则:
\(i^{-1}=\begin{cases}i&i=1\\-\left\lfloor\dfrac{p}{i}\right\rfloor (p \bmod i)^{-1}&i>1\end{cases}\)
- 求任意 \(n\) 个数的乘法逆元:
计算 \(n\) 个数的前缀积 \(s_i\),在求出 \(s_n\) 的逆元 \(sv_n\),若 \(sv_n\) 乘上 \(a_n\),则与 \(a_n^{-1}\) 抵消,
便能求出 \(sv_{n-1}\)。同理可以计算出所有 \(sv_i\)。则 \(a_i^{-1}\) 就等于 \(s_{i-1} \times sv_i\)
inv[0] = fac[0] = 1;
for (int i = 1; i < N; i++) fac[i] = fac[i - 1] * i % Mod;
inv[N - 1] = qpow(fac[N - 1]);
for (int i = N - 2; i >= 1; i--)
inv[i] = inv[i + 1] * (i + 1) % Mod;
素数筛:
1.循环判断 复杂度:\(O(n\sqrt{n})\)
2.埃式筛 复杂度:\(O(n\log\log n)\)
3.线性筛 复杂度:\(O(n)\):
for (int i = 2; i <= n; i++) {
if (!vis[i]) prime[++ts] = i;
for (int j = 1; j <= ts && i * prime[j] <= n; j++) {
vis[i * prime[j]] = 1;
if (i % prime[j] == 0) break;
}
}
}
组合数学:
加法原理:做一件事情,完成它有 \(n\) 类方式,第一类方式有 \(m_1\) 种方法,第二类方式有 \(m_2\)种方法……,第 \(n\) 类方式有 \(m_n\) 种方法,那么完成这件事情共有 \(m_1+m_2+\cdots+m_n\) 种方法
乘法原理:做一件事,完成它需要分成 \(n\) 个步骤,做第一步有 \(m_1\) 种不同的方法,
做第二步有 \(m_2\) 种不同的方法…… ,做第 \(n\) 步有 \(m_n\)种不同的方法。
那么完成这件事共有 \(N=m_1\times m_2\times m_3\times \cdots\times m_n\)种不同的方法
排列组合:
\(A_n^m\)表示从 \(n\) 个不同的元素中选出 \(m\) 个,按一定顺序排成一列的方案数
\(C_n^m\)表示从 \(n\) 个不同的元素中选出 \(m\) 个的方案数
计算:若 \(m>n\),则 \(A_n^m=C_n^m=0\)
否则 \(C_n^m=\tfrac{n!}{m!(n-m)!}\),\(A_n^m=C_n^m\times m!=\tfrac{n!}{(n-m)!}\)
int C(int n, int m) {
return fac[n] * inv[m] % Mod * inv[n - m] % Mod;
}
例题:
-
\(n\) 个相同的球放入 \(m\) 个不同的盒子里,不允许空盒:“插板法”——\(C_{n-1}^{m-1}\)
-
\(n\) 个相同的球放入 \(m\) 个不同的盒子里,允许空盒:我们先给借来 \(m\) 个球,再套用上面的模型,并在最后数的时候给每个盒子去掉一个球——\(C_{m-1}^{n+m-1}\)
-
\(n\) 个不相同的球放入 \(m\) 个不同的盒子里,不允许空盒:$f(n,m)=mf(n-1,m)+f(n-1,m-1)(1\le m <n),f(n,n)=1,f(n,0)=0 $(第二类斯特林数)
-
在排成一行的 \(n\) 个元素选择 \(m\) 个,要求选择的 \(m\) 个元素不相邻:\(C_m^{n-m+1}\)
二项式定理:
\((a+b)^n=\sum_{i=0}^n\dbinom{n}{i} a^i b^{n-i}\)
容斥原理:
集合:
- 集合中元素的个数称为集合的大小,集合 \(A\) 的大小为 \(\left\vert A\right\vert\)
- 如果一个集合不包含任何元素,则称之为空集,记作 \(\varnothing\)
- 如果元素 \(a\) 属于集合 \(A\) 就记作 \(a\in A\),否则记作 \(a\notin A\)
- 若集合 \(A\) 的每一个元素 \(a\) 都有 \(a\in B\),则称 \(A\) 是 \(B\) 的子集,记作 \(A\subseteq B\)
- 由属于集合 \(A\) 的元素且属于集合 \(B\) 的元素组成的集合称作 \(A\) 与 \(B\) 的交集,记作 \(A\cap B\),读作 \(A\) 交 \(B\)
- 由属于集合 \(A\) 的元素或属于集合 \(B\) 的元素组成的集合称作 \(A\) 与 \(B\) 的并集,记作 \(A\cup B\),读作 \(A\) 并 \(B\)
- 交换律:\(A\cap B=B\cap A,A\cup B=B\cup A\)
- 结合律:\((A\cap B)\cap C=A\cap(B\cap C),(A\cup B)\cup C=A\cup(B\cup C)\)
- 分配对偶律:\(A\cap(B\cup C)=(A\cap B)\cup(A\cap C),A\cup(B\cap C)=(A\cup B)\cap(A\cup C)\)
设 \(\bigcup\) 中元素由 \(n\) 种不同的属性,第 \(i\) 种属性称为 \(p_i\),拥有属性 \(p_i\) 的元素构成集合 \(S_i\),则:
\(\bigcup_{i=1}^nS_i=\sum_{m=1}^n(-1)^{m-1}\sum_{a_i<a_{i+1}}\bigcap_{i=1}^mS_{a_i}\)
向量:
定义:既有大小又有方向的量
表示:用 \(n\) 个数字表示\(n\)维空间的向量:
\(\begin{bmatrix}a_1\\a_2\\\cdots\\a_n\end{bmatrix}\)
概念:
- 模:向量大小,常用\(\left|a\right|\)表示
- 零向量:模为零的向量
- 单位向量:模为 1 的向量称为该方向的单位向量
- 平行向量:方向相同或相反的向量
运算:
向量相加减:\(\begin{bmatrix}a_1\\a_2\\\cdots\\a_n\end{bmatrix}+\begin{bmatrix}b_1\\b_2\\\cdots\\b_n\end{bmatrix}=\begin{bmatrix}a_1+b_1\\a_2+b_2\\\cdots\\a_n+b_n\end{bmatrix}\)
向量数乘:\(k\times\begin{bmatrix}a_1\\a_2\\\cdots\\a_n\end{bmatrix}=\begin{bmatrix}ka_1\\ka_2\\\cdots\\ka_n\end{bmatrix}\)
矩阵:
\(\begin{cases}a_{1,1}x_1+a_{1,2}x_2+\cdots+a_{1,n}x_n=b_1\\a_{2,1}x_1+a_{2,2}x_2+\cdots+a_{2,n}x_n=b_2\\ {\color{white}>>>>>>>>}\cdots \\a_{m,1}x_1+a_{m,2}x_2+\cdots+a_{m,n}x_n=b_m\end{cases}\Longrightarrow \begin{bmatrix}a_{1,1}&a_{1,2}\cdots&a_{1,n}\\a_{2,1}&a_{2,2}\cdots&a_{2,n}\\\vdots&\ddots&\vdots\\a_{m,1}&a_{m,2}\cdots&a_{m,n}\end{bmatrix}\)
概念:
- 同型矩阵:两个矩阵,行数与列数对应相同,称为同型矩阵
- 方阵:行数等于列数的矩阵称为方阵。方阵是一种特殊的矩阵
- 主对角线:方阵中行数等于列数的元素构成主对角线。
- 对角矩阵:主对角线之外的元素均为 0 的方阵称为对角矩阵
- 单位矩阵:对角矩阵主对角线上的元素均为 1 的对角矩阵称为单位矩阵,记作 \(\mathtt{I}\)
运算:
- 转置:将矩阵行与列交换
\(A=\begin{bmatrix}a_{1,1}&a_{1,2}\cdots&a_{1,n}\\a_{2,1}&a_{2,2}\cdots&a_{2,n}\\\vdots&\ddots&\vdots\\a_{m,1}&a_{m,2}\cdots&a_{m,n}\end{bmatrix},A^T=\begin{bmatrix}a_{1,1}&a_{2,1}\cdots&a_{m,1}\\a_{1,2}&a_{2,2}\cdots&a_{m,2}\\\vdots&\ddots&\vdots\\a_{1,n}&a_{2,n}\cdots&a_{n,m}\end{bmatrix}\)
- 矩阵相加减:同型矩阵才能相加减
\(A+B=\begin{bmatrix}a_{1,1}+b_{1,1}&a_{1,2}+b_{1,2}\cdots&a_{1,n}+b_{1,n}\\a_{2,1}+b_{2,1}&a_{2,2}+b_{2,2}\cdots&a_{2,n}+b_{2,n}\\\vdots&\ddots&\vdots\\a_{m,1}+b_{m,1}&a_{m,2}+b_{m,2}\cdots&a_{m,n}+b_{m,n}\end{bmatrix}\)
- 矩阵乘法:矩阵乘法有意义的前提是前一矩阵行数等于后一矩阵列数
设 \(A=P\times M,B=M\times Q,C=A\times B\),则:
\(C_{i,j}=\sum_{k=1}^Ma_{i,k}b_{k,j}\),相乘后的矩阵 \(C\) 大小为 \(P\times Q\)
性质:
- 任何矩阵和单位矩阵相乘都等于本身
- 矩阵乘法不满足交换律,但满足结合律和分配律
加速线性递推:
例题:求斐波那契数列的第 \(n\) 项 \((n\le 10^{18})\)
设我们现在有一个 \(1\times 2\) 的矩阵,分别存了数列的第 \(n-1\) 和第 \(n\) 项:\(\begin{bmatrix}fib(n-1)&fib(n)\end{bmatrix}\),考虑构造一个矩阵 \(A\) 使得:\(\begin{bmatrix}fib(n-1)&fib(n)\end{bmatrix}\times A=\begin{bmatrix}fib(n)&fib(n+1)\end{bmatrix}\)。
首先 \(A\) 肯定是一个 \(2\times 2\) 的矩阵:\(\begin{bmatrix}a&b\\c&d\end{bmatrix}\),根据矩阵乘法的定义:
\(fib(n)=fib(n-1)\times a+fib(n)\times c\)
\(fib(n+1)=fib(n-1)\times b+fib(n)\times d\)
可得:\(a=0,b=1,c=1,d=1\),而题目要求到第 \(n\) 项,只要乘上 \(n-2\) 个 \(A\) 即可。由于矩阵乘法满足结合律,我们可以用快速幂的思想进行 \(A^{n-2}\) 的计算,在矩阵快速幂时,我们把初始矩阵设为单位矩阵。
#include<bits/stdc++.h>
typedef long long ll;
const int Mod = 1e9 + 7;
struct Matrix{
int a[2][2];
Matrix(int a = 0, int b = 0, int c = 0, int d = 0): a{{a, b}, {c, d}} {}
};
Matrix operator*(Matrix A, Matrix B) {
Matrix C;
for(int i = 0; i < 2; i++)
for(int j = 0; j < 2; j++)
for(int k = 0; k < 2; k++)
C.a[i][j] = (C.a[i][j] + (ll)A.a[i][k] * B.a[k][j]) %Mod;
return C;
}
Matrix operator^(Matrix A, ll n) {
Matrix ret(1, 0, 0, 1);
for(; n; n >>= 1, A = A * A)
if(n & 1) ret = ret * A;
return ret;
}
int main() {
ll n;
scanf(”%lld”, &n);
if(n == 1) return 0 * puts(”1”);
Matrix beg(1, 1), A(0, 1, 1, 1);
beg = beg * (A ^ (n − 2));
printf(”%d\n”, beg.a[0][1]);
}
时间复杂度为 \(O(a^3logn)\),其中 \(a\) 为转移矩阵 \(A\) 的大小
Day 3 数据结构基础
树状数组:
原理:每个位置管理 Lowbit 元素的和
要求:(普通实现的)答案必须可加
作用:树状数组可以做区间加,单点求值
线段树:
原理:
-
单点修改,区间求和:从根节点一路访问到目标节点,修改信息,然后一路合并上去即可
-
区间修改,区间求和:
- 用少量信息记录下一区间的答案
- 用在节点上打标记代替修改区间内的每个数
\(\Longrightarrow\)快速合并,快速打标记,快速下传
例题:
1.区间加法,区间求和
-
快速合并:
sum[nod]=sum[ls]+sum[rs]
-
快速打标记:
sum[nod]+=len*val,tag[nod]+=val(addtag nod)
-
快速下传:
val<-tag[nod],addtag ls,addtag rs,tag[nod]=0
2.区间加法,区间乘法,区间求和
-
快速合并:
sum[nod]=sum[ls]+sum[rs]
-
快速打标记(先乘再加):
乘法:先\(\times a+b\),再\(\times c+d \Longrightarrow \times ac +(dc+d)\)
tag2[nod]*=val,tag1[nod]*=val,sum[nod]*=val
加法:tag1[nod]+=val,sum[nod]+=val
- 快速下传标记:
gettag
3.单点修改,区间取模,区间求和
- 快速打模标记:记录每个区间的最大值,每次访问到一个区间,如果最大值小于模数就返回,否则左右子区间都访问
复杂度:\(O(M\log N\log X)\)
4.区间所有数变成\(min(a_i,x)\),所有数加上\(x\),查询区间和
- 快速合并:记录一个最大值,次大值和最大值出现次数,这样如果 \(x\) 大于最大值,则跳过。若 \(x\) 大于次大值,就知道什么时候只是操作最大值以及操作完区间和会变成多少。若 \(x\) 小于次大值,则继续遍历
复杂度:\(O(n\log n)\)
无旋treap:
平衡树“平衡”:左节点和右节点高度最多差 1
原理:包括两种操作分裂和合并,区间操作时,将这个区间分离出来,然后在根节点打标记
特点:中序遍历是有序的
时间复杂度:\(\log\)
主席树:
原理:在进行单点操作时不选择修改,而是在原基础上新建,就可以通过访问不同的根回到任意一个历史版本
例题:
1.可持久化线段树1
2.可持久化线段树2(静态区间第 \(k\) 小)
分块:
原理:每个块都是并行的,不需要考虑快速合并和快速下传,只需考虑对一个块快速打上标记
时间复杂度:\(O(\sqrt{n})\)
例题:
静态区间众数
-
预处理出 \(f_{i,j}\) 表示第 \(i\) 块到第 \(j\) 块的众数
-
询问时找到询问包含第几块到第几块,众数就是 \(f_{i,j}\) 或者边界零散数中的一个,再暴力以下零散的数的出现次数
复杂度:\(O(n \sqrt{n} \log n)\)
莫队:
原理:只要保证将一个元素加入或移除当前集合时,答案的增量可简单计算即可
实现:
-
将询问离线,按左端点除 \(\sqrt{n}\) 为第一关键字,右端点为第二关键字排序
-
维护一对左右指针表示当前的数据结构维护了哪个区间的信息,然后在询问中暴力移动指针
时间复杂度:\(O(n\sqrt{n})\)
其他:带修莫队,树上莫队(欧拉序)、树上带修莫队、回滚莫队(不删除莫队)
字符串哈希:
把字符串看成 26 进制的超大的数字,串与穿之间匹配的时候,直接比较他们看成数字是否相同。比较大数字就看两数模一个数是否相同,但这样会有风险。减小风险的方法:
1.模取大质数
2.双哈希(出错的概率变成平方)
3.放弃优化:自然溢出(用 unsigned long long
来存大数字)
哈希的好处:
1.直观、具体
2.支持二维,便于数据结构维护
KMP:
给定一个长串 \(T\) 和一个短串 \(S\),问 \(S\) 在 \(T\) 的那些位置中出现(模式串匹配)。暴力枚举的复杂度是 \(O(n^2)\),我们希望快速求出一个 \(Fail\) 数组告诉我们枚举失配后该去哪里
快速求 \(Fail\) 数组:
-
初始化:\(Fail_1=0\)
-
假设已经求出了 \(Fail_1=0\) 到 \(Fail_i-1=0\),如果 \(S_i\)和\(S_{Fail_{i-1}+1}\) 相等,则 \(Fail_i=Fail_{i-1}+1\),否则就相当于一次失配,做一次失配操作即可
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
char a[N], b[N];
int nxt[N];
int main() {
cin >> a + 1 >> b + 1;
int n = strlen(a + 1), m = strlen(b + 1);
for (int i = 2; i <= m; i++) {
int j = nxt[i - 1];
while (j && b[j + 1] != b[i]) j = nxt[j];
if (b[j + 1] == b[i]) j++;
nxt[i] = j;
}
for (int i = 1, j = 0; i <= n; i++) {
while (j && a[i] != b[j + 1]) j = nxt[j];
if (a[i] == b[j + 1]) j++;
if (j == m) cout << i - m + 1 << '\n', j = nxt[j];
}
for (int i = 1; i <= m; i++)
cout << nxt[i] << ' ';
return 0;
}
另一种写法:
- 新建一个字符串,把短串放进去,再放一个 # 号,再把长串放进去然后求他的 \(Fail\),新串的 \(Fail\) 值等于短串的长度就是短串匹配的位置
Day4 动态规划
树形dp
例题:
1.树上带权最大独立集
状态表示:\(f_{i,0/1}\) 表示只考虑以 \(i\) 为根的子树,点 \(i\) 在(1)不在(0)独立集的权值最大和
状态转移:
-
\(f_{i,0}+=\sum_{j\in son_i} f_{j,0}\)
-
\(f_{i,1}=f_{i,1}+\sum_{j\in son_i} max(f_{j,0},f_{j,1})+w_i\)
复杂度:\(O(n)\)
2.树上带权最大独立集且独立集大小不能超过 \(m\)(树上背包)
状态表示:\(f_{i,j,0/1}\)表示只考虑以\(i\)为根的子树,选了 \(j\) 个点,点 \(i\) 在(1)不在(0)独立集的权值最大和,\(g_{i,j,0/1}\) 表示已经考虑了前 \(i\) 棵子树,总共用了 \(j\) 个点,根节点不在/在独立集中的权值最大和为多少
状态转移:将 \(f_{i,k,0/1}+g_{i,j,0/1}\) 转移到 \(g_{i+1,j+k,0/1}\)
复杂度:\(O(n^2)\)
3.CF1084D The Fair Nut and the Best Path
状态表示:\(f_i\)表示从点 \(i\) 出发走到某个 \(i\) 的子树内的点的最大收益
状态转移:\(f_i=max(f_j-cost_{j,i}+w_i)\)(\(j\) 为 \(i\) 的孩子节点)
答案表示:
-
(第一种)对每个点 \(i\),找两条通往其子树的不同路径拼起来,然后在所有点中取最大值
-
(第二种)在转移过程中同时记录最大值和次大值
时间复杂度:\(O(n)\)
4.CF461B Appleman and Tree
状态表示:\(f_{i,0/1}\) 表示考虑以 \(i\) 为根的连通块,\(i\) 所在的连通块没有/有一个黑点的方案数
状态转移:
-
\(f_{i,1}=f_{i,1}\times f_{j,0}+f_{i,1}\times f_{j,1}+f_{i,0}\times f_{j,1}\)
-
\(f_{i,0}=f_{i,0}\times f_{j,0}+f_{i,0}\times f_{j,1}\)
时间复杂度:\(O(n)\)
5.CF1499F Diameter Cuts
直径不超过 \(m\) 等价于对于每个点 \(i\),它到子树内的最长链和次长链(不通往同一个子树)长度之和不超过 \(m\)
状态表示:\(f_{i,j}\) 表示以 \(i\) 为根的子树,\(i\) 所在连通块的最深的点距离为 \(j\) 的方案数
状态转移:按顺序合并\(i\)与每个子节点 \(i'\):\(f_{i,max(j,k+1)}=f_{i,j}\times f_{i',k}(j+k+1\le m)\)
时间复杂度:\(O(n^2)\)
6.CF735E Ostap and Tree
状态表示:\(f_{i,j,k}\) 表示考虑以 \(i\) 为根的子树,\(i\) 到最近黑色点的距离为 \(j\),到最远不合法点距离为 \(k\) 的方案数
状态转移:通过判断 \(j+k'+1,j'+k+1\) 和 \(m\) 的大小关系即可得合并后离 \(i\) 的最远的不合法距离总而完成转移
时间复杂度:\(O(nk^4)\)
状压dp
例题:
1.给定一张带权无向完全图,求路径最小的哈密顿回路
状态表示:\(f_{S,i}\) 表示已经考虑好了 \(S\) 集合中的点的哈密顿路径,\(i\) 是该路径的最后一个点。一般用二进制数表示集合 \(S\),若 \(i\in S\),二进制第 \(i\) 位为1,否则为0
状态转移:\(f_{S,i}=min(f_S-i,j)+cost(j,i)(_{j\ne i,j\in S})\)
时间复杂度:\(O(2^n n^2)\)
2.给定一张无向图,将每个点染上一种颜色使得相邻点颜色不相同,求最少需要几种颜色
状态表示:\(f_S\) 表示对 \(S\) 集合内的点最小需要的颜色数,预处理 \(g_S\) 表示 \(S\) 集合是否是一个独立集
状态转移:\(f_S=min(f_{S-S'}+1)(S'\subseteq S,g_{S'}=1)\)
时间复杂度:\(O(3^n)\)
3.矩阵铺砖问题
状态表示:\(f_{i,j,S}\) 表示到第 \(i\) 行第 \(j\) 列,前 \(i\) 行以及第 \(i\) 行前 \(j\) 列均已被铺满,第 \(i+1\) 行的前 \(j\) 列与第 \(i\) 行的 \(j+1\) 到 \(m\) 列的格子被覆盖的状态为 \(S\)
状态转移:枚举在第 \(i\) 行的第 \(j+1\) 列摆着一个横着的砖块还是竖着的砖块转移
时间复杂度:\(O(nm2^m)\)
4.CF1391D 505
当 \(min(n,m)\ge 4\) 时无解,不妨假设 \(n\le 3\)
状态表示:\(f_{i,S}\) 表示考虑前 \(i\) 列均合法,第 \(i\) 列填的数字为 \(S\)
状态表示:\(f_{i,S}=min(f_{i-1,T}+bitcount(S\;xor\;a_i))\)(\(T\) 需满足 \(i-1\) 行和 \(i\) 列形成的所有 \(2\times 2\) 的子正方形的 1 的个数均为奇数
时间复杂度:\(O(m 2^{2n})\)
5.CF1215E Marbles
状态表示:\(f_S\) 表示只考虑 \(S\) 集合颜色的珠子,相同颜色的珠子排列在相连的一段所需要的最小操作次数
状态转移:
-
\(f_s=min(f_{S-x+cost})(x\in S)\)
-
\(cost=\sum_{i<j} [a_i=x][a_j\in S-{x}]\)
-
\(s_{x,y}=\sum_{i<j}[a_i=x][a_j=y]\)
时间复杂度:\(O(m^2 2^m+nm)\)
6.CF1242C Sum Balance
操作结束后每个盒子中的数字之和是一个定值\((\sum a_i)/k\),当取出一个数\(x\)后,要放入盒子的\(y\)也被确定,考虑从\(x\)向\(y\)连一条边,那么整张图就会变成一个基数环森林。
状态表示:$f_S $表示 \(S\) 集合的盒子是否可以由图中若干个环拼成
状态转移:枚举 \(S\) 的子集 \(T\)(\(T\) 是某个环上数字所在的盒子的集合),若 \(f_{S\div T}\) 为真则 $f_{S} $为真,否则若所有 $f_{S\div T} $ 均为假,则 \(f_{S}\) 为假
时间复杂度:\(O(3^k)\)
数位dp
例题:
1.求 1 到 \(n\) 中满足含有字串 13 且能被 13 整除的数的个数
状态表示:从高位往低位考虑,\(f_{i,j,0/1/2,0/1}\) 表示考虑到第 \(i\) 位,模 13 余 \(j\),第三位 0 表示第 \(i\) 位不为 1,1 表示第 \(i\) 位为 1,2 表示大于等于 \(i\) 的位存在字串 13,第四位表示是否取到上界
状态转移:
\(f_{i,j,k,l}\)
-
若 \(l=1\),则 \(d\) 不能大于 \(n\) 第 \(i-1\) 位的数字
-
若 \(l=0\) 或 \(l=1\) 但 \(d\) 小于 \(n\) 第 \(i-1\) 的数字则 \(l'=0\),否则 \(l'=1\)
-
若 \(k=2\) 或 \(k=1\) 且 \(d=3\) 则 \(k'=2\),否则若 \(d=1\) 则 \(k'=1\),否则 \(k'=0\)
-
\(f_{i,j,k,l}\) 从 \(f_{i-1,(j+d \times 10^{i-1})\%13,k',l'}\) 转移
-
对于位数小于 \(n\) 的满足条件的数,使用前导零将其位数补到和 \(n\) 的位数相同
时间复杂度:\(O(log n\times 13\times 3\times 2\times 10)\)
2.CF55D Beautiful numbers
- 统计\([1,r]\)与\([1,l-1]\)的答案来算出\([l,r]\)的答案
状态表示:\(f_{i,j,k,0/1}\) 表示从高到低考虑到了第 \(i\) 位,大于等于 \(i\) 的位填的数的lcm
为 \(j\),模 2520 余 \(k\),第四位表示是否取到上界
状态转移:\(f_{i,j,k,0/1}\) 从 \(f_{i-1,lcm(j,d),(k+d\times 10^{i-1})\%2520,0/1}\) 转移
时间复杂度:\(O(\log r\times 48\times 2520\times 2\times 10)\)
数据结构优化dp
例题:
1.最长上升子序列
-
\(f_i\) 表示以 \(i\) 结尾的最长上升子序列长度
-
求出 \(f_i\) 后用树状数组/线段树 \(A_i\) 位置插入 \(f_i\) 的值,查询前缀和最大值来转移
时间复杂度:\(O(n log n)\)
2.单调队列优化多重背包
状态表示:第二位按模 \(w_i\) 分组,\(f_{i,j}\) 表示考虑前 \(i\) 个物品,选取了重量和为 \(j\times w_i\) 的物品的最大价值
状体转移:\(f_{i,j}=max(f_{i-1,k}+(j-k)\times v_i)(j-a_i\le k\le j)\),那么就是求区间 \([j-a_{i,j}]\) 的 \(f_{i-1,k}-k\times v_i\) 的最大值,用单调队列维护
时间复杂度:\(O(nm)\)
3.CF1256E Yet Another Division Into Teams
状态表示:\(f_i\) 表示只考虑前 \(i\) 个数,得到的极差之和的最小值
状态转移:\(f_i=min(f_j+a_i-a_{j+1})(a\le i-3)\)
时间复杂度:\(O(n^2)\)
优化:
F1:
-
\(a_i\) 是定值,所以 \(f_i=min(f_j-a_{j+1})+a_i\)
-
记录 \(f_j-a_{j+1}\) 的前缀最小值即可将转移优化至 \(O(n)\)
-
时间复杂度:\(O(n \log n)\)
F2:
每组的大小一定不超过 5,否则可将其分为多组来获得更小的极差之和
状态转移:\(f_i=min(f_j+a_j-a_{j+1})(i-5\le j\le i-3)\)
时间复杂度:\(O(n \log n)\)
4.CF1427C The Hard Work of Paparazzi
状态表示:\(f_i\) 表示考虑了前 \(i\) 个人,目前正好见到了第 \(i\) 个人时最多能见到多少人
状态转移:\(f_i=max(f_j)+1(1\le j<i,\left| x_i-x_j\right |+\left| y_i-y_j\right| \le t_i-t_j)\)
时间复杂度:\(O(n^2)\)
优化:
注意到 \(r\) 很小,且任意两点间移动时间均不超过 \(2r\),所以对于 \(i\le j<i-2r\),\(i\) 一定能从 \(j\) 转移过来,于是维护前缀最大值,仅需枚举 \(i-2r\le j<i\) 的 \(j\) 并进行判断即可
时间复杂度:\(O(nr)\)
5.arc108e Random IS
状态表示:\(f_{i,j}\) 表示只考虑区间 \([i,j]\),且 \(i,j\) 均已被标记,期望还会标记多少个数
状态转移:\(f_{i,j}=\frac{1}{cnt}\sum_k(f_{i,k}+f{k,j})+1(a_i\le a_k \le a_j)\)(\(cnt\) 为合法的 \(k\) 的个数)
优化:
\(f_{i,k}\) 与 \(f_{k,j}\) 无关 \(\Rightarrow f_{i,j}=\frac{1}{cnt}\sum_k f_{i,k}+\frac{1}{cnt}\sum_k f_{k,j}+1\),可以用线段树分开维护两部分的贡献
时间复杂度:\(O(n^2\log n)\)
Day 5 图论
图的存储结构:
1.图的直接存储:直接使用一个大小为 \(\left\vert E\right\vert\) 的数组存储所有的信息
2.图的邻接矩阵:使用一个二位数组 \(G\) 来存边
-
优点:在简单稠密图图中简单直接
-
缺点:空间开销大,不能很好地适应重边和自环等复杂的情况
3.图的邻接表:对于每一个顶点 \(u\) 使用一个表 \(G_u\) 来存下这个点所有出边的信息
最短路:
如果两个点不连通,那么他们之间的最短路径不存在,最短路径的权值为 \(+inf\)
所有最短路算法都基于一个原则(三角形不等式):\(d_{u,v}\le d_{u,k}+d_{k,v}\),如果发现 \(d'_{u,v}>d'_{u,k}+d'_{k,v}\) 那么一定可将 \(d'_{u,v}\) 更新成 \(d'_{u,k}+d'_{k,v}\),这个操作也被称为松弛操作
全源最短路——Floyd算法
(记录路径)在更新的过程中如果 \(G_{u,v}>G_{u,k}+G_{k,v}\),则 \(G_{u,v}=G_{u,k}+G_{k,v}\),\(pre_{i,j}=k\)
时间复杂度:\(O(|V|^3)\)
for (int k = 1; k <= n; k++)
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
f[i][j] = min(f[i][j], f[i][k] + f[k][j]);
单源最短路——Bellman-Ford算法
Bellman-Ford算法核心在于最短路的性质:对于一个长度有限队最短路径,经过的边数一定是小于 \(|V|\),由于一次松弛操作会使最短路的边数至少加 1,因此整个算法最多执行 \(|V|-1\) 轮
记录路径:pre[e[i].to]=i;
负环判断:在\(n\)轮松弛时,如果有顶点被更新,则这个图存在负环
时间复杂度:\(O(|V||E|)\)
单源最短路——SPFA算法(Bellman-Ford算法的队列实现)
平均时间复杂度:\(O(k|E|)\),其中 \(k\) 可能被卡到 \(|V|\) 的级别
单源最短路——dijkstra算法
dijkstra 在非负权图上基于贪心的想法:将节点分成两个集合,已确定最短路长度的点集和未确定的点集,每次从未确定的点集中选取一个当前的最短路长度最小的节点,移到已确定的节点中,对其邻接点进行松弛
朴素实现的时间复杂度:\(O(|V|^2+|E|)\)
堆优化:在寻访当前最优未访问节点时使用数据结构优化
时间复杂度:\(O((|V|+|E|)log|V|)\)
差分约束:
-
差分约束系统是一种特殊的 \(n\) 元一次不等式,包含 \(n\) 个变量 \(x_1,x_2,\cdots,x_n\) 以及 \(m\) 个约束条件,每个条件是有两个其中的变量做差构成的如:\(x_i-x_j\le c_k\)(\(c_k\) 是常数)
-
我们要解决的问题是求一组解 \(x_1=a_1,x_2=a_2,\cdots,x_n=a_n\) 使得所有约束条件满足,否则无解。
-
对于不同形式的(不)等式,可将其统一变形成 \(x_i-x_j\le c_k\):
\(x_a-x_b\ge c \Rightarrow x_b-x_a\le -c\)
\(x_a=x_b \Rightarrow\) \(x_a-x_b \le 0\)或\(x_b-x_a \le 0\)
-
其中每个条件都可以变形成 \(x_i\le x_j+c_k\),我们可以把每个 \(x_i\) 看作图中的一个节点,每个约束条件就从 \(j\) 节点向\(i\)节点连一条长度为 \(c_k\) 的有向边
-
设 \(dist_0=0\) 并向每个点连一条边权为 0 的边,跑单源最短路,若图中有负环则无解,否则 \(x_i=dist_i\) 为差分约束系统中的一组解
欧拉路:
- 欧拉路是指通过图中每条边恰好一次的迹(一笔画)
充要条件:非零度顶点是联通的;存在零个或两个奇点
寻找:找到任意一条两个奇点之间的路径,每次从当前路中选一个点,用新的点包含该点的回路替代之(DFS 树+栈)
时间复杂度:\(O(|V|+|E|)\)
- 欧拉回路是指图中每条边恰好一次的回路
充要条件:非零度顶点是联通的;不存在奇点。
寻找:每次从当前回路中选一个点,用新的点包含该点的回路替代之
无根树编码——Prufer序列
树的搜索序列
树的遍历:深度优先搜索和广度优先搜索
深度优先遍历(能维持树结构的连贯性):
- dfs序:
实现:从根开始遍历,首次进入一个节点时将编号记录下来
优点:子树连续性
- 欧拉序:
实现:从根开始遍历,每次访问进入与回溯每一个节点时将编号记录下来的序列
主要是解决 LCA 问题
- 括号序:
实现:从根开始遍历,进入与回溯每一个节点时将编号记录下来
倍增思想
核心在于信息的可加性。
-
我们可以先求出 \((x,x+1)\) 的答案,由此合并 \((x,x+1)\) 和 \((x+2,x+3)\) 得到 \((x,x+3)\)。这样我们就可以知道所有长度为 \(2^k\) 区间的答案。时空复杂度:\(O(Cnlogn)\)
-
对于查询任意区间的答案,将区间长度进行二进制分解。时间复杂度:\(O(Clogn)\)
\(\Rightarrow\) 已知区间长度求答案
\(\Rightarrow\) 已知答案限制求区间长度
RMQ算法
核心在于信息对于并操作可以计算
给定一个恒定序列,有若干次询问,每次询问某个区间的最值大小。
-
因为 \([L,x2][x1,R](L\le x_1 \le x_2 R)\) 可以推出 \([L,R]\) 的信息,所以可以预处理每点为左端点区间长度为 \(2^i\) 的区间最值
-
对于每次询问区间 \([L,R]\),区间长度就是 \(len=R-L+1\),答案就是 \([L,L+2^{log(len)}]\) 与 \([R-2^{log(len)}+1,R]\) 的最值合并
最近公共祖先(LCA)
在一棵有根树中对于任意两个节点,找到他们路径上深度最小的那个点
暴力实现:
先把两个节点跳到同一深度,然后每次暴力一起往上跳直到他们相遇
时间复杂度:\(O(n)\)
倍增优化:
-
已知深度差跳到同一深度 \(\Rightarrow\) 把深度差二进制拆解后暴力往上跳
-
共同找一个祖先使得深度最小 \(\Rightarrow\) 先让他们都调到尽可能远的祖先,如果祖先相同就说明祖先不是最近的,不应该跳这一步,反复处理就可以跳到 LCA 的下一层,再暴力往上跳一次
RMQ求解:
如果按欧拉序记录每个节点的深度,将两个节点第一次进入时的位置为左右端点,这个区间的深度最小值就是他们 LCA 的深度
最小生成树
Krusual算法
Krusual 算法采用了贪心的思想,从最小边权开始,按边权从小到大依次加入,如果某次加边产生了环(用并查集维护),就扔掉这条边
时间复杂度:\(O(|E|log|E|)\)
Prim算法
Prim 算法是基于点的方向:将节点分成两个集合,已经在生成树里和没在的,每次从当前生成树点集中选取一个当前代价最小的节点加到生成树中
时间复杂度:\(O(|V|^2+|E|)\)
堆优化时间复杂度:\(O((|V|+|E|)log|V|)\)
Boruvka算法
定义 \(E'\) 为当前找到的最小生成树的边,执行过程中逐步向 \(E'\) 加边,定义一个连通块的最小边为它连向其他连通块边中权值最小的一条。重复执行:
-
记录每个点分别属于哪个连通块,将每个连通块设为没有最小边
-
遍历每条边 \((u,v)\) 如果 \(u\) 和 \(v\) 不在同一个连通块,就用这条边分别更新 \(u,v\) 所在连通块的最小边
-
如果所有连通块都没有最小边则退出程序
-
此时的 \(E'\) 就是原图最小生成树的边集
时间复杂度:\(O(|E|log|V|)\)
次小生成树
分为非严格次小生成树和严格次小生成树
考虑更换的边加到次小生成树中一定会形成一个环,这个环包括的树边是树上 \(u\) 到 \(v\) 的简单路径
-
非严格次小生成树:找到这条简单路径上的边权最大值替换
-
严格次小生成树:还需维护次大值