*【整理】一些基础板子

一些基础板子,不打算新开一篇学习笔记,就都整理在这里。

Ⅰ.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\)) 时,你有以下几点需要做的。

  1. 将当前结点标记为已经访问。

  2. 递归遍历所有它的子节点(称之为 \(y\)),并在递归执行完后用并查集合并 \(x\)\(y\)

  3. 遍历与当前节点有查询关系的结点(称之为 \(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;
}

参考资料:https://www.cnblogs.com/alex-wei/p/Tree_Tree_Tree_Tree_Tree_Tree_Tree_Tree_Tree_Tree_Tree_Tree_Tree_Tree_Tree.html

posted @ 2023-08-27 18:04  Aurora-JC  阅读(19)  评论(0编辑  收藏  举报