【树】一步一步写线段树(三)——实战演练(二)PushUp的应用

查询最小值与修改一个值

题目:RMQ问题再临-线段树【HihoCoder-1077】

描述

上回说到:小Hi给小Ho出了这样一道问题:假设整个货架上从左到右摆放了 N 种商品,并且依次标号为1到 N ,每次小Hi都给出一段区间 [ L , R ] ,小Ho要做的是选出标号在这个区间内的所有商品重量最轻的一种,并且告诉小Hi这个商品的重量。但是在这个过程中,可能会因为其他人的各种行为,对某些位置上的商品的重量产生改变(如更换了其他种类的商品)。
小Ho提出了两种非常简单的方法,但是都不能完美的解决。那么这一次,面对更大的数据规模,小Ho将如何是好呢?

输入

每个测试点(输入文件)有且仅有一组测试数据。
每组测试数据的第1行为一个整数 N ,意义如前文所述。
每组测试数据的第2行为 N 个整数,分别描述每种商品的重量,其中第i个整数表示标号为i的商品的重量weight_i。
每组测试数据的第3行为一个整数 Q ,表示小Hi总共询问的次数与商品的重量被更改的次数之和。
每组测试数据的第 N + 4 ~ N + Q + 3 行,每行分别描述一次操作,每行的开头均为一个属于0或1的数字,分别表示该行描述一个询问和描述一次商品的重量的更改两种情况。对于第 N + i + 3 行,如果该行描述一个询问,则接下来为两个整数 L i , R i ,表示小Hi询问的一个区间 [ L i , R i ] ;如果该行描述一次商品的重量的更改,则接下来为两个整数 P i W i ,表示位置编号为 P i 的商品的重量变更为 W i
对于100%的数据,满足 N 10 6 Q 10 6 , 1 L i R i N 1 P i N , 0 & l t w e i g h t i , W i 10 4

输出

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

样例

输入

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

分析

先来看看hihocoder给的提示:

提示:其实只是比ST少计算了一些区间而已
“在我介绍别的算法之前,你先来讲一讲你是准备如何使用线段树来解决这个问题的吧?”小Hi虽然做好了讲解的准备,但是还是希望能够一步步引导小Ho进行思考,于是这般说道。
“唔……那我先从线段树的定义说起吧:线段树其实本质就是用一棵树来维护一段区间上和某个子区间相关的值——例如区间和、区间最大最小值一类的。”小Ho说道:“它的具体做法是这样的,这棵树的根节点表示了整段区间,根节点的左儿子节点表示了这段区间的前半部分,根节点的右儿子节点表示了这段区间的后半部分——并以此类推,对于这棵树的每个节点,如果这个节点所表示的区间的长度大于1,则令其左儿子节点表示这段区间的前半部分,令其右儿子表示这段区间的后半部分。以一段长度为10的区间为例,我所建立出的线段树应该是这样子的。”

“画的还凑合,但是这样一棵树有什么用呢?”小Hi明知故问道。
“就以RMQ问题为例吧——RMQ问题要求的是求解一段区间中的最小值,那么我不妨效仿ST算法,先对一些可能会用到的区间求解这个最小值!而既然我要是用线段树来解决这个问题的,那么我不妨就将每一个节点对应的区间中的最小值都求解出来。”小Ho道。
“那我给你这样一组数据,你将这个预处理的结果给我计算一下?”小Hi又出难题。

“好啊!你这数据正好也只有10个位置,那么我便直接用这棵树了。”只见小Ho刷刷两笔便在之前绘下的二叉树上写下了每个节点对应的区间中的最小值:“事实上这样一步相当好计算,由于每个非叶子节点所对应的区间都正好由它的两个儿子节点所对应的区间拼凑而成——那么只需要像ST那样,这样一个节点所对应的区间中的最小值便是它的两个儿子节点所对应的区间中的最小值中更小的那一个。这样我只需要 O ( N ) 的时间复杂度就可以计算出这棵树来。”

小Hi点了点头,继续问道:“我算是明白了,但是你这样一棵树统计出来的区间比ST算法统计出来的区间要少了很多,你还能够使用很快的算法进行查询么?更何况还有修改呢?”
小Ho笑了笑:“我先从简单的说起吧——修改,当某个位置的商品的重量发生改变的时候,对应的,就是这棵树的某个叶子节点的值发生了变化——但是和ST算法不同,包含这个节点的区间,便只有这个节点的所有祖先节点,而这样的节点数量事实上是很少的——只有 O ( log 2 N ) 级别。也就是说,当一次修改操作发生的时候,我只需要改变数量在 O ( log N ) 级别的节点的值就可以完成操作了,修改的时间复杂度是 O ( log 2 N ) 。”
小Hi道:“是这样没错~算你过关!但是呢,还是像我之前所说的,你是准备如何使用数量在 O ( N ) 级别的区间来应付所有的询问呢?”
小Ho的笑容仍未退去:“这个其实也很简单!我要做的事情其实就是——将一个询问的区间拆成若干个我已经计算出来的区间(在ST算法中是拆成了2个的区间),这样对于这些区间已经计算出的最小值求最小值的话,我就可以知道询问的整个区间中的最小值是多少了!”
“那你准备如何分解询问的区间呢?”小Hi问道。
小Ho思索了一会,道:“这个问题其实很简单!我从线段树的根开始,对于当前访问的线段树节点 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 ] 这段区间,我的最终结果会是这样的——橙色部分标注的区间。”

“首先 [ 3 , 9 ] 分解成了 [ 3 , 5 ] [ 6 , 9 ] 两个区间,而 [ 3 , 5 ] 分解成了 [ 3 , 3 ] [ 4 , 5 ] ——均没有必要继续分解, [ 6 , 9 ] 分解成了 [ 6 , 8 ] [ 9 , 9 ] ——同样也没有必要继续分解了。每一步的分解都是必要的,所以这已经是最好的分解方法了。”
小Hi满意的点了点头,道:“听起来还不错?但是你这样分解的话,区间的个数你能保证么?万一很多怎么办?”
小Ho思索了一会,接着道:“不会的,除了第一次分解成2个区间的操作可能会将区间个数翻倍外,之后每一次分解的时候所处理的区间都肯定有一条边是和当前节点对应的重合的(即 l = A 或者 r = B ),也就是说即使再进行分解,分解出的两个区间中也一定会有一个区间是不需要再进行分解的,也就是区间的总数是在深度这个级别的,所以也是 O ( log 2 N ) 的。”
“看来你还挺清楚的,那么要不你再总结一下,我就算你过关,然后我们就可以开始讲解后面的问题了~”
小Ho道:“好的!首先我会根据初始数据,使用 O ( N ) 的时间构建一棵最原始的线段树,这个过程中我会使用子节点的值来计算父亲节点的值,从而避免冗余计算。然后对于每一次操作,如果是一次修改的话,我就将修改的节点和这个节点的所有祖先节点的值都进行更新,可以用 O ( log 2 N ) 的时间复杂度完成。而如果是一次询问的话,我会使用上面描述的方法来对询问区间进行分解,这样虽然不像ST算法那样是 O ( 1 ) ,但是却实现了上一次所提到的‘平衡’,无论是修改还是查询的时间复杂度都是 O ( log 2 N ) 的,所以我这个算法最终的时间复杂度会是 O ( N + Q log 2 N ) ,在这个数据规模下是绰绰有余的!”
“嗯~ o( ̄▽ ̄)o 不错哟~那么就到这里吧!”小Hi笑容满满道:“赶紧去吃早饭吧!”

看完了hihocoder的提示,相信大家一定有了思路,这里我只讲一些大家值得注意的一些东西。
首先是建树。建树时可采用边建边读的方法,在到达边界时(即单位区间)读入并记入其域minx中,在回溯的过程中执行PushUp操作,即可建立一棵线段树。
接下来是更新操作。我们使用暴力方法更新,在回溯的时候执行一次PushUp操作,即可正确更新。
在查找时,应注意返回时取min。
至于其他的,那些都是板子。

代码

#include<cstdio>
#include<algorithm>
using namespace std;
const int Maxn=1e6;
const int INF=1e5;
struct node {
    int l,r;
    int minx;
}Tree[Maxn*4+5];
int N,W[Maxn+5],Q;
void Build(int i,int l,int r) {
    Tree[i].l=l;
    Tree[i].r=r;
    if(l==r) {
        Tree[i].minx=W[l];
        return;
    }
    int mid=(l+r)/2;
    Build(i*2,l,mid);
    Build(i*2+1,mid+1,r);
    Tree[i].minx=min(Tree[i*2].minx,Tree[i*2+1].minx);
}
int Find(int i,int l,int r) {
    if(l>Tree[i].r||r<Tree[i].l)
        return INF;
    if(l<=Tree[i].l&&r>=Tree[i].r)
        return Tree[i].minx;
    return min(Find(2*i,l,r),Find(2*i+1,l,r));
}
void Update(int i,int id,int to) {
    if(Tree[i].l==Tree[i].r) {
        Tree[i].minx=to;
        return;
    }
    int mid=(Tree[i].l+Tree[i].r)/2;
    if(id<=mid)Update(i*2,id,to);
    else Update(i*2+1,id,to);
    Tree[i].minx=min(Tree[i*2].minx,Tree[i*2+1].minx);
}
int main() {
    #ifdef LOACL
    freopen("in.txt","r",stdin);
    freopen("out.txt","w",stdout);
    #endif
    scanf("%d",&N);
    for(int i=1;i<=N;i++)
        scanf("%d",&W[i]);
    Build(1,1,N);
    scanf("%d",&Q);
    while(Q--) {
        int op;
        scanf("%d",&op);
        if(op==0) {
            int l,r;
            scanf("%d %d",&l,&r);
            int ans=Find(1,l,r);
            printf("%d\n",ans);
        }
        else if(op==1) {
            int i,x;
            scanf("%d %d",&i,&x);
            W[i]=x;
            Update(1,i,x);
        }
    }
    return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_37656398/article/details/79320320