RMQ
RMQ (Range Minimum/Maximum Query)问题是指:对于长度为n的数列A,回答若干询问RMQ(A,i,j)(i,j<=n),返回数列A中下标在i,j里的最小(大)值,也就是说,RMQ问题是指求区间最值的问题。数组大小为n,查询次数为q。
解决办法:
1.直接遍历查询,复杂度O(q*n)
2.线段树,复杂度O(n+q*logn)
3.ST算法(Sparse Table),复杂度O(nlogn+q)
ST算法中用mini[i][j]、maxi[N][MAXN]表示[i,i+2^j-1]这个区间内的最值,询问到[a,b]区间的最大值时答案就是max(m[a][k], m[b-2^k+1][k]),其中k是满足2^k<=b-a+1(即长度)的最大的k,即k=[ln(b-a+1)/ln(2)]
初始化(动态规划):m[i,j]=max(m[i][j-1],m[i+2^(j-1)][j-1])
#define N 50001
#define MAXN 16
int mini[N][MAXN],maxi[N][MAXN];
int max(int x,int y){return x>y?x:y;}
int min(int x,int y){return x>y?y:x;}
int bitnum(int v)
{
int num=0;
while(v=v>>1)
num++;
return num;
}
void init(int n,int h[])
{
int i,j,m=bitnum(n);
for(i=1;i<=n;i++)
mini[i][0]=h[i];
for(i=1;i<=n;i++)
maxi[i][0]=h[i];
int k=1;
for(j=1;j<=m;j++)
{
for(i=1;i+k<=n;i++){
maxi[i][j]=max(maxi[i][j-1],maxi[i+k][j-1]);
mini[i][j]=min(mini[i][j-1],mini[i+k][j-1]);
}
k=k<<1;
}
}
void query(int a,int b)
{
int k=bitnum(b-a+1);
int low=min(mini[a][k],mini[b-(1<<k)+1][k]);
int up=max(maxi[a][k],maxi[b-(1<<k)+1][k]);
return up-low;
}
变种:求从左至右连续k个数的最值
一种新思路:利用堆排序思想O(nlogn)
struct Node
{
int v,index;
}nod,node[N];
int a[N];
void add(int n)
{
while(n>1)
{
if(node[n].v>=node[n/2].v)
break;
nod=node[n];
node[n]=node[n/2];
node[n/2]=nod;
n/=2;
}
}
void restore(int n)
{
int j,k;
j=1;
while(j<=n/2)
{
k=2*j;
if(k+1<=n&&node[k+1].v<node[k].v)
k++;
if(node[j].v<=node[k].v)
break;
nod=node[k];
node[k]=node[j];
node[j]=nod;
j=k;
}
}
/*返回最小值*/
for(i=1;i<=k;i++){
node[i].v=a[i];
node[i].index=i;
add(i);
}
printf("%d",node[1].v);
l=k;
for(i=k+1;i<=n;i++)
{
l++;
node[l].index=i;
node[l].v=a[i];
add(l);
while(node[1].index<i-k+1){
node[1]=node[l];
l--;
restore(l);
}
printf(" %d",node[1].v);
}
LCA 最近公共祖先问题
(Least Common Ancestors)对于有根树T的两个结点u、v,最近公共祖先LCA(T,u,v)表示一个结点x,满足x是u、v的祖先且x的深度尽可能大。另一种理解方式是把T理解为一个无向无环图,而LCA(T,u,v)即u到v的最短路上深度最小的点。
类似问题:
1. 求树中任意两点的距离:两点到根节点的距离和-公共祖先到根节点距离*2
2. 以一个节点x为根求另一节点y的父节点:x和y有共同的祖先z,y的父节点就是解;x和y的共同祖先为x,y的父节点就是解;x和y的共同祖先为y,x到y的路径的y的前一个节点(y的子节点)为解。
1. 离线算法Tarjan
利用并查集优越的时空复杂度,我们可以实现LCA问题的O(n+Q)算法,这里Q表示询问的次数。Tarjan算法基于深度优先搜索的框架,对于新搜索到 的一个结点,首先创建由这个结点构成的集合,再对当前结点的每一个子树进行搜索,每搜索完一棵子树,则可确定子树内的LCA询问都已解决。其他的LCA询 问的结果必然在这个子树之外,这时把子树所形成的集合与当前结点的集合合并,并将当前结点设为这个集合的祖先。之后继续搜索下一棵子树,直到当前结点的所 有子树搜索完。这时把当前结点也设为已被检查过的,同时可以处理有关当前结点的LCA询问,如果有一个从当前结点到结点v的询问,且v已被检查过,则由于 进行的是深度优先搜索,当前结点与v的最近公共祖先一定还没有被检查,而这个最近公共祖先的包涵v的子树一定已经搜索过了,那么这个最近公共祖先一定是v 所在集合的祖先。
参考: http://poj.org/showsource?solution_id=4237726
vector<int> v[N];
bool tag[N];
int d[2*N],start[N],p[2*N],mini[2*N][16];
int cnt;
void dfs(int i,int deep)
{
d[cnt]=deep;
start[i]=cnt;
p[cnt++]=i;
int j;
for(j=0;j<v[i].size();j++)
{
dfs(v[i][j],deep+1);
d[cnt]=deep;
p[cnt++]=i;
}
}
int bitnum(int v)
{
int num=0;
while(v=v>>1)
num++;
return num;
}
void init(int n)
{
int i,j,m=bitnum(n);
for(i=1;i<=n;i++)
mini[i][0]=i;
int k=1;
for(j=1;j<=m;j++)
{
for(i=1;i+k<=n;i++){
if(d[mini[i][j-1]]<=d[mini[i+k][j-1]])
mini[i][j]=mini[i][j-1];
else
mini[i][j]=mini[i+k][j-1];
}
k=k<<1;
}
}
void query(int a,int b)
{
int low,k=bitnum(b-a+1);
if(d[mini[a][k]]<=d[mini[b-(1<<k)+1][k]])
low=mini[a][k];
else
low=mini[b-(1<<k)+1][k];
printf("%d\n",p[low]);
}
2.在线算法 倍增法
复杂度:每次询问O(logN),总复杂度O(nlogn+qlogn)
思路:d[i]表示 i节点的深度, p[i,j] 表示 i 的 2^j 倍祖先。那么就有一个递推式子 p[i,j]=p[p[i,j-1],j-1]。这样子一个O(NlogN)的预处理求出每个节点的 2^k 的祖先。然后对于每一个询问的点对a, b的最近公共祖先就是:先判断是否 d[a] > d[b] ,如果是的话就交换一下(保证 a 的深度小于 b 方便下面的操作)然后把b 调到与a 同深度, 同深度以后再把a, b 同时往上调(dec(j)) 调到有一个最小的j 满足p[a,j]!=p[b,j] (a b 是在不断更新的), 最后再把 a, b 往上调 (a=p[a,0], b=p[b,0]) 一个一个向上调直到a = b, 这时 a or b 就是他们的最近公共祖先。
一个初始化的算法:若祖先不存在p[i,j]=-1
vector<int> v[N];
int p[N][M],d[N];
void getparent(int u,int r,int l)
{
d[u] = l;
int xx = r;
int i = 0;
while(xx != -1)
{
p[u][i++] = xx;
xx = p[xx][i-1];
}
for(i = 0;i < v[u].size();i++){
if(v[u][i] != r){
getparent(v[u][i],u,l+1);
}
}
}