*【整理】一些基础板子
一些基础板子,不打算新开一篇学习笔记,就都整理在这里。
Ⅰ.LCA
1.倍增
预处理和单次查询的复杂度分别为 \(O(nlogn)\) 和 \(O(logn)\),空间 \(O(nlogn)\)。
倍增预处理 $i 的 \(2^k\) 级祖先,记为 \(f_{i,k}\),求出每个节点的深度 \(d_i\) 。
查询 \(u,v\) LCA 时,先将 \(u,v\) 跳到同一深度,再 同时 向上跳到 \(u,v\) 深度最小的祖先 \(anc_u,anc_v\) 满足 \(anc_u≠anc_v\),则 \(fa(anc_u)=fa(anc_v)=lca(u,v)\)。注意特判 \(u,v\) 跳到同一深度后 \(u=v\) 的情况,此时原 \(u,v\) 为祖先后代关系。
- 易错点:若采用下面代码写法,根节点深度应设为 \(1\) 而非 \(0\),否则若 \(v\) 为根节点,因 \(dep_0\) 没有初始化,故当 \(2^k≥dep_u\)时 \(dep(f_{u,k})=dep(f_0)=0\),且 \(depv=0\),导致 \(u←f_{u,k}=0\)。
#include<bits/stdc++.h>
#define N 500005
#define INF 0x3f3f3f3f
using namespace std;
int fa[N],d[N];
int Head[N],to[N*2],Next[N*2];
int f[N][25];
int n,m,t,s,tot;
void add(int u,int v){
to[++tot]=v;
Next[tot]=Head[u];
Head[u]=tot;
}
void bfs(int s){
queue<int> q;
q.push(s);
d[s]=1;
while(!q.empty()){
int x=q.front();
q.pop();
for(int i=Head[x];i;i=Next[i]){
int y=to[i];
if(d[y]) continue;
d[y]=d[x]+1;
f[y][0]=x;
for(int j=1;j<=t;j++){
f[y][j]=f[f[y][j-1]][j-1];
}
q.push(y);
}
}
}
int lca(int x,int y){
if(d[x]>d[y]) swap(x,y);
for(int i=t;i>=0;i--){
if(d[f[y][i]]>=d[x]){
y=f[y][i];
}
}
if(x==y) return x;
for(int i=t;i>=0;i--){
if(f[x][i]!=f[y][i]){
x=f[x][i];
y=f[y][i];
}
}
return f[x][0];
}
int main(){
scanf("%d%d%d",&n,&m,&s);
for(int i=1;i<n;i++){
int x,y;
scanf("%d%d",&x,&y);
add(x,y);
add(y,x);
}
t=(int)(log(n)/log(2))+1;
bfs(s);
for(int i=1;i<=m;i++){
int x,y;
scanf("%d%d",&x,&y);
printf("%d\n",lca(x,y));
}
return 0;
}
2.欧拉序
\(O(nlogn)−O(1)\),空间 \(O(nlogn)\),有两倍常数。
欧拉序有以下两条性质:
- 任意两点简单路径上所有节点均在它们的欧拉序之间出现。
- 任意两点欧拉序之间不会出现它们 LCA 子树外的点。
使用 ST 表,设 \(f_{k,i}\) 表示欧拉序 $i∼i+2^k−1 $ 位置上 深度 最小的节点,查询可做到 \(O(1)\)。
int ol[N << 1][21], lg[N << 1], pos[N], d[N];
void dfs0(int x, int f){
ol[++cnt][0] = x, pos[x] = cnt, d[x] = d[f] + 1;
for(int i = Head[x]; i; i = Next[i]){
int y = to[i]; if(y == f) continue;
dfs0(y, x), ol[++cnt][0] = x;
}
}
int get_min(int a, int b){
return d[a] < d[b] ? a : b;
}
void get_ol(){
for(int i = 2; i <= cnt; ++i) lg[i] = lg[i >> 1] + 1;
for(int t = 1; (1 << t) <= cnt; ++t)
for(int i = 1; i + (1 << t) <= cnt; ++i)
ol[i][t] = get_min(ol[i][t - 1], ol[i + (1 << t - 1)][t - 1]);
}
int lca(int x, int y){
if(pos[x] > pos[y]) swap(x, y);
int xx = pos[x], yy = pos[y], len = yy - xx + 1;
int lca = get_min(ol[xx][lg[len]], ol[yy - (1 << lg[len]) + 1][lg[len]]);
return lca;
}
3.dfs 序
考虑树上的两个结点 \(u, v\) 及其最近公共祖先 \(d\),我们不得不使用欧拉序求 LCA 的原因是在欧拉序中,\(d\) 在 \(u, v\) 之间出现过,但在 DFS 序中,\(d\) 并没有在 \(u, v\) 之间出现过。对于 DFS 序而言,祖先一定出现在后代之前(性质)。
不妨设 \(u\) 的 DFN 小于 \(v\) 的 DFN(假设)。
当 \(u\) 不是 \(v\) 的祖先 时(情况 1),DFS 的顺序为从 \(d\) 下降到 \(u\),再回到 \(d\),再往下降到 \(v\)。
根据性质,任何 \(d\) 以及 \(d\) 的祖先均不会出现在 \(u\sim v\) 的 DFS 序中。
考察 \(d\) 在 \(v\) 方向上的第一个结点 \(v'\),即设 \(v'\) 为 \(d\) 的 / 子树包含 \(v\) 的 / 儿子。根据 DFS 的顺序,显然 \(v'\) 在 \(u\sim v\) 的 DFS 序之间。
这意味着什么?我们只需要求在 \(u\) 的 DFS 序和 \(v\) 的 DFS 序之间深度最小的任意一个结点,那么 它的父亲 即为 \(u, v\) 的 LCA。
这样做的正确性依赖于在 DFS 序 \(u\) 到 \(v\) 之间,\(d\) 以及 \(d\) 的祖先必然不会存在,且必然存在 \(d\) 的儿子。
\(u, v\) 成祖先后代关系(情况 2)是容易判断的,但这不优美,不能体现出 DFS 求 LCA 的优势:简洁。为了判断还要记录每个结点的子树大小,但我们自然希望求 LCA 的方法越简单越快越好。
根据假设,此时 \(u\) 一定是 \(v\) 的祖先。因此考虑令查询区间从 \([dfn_u, dfn_v]\) 变成 \([dfn_u + 1, dfn_v]\)。
对于情况 1,\(u\) 显然一定不等于 \(v'\),所以情况 2 对于算法进行的修改仍然适用于情况 1。
综上,若 \(u\neq v\),则 \(u, v\) 之间的 LCA 等于在 DFS 序中,位置在 \(dfn_u + 1\) 到 \(dfn_v\) 之间的深度最小的结点的父亲。若 \(u = v\),则它们的 LCA 就等于 \(u\),这是唯一需要特判的情况。
预处理 ST 表的复杂度仍为 \(\mathcal{O}(n\log n)\),但常数减半。
int get_min(int x, int y){return dep[x] < dep[y] ? x : y;}
void dfs(int x, int ff){
dfn[x] = ++dn, dep[x] = dep[ff] + 1, mi[0][dn] = ff;
for(auto y : e[x]) if(ff != y) dfs(y, x);
}
int lca(int u, int v){
if(u == v) return u;
if((u = dfn[u]) > (v = dfn[v])) swap(u, v);
int d = lg[v - u++];
return get_min(mi[d][u], mi[d][v - (1 << d) + 1]);
}
int main(){
dfs(1, 0);
for(int i = 2; i <= n; ++i) lg[i] = lg[i >> 1] + 1;
for(int i = 1; i <= lg[n]; ++i)
for(int j = 1; j + (1 << i) - 1 <= n; ++j)
mi[i][j] = get_min(mi[i - 1][j], mi[i - 1][j + (1 << i - 1)]);
return 0;
}
4.树链剖分
\(O(n)−O(logn)\),空间 \(O(n)\)。
树剖求 LCA 的空间复杂度更优,可配合需要树剖的题目使用。
查询 \(u,v\) 的 LCA 时,只要 \(u,v\) 不在同一重链,设 \(u\) 重链顶端的深度不小于 \(v\) 重链顶端的深度,则令 \(u\) 变为其重链顶的父亲。最终 \(u,v\) 在同一重链,深度较小的节点即原 \(u,v\) LCA。
int LCA(int x, int y){
while(top[x] != top[y]){
if(d[top[x]] < d[top[y]]) swap(x, y);
x = fa[top[x]];
}
if(d[x] > d[y]) swap(x, y);
return x;
}
5.Tarjan
遍历每一个结点并使用并查集记录父子关系。
Tarjan 是一种 DFS 的思想。我们需要从根结点去遍历这棵树。
当遍历到某一个结点(称之为 \(x\)) 时,你有以下几点需要做的。
-
将当前结点标记为已经访问。
-
递归遍历所有它的子节点(称之为 \(y\)),并在递归执行完后用并查集合并 \(x\) 和 \(y\)。
-
遍历与当前节点有查询关系的结点(称之为 \(z\))(即是需要查询 LCA 的另一些结点),如果 \(z\) 已经访问,那么 \(x\) 与 \(z\) 的 LCA 就是 find(z)
(即 \(z\) 回溯过的深度最小的祖先(想一下显然这就是LCA)),记录下来就可以了。
#include<bits/stdc++.h>
#define N 500005
using namespace std;
int read(){
int x = 0, f = 1; char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-')f = -f; ch = getchar();}
while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();}
return x * f;
}
int n, m, rt, tot;
int Head[N], to[N<<1], Next[N<<1];
int fa[N], ans[N], vis[N];
vector<int> q[N], q_id[N];
int find(int x){return fa[x] == x ? x : fa[x] = find(fa[x]);}
void add(int u, int v){
to[++tot] = v, Next[tot] = Head[u], Head[u] = tot;
}
void tarjan(int x){
vis[x] = 1;
for(int i = Head[x];i ;i = Next[i]){
int y = to[i];
if(vis[y]) continue;
tarjan(y); fa[y] = find(x);
}
for(int i = 0; i < q[x].size(); ++i){
int y = q[x][i], id = q_id[x][i];
if(vis[y]) ans[id] = find(y);
}
}
signed main(){
n = read(), m = read(), rt = read();
for(int i = 1; i < n; ++i){
int u = read(), v = read();
add(u, v), add(v, u);
}
for(int i = 1; i <= n; ++i) fa[i] = i;
for(int i = 1; i <= m; ++i){
int u = read(), v = read();
q[u].push_back(v), q[v].push_back(u);
q_id[u].push_back(i), q_id[v].push_back(i);
}
tarjan(rt);
for(int i = 1; i <= m; ++i) printf("%d\n", ans[i]);
return 0;
}
*6.The Method of Four Russians
Ⅱ.并查集
int find(int x){return x == fa[x] ? x : fa[x] = find(fa[x]);}
int main(){
for(int i = 1; i <= n; ++i) fa[i] = i;
}
Ⅲ.快速幂
int qsm(int a, int b){
int res = 1;
for(; b; b >>= 1, a = a * a % mod) if(b & 1) res = res * a % mod;
return res;
}
Ⅳ.线性筛
void init(){
for(int i = 2; i <= 1e7; ++i){
if(!v[i]) prim[++tot] = i;
for(int j = 1; j <= tot&& 1ll * i * prim[j] <= 1e7; ++j){
v[i * prim[j]] = 1;
if(i % prim[j] == 0) break;
}
}
}
Ⅴ.ST表
lg[1] = 0;
for(int i = 2; i <= n; ++i) lg[i] = lg[i >> 1] + 1;
void ST_prework(){
for(int i = 1; i <= n; ++i) f[i][0] = a[i];
int t = log(n) / log(2) + 1;
for(int j = 1; j < t; ++j)
for(int i = 1; i <= n - (1 << j) + 1; ++i)
f[i][j] = max(f[i][j - 1], f[i + (1 << (j - 1))][j - 1]);
}
int ST_query(int l, int r){
int k = lg[r - l + 1];
return max(f[l][k], f[r - (1 << k) + 1][k]);
}
Ⅵ.三分法
while(l + eps < r){
db len = (r - l) / 3.0;
db midl = l + len, midr = r - len;
if(f(midl) > f(midr)) ans = midl, r = midr;
else l = midl;
}