(hiho一下 第十九周)线段树之查询空间最小值

题目1 : RMQ问题再临-线段树

时间限制: 10000ms
单点时限: 1000ms
内存限制: 256MB

描述

上回说到:小Hi给小Ho出了这样一道问题:假设整个货架上从左到右摆放了N种商品,并且依次标号为1到N,每次小Hi都给出一段区间[L, R],小Ho要做的是选出标号在这个区间内的所有商品重量最轻的一种,并且告诉小Hi这个商品的重量。但是在这个过程中,可能会因为其他人的各种行为,对某些位置上的商品的重量产生改变(如更换了其他种类的商品)。

小Ho提出了两种非常简单的方法,但是都不能完美的解决。那么这一次,面对更大的数据规模,小Ho将如何是好呢?

提示:其实只是比ST少计算了一些区间而已

输入

每个测试点(输入文件)有且仅有一组测试数据。

每组测试数据的第1行为一个整数N,意义如前文所述。

每组测试数据的第2行为N个整数,分别描述每种商品的重量,其中第i个整数表示标号为i的商品的重量weight_i。

每组测试数据的第3行为一个整数Q,表示小Hi总共询问的次数与商品的重量被更改的次数之和。

每组测试数据的第N+4~N+Q+3行,每行分别描述一次操作,每行的开头均为一个属于0或1的数字,分别表示该行描述一个询问和描述一次商品的重量的更改两种情况。对于第N+i+3行,如果该行描述一个询问,则接下来为两个整数Li, Ri,表示小Hi询问的一个区间[Li, Ri];如果该行描述一次商品的重量的更改,则接下来为两个整数Pi,Wi,表示位置编号为Pi的商品的重量变更为Wi

对于100%的数据,满足N<=10^6,Q<=10^6, 1<=Li<=Ri<=N,1<=Pi<=N, 0

输出

对于每组测试数据,对于每个小Hi的询问,按照在输入中出现的顺序,各输出一行,表示查询的结果:标号在区间[Li, Ri]中的所有商品中重量最轻的商品的重量。

样例输入
10
3655 5246 8991 5933 7474 7603 6098 6654 2414 884 
6
0 4 9
0 2 10
1 4 7009
0 5 6
1 3 7949
1 3 1227
样例输出
2414
884
7474

提示思路:

1.线段树的定义

线段树其实本质就是用一棵树来维护一段区间上和某个子区间相关的值——例如区间和、区间最大最小值一类的。

它的具体做法是这样的,这棵树的根节点表示了整段区间,根节点的左儿子节点表示了这段区间的前半部分,根节点的右儿子节点表示了这段区间的后半部分——并以此类推,对于这棵树的每个节点,如果这个节点所表示的区间的长度大于1,则令其左儿子节点表示这段区间的前半部分,令其右儿子表示这段区间的后半部分。以一段长度为10的区间为例,所建立出的线段树如下:

(hiho一下 第十九周)线段树之查询空间最小值_第1张图片


以RMQ问题为例 —— RMQ问题要求的是求解一段区间中的最小值,那么我们先对一些可能会用到的区间求解这个最小值!而既然要用线段树来解决这个问题的,那么不妨就将每一个节点对应的区间中的最小值都求解出来。

给定一组数据如:

N = 10

Weight = {3, 14, 15, 9, 26, 53, 5, 8, 9, 79}

这个预处理的结果如下:

(hiho一下 第十九周)线段树之查询空间最小值_第2张图片

事实上上面这一步相当好计算,由于每个非叶子节点所对应的区间都正好由它的两个儿子节点所对应的区间拼凑而成,这样一个节点所对应的区间中的最小值便是它的两个儿子节点所对应的区间中的最小值中更小的那一个。这样我只需要O(N)的时间复杂度就可以计算出这棵树来。”

修改:

当某个位置的商品的重量发生改变的时候,对应的,就是这棵树的某个叶子节点的值发生了变化——但包含这个节点的区间,便只有这个节点的所有祖先节点,而这样的节点数量事实上是很少的——只有O(log(N))级别。也就是说,当一次修改操作发生的时候,我只需要改变数量在O(log(N))级别的节点的值就可以完成操作了,修改的时间复杂度是O(log(N))

查询:

我要做的事情其实就是,将一个询问的区间拆成若干个我已经计算出来的区间,这样对于这些区间已经计算出的最小值求最小值的话,我就可以知道询问的整个区间中的最小值是多少了!

如何分解询问的区间——

从线段树的根开始,对于当前访问的线段树节点t, 设其对应的区间为[A, B], 如果询问的区间[l, r]完全处于前半段或者后半段——即r <= (A + B)/2或者l > (A + B) / 2,那么递归进入t对应的子节点进行处理(因为另一棵子树中显然不会有任何区间需要用到)。否则的话,则把询问区间分成2部分[l, (A + B) / 2]和[(A + B) / 2 + 1, r],并且分别进入t的左右子树处理这两段询问区间(因为2棵子树中显然都有区间需要用到)!当然了,如果[A, B]正好覆盖了[l, r]的话,就可以直接返回之前计算的t这棵子树中的最小值了。还是之前那个例子,如果我要询问[3, 9]这段区间,我的最终结果会是这样的——橙色部分标注的区间。”

(hiho一下 第十九周)线段树之查询空间最小值_第3张图片

首先[3, 9]分解成了[3, 5]和[6, 9]两个区间,而[3, 5]分解成了[3, 3]和[4, 5]——均没有必要继续分解,[6, 9]分解成了[6, 8]和[9, 9]——同样也没有必要继续分解了。每一步的分解都是必要的,所以这已经是最好的分解方法了。

区间的个数也不会很多,因为除了第一次分解成2个区间的操作可能会将区间个数翻倍外,之后每一次分解的时候所处理的区间都肯定有一条边是和当前节点对应的重合的(即l=A或者r=B),也就是说即使再进行分解,分解出的两个区间中也一定会有一个区间是不需要再进行分解的,也就是区间的总数是在深度这个级别的,所以也是O(logN)的。


总结:

首先我会根据初始数据,使用O(N)的时间构建一棵最原始的线段树,这个过程中我会使用子节点的值来计算父亲节点的值,从而避免冗余计算。然后对于每一次操作,如果是一次修改的话,我就将修改的节点和这个节点的所有祖先节点的值都进行更新,可以用O(logN)的时间复杂度完成。而如果是一次询问的话,我会使用上面描述的方法来对询问区间进行分解,这样虽然不像ST算法那样是O(1),但是却实现了上一次所提到的‘平衡’,无论是修改还是查询的时间复杂度都是O(logN)的,所以我这个算法最终的时间复杂度会是O(N + Q * log(N))。


代码一:

#include 
#include 
using namespace std;

const int MAXN = 1000005;
const int INF = 0x7fffffff;
int segTree[MAXN*3];

//构造线段树,算法复杂度为O(n)
void build(int node, int begin, int end){
    if(begin == end) scanf("%d", &segTree[node]);
    else{
        build(2*node, begin, (begin+end)/2);
        build(2*node+1, (begin+end)/2+1, end);
        segTree[node] = min(segTree[2*node], segTree[2*node+1]);
    }
}

//单点更新, 仿照建树的样子更新
void update(int node, int begin, int end, int k, int a){
    if(begin == end) segTree[node] = a;
    else{
        int m = (begin+end)/2;
        if(k<=m) update(2*node, begin, m, k, a);
        else update(2*node+1, m+1, end, k, a);
        segTree[node] = min(segTree[2*node], segTree[2*node+1]);
    }
}

//查询[l, r]区间的最小值
int query(int node, int begin, int end, int l, int r){
    int p1, p2;
    if(r end) return INF;

    if(l<=begin && end<=r) return segTree[node];
    else{
        p1 = query(2*node, begin, (begin+end)/2, l, r);
        p2 = query(2*node+1, (begin+end)/2+1, end, l, r);
        return min(p1, p2);
    }
}

//查询区间最小值
int main(){
    int n, Q;
    for(int i=0;i

 
  

代码二:

#include 
#include 
using namespace std;

const int INF = 0x7fffffff;
const int MAXN = 1000005;

//存储线段树的全局数组
int n, dat[3*MAXN];

//初始化
void init(int nn){
    //为了简单起见,把元素个数扩大到2的幂
    n = 1;
    while(n0){
        k = (k-1)/2;
        dat[k] = min(dat[k*2+1], dat[k*2+2]);
    }
}

/****
* 求查询[a, b)的最小值
* 后面的参数是为了计算起来方便而传入的。
* k是节点的编号,l、r表示这个节点对应的是[l, r)区间。
* 在调用时,用query(a, b, 0, 0, n)
***/
int query(int a, int b, int k, int l, int r){
    //如果[a, b)和[l, r)不相交,则返回INF(技巧)
    if(r<=a || b<=l) return INF;

    //如果查询区间[a, b)完全包含节点对应区间[l, r),则返回当前节点的值
    if(a<=l && r<=b) return dat[k];
    else{
        //否则返回两个儿子中值较小者
        int vl = query(a, b, k*2+1, l, (l+r)/2);
        int vr = query(a, b, k*2+2, (l+r)/2, r);
        return min(vl,vr);
    }
}

int main(){
    int nn, q;
    scanf("%d", &nn);
    init(nn);

    for(int i=0;i

你可能感兴趣的:(RMQ问题,线段树,hihocoder)