RMQ的两种实现ST算法,和线段树

RMQ就是一种nlog实现的区间查询最值得一种东西。
就是先划分区间,对于每一个节点存储其左右区间范围,和区间最值,我是用结构体来实现。用线段树的方法。

可以参看下这个视频

https://www.bilibili.com/video/av9350697/?from=search&seid=16954174042578847354

里面讲解了线段树的实现。我们参看NYOJ 119的这个例题
链接http://acm.nyist.net/JudgeOnline/problem.php?pid=119
题目描述如下:

士兵杀敌(三)
时间限制:2000 ms | 内存限制:65535 KB
难度:5

描述

南将军统率着N个士兵,士兵分别编号为1~N,南将军经常爱拿某一段编号内杀敌数最高的人与杀敌数最低的人进行比较,计算出两个人的杀敌数差值,用这种方法一方面能鼓舞杀敌数高的人,另一方面也算是批评杀敌数低的人,起到了很好的效果。

所以,南将军经常问军师小工第i号士兵到第j号士兵中,杀敌数最高的人与杀敌数最低的人之间军功差值是多少。

现在,请你写一个程序,帮小工回答南将军每次的询问吧。

注意,南将军可能询问很多次。

输入
只有一组测试数据
第一行是两个整数N,Q,其中N表示士兵的总数。Q表示南将军询问的次数。(1<N<=100000,1<Q<=1000000)
随后的一行有N个整数Vi(0<=Vi<100000000),分别表示每个人的杀敌数。
再之后的Q行,每行有两个正正数m,n,表示南将军询问的是第m号士兵到第n号士兵。
输出
对于每次询问,输出第m号士兵到第n号士兵之间所有士兵杀敌数的最大值与最小值的差。
样例输入

5 2
1 2 6 9 3
1 2
2 4

样例输出

1
7

我们可以看到,这个数据范围的话,暴力是根本没有可能过的,所以我们用一下区间线段树的方法。如果你看那个视频的话,肯定会明白,代码如下:


#include<cstdio>
#include<cstring>
#include<string>
#include<cmath>

using namespace std;
const int MAX_N = 1e6*3;
const int MAX = 1e6 + 10;
const int INF = 0x7fffffff;
struct node{
    int l,r;
    int max_value,min_value;
}node[MAX_N];

int leaf[MAX];
void BuildTree(int i,int left,int right){
    node[i].max_value = 0;//这里不要初始化为INF,因为数据太大了。两个耗时不一样
    node[i].min_value = 0;
    node[i].l = left;
    node[i].r = right;
    
    if(left == right){
        leaf[left] = i;//这个是存储叶子节点。
        return;
    }
    //左右儿子开始递归
    BuildTree(i<<1,left,(left+right)/2);
    BuildTree((i<<1)+1,(left+right)/2+1,right);
}

void UpdateTree(int ri){
    if(ri == 1) return;//如果到达根节点
    
    int fi = ri/2;
    node[fi].max_value = max(node[fi*2].max_value,node[fi*2+1].max_value);
    node[fi].min_value = min(node[fi*2].min_value,node[fi*2+1].min_value);
    
    UpdateTree(fi);
}

int Max,Min;
void Query(int l,int r,int i){
    if(node[i].l == l && node[i].r == r){//如果区间完全覆盖
        Max = max(Max,node[i].max_value);
        Min = min(Min,node[i].min_value);
        return;
    }
    i *= 2;//对边左二子区间
    if(l <= node[i].r){
        if(r <= node[i].r)  Query(l,r,i);//如果要查询的区间仍然在这个儿子的区间范围内,就继续查询这个儿子的区间
        else    Query(l,node[i].r,i);//否则就只查询这个儿子覆盖的区间
    }
    i++;
    if(r >= node[i].l){
        if(l >= node[i].l)  Query(l,r,i);
        else    Query(node[i].l,r,i);
    }
}
int main(void){
    int n,q;
    scanf("%d %d",&n,&q);
    BuildTree(1,1,n);
    int a;
    for(int i=1;i<=n;i++){
        scanf("%d",&a);
        node[leaf[i]].max_value = node[leaf[i]].min_value = a;
        UpdateTree(leaf[i]);
    }
    int le,ri;
    for(int i=1;i<=q;i++){
        scanf("%d %d",&le,&ri);
        Max = 0,Min = INF;//这里要记得初始化,每次查询前
        Query(le,ri,1);
        printf("%d\n",Max-Min);
    }
    return 0;
}

这是线段树实现RMQ的算法,这是比较耗时的。我第一次把全部数据初始化为INF就超时了,后来改为初始化为0就过了。。
但也是1640ms,很慢的。

下面是一种ST算法是实现的RMQ,也是一中DP的算法。
dp[i][j],表示的就是从i开始2^j的长度区间内的范围,即(i , i+2^j-1)这个区间。而dp[i][0] 就是a[i]的值,所以我们可以从上而下的对每个范围都进行预处理。如果是对最大值进行预处理。我们可以知道dp[i][j] = max(dp[i][j-1],dp[i+2^(j-1)][j-1]);
就是dp[i][j]的值就是他两个儿子里面的最大值。
在查询是我们也是通过这种方法,如我们要查询(l,r)这个区间的最大值,我们先获取这个区间的长度即len = r-l+1;然后我们要知道这个长度的对2求对数后的数即k = log(len)/log(2.0);
对两个儿子进行比较就能知道了,就(l, l + 2k-1)和(r-2k+1,r)这两个儿子区间的最大值就行了,
代码如下:


#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<string>

using namespace std;

const int MAX_N = 1e5;
struct node{
    int max_value;
    int min_value;
}dp[MAX_N][30];
int a[MAX_N];
int n,q,m;
void RMQ(){
    for(int j=1;j<=m;j++){
        for(int i=1;i+(1<<j)-1<=n;i++){//注意这个判断
            dp[i][j].max_value = max(dp[i][j-1].max_value,dp[i+(1<<(j-1))][j-1].max_value);
            dp[i][j].min_value = min(dp[i][j-1].min_value,dp[i+(1<<(j-1))][j-1].min_value);
        }
    }
}
int main(void){
    scanf("%d %d",&n,&q);
    for(int i=1;i<=n;i++){
        scanf("%d",&a[i]);
        dp[i][0].max_value = dp[i][0].min_value = a[i];
    }
    m = log(n*1.0)/log(2.0);//用于划分区间
    RMQ();
    int l,r;
    for(int i=1;i<=q;i++){
        scanf("%d %d",&l,&r);
        int k = log((r-l+1)*1.0)/log(2.0);
        int Max = max(dp[l][k].max_value,dp[r-(1<<k)+1][k].max_value);
        int Min = min(dp[l][k].min_value,dp[r-(1<<k)+1][k].min_value);
        printf("%d\n",Max-Min);
    }
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值