很久以前学习过,看了很多花花绿绿的博客,但好像理解错了。直到最近打了一道非严格次小生成树(Kruskal+LCA),才发现我写的整个倍增全是错的,然后树剖、塔尖一点都想不起来,所以在一个无聊的下午整理整理。
一、倍增
O(nlgn)建表,询问O(lgn),直接上两段代码(因为具体思路挺难说明白的):
void DFS(int u, int fa)//一遍dfs求出f数组(或者还有g数组,记录长度啊,最值啊之类的东西)
{
f[u][0] = fa;
for (int i = 1; i <= 17; i++) //u向上跳(1<<i)次到的点就是他跳(1<<(i-1))再跳(1<<(i-1))
f[u][i] = f[f[u][i-1]][i-1], g[u][i] = g[u][i-1]+g[f[u][i-1]][i-1];
for (int i = 0; i < to[u].size(); i++){
int v = to[u][i];
int w = val[u][i];
if (v == fa) continue;
dpt[v] = dpt[u]+1;
g[v][0] = w;
DFS(v, u);
}
}
void LCA(int x, int y, int &u, int &l)//求x和y的LCA,再顺便求两个点之间的距离(或者其他东西)
{
l = 0;
if (x < y) swap(x, y);
for (int i = 17; i >= 0; i--)//如果深度不同先跳成相同
if (dpt[x]-(1<<i) >= dpt[y])
l += g[x][i], x = f[x][i];
for (int i = 17; i >= 0; i--)
if (f[x][i] != f[y][i]){//跳到不一样的祖先节点,防止跳过头
l += g[x][i]+g[y][i];
x = f[x][i], y = f[y][i];
}
if (x != y) l += g[x][0]+g[y][0], x = f[x][0];//把最后一步补上
u = x;
}
大致思路就是酱紫
二、tarjan
这是一个离线算法,O(n+q)。
大致步骤:(摘自JVxie的博客)
1.任选一个点为根节点,从根节点开始。
2.遍历该点u所有子节点v,并标记这些子节点v已被访问过。
3.若是v还有子节点,返回2,否则下一步。
4.合并v到u上。
5.寻找与当前点u有询问关系的点e。
6.若是e已经被访问过了,则可以确认u和e的最近公共祖先为e被合并到的父亲节点。
void Tarjan(int u)//marge和find为并查集合并函数和查找函数
{
fa[u] = u;
vis[u] = 1; //标记u被访问过;
for (int i = 0; i < to[u].size(); i++){ //访问所有u子节点v
int v = to[u][i];
if (!vis[v]){
Tarjan(v); //继续往下遍历
fa[v] = u; //合并v到u
}
}
for (int i = 0; i < query[u].size(); i++){ //访问所有和u有询问关系的e
int e = query[u][i];
int fa;
if (vis[e]) FIND(e); //LCA就是e的祖先
}
}
怎么证明他是对的呢?首先可以手膜,找个几组数据一模就懂了。
然后是正经的证明:
在对于某个节点u处理他的询问时,已访问的e有两种状态。
- 一个是在u的子树里,那他已经被连到u上了
- 然后就是不在u的子树里,这种情况下tarjan()必定是从e一路回溯到LCA然后再向下搜到u的。如果回溯到的最高点深度小于于LCA了,那时整个u所在的子树都已经被搜过一遍,不会再搜到u,不可能存在这种情况。如果回溯到的点比LCA深,那u和e还是被隔离开的,也就是说e还没被访问,与条件不符。
三、RMQ
利用欧拉序(就是搜到就记,回溯还记录的那个,上面那张图的欧拉序:1-2-5-2-6-2-1-3-7-3-8-9-11-9-8-10-12-10-8-3-1-4-1)因为这个奇怪的序列的某些奇怪的性质,在某两个点最早出现的位子之间的那些点里,深度最浅的就是LCA了。
于是问题转化为了RMQ(区间最小值),用st表求解,O(nlgn)建表,O(1)询问。
四、树链剖分
懒懒懒懒懒懒懒懒懒懒懒懒懒懒懒懒
剖轻重链,如果两个点在同一根链上,LCA就是深度较浅的那个点。如果不在同一条链上,就选深的点往上跳。具体实现还挺简单的,比前面几种都要短。