【算法】最近公共祖先之在线算法(RMQ-ST)

来源:互联网 发布:js 判断数组是否为空 编辑:程序博客网 时间:2024/06/05 19:55

在线算法是基于RMQ-ST算法的基础上进行的

RMQ问题求解算法

给定一个整型数组,长度为n,寻找区间内的极值,m表示询问的次数。求解算法不外乎下面两种:
1、最基础的算法就是每次都遍历一次区间则时间复杂度为o(k*m)(k<=n)
2、对数组预处理,即先求出所有区间组合的极值,这样在给定询问区间时求极值时间复杂度降低,但是预处理又分为两种情况:

  • 2.1 两层循环,时间复杂度o(n2),总的时间复杂度为o(n2+m)
  • 2.2 分段处理,分成大小为sqrt(n)的段,记录段内极值,这样预处理的时间复杂度是o(n),整的时间复杂度是o(n+msqrt(n))
    但是有一种巧妙的算法即ST算法,时间复杂度是o(nlogn+m),试想如果询问次数m>>n时,这个算法的效率就明显提升了。

稀疏表算法(ST)

预处理

这个算法其实是转化为长度为2k的子数组利用dp来求解,设A[0,…,n-1]是需要求极值的数组,M[i][j]表示A[i]到A[i+2j-1]这个区间的极值(假设求最小值),则M[i][j]可以用下面求最小值,即

  • M[i][j]=min{M[i][j-1],M[i+2j1-1][j-1]}
    这个式子意思很明显,如果求区间A[i,….,i+2j-1]这个长度以i为起始,2j为长度区间的最小值,可以将这个区间划分为相等的两部分,即A[i,….,i+2j1-1]和A[i+2j1,….,i+2j-1],则原区间A[i,….,i+2j-1]最小值必然在A[i,….,i+2j1-1]和A[i+2j1,….,i+2j-1]中的最小一方。
    以此往前递推,当j=0时,M[i][0]=A[i],长度为1时,最小值就是元素自己,而j取的最大值是log2n
    求M[i][j]算法代码如下:
private static void RMQ_ST(int[][] M, int[] A,int len) {               //j=0时,M最小值就是元素本身        for(int i=0;i<len;i++){            M[i][0]=A[i];        }        int elen = (int)(Math.log(len)/Math.log(2));        //求从1,2,4...到小于n但是却是2的幂数的区间段的最小值        for(int j=1;j<=elen;j++){            for(int i=0;i+(1<<j)-1<len;i++){                M[i][j][0]=Math.min(M[i][j-1], min[i+(1<<(j-1))][j-1]);            }        }    }

计算给定任意区间的最小值

假设求A[p,q]区间的最小值,则转化为长度以2为指数的区间,利用M数组计算,而且转换的两个区间段至少是覆盖了p,q区间,允许而且也是最有可能两段区间重叠。如何转化长度以2为指数的区间呢?公式如下:

  • j=log2(pq+1)
  • minA[p,q]=min{ M[p][j],M[q-2j-1][j] }
    所以只要一步就可以直接求出询问区间的最小值,时间复杂度o(1)

利用RMQ-ST求解最近公共祖先

假设一棵树
这里写图片描述
首先进行深度遍历,而且进入和出来访问到节点都要记录,则得到序列

  • 数组E(存储遍历结果):
遍历顺序 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 遍历节点 1 2 1 3 5 3 6 8 6 9 6 3 7 10 12 10 13 10 7 11 7 3 1 4 1
  • 数组L(存储节点深度):
遍历顺序 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 节点深度 0 1 0 1 2 1 2 3 2 3 2 1 2 3 4 3 4 3 2 3 2 1 0 1 0
  • Map nodesfirstindex(记录深度遍历时第一次出现的位置)
key(结点) 1 2 3 4 5 6 7 8 9 10 11 12 13 第一次出现的位置 0 1 3 23 4 6 12 7 9 13 19 14 16

在上面这些遍历得到的情况下,如果我们求某两个节点p=9、q=12的最近祖先,则首先根据节点p、q在Map nodesfirstindex找到深度遍历第一次出现的位置对应为9和14,然后在数组L则求区间L[9,..,14]的最小深度值,毫无疑问这个就是用RMQ-ST算法求区间极值啦。求出最小深度值为3对应的顺序下标为11,然后在存储遍历结果的数组E中找到元素E[11]=3,即p=9、q=12的最近祖先节点为3.

最终求解程序代码如下,该代码稍微去掉一些打印信息就可以被Hiho#1069 : 最近公共祖先·三AC:

import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Map;import java.util.Map.Entry;import java.util.Scanner;public class LCA_online {    static int indexs=0;//遍历的节点顺序下标    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        Map<String,List<String>> nodes = new HashMap<>();//记录这棵树        Map<String,Integer> nodesfirstindex = new HashMap<>();//记录深度遍历时第一次出现的位置        int N = sc.nextInt();        int len=2*N+1;        String[] E = new String[len];//存储遍历结果        int[] L = new int[len];//存储当前节点深度        int elen = (int) (Math.log(len)/Math.log(2));        int[][][] min = new int[len][elen+1][2];        String head=null;        boolean iffirst=true;        while(N-->0){            String node1 = sc.next();            String node2 = sc.next();            if(iffirst){                head=node1;                iffirst=false;            }            List<String> childs = nodes.get(node1);            if(childs==null){                childs = new ArrayList<>();            }            childs.add(node2);            nodes.put(node1, childs);        }        int deep=0;        //深度遍历这棵树        dfs(head,nodes,nodesfirstindex,E,L,deep);        //打印相关信息        System.out.println(java.util.Arrays.toString(E));        System.out.println(java.util.Arrays.toString(L));        for(Entry<String,Integer>entry:nodesfirstindex.entrySet()){            System.out.print(entry.getKey()+":"+entry.getValue()+"\t");        }        System.out.println();        //RMQ_ST        RMQ_ST(min,L,len,elen);        //打印min        for(int ej=0;ej<=elen;ej++){            for(int i=0;i<len;i++){                System.out.print(min[i][ej][0]+","+min[i][ej][1]+"\t");            }            System.out.println();        }        int M = sc.nextInt();        while(M-->0){            String node1 = sc.next();            String node2 = sc.next();            int node1index = nodesfirstindex.get(node1);            int node2index = nodesfirstindex.get(node2);            int temp = node1index;            if(node1index>node2index){                node1index=node2index;                node2index=temp;            }            System.out.println(E[getMin(node1index,node2index,min)]);        }    }    //获取指定区间的最短深度    private static int getMin(int node1index, int node2index,int [][][] min) {        int ej = (int) (Math.log(node2index-node1index+1)/Math.log(2));        int s1 = min[node1index][ej][0];        int s2 = min[node2index-(1<<ej)+1][ej][0];        if(s1<s2){            return min[node1index][ej][1];        }else{            return min[node2index-(1<<ej)+1][ej][1];        }    }    //利用RMQ-ST算法求区间内的节点最短深度    private static void RMQ_ST(int[][][] min, int[] L,int len,int elen) {        for(int i=0;i<len;i++){            min[i][0][0]=L[i];            min[i][0][1]=i;        }        for(int ej=1;ej<=elen;ej++){            for(int i=0;i+(1<<ej)-1<len;i++){                min[i][ej][0]=Math.min(min[i][ej-1][0], min[i+(1<<(ej-1))][ej-1][0]);                if(min[i][ej][0]==min[i][ej-1][0])                    min[i][ej][1]=min[i][ej-1][1];                else                     min[i][ej][1]=min[i+(1<<(ej-1))][ej-1][1];            }        }    }    //深度遍历    private static void dfs(String head,Map<String, List<String>> nodes, Map<String, Integer> nodesfirstindex, String[] E, int[] L,            int deep) {        if(head==null)            return;        if(nodesfirstindex.get(head)==null){//记录第一次遍历到的位置            nodesfirstindex.put(head, indexs);        }        E[indexs]=head;//记录当前遍历节点        L[indexs]=deep;//记录当前节点深度        indexs++;        List<String> childs = nodes.get(head);        if(childs==null)            return;        for(int i=0;i<childs.size();i++){            dfs(childs.get(i),nodes,nodesfirstindex,E,L,deep+1);            E[indexs]=head;//记录当前遍历节点            L[indexs]=deep;//记录当前节点深度            indexs++;        }    }}

运行结果:
这里写图片描述

结论

本算法参考了编程之法上的求解算法,和其他博客帖子,在此不一一鸣谢,如有违权,深表歉意,小弟不才,欢迎大牛拍砖。

1 0
原创粉丝点击