zoukankan      html  css  js  c++  java
  • 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

    Jfhum8.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
    ", 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
            }
    

    例题

    【高手训练】【图论】汉堡店

    首先最小生成树是必要的,我们尽量使除此边以外的边小,MST很稳。

    答案(frac{A}{B}),我们需要使A尽量大,B尽量小。

    但我们也不能一味找(P_i)最大的两家店或者找MST中最大的那条边。因为可能存在相对折中的方案使答案最大。

    但是我们发现数据范围允许我们(n^2)枚举每一对汉堡店并验证其相连边(x,y)。

    • 若加入边(x,y),若在生成树中,直接计算贡献即可。

    • 若不在生成树中,则需删去一边,即生成树中x,y路径上的最大边

      这就转换成了最小瓶颈路问题,直接在原树上找就行了,复杂度(O(log n))

    流程如下:

    • 1.原图求MST,预处理最小瓶颈路,可以使用MST+倍增或重构树。
    • 2.枚举每对边,若不在生成树中找路径上最大边删去并计算贡献。

    So easy,isn't it?

    (mathrm{Code(倍增)}:)

    #include<bits/stdc++.h>
    #define N 2010
    using namespace std;
    int n;
    struct Tree
    {
        int to[N << 1], net[N << 1], len, fl[N];
        double w[N << 1];
        inline void inc(int x, int y, double z)
        {
            to[++len] = y;
            w[len] = z;
            net[len] = fl[x];
            fl[x] = len;
        }
    } T;
    struct hamber_store
    {
        int x, y;
        double P;
    } a[N] = {};
    struct rode
    {
        int x, y;
        double z;
    } e[N * N] = {};
    int len = 0;
    struct US_find
    {
        int f[N], n;
        inline void build(int m)
        {
            n = m;
            for(int i = 1; i <= n; ++i)
                f[i] = i;
        }
        int get(int x)
        {
            return x == f[x] ? x : f[x] = get(f[x]);
        }
    };
    int read()
    {
        int s = 0, w = 1;
        char c = getchar();
        while((c < '0' || c > '9') && c != '-')
            c = getchar();
        if(c == '-')w = -1, c = getchar();
        while(c <= '9' && c >= '0')
            s = (s << 3) + (s << 1) + c - '0', c = getchar();
        return s * w;
    }
    inline bool cmp(rode x, rode y)
    {
        return x.z < y.z;
    }
    int d[N] = {};
    int f[N][35] = {};
    double z[N][35] = {};
    void dfs(int u, int fa)
    {
        for(int j = 1; j <= 30; ++j)
        {
            if(d[u] < (1 << j))break;
            f[u][j] = f[f[u][j - 1]][j - 1];
            z[u][j] = max(z[u][j - 1], z[f[u][j - 1]][j - 1]);
        }
        for(int i = T.fl[u]; i; i = T.net[i])
        {
            int v = T.to[i];
            if(v == fa)continue;
            d[v] = d[u] + 1;
            f[v][0] = u;
            z[v][0] = T.w[i];
            dfs(v, u);
        }
    }
    int lca(int x, int y)
    {
        if(d[x] < d[y])swap(x, y);
        int t = d[x] - d[y];
        for(int i = 0; i <= 30; ++i)
            if((1 << i)&t)x = f[x][i];
        for(int i = 30; i >= 0; --i)
            if(f[x][i] != f[y][i])
            {
                x = f[x][i];
                y = f[y][i];
            }
        if(x == y)return x;
        return f[x][0];
    }
    double ask(int x, int LCA)
    {
        double maxn = 0.0;
        int tmp = d[x] - d[LCA];
        for(int i = 0; i <= 30; ++i)
            if(tmp & (1 << i))
            {
                maxn = max(maxn, z[x][i]);
                x = f[x][i];
            }
        return maxn;
    }
    int main()
    {
        memset(z, -0x3f, sizeof(z));
        n = read();
        for(int i = 1; i <= n; ++i)
        {
            a[i].x = read();
            a[i].y = read();
            a[i].P = 1.0 * (double)read();
        }
        for(int i = 1; i <= n; ++i)
            for(int j = i + 1; j <= n; ++j)
            {
                double t = sqrt((a[i].x - a[j].x) * (a[i].x - a[j].x) + (a[i].y - a[j].y) * (a[i].y - a[j].y));
                e[++len] = (rode)
                {
                    i, j, t
                };
            }
        sort(e + 1, e + len + 1, cmp);
        US_find F;
        F.build(n);
        double sum = 0.0;
        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);
            }
        }
        dfs(1, 0);
        double maxx = 0.0;
        for(int i = 1; i <= n; ++i)
            for(int j = 1; j <= n; ++j)
            {
                int LCA = lca(i, j);
                double maxn = max(ask(i, LCA), ask(i, LCA));
                maxx = max(maxx, (a[i].P + a[j].P) * 1.0 / (sum - maxn));
            }
        printf("%.2lf
    ", maxx);
        return 0;
    }
    
    

    其实套路都这样差不多,以下几题很好:

    【高手训练】【图论】生成树

    【WC2006】水管局长

    「from CommonAnts」寻找 LCR

    总结

    没啥好总结的。

    大概就是多了一些奇怪的点子。

    以后看到诸如区间最值延展、瓶颈路问题、奇怪复杂度LCA时能有更多的想法


    <后记>

    学习笔记,我不确定以后的自己看不看得懂,但是写写就挺好。

    要开最短路了,回见,zqy!

  • 相关阅读:
    Java多线程系列--“JUC集合”08之 LinkedBlockingQueue
    Java多线程系列--“JUC集合”07之 ArrayBlockingQueue
    Java多线程系列--“JUC集合”06之 ConcurrentSkipListSet
    Java多线程系列--“JUC集合”05之 ConcurrentSkipListMap
    Java多线程系列--“JUC集合”04之 ConcurrentHashMap
    Java多线程系列--“JUC集合”03之 CopyOnWriteArraySet
    Java多线程系列--“JUC集合”02之 CopyOnWriteArrayList
    Java多线程系列--“JUC集合”01之 框架
    Java多线程系列--“JUC锁”11之 Semaphore信号量的原理和示例
    Java多线程系列--“JUC锁”10之 CyclicBarrier原理和示例
  • 原文地址:https://www.cnblogs.com/yywxdgy/p/12733855.html
Copyright © 2011-2022 走看看