在线算法是基于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+m
sqrt(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+
2j−1
-1][j-1]}
这个式子意思很明显,如果求区间A[i,….,i+ 2j -1]这个长度以i为起始, 2j 为长度区间的最小值,可以将这个区间划分为相等的两部分,即A[i,….,i+ 2j−1 -1]和A[i+ 2j−1 ,….,i+ 2j -1],则原区间A[i,….,i+ 2j -1]最小值必然在A[i,….,i+ 2j−1 -1]和A[i+ 2j−1 ,….,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(p−q+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++;
}
}
}
运行结果:
结论
本算法参考了编程之法上的求解算法,和其他博客帖子,在此不一一鸣谢,如有违权,深表歉意,小弟不才,欢迎大牛拍砖。