RMQ问题之Sparse-Table算法

本文介绍了一种高效处理数组中区间最小值查询问题的方法——Sparse Table算法。该算法通过预处理实现快速查询,适用于需要频繁进行区间最小值查询的应用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

范围最小值问题(Range Minimum Query)

算法简述

给出一个数组,设计一个数据结构,支持查询操作,计算A[l]到A[r]的最小值。每次用一个循环来计算显然不够快,前缀和思想也不能提高效率。可以写一个线段树,但是预处理和查询的复杂度都是O(logn)。实践中最常用的是Tarjan的Sparse-Table算法,预处理时间是O(nlogn),但是查询只需要O(1),而且常数很小。最重要的是这个算法非常好写,并且不容易写错。

预处理

使用dp的思想方法。设a[i]是要求区间最值的数列,f[i,j]表示从第i个数起连续2^j个数中的最大值。例如数列3 2 4 5 6 8 1 2 9 7 ,f[1,0]表示第1个数起,长度为2^0=1的最大值,其实就是3这个数。f[1,2]=5,f[1,3]=8,f[2,0]=2,f[2,1]=4……从这里可以看出f[i,0]其实就等于a[i]。这样,Dp的状态、初值都已经有了,剩下的就是状态转移方程。我们把f[i,j]平均分成两段(因为f[i,j]一定是偶数个数字),从i到i+2^(j-1)-1为一段,i+2^(j-1)到i+2^j-1为一段(长度都为2^(j-1))。用上例说明,当i=1,j=3时就是3,2,4,5 和 6,8,1,2这两段。f[i,j]就是这两段的最大值中的最大值。于是我们得到了动规方程F[i,j]=max(F[i,j-1],F[i+2^(j-i),j-1])。


查询操作

找到最大的k使得2^k<=(r-l+1),这样以l开头,r结尾的长度为2^k的两个区间就可以完全覆盖查询区间。

例题:hdu 3183

对于序列A[1...N],选取N-M个数,使组成的值最小,而且顺序不能交换,既然要选取N-M个,那么可以容易知道这N-M位数的第一位一定在数组A中的区间[1M+1]中出现,为什么是这样呢?

假设A数组就只有6个数,分别是A[1]A[2]A[3]A[4]A[5]A[6],我们去掉M=2个数,使形成的值最小。那么我们此时的N=6,M=2N-M=4。则我们说形成的4位数的第一位一定在区间[13]中出现,因为如果区间范围再大点,比如[14],这样就不科学了,因为第一位一定不会是A[4],更不会是A[5]A[6],我们假设可以是A[4],那么后面只有A[5]A[6]两位数了,这样的话最多只可能形成3位数,绝对不能形成N-M=4位了。

当然到了这里,我们就可以这样做了,第一位可以在区间[1M+1]里面找,假设第一位在位置x,因为第二位肯定在第一位的后面,所以第二位一定存在于区间[x+1M+2],为什么是M+2,因为第一位已经确定了,现在只需要确定N-M-1位了,所以区间就可以向后增加1,一直这样循环下去,就可以找到了。(借鉴大牛的做法)

#include <math.h>
#include <stdio.h>
#include <string.h>
#include <iostream>
#include <algorithm>

using namespace std;

const int MAX = 1010;


int n,m;
int dp[MAX][MAX];
char a[MAX],num[1010];

int Min(int i, int j){//返回小的元素的下标 便于操作
    if(a[i] <= a[j]) return i;
    return j;
}

void ST(){
    for(int i=0; i<n; i++) dp[i][0] = i;
    for(int j=1; j<=(int)(log((double)n)/log(2.0)); j++)//换底公式
        for(int i=0; (i+(1<<j)-1)<n; i++)
            dp[i][j] = Min(dp[i][j-1], dp[i+(1<<(j-1))][j-1]);
}

int Query(int l, int r){
    int k = (int)(log((double)(r-l+1))/log(2.0));
    return Min(dp[l][k], dp[r-(1 << k)+1][k]);
}

int main(){
    while(scanf("%s %d",a,&m) != EOF){
        n = strlen(a);
        m = n - m;
        ST();
        int i,j;
        i = j = 0;
        while(m--){
            i = Query(i, n-m-1);
            num[j++] = a[i++];
        }
        for(i=0; i<j; i++)
            if(num[i] != '0') break;
        if(i == j) {puts("0"); continue;}
        while(i < j){
            printf("%c",num[i]);
            i++;
        }
        puts("");
    }
    return 0;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值