【知识点】 ---线段树的常用操作

xiaoxiao2021-02-28  129

1.什么是线段树?

线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。

性质:对于线段树中的每一个非叶子节点[a,b],它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b]。因此线段树是平衡二叉树,最后的子节点数目为N,即整个线段区间的长度。

举例说明:

2.构造线段树

成员声明: struct node { int l, r; int sum; } tree[MAX<<2]; 建树思想:递归建树,遇到叶子节点直接赋值,否则递归左右建树,回溯即可。 void build(int l, int r, int rt) { tree[rt].l = l; tree[rt].r = r; tree[rt].sum = 0; if(l == r) //叶子结点 { scanf("%d",&tree[rt].sum); return ; } int mid = (l+r)>>1; //递归建树 build(l, mid, rt<<1); build(mid+1, r, rt<<1|1); pushup(rt); }

3.更新线段树(单点)

更新思路:递归更新,递归至要更新的叶子节点,回溯更改该叶子节点。 void update(int pos, int val, int rt) { //更新这个区间的值 if(tree[rt].l == tree[rt].r) { tree[rt].sum += val; return ; } int mid = (tree[rt].l+tree[rt].r)>>1; if(pos <= mid) update(pos, val, rt<<1); else update(pos, val, rt<<1|1); pushup(rt); }

4.区间查询

查询思路:

(1).区间求和

void query(int x, int y, int rt) { if(x == tree[rt].l && y == tree[rt].r) { ans += tree[rt].sum; return ; } int mid = (tree[rt].l+tree[rt].r)>>1; if(y <= mid) query(x, y, rt<<1); else if(x > mid) query(x, y, rt<<1|1); else { query(x, mid, rt<<1); query(mid+1, y, rt<<1|1); } }

(2).区间最大值

void qMax(int x, int y, int rt) { if(tree[rt].l == x && tree[rt].r == y) { ans1 = max(ans1, tree[rt].Max); return ; } int mid = (tree[rt].l+tree[rt].r)>>1; if(y <= mid) qMax(x, y, rt<<1); else if(x > mid) qMax(x, y, rt<<1|1); else { qMax(x, mid, rt<<1); qMax(mid+1, y, rt<<1|1); } }

(3).区间最小值

void qMin(int x, int y, int rt) { if(tree[rt].l == x && tree[rt].r == y) { ans2 = min(ans2, tree[rt].Min); return ; } int mid = (tree[rt].l+tree[rt].r)>>1; if(y <= mid) qMin(x, y, rt<<1); else if(x > mid) qMin(x, y, rt<<1|1); else { qMin(x, mid, rt<<1); qMin(mid+1, y, rt<<1|1); } }

5.更新线段树(区间)

区间更新是指更新某个区间内的叶子节点的值,因为涉及到的叶子节点不止一个,而叶子节点会影响其相应的非叶父节点,那么回溯需要更新的非叶子节点也会有很多,如果一次性更新完,操作的时间复杂度肯定不是O(lgn),例如当我们要更新区间[0,3]内的叶子节点时,需要更新出了叶子节点3,9外的所有其他节点。为此引入了线段树中的延迟标记概念,这也是线段树的精华所在。

每个节点新增加一个标记,记录这个节点是否进行了某种修改(这种修改操作会影响其子节点),对于任意区间的修改,我们先按照区间查询的方式将其划分成线段树中的节点,然后修改这些节点的信息,并给这些节点标记上代表这种修改操作的标记。在修改和查询的时候,如果我们到了一个节点p,并且决定考虑其子节点,那么我们就要看节点p是否被标记,如果有,就要按照标记修改其子节点的信息,并且给子节点都标上相同的标记,同时消掉节点p的标记。

(1).延迟操作的实现:

void pushdown(int rt) //延迟操作,更新当前结点的叶子 { int len = tree[rt].r - tree[rt].l + 1; tree[rt<<1].sum += tree[rt].lazy*(len-len/2); tree[rt<<1|1].sum += tree[rt].lazy*(len/2); tree[rt<<1].lazy += tree[rt].lazy; tree[rt<<1|1].lazy += tree[rt].lazy; tree[rt].lazy = 0; }

(2).更新时的实现:

void update(int x, int y, long long val, int rt) { //更新这个区间的值 if(tree[rt].l == x && tree[rt].r == y) { tree[rt].lazy += val; tree[rt].sum += (y-x+1)*val; return ; } if(tree[rt].lazy) pushdown(rt); //向下更新枝叶的值 int mid = (tree[rt].l+tree[rt].r)>>1; if(y <= mid) update(x, y, val, rt<<1); else if(x > mid) update(x, y, val, rt<<1|1); else { update(x, mid, val, rt<<1); update(mid+1, y, val, rt<<1|1); } pushup(rt); }

(3).查询时的实现:

long long query(int x, int y, int rt) { if(tree[rt].l == x && tree[rt].r == y) return tree[rt].sum; if(tree[rt].lazy) pushdown(rt); //向下更新枝叶的值 int mid = (tree[rt].l+tree[rt].r)>>1; if(y <= mid) return query(x, y, rt<<1); else if(x > mid) return query(x, y, rt<<1|1); else return query(x, mid, rt<<1) + query(mid+1, y, rt<<1|1); }

说明:

建树时的成员变量根据题目要求自行增添即可,对应的pushup也是一样,一般情况下的pushup: void pushup(int rt) { tree[rt].sum = tree[rt<<1].sum + tree[rt<<1|1].sum; //tree[rt].Max = max(tree[rt<<1].Max, tree[rt<<1|1].Max); //tree[rt].Min = min(tree[rt<<1].Min, tree[rt<<1|1].Min); } 虽说以上为线段树的常用操作,但是线段树的功能不仅仅是这些。就我做过的题目而言,还有在建树的成员中添加flag标记,表示下次是否还需更新以及更新的优先级等等。要想灵活运用线段树,还需继续深入学习呐。
转载请注明原文地址: https://www.6miu.com/read-63317.html

最新回复(0)