LCA(最近公共祖先) & 树链剖分 学习笔记

LCA(最近公共祖先)

什么是LCA?

顾名思义自然 就是对于树上两个节点,  距离它们最近的公共祖先

比如上图中,3和4、2和5的最近公共祖先都是1,5和6的最近公共祖先是3。

当然并没有什么题会让你直接打模板求两个点的最近公共祖先除非出题人喝了假酒,更多的时候都是:给你一棵树才怪一个图求出MST之类的东西,然后在MST上求两个点之间权值之和或者极值或者别的什么东西


如何求LCA?

在求两个点u和v的LCA 的时候,显然u和v的深度不一定相同,于是很容易想到先将深的那个跳到和浅的那个相同的深度,然后两个一起往上跳,直到到达一个相同的点为止,这个点显然就是它俩的LCA。

大概是在侮辱智商如果这个树刚好分成两叉时间复杂度会达到O(n)...

上面这个小学生都会的求LCA的方法是没有意义的orz

首先求LCA有一种tarjan算法,时间复杂度是非常优秀的O(n+q),然而这是一个离线算法,所以并不打算写...看一篇优秀的博客

LCA主要是两个在线的算法:


ST(RMQ)算法求LCA

这种方法的思想就是把LCA问题转化成RMQ问题,用ST算法来求解。

首先转化为RMQ,还是这棵树:

我们对它深度优先遍历,得到了一个dfs序。如果将1作为根节点,dfs序就是:1-2-4-2-1-3-5-3-6-3-1(到叶子节点后就返回父节点)

观察这个dfs序我们可以发现:

对于两个点x和y,用pos[i]表示i第一次在dfs序中出现的位置,那么从pos[x]到pos[y]的路径必然是在dfs序中连续的一段,显然这一段中必然包括x和y的最近公共祖先,且lca(x, y)必然是这一段中深度最小的。

所以我们只需要用ST表维护这个dfs序的区间最小值,查询出来的就是最近公共祖先。

需要注意的是dfs序列的长度是2n-1,O(n)地处理出deep和dfs序后就用一个ST表来维护它,dp[i][j]表示dfs序为i~i+2^j-1的点中,深度最小的点。

这样就可以实现O(Nlogn)的预处理与O(1)的查询。

LCA-脆弱不堪.jpg

代码:

void dfs(int u, int fa, int num)
{
    dfo[p++] = u;//dfs序
    dep[u] = num;
    for(int i = head[u];i != -1;i = e[i].nxt)
    {
        int v = e[i].to;
        if(v == fa) continue;
        dis[v] = dis[u] + e[i].w;
        dfs(v, u, num + 1);
        dfo[p++] = u;
    }
}

void init_lca()//ST表
{
    m[0] = -1;
    for(int i = 1;i <= p;i++)
    {
        if((i & (i - 1)) == 0) m[i] = m[i - 1] + 1;
        else m[i] = m[i - 1];
        dp[i][0] = dfo[i];
    }
    for(int j = 1;j <= m[p];j++)
    {
        for(int i = 1;i + (1<<j) - 1 <= n;i++)
            dp[i][j] = Min(dp[i][j - 1], dp[i + (1<<(j - 1))][j - 1]);
    }
}

int lca(int x, int y)
{
    if(x > y) swap(x, y);
    int k = m[y - x + 1];
    return Min(dp[x][k], dp[y - (1<<k) + 1][k]);
}

树上倍增求LCA

倍增是一种丧心病狂  比较简单 只可意会不可言传应用很广的思想,就是在处理信息的时候,根据已经得到的信息,不断成倍扩大处理范围加速操作。

树上倍增采用的是二进制的思想,编码比较简洁,时空复杂度也比较优秀。就是功能稍微欠缺一点

和RMQ类似预处理pre[i][j]表示i节点的第2^j个祖先,显然pre[i][0]就是i的父节点,也就是并查集中的fa[i]。

由倍增的思想我们可以得到一个非常优秀的性质: pre[i][j] = pre[ pre[i][j-1] ][j-1] 显然,i的第2^j个父亲就是i的第2^(j-1)个父亲的第2^(j-1)个父亲。人伦惨剧

也就是说,如果要求i的第n个祖先,暴力复杂度是O(n),现在变成了O(logn)。

仍然是用dfs,预处理出deep和pre[x][0],然后进行lca预处理得到pre[i][j],预处理总的复杂度是O(nlogn)。

现在就要求LCA了。先回到上面那个脑残的方法:显然u和v的深度不一定相同,于是很容易想到先将深的那个跳到和浅的那个相同的深度,然后两个一起往上跳,直到到达一个相同的点为止,这个点显然就是它俩的LCA。

实际上倍增法求LCA使用的也是这个思想。只不过暴力求解是一步一步往上跳,而倍增法在这个过程中是一次跳2^k步的,将查询的复杂度降到了优秀的O(logn)。

存在一个问题,就是跳2^k可能会跳到LCA的祖先上。解决的方法是只有在2^k步之后的节点不同时,才会让两个点同时跳。

代码:

void dfs(int u, int fa, int num)
{
    pre[u][0] = fa;
    dep[u] = num;
    for(int i = head[u];i != -1;i = e[i].nxt)
    {
        int v = e[i].to;
        if(v == fa) continue;
        dis[v] = dis[u] + e[i].w;
        dfs(v, u, num + 1);
    }
}

void init_lca()
{
    for(int j = 1;(1<<j) <= n;j++)
    {
        for(int i = 1;i <= n;i++)
            pre[i][j] = -1;
    }
    for(int j = 1;(1<<j) <= n;j++)
    {
        for(int i = 1;i <= n;i++)
        {
            if(pre[i][j - 1] != -1)
                pre[i][j] = pre[pre[i][j - 1]][j - 1];
        }
    }
}

int lca(int x, int y)
{
    if(dep[x] < dep[y]) swap(x, y);
    int mlg = 0;
    while((1<<mlg) <= dep[x]) mlg++;
    mlg--;
    for(int i = mlg;i >= 0;i--)
    {
        if(dep[x] - (1<<i) >= dep[y])
            x = pre[x][i];
    }
    if(x == y) return x;
    for(int i = mlg;i >= 0;i--)
    {
        if(pre[x][i] != -1 && pre[x][i] != pre[y][i])
            x = pre[x][i], y = pre[y][i];
    }
    return pre[x][0];
}

倍增求LCA实现了O(nlogn)的预处理和O(logn)的在线查询,看起来比RMQ要快一点。然而举个栗子,要求路径上权值最小值,就可以用W[i][j]表示i的第2^j个祖先上的最小值,然后就有w[i][j] = min(w[i][j-1], w[ w[i][j-1] ][j-1]),依此类推。。。

这才是倍增思想的真正意义,比如权值和权值极值什么的很多问题是辣鸡RMQ解决不了的。


树链剖分

 如果我在求路径权值和的时候还想要更新某些边的权值怎么办呢?

RMQ和倍增都tm是预处理好的啊(当然可以每次都O(logn)预处理了)(呵呵)

这个时候就需要使用一种丧心病狂好用的方法——树链剖分。

树链就是树上的路径,顾名思义树链剖分就是维护树上路径的信息,将整棵树通过轻重边剖分分成多条链,显然每个点属于且只属于一条链。然后用某种数据结构(线段树树状数组之类的)去维护每一条链的信息。


一些概念

设size(x)表示以x为根的子树的节点数目,对于一个非叶子节点u和它的所有儿子v,size()最大的儿子就叫做u的重儿子,其他儿子叫做u的轻儿子

重儿子与其父亲之间连的边叫做重边,其余的边叫做轻边。若干条重边相连就构成了一条重链。(轻儿子自身可看做一条长度为1的重链)

如上图,黄色节点是重儿子,红色边是重边。图中长度大于1的重链有1-3-6-10,5-9,2-4-8三条。


两个结论

对于一个轻儿子x,size(x) <= size(fa(x)) / 2 (显然,因为重儿子有且仅有一个)

从根到某节点的路径上,最多有logn条重链和logn条轻链。(不需要证)(不会证)


预处理

预处理需要两个dfs来实现。

第一个dfs来保存:x的父节点fa(x);x的儿子son(x);x的深度dep(x);以x为根的子树的节点数目tot(x)。

第二个dfs来进行轻重链剖分,保存:x在剖分后的序号idx(x);x所在重链的最浅的节点top(x)。

在轻重链剖分dfs时,遵循的原则是先遍历重儿子即先走重边,比如下图经过轻重链剖分之后的新编号就是:

可以发现一个非常重要的结论,一棵子树上的节点,它在新的编号中必然是连续的一段;一条重链上的点,它的编号也是连续的一段。(不然拿头更新查询啊)

利用这个性质,我们就可以用各种数据结构来进行区间更新与查询操作。这里用的是线段树,虽然码量大一点但是容易查错。(为什么非要写错呢)

常规建树就不用再说了吧


路径上的更新与查询

在两个点之间的路径上进行操作,可以像LCA一样不断地向上跳。这里显然是没有什么倍增的预处理了,向上跳是不断地跳到所在重链的顶端节点也就是预处理出的top。在跳的过程中不断地对当前所在的链进行更新或者查询,直到二者到达同一条链为止。最后再加上同在的这条链的结果即可。

两点间路径更新的代码:

void update_tree(int root, int l, int r, int aiml, int aimr, int val)
{
    if(l > aimr || r < aiml) return;
    if(l >= aiml && r <= aimr)
    {
        tree[root] += val*(r - l + 1);
        lazy[root] += val;
        tree[root] %= mod, lazy[root] %= mod;
        return;
    }
    pushdown(root, l, r);
    int mid = (l + r) >> 1;
    update_tree(root*2, l, mid, aiml, aimr, val);
    update_tree(root*2 + 1, mid + 1, r, aiml, aimr, val);
    tree[root] = (tree[root*2] + tree[root*2 + 1]) % mod;
}

void update_path(int u, int v, int val)
{
    while(top[u] != top[v])
    {
        if(dep[top[u]] < dep[top[v]]) swap(u, v);
        update_tree(1, 1, n, idx[top[u]], idx[u], val);
        u = fa[top[u]];
    }
    if(dep[u] > dep[v]) swap(u, v);
    update_tree(1, 1, n, idx[u], idx[v], val);
}

时间复杂度

线段树维护的树链剖分中, dfs预处理与建树都是O(n)的复杂度,在每次操作中向上跳边需要O(logn)次,每次跳边在线段树上对区间进行操作又需要O(logn)的时间,因此总的复杂度是O\left ( n+qlog_{2}^{2}n \right )


洛谷3384的树链剖分模板...支持子树与路径的修改与查询操作。

/*ргргрг*/
#include <cstdio>
#include <cstring>
#include <cmath>
#include <queue>
#include <map>
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
const int maxn = 100050;
const int INF = 0x3f3f3f3f;
const double eps = 1e-8;

int n, m, rt, mod, no, cnt, w[maxn];
int head[maxn], fa[maxn], vis[maxn];
int tot[maxn], son[maxn], dep[maxn];
int top[maxn], idx[maxn], val[maxn];
int tree[maxn << 2], lazy[maxn << 2];
struct node
{
    int to;
    int nxt;
}e[maxn << 1];

void add(int a, int b)
{
    e[no].to = b;
    e[no].nxt = head[a];
    head[a] = no++;
}

int dfs1(int u, int f, int d)
{
    dep[u] = d, fa[u] = f;
    tot[u] = 1;
    int maxs = -1;
    for(int i = head[u];i != -1;i = e[i].nxt)
    {
        int v = e[i].to;
        if(v == f) continue;
        tot[u] += dfs1(v, u, d + 1);
        if(tot[v] > maxs)
        {
            maxs = tot[v];
            son[u] = v;
        }
    }
    return tot[u];
}

void dfs2(int u, int f)
{
    idx[u] = ++cnt;
    val[cnt] = w[u];
    top[u] = f;
    if(!son[u]) return;
    dfs2(son[u], f);
    for(int i = head[u];i != -1;i = e[i].nxt)
    {
        int v = e[i].to;
        if(!idx[v]) dfs2(v, v);
    }
}

void build(int root, int l, int r)
{
    lazy[root] = 0;
    if(l == r)
    {
        tree[root] = val[l];
        return;
    }
    int mid = (l + r) >> 1;
    build(root*2, l, mid);
    build(root*2 + 1, mid + 1, r);
    tree[root] = (tree[root*2] + tree[root*2 + 1]) % mod;
}


void pushdown(int root, int l, int r)
{
    int mid = (l + r) >> 1;
    if(lazy[root] != 0)
    {
        tree[root*2] = (tree[root*2] + lazy[root]*(mid - l + 1)) % mod;
        tree[root*2 + 1] = (tree[root*2 + 1] + lazy[root]*(r - mid)) % mod;
        lazy[root*2] = (lazy[root*2] + lazy[root]) % mod;
        lazy[root*2 + 1] = (lazy[root*2 + 1] + lazy[root]) % mod;
        lazy[root] = 0;
    }
}

void update_tree(int root, int l, int r, int aiml, int aimr, int val)
{
    if(l > aimr || r < aiml) return;
    if(l >= aiml && r <= aimr)
    {
        tree[root] += val*(r - l + 1);
        lazy[root] += val;
        tree[root] %= mod, lazy[root] %= mod;
        return;
    }
    pushdown(root, l, r);
    int mid = (l + r) >> 1;
    update_tree(root*2, l, mid, aiml, aimr, val);
    update_tree(root*2 + 1, mid + 1, r, aiml, aimr, val);
    tree[root] = (tree[root*2] + tree[root*2 + 1]) % mod;
}

int query_tree(int root, int l, int r, int aiml, int aimr)
{
    if(l > aimr || r < aiml) return 0;
    if(l >= aiml && r <= aimr) return tree[root];
    pushdown(root, l, r);
    int mid = (l + r) >> 1;
    return (query_tree(root*2, l, mid, aiml, aimr) +
            query_tree(root*2 + 1, mid + 1, r, aiml, aimr)) % mod;
}

void update_path(int u, int v, int val)
{
    while(top[u] != top[v])
    {
        if(dep[top[u]] < dep[top[v]]) swap(u, v);
        update_tree(1, 1, n, idx[top[u]], idx[u], val);
        u = fa[top[u]];
    }
    if(dep[u] > dep[v]) swap(u, v);
    update_tree(1, 1, n, idx[u], idx[v], val);
}

int query_path(int u, int v)
{
    int ans = 0;
    while(top[u] != top[v])
    {
        if(dep[top[u]] < dep[top[v]]) swap(u, v);
        ans = (ans + query_tree(1, 1, n,idx[top[u]], idx[u])) % mod;
        u = fa[top[u]];
    }
    if(dep[u] > dep[v]) swap(u, v);
    ans = (ans + query_tree(1, 1, n, idx[u], idx[v])) % mod;
    return ans;
}

int main()
{
    scanf("%d%d%d%d", &n, &m ,&rt, &mod);
    no = cnt = 0;
    memset(head, -1, sizeof(head));
    for(int i = 1;i <= n;i++)
        scanf("%d", &w[i]);
    int flag, x, y, z;
    for(int i = 1;i < n;i++)
    {
        scanf("%d%d", &x, &y);
        add(x, y), add(y, x);
    }
    int tp = dfs1(rt, 0, 1);
    dfs2(rt, rt);
    build(1, 1, n);
    for(int i = 1;i <= m;i++)
    {
        scanf("%d", &flag);
        if(flag == 1)
        {
            scanf("%d%d%d", &x, &y, &z);
            update_path(x, y, z % mod);
        }
        else if(flag == 2)
        {
            scanf("%d%d", &x, &y);
            printf("%d\n", query_path(x, y) % mod);
        }
        else if(flag == 3)
        {
            scanf("%d%d", &x, &z);
            update_tree(1, 1, n, idx[x], idx[x] + tot[x] - 1, z % mod);
        }
        else if(flag == 4)
        {
            scanf("%d", &x);
            printf("%d\n", query_tree(1, 1, n, idx[x], idx[x] + tot[x] - 1) % mod);
        }
    }
    return 0;
}

猜你喜欢

转载自blog.csdn.net/NPU_SXY/article/details/81745165