1090 字
5 分钟
最近公共祖先
向上标记法
// 第一次dfs标记根节点的该结点之间所有点bool dfs(int cur, int fa, int val) { if (cur == val) { id[cur] = true; return true; } for (int i = h[cur]; ~i; i = ne[i]) { int v = e[i]; if (v == fa) continue; if (dfs(v, cur, val)) { id[cur] = true; return true; } } return false;}// 第二次dfs遇到有标记的点就是lca(a,b)int redfs(int cur, int fa, int val) { if (cur == val) { if (id[cur]) return cur; return -1; } for (int i = h[cur]; ~i; i = ne[i]) { int v = e[i]; if (v == fa) continue; int t = redfs(v, cur, val); if (t > 0) return t; else if (t == -1) { if (id[cur]) return cur; return -1; } } return 0;}倍增法
预处理
- :结点 的第 个祖先 ,其中 是结点的数量,注意下标从 开始,因为这样方便结点 充当一个虚拟最大祖先,也就是哨兵的意思
- 表示结点 的深度,注意 ,因为这样方便
根据预处理求结点 的第 个祖先 对 进行二进制拆分: ,因此求 的第 个祖先就是
求
- 对齐深度,把深度较高的点,假设为 ,找到 的第 个祖先结点 ,这样就对齐了两点的深度。
- 同样地,利用二进制拆分将 移动到 的下一个结点,最终再将变化后的 或 再往上跳一格,也就是 就是最近公共祖先
代码如下:
/*dfs调用: dfs(r, 0, 1)*/int h[N], e[N << 1], ne[N << 1], idx = 0;int f[N][18], dep[N];int n, r, tot = 0;
void dfs(int u, int fa, int cur) {
dep[u] = cur; f[u][0] = fa; for (int i = 1; i < 18; i ++ ) f[u][i] = f[f[u][i - 1]][i - 1]; for (int i = h[u]; ~i; i = ne[i]) { int v = e[i]; if (v == fa) continue; dfs(v, u, cur + 1); }}// bfs 预处理更加保险,不会爆栈void bfs() { queue<int> Q; Q.push(r); dep[r] = 1; st[r] = true;
while (Q.size()) { int u = Q.front(); Q.pop(); for (int i = h[u]; ~i; i = ne[i]) { int v = e[i]; if (st[v]) continue; dep[v] = dep[u] + 1; f[v][0] = u; for (int i = 1; i < 18; i ++ ) f[v][i] = f[f[v][i - 1]][i - 1]; Q.push(v); st[v] = true; } }}
int lca(int x, int y) {
if (dep[x] > dep[y]) swap(x, y); for (int i = 17; i >= 0; i -- ) if (dep[f[y][i]] >= dep[x]) y = f[y][i];
if (x == y) return x; for (int i = 17; i >= 0; i -- ) if (f[x][i] != f[y][i]) x = f[x][i], y = f[y][i]; return f[x][0];}tarjan 离线算法
离线算法和在线算法
离线算法:一般的面向有多次询问的题面,然后一次性把所有询问读入,然后再输出结果的算法 在线算法:即问即答
tarjan 求 lca
如图所示:在 dfs 的过程中访问到某点时我们可以将点分为三类,其中蓝色的点表示已经访问完的点,绿色的点相当于在递归栈中的点,也就是正在访问的点,白色的点表示未尚未标记的点。
根据这样对点进行划分,有一个结论:如果要求两个点的最近公共祖先,其中一个点在已经访问完的点中,一个点就是当前正在访问的点,那么这些点对的最近公共祖先一定是绿色集合中的点。为此,可以提前将所有询问存储,进而设计一个一次遍历整棵树的同时回答所有询问的离线算法。
算法流程:
- 首先存储所有询问,存储方式类似于
vector写法的邻接表,query[i]存储所有与点 相关的查询与其查询索引 - 然后从任意一个点做 tarjan 算法 2.1 首先遍历当前点的所有孩子,然后遍历完利用并查集把当前孩子的双亲设置为当前结点 2.2 利用并查集求解与这个点所有相关的,另外一个点是已经访问完的所有询问d 2.3 将当前结点标记为已经访问完的状态 代码如下:
int h[N], e[M], ne[M], idx = 0;int st[N], fa[N];vector<PII> query[M];int lca[M];int n, m;
void tarjan(int u) {
st[u] = 1; for (int i = h[u]; ~i; i = ne[i]) { int v = e[i]; if (st[v]) continue; tarjan(v); fa[v] = u; } for (auto [v, id]: query[u]) if (st[v] == 2) lca[id] = get(v); st[u] = 2;}