RMQ&LCA

<前言>

有这么一个神奇的ppt:3.郭华阳《RMQ与LCA问题》

讲了LCA和RMQ的玄妙关系。两者如何在优秀的时间内相互转化。

也讲述了克鲁斯卡尔重构树内容。

本篇blog就是相关学习总结。

<正文>

RMQ&LCA学习笔记

引入

关于LCA最近公共祖先和RMQ区间最值问题。

我们首先看一下复杂度。

当LCA问题与RMQ问题可以相互转换时,可以大大拓宽其应用面。

今天我任务的二分之一就是大概复述一遍这个内容,避免以后自己忘掉。

RMQ->LCA:笛卡尔树

参考博客

还有lzh大佬的pdf讲案。

定义及应用

笛卡尔树是一种二叉树,每一个结点由一个键值二元组\((k,w)\)构成。

要求\(k\)满足二叉搜索树的性质,而\(w\)满足堆的性质。

对于长度为n的序列\(a_i\)

  • 找到最小值\(A_k\)位置k,建立根节点\(T_k\),点权为\(A_k\)

  • \(1...k-1\)递归建树作为\(T_k\)的左子树。

    \(k+1...n\)递归建树作为\(T_k\)的右子树。

这样建完一棵树之后,显然这是一棵优先级树。大概长这个样子:

区间\(\mathrm{[l,r]}\)之间的最值显然就是树中\(T_l\)\(T_r\)的LCA。

其实,笛卡尔树满足以下特征:

  • 它的中序遍历是原数列。
  • 任意一个节点的值都 < 它的两个儿子节点的值。
  • 任意两点的\(LCA\)就是它们的\(RMQ\)

支持的复杂度(以Tarjan离线LCA为例)大概就是 **预处理\(O(n)\)+每次查询\(O(1)\) **。

emmm感觉和RMQ没啥区别。但是这给更多的操作提供了条件,比如树上dp如何如何的。

还有应用:求左右延伸区间\(O(n)\)预处理+\(O(1)\)询问。

构造

转换有多种方式,但最优秀的是\(O(n)\)建树。

用到一个单调栈维护最右边的链,是一种妙啊巧妙的方法。

流程如下:

  • 1.单调递增(or递减)的单调栈维护最右边的树链。
  • 2.对于新增节点x,弹出的那些数直接挂在当前节点x左子树
  • 3.其实更像是把整一棵树直接挂上去。

图解样例:

JKHDdf.png

JKHyFS.png
JKH6Jg.png

还是十分欢乐的。

建树\(\mathrm{Code:}\)

//stk[]为栈,存储key(下标),h[]数组存储值
for (int i = 1; i <= n; i++)
{
    int k = top;
    while (k > 0 && h[stk[k]] > h[i]) k--;
    if (k) rs[stk[k]] = i; // rs代表笛卡尔树每个节点的右儿子
    if (k < top) ls[i] = stk[k + 1]; // ls代表笛卡尔树每个节点的左儿子
    stk[++k] = i;
    top = k;
}

应用

主要是最值延展。

还有

imagec69f361ca4088d0a.png

这个最大矩阵问题。

具体题解不说了可以参考上面的blog。

毕竟本篇重点不在笛卡尔树。


LCA->RMQ:欧拉序O(n)LCA

比起笛卡尔树,这个我觉得是更大的扩展,毕竟复杂度变优秀了。

预处理\(O(n(dfs)+n\ log n(ST表))\),询问\(O(1)(ST表)\),还是在线算法。

可能预处理复杂度大一点,但比起离线Tarjan在线还是有优势的。

推荐LCA博文 还是很不错的。

定义及要点

对有根树T进行DFS,将遍历到的结点按照顺序记下,我们将得到一个长度为\(2N – 1\)的序列,称之为T的欧拉序列F。

每个结点都在欧拉序列中出现,我们记录结点u在欧拉序列中第一次出现的位置为pos(u)。

image1ce63983bbf89d0f.png

image604bd5c22c3a38e9.png

操作十分简单明了。

根据DFS的性质,对于两结点u、v,从\(pos(u)\)遍历到\(pos(v)\)的过程中经过LCA(u, v)有且仅有一次,且深度是深度序列\(B[pos(u)…pos(v)]\)中最小的。

也就是说我们的LCA就是\(pos(u)\)\(pos(v)\)中深度最小的那个点。

构造与解决

然后就十分快乐了。

开局一次dfs,反手一个ST表,每个询问\(O(1)\)解决。

都是学过的知识点的总结,也没啥流程。

ST表离线操作不会的话我也无能为力。

至此,LCA与RMQ问题可以互相在\(O(n)\)时间内转换。

\(\mathrm{Code:}\)

struct LCA
{
    int dfn[N << 1], tr[N], d[N << 1];
    int vs;
    void dfs(int u, int fa, int deh)
    {
        dfn[++vs] = u;//dfs序
        d[vs] = deh;  //每个点深度
        tr[u] = vs;   //第一次出现位置,即pos[]
        for(int i = T.fl[u]; i; i = T.net[i])
        {
            int v = T.to[i];
            if(v == fa)continue;
            dfs(v, u, deh + 1);
            dfn[++vs] = u;     //每次出子节点再加入一次
            d[vs] = deh;
        }
    }
    int f[N << 1][31];
    inline int calc(int x, int y)
    {
        return d[x] < d[y] ? x : y;   
    }
    int lca(int x, int y)
    {
        int l = tr[x], r = tr[y];
        if(l > r)swap(l, r);
        int block = log(r - l + 1) / log(2);
        return dfn[calc(f[l][block], f[r - (1 << block) + 1][block])];
        //快乐的LCA
    }
    void pre()
    {
        for(int i = 1; i <= vs; ++i)
            f[i][0] = i;
        for(int i = 1; i < 30; ++i)
            for(int j = 1; j + (1 << i) - 1 <= vs; ++j)
                f[j][i] = calc(f[j][i - 1], f[j + (1 << (i - 1))][i - 1]);
        //ST表预处理
    }
    void work(int root, int m)
    {
        dfs(root, 0, 1);
        pre();
        for(int i = 1; i <= m; ++i)
        {
            int l = read(), r = read();
            printf("%d\n", lca(l, r));
        }
    }
};

应用

要说\(O(n)\)LCA的应用,那就多了。

对于修改操作,你甚至可以每次都重新构造,完全莫得问题。


总结

RMQ&LCA算法关系图

9R2UOBLLO7WFOLE6G.png

然后就是可以各种转换各种乱搞。

高手训练上有相关练习题。


Kruskal重构树&顺序生成森林

引出

以一道例题引出。

水管局长(2006年冬令营试题)

题目大意:

有修改操作(删边)的最小瓶颈路问题,多组询问。

结点数 N ≤ 1000; 边数 M ≤ 100000;
操作数 Q ≤ 100000; 删边操作 D ≤ 5000;

类Prim算法复杂度\(O(N^2)\),可过,但不够优秀。

复杂度瓶颈:边数过多;询问的复杂度过高。

然后我们需要找到方法解决这个问题。

最小生成森林

定义:其实就是从一棵树变成了一片树,没啥本质区别。

  • 引理一:任意询问可以在G的最小生成森林中找到最优解。证明

根据引理,我们只需要保存所有树边即可,这样边数下降到\(O(N)\)级别,第一个问题被解决。

关于实现:其实就是不用判定连了多少条边,有多少连多少连到底就行。

\(\mathrm{Code:}\)

	for(int i = 1; i <= len; ++i)
    {
        int u = F.get(e[i].x), v = F.get(e[i].y);
        if(u != v)
        {
            F.f[u] = v;
            sum += e[i].z;
            T.inc(e[i].x, e[i].y, e[i].z);
            T.inc(e[i].y, e[i].x, e[i].z);
        }
    }

没错你没看出任何区别。但是在一些不连通的图中会有差别。

Kruskal重构树

对于第二个问题,我们需要找到生成森林上两点间路径上的最大值。

你当然可以用LCT或者树剖来解决这个问题,搞不好倍增也行。

但是关于本专题有一个十分方便的算法:Kruskal重构树。

重构树是啥自行搜索即可,我们说说这有啥用。

我们进行Kruskal算法时,进行了排序,故关于当前边连接的两个集合,它们间路径最大值必定是当前边

根据这个原理,我们对这颗生成树进行重构。结果如下:

JMSsuF.png

其中E代表边,V代表点。

我们可以发现,重构树中两点间路径上最长边就是两点LCA

这就舒服了,接下来想怎么求LCA就怎么求。

用上个离线Tarjan或欧拉序LCA都不是问题。每次操作完更新一次,处理得可得劲了。

算法流程:

  • 1.生成结束时的最小生成森林和顺序森林;

  • 2.从后往前完成操作:对于删边操作,重新生成最小生成森林和顺序森林;

    对于连续的询问操作,将其作为离线LCA询问在顺序森林上处理;

  • 3.输出答案;

同时你可以据此更方便得解决更多问题。

就是代码实现有点问题了,挺繁琐的,但也不算难。

重构树\(\mathrm{Code:}\)

	US_find F;
    F.build(n + m);
    int cnt = n;
    sort(e + 1, e + m + 1, cmp);
    for(int i = 1; i <= m; ++i)
    {
        int u = F.get(e[i].x), v = F.get(e[i].y);
        if(u != v)
        {
            F.f[u] = F.f[v] = ++cnt;
            a[cnt] = e[i].z;
            T.inc(cnt, u);
            T.inc(cnt, v);
        }
    }
    for(int i = 1; i <= n; ++i)
        if(!vis[i])
        {
            dfs(F.f[i], 0);
            dfs1(F.f[i], F.f[i]); //树剖LCA
        }

回寝了,暂时鸽了。

猜你喜欢

转载自www.cnblogs.com/zqytcl/p/12733855.html