线段树

线段树

线段树,一种拿来解决 \(RMQ\) 与区间和问题的高效的数据结构,对于这两种问题,线段树均能在 \(O(mlog_{2}n)\) 的时间复杂度内解决。在这两个基础的应用场景的基础上,线段树还发展出了各种丰富的应用。

总的来说,线段树是一个应用广泛,功能强大的数据结构,本章将介绍其概念及基本操作。

本章将包括:

一 概念

概括的说,线段树可以理解为“分治法+二叉树结构+ \(Lazy-tag\) 技术”。

(1.原理

线段树是分治法与树的有机结合,可将线段树上的每个节点视为“线段”(或“区间”),区间是由分治得到的,下图为一棵表示区间 \([1,10]\) 的线段树:

(2.分析

由图可以看出其有几点基本特征:

​ 1.用分治法自顶向下建立,每次分治左右子树各一半。

​ 2.每个节点都表示一个区间 \([L,R]\) 叶节点 \(L==R\) ,非叶结点 \(L<R\)

​ 3.除最后一层其它层都是满的。

对于每个区间 \([L,R]\) 区间都有:

​ 若 \(L \ne R\) ,即 \([L,R]\) 不为叶子结点;则可将其分为 \([L,M]\)\([M + 1,R]\) 两部分,其中 \(M=(L+R)/2\) ;

线段树节点(即线段)表示的值,可以为线段上的区间和,也可定义为区间最值或其他要求值,这也是线段树为什么灵活多变的原因

由上述条件可知,线段树适合解决的问题的特征是:大区间的解可以由小区间的解合并而来。

二 实现

线段树的代码实现。

(1.建树

分治建树,顺带维护线段;

int ls(int p){return p<<1;}    //左儿,2*n
int rs(int p){return p<<1|1;}  //右儿,2*n+1
int a[maxn],tree[maxn];        //a数组存储数列元素,tree树组表示一个线段区间的值

void push_up(int p){           //维护线段
    return tree[p]=tree[ls(p)]+tree[rs(p)];
    //若题目为求最小值,则此处为 tree[p]=min(tree[ls(p)],tree[rs(p)]);
}

void build_tree(ll p,ll pl,ll pr){//p为节点编号,其指向区间[pl,pr]
    if(pl==pr){
        tree[p]=a[pl];          //叶子结点,赋值
        return;
    }
    ll mid=(pl+pr)>>1;          //分治,折半
    build_tree(ls(p),pl,mid);   //左子树
    build_tree(rs(p),mid+1,pr); //右子树
    push_up(p);                 //从下往上维护线段树数组
}

关于区间问题:

对于区间问题,我们采用与分治一样的手段,(即:大区间的解由小区间的解合并而来)。具体是怎样操作呢?模版如下

//在递归函数中
void mo(int l,int r,int p,int pl,int pr){
//要进行__操作的区间[l,r],现在查询到p线段[pl,pr];
	if(l<=pl&&r>=pr){      //[pl,pr]包含于[l,r]或[pl,pr]等于[l,r]
   		//todo 即进行操作
    	return;
    }
}

但如果你对分治掌握的并不好(像我一样),理解起来可能有一点难度,用下图来解释一下(掌握的可以跳过)

上图即为 l<=pl&&r>=pr 的情况。当然,情况不唯一,我们可以简单的理解为区间 \([L,R]\) 由三个小区间 \([L,pl]\)\([pl,pr]\)\([pr,R]\) 组成,由是,对于区间 \([L,R]\) 的操作便可由三个小区间的解合并而来。

(2.区间修改

首先,我们需要引入 \(Lazy-tag\) 的概念:技如其名,懒标记,指在进行区间修改时,不一次执行到底,而是打下标记,当用到时,再进行修改。(不用就不修改,为我们贪心到了不少时间复杂度)

int tag[maxn];                                 //tag数组用来存储懒标记
void add_tag(int p,int pl,int pr,int d){       //添加标记d
    tag[p]+=d;
    tree[p]+=d*(pr-pl);
}

void push_tag(int p,int pl,int pr){           //维护懒数组标记  
    if(tag[p]){                               //之前留下的懒标记,为了不被覆盖,向下传给子树
        int mid=(pl+pr)>>1;
        add_tag(ls(p),pl,mid);
        add_tag(rs(p),mid+1,pr);
        tag[p]=0;                             //标记被传走
    }
}

void up_date(int l,int r,int p,int pl,int pr,int d){
    if(l<=pl&&r>=pr){
        add_tag(p,pl,pr,d);                     //打上懒标记,毕竟我是懒人
        return;
    }
    push_down(p,pl,pr);                        //如不能覆盖,把tag传给子树
    ll mid=(pl+pr)>>1;
    if(l<=mid) updata(l,r,ls(p),pl,mid,d  );    //递归左子树
    if(r>mid)  updata(l,r,rs(p),mid+1,pr,d);    //递归右子树
    push_up(p);                                 //维护线段树
}

代码如上所示,懒标记不止能记录加法,如乘,除,乘方,开方,异或等。都可以以懒标记的形式存储,但当存在多个不同的懒标记时,注意懒标记的处理顺序,顺序不当可能会导致精度出现差异。

当然,对于单点修改问题,区间修改也可以兼任,只需查询时让 \(l=r\) 即可。

(3.区间查询

int query(int l,int r,int p,int pl,int pr){
    if(l<=pl&&r>=pr){
        return tree[p];
    }
    push_down(p,pl,pr);
    int res=0,mid=(pl+pr)>>1;
    if(l>pl) res+=query(l,r,ls(p),pl,mid);
    if(p<pr) res+=query(l,r,rs(p),mid+1,pr);
    return res;
}

此为区间求和的的代码,挨个加起来,没什么好说的。

三 例题

  1. 3372 【模板】线段树

最经典的班子题,区间加+区间求和

  1. P3870 [TJOI2009] 开关

对于线段树的另一种应用方式,好好利用异或,此题会简单不少

3.P2357守墓人

花哨了一点,仔细一看,还是区间加+区间求和的版子
4.P1253 扶苏的问题
区间加和区间修改的板子,安利一下我的题解

posted @ 2024-05-25 09:16  adsd45666  阅读(44)  评论(1编辑  收藏  举报