RMQ算法

还没有去省里竞赛过~~这回怎么也得争到名额,加油~~一步步来,九月份去了~我只是菜鸟,还太多不懂,只能一个个来,前两天看了下LCA问题,和它与之紧密联系的是RMQ算法,今天无论如何得把这个弄懂了~~得意

RMQ算法全称为(Range Minimum/Maximum Query)意思是给你一个长度为n的数组A,求出给定区间的最值的下标。当然我们可以采用枚举,但是我们也可以使用线段树来优化,复杂度为(nlogn),但是最好的办法是采用Sparse_Table算法,简称ST算法。他能在进行(nlogn)的预处理后达到n(1)的效率。下面来分析下最大值和最小值,都要用到DP的思想。

最小值(Mininun):我们可以用F(i,j)表示区间[i,i+2^j-1]间的最小值。我们可以开辟数组来保存F(i,j)的值,例如:F(2,4)就是保存区间[2,2+2^4-1]=[2,17]的最小值。那么F(i,0)的值是确定的,就为i这个位置所指的元素值,这时我们可以把区间[i,i+2^j-1]平均分为两个区间,因为j>=1的时候该区间的长度始终为偶数,可以分为区间[i,i+2^(j-1)-1]和区间[i+2^(j-1)-1,i+2^j-1],即取两个长度为2^(j-1)的块取代和更新长度为2^j的块,那么最小值就是这两个区间的最小值的最小值,动态规划为:F[i,j]=min(F[i,j-1],F[i+2^(j-1),j-1]).同理:最大值就是F[i,j]=max(F[i,j-1],F[i+2^(j-1),j-1]).

现在求出了F[i,j]之后又是怎样求出最大值或者最小值的,怎么转换为o(1)这种算法的~这就是ST算法:

这个时候询问时只要取k=ln(j-i+1)/ln2即可,那么可以令A为i到2^k的块,和B为到2^k结束的长度为2^k的块;那么A,B都是区间[i,j]的子区间,所以即求A区间的最小值和B区间的最小值的最小值。这个时候动态规划为:RMQ(i,j)=min(F[i,k],F[j-2^k+1,k]);

下面是求区间最小值的模板:

#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
using namespace std;
const int MAX=10010;
#define max(a,b) a>b?a:b
#define min(a,b) a<b?a:b
#define CLR(arr,val)  memset(arr,val,sizeof(arr))
int n,m,low,high,a[MAX];
class RMQ{ 
public: 
    void rmq()
    {   int temp=(int)(log((double)n)/log(2.0));
        for(int i=0;i<n;i++)
            DP[i][0]=a[i];
        for(int j=1;j<=temp;j++)
            for(int i=0;i<n;i++)
                if(i+(1<<j)<n) DP[i][j]=min(DP[i][j-1],DP[i+(1<<(j-1))][j-1]); 
    }    
    int Minimum(int L,int H)
    {   int k=(int)(log((double)H-L+1)/log(2.0));
        return min(DP[L][k],DP[H-(1<<k)+1][k]);
    }
    void Init(){CLR(DP,0);}
private:
    int DP[MAX][20];    
};
int main()
{   RMQ R;
    R.Init();
    scanf("%d%d",&n,&m);
    for(int i=0;i<n;i++)   
        scanf("%d",&a[i]);
    R.rmq();
    while(m--)
    {   scanf("%d%d",&low,&high);
        printf("%d\n",R.Minimum(low,high));
    }
    return 0;
}

当然对于RMQ并不只有这个用法,我们可以用它来解决LCA问题。
 假设LCA(T,u,v)表示在有根树T中,询问一个离根最远的结点x,使得x为u,v的公共祖先。现在分析下LCA向RMQ问题转化的过程:

对有根树T进行深度优先遍历(DFS),将遍历到的结点按照顺序记录下来,那么我们会得到一个长度为2N-1的序列,称之为T的欧拉序列F,设序列Depth是DFS遍历过程中的结点深度的变化情况。其中每一个结点都会出现在欧拉序列F中,我们记录结点u在欧拉序列中出现的第一个位置pos(u);根据DFS的特性,对于任意两个结点u,v,那么从pos(u)(也就是第一次访问u的时候)到pos(v)(第一次访问v)的过程中,所经历的路径为F(pos(u).....pos(v)),虽然这些包括u的后代,但是其深度最小的结点一定是u和v的LCA(公共祖先),不论pos(u)与pos(v)的关系如何,都一定有LCA(T,u,v)=RMQ(Depth,pos(u),pos(v));

下面这个图是有根树的欧拉序列F和深度序列B已经pos(u)的变化情况:

                                           1                   深度为0

                                      /     \     \

                                2          3     4          深度为1

                              /   \

                         5        6                          深度为2

那么欧拉序列F:1 2 5 2 6 2 1 3 1 4 1;深度序列Depth为: 0 1 2 1 2 1 0 1 0 1 0,pos(u)为:1 2 8 10 3 5

还是以poj 1330为例:将LCA转化为RMQ求解,见代码:

#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<vector>
using namespace std;
const int MAX=20001;//定义10001RE了n次,定义太小了,以后RE后可以适当的把数组开大点 
#define CLR(arr,val) memset(arr,val,sizeof(arr))
int n,total,Depth[2*MAX],pos[MAX],F[2*MAX],Indegree[MAX];
vector<int> Adj[MAX];
bool visited[MAX];
void Init()
{   total=0;
    CLR(pos,0);
    CLR(Indegree,0);
    CLR(visited,false);
    for(int i=0;i<MAX;i++) Adj[i].clear();
}
void DFS(int u,int cur)//cur记录深度 
{   if(!visited[u])//说明该节点未被访问过,那么记录第一次出现的位置 
    {   visited[u]=true;
        pos[u]=total;
    }
    Depth[total]=cur;//记录深度序列 
    F[total++]=u;//记录欧拉序列 
    for(vector<int>::size_type i=0;i<Adj[u].size();i++)
    {   DFS(Adj[u][i],cur+1);
        Depth[total]=cur;
        F[total++]=u;
    }
} 
class RMQ{ 
public:
    void clear(){CLR(DP,0);}
    void rmq()
    {   int temp=(int)(log((double)total)/log(2.0));
        for(int i=0;i<total;i++) 
            DP[i][0]=i;   
        for(int j=1;j<=temp;++j)   
            for(int i=0;i<total;++i)   
                DP[i][j]=Depth[DP[i][j-1]]<Depth[DP[i+(1<<(j-1))][j-1]]?DP[i][j-1]:DP[i+(1<<(j-1))][j-1];   
    }   
    int Minimum(int L,int H)
    {   int k=(int)(log((double)H-L+1)/log(2.0)); 
        return Depth[DP[L][k]]<Depth[DP[H-(1<<k)+1][k]]?DP[L][k]:DP[H-(1<<k)+1][k];
    } 
private:
    int DP[2*MAX][20];        
}R;
int LCA(int u,int v)
{   return u<=v?R.Minimum(u,v):R.Minimum(v,u);
}
int main()
{   int k,u,v;
    scanf("%d",&k);
    while(k--)
    {   scanf("%d",&n);
        R.clear();
        Init();
        for(int i=0;i<n-1;i++)
        {   scanf("%d%d",&u,&v);
            Adj[u].push_back(v);
            Indegree[v]++;
        }
        for(int i=1;i<=n;i++)//从根结点开始DFS 
            if(!Indegree[i]) {DFS(i,0);break;}
        R.rmq();      
        scanf("%d%d",&u,&v);
        printf("%d\n",F[LCA(pos[u],pos[v])]);
    }
    return 0; 
}

例题2:NYOJ 119(士兵杀敌),一看就知道是RMQ算法,但是以前不会做,随便粘贴了别人的代码,过了,今天才自己做下,人总是再进步,谁都有过粘贴别人的代码过题的时候,但是你也在进步,重要的是坚持,RLE了n次,少在rmq()中加了if语句。

#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
using namespace std;
const int MAX=100010;
#define max(a,b) a>b?a:b
#define min(a,b) a<b?a:b
#define CLR(arr,val) memset(arr,val,sizeof(arr))
int n,m,low,high,Kill[MAX];  
class RMQ{
public:
    void rmq()
    {   int temp=(int)(log((double)n)/log(2.0));
        for(int i=0;i<n;i++)
            Min_DP[i][0]=Max_DP[i][0]=Kill[i];
        for(int j=1;j<=temp;j++)  
            for(int i=0;i<n;i++)  
                if(i+(1<<(j-1))<n)
                {   Min_DP[i][j]=min(Min_DP[i][j-1],Min_DP[i+(1<<(j-1))][j-1]);       
                    Max_DP[i][j]=max(Max_DP[i][j-1],Max_DP[i+(1<<(j-1))][j-1]); 
                }
    }   
    int Result(int L,int H)
    {   int k=(int)(log((double)H-L+1)/log(2.0));  
        int Min=min(Min_DP[L][k],Min_DP[H-(1<<k)+1][k]);
        int Max=max(Max_DP[L][k],Max_DP[H-(1<<k)+1][k]);
        return Max-Min;  
    }
    void Init()
    {   CLR(Min_DP,0);
        CLR(Max_DP,0);
    }
private:       
    int Min_DP[MAX][20],Max_DP[MAX][20];           
}R; 
int main()
{   scanf("%d%d",&n,&m);
    for(int i=0;i<n;i++)
        scanf("%d",&Kill[i]);
    R.Init();
    R.rmq();
    while(m--)
    {   scanf("%d%d",&low,&high);
        printf("%d\n",R.Result(low-1,high-1));
    }    
    return 0;
}




 

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值