线段树入门讲解

有一段时间没有更新了,前面比较忙,所以知识上会有一些跳跃,后面看看有没有时间去补一下吧,没有就算了

那现在就开始说一下线段树

线段树是一种数据结构,他主要是用于实现快速的区间修改和区间求和这两个功能,同时,有别于树状数组,线段树还有更多的是在于其功能的强大和灵活性上,就比如说,树状数组可以用来维护区间和,进行单点修改,但是若是要做到区间修改就需要去使用差分,但是线段树不用,那么线段树又是怎么形成的呢?可以看下面的代码来讲解

(后面模版代码会使用python和C++,题目的代码会主要使用C++来演示(因为python被卡了语言TLE,太难受了))

那么先看一个线段树的模版题

P3372 【模板】线段树 1 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

这个题是线段树最经典的题目

是维护区间和,那么首先先看看测试用例在使用了线段树之后会变成什么样

(作者手绘的优点抽象)

但是就是这样子,我对于一个区间去做二分,不断地二分,直到左右指针相互到达了同一位置,这时我将其设置为递归出口,标志着递归结束,作为树的叶子节点,标志着结束,之后对于每一个上面的节点,他的值就等于他的左儿子节点和右儿子节点的值的和,这样子,就可以得到线段树了

那么接下来就是代码实现(直接先展示完整代码,剩下的后面再详细讲解)

Python


class SegmentTree:
    def __init__(self, n):
        self.n=n
        self.tag=[0]*(4*(n+1))
        self.val=[0]*(4*(n+1))

    def pushdown(self,id,ls,rs):
        self.tag[id*2]+=self.tag[id]#标记传给左儿子和右儿子
        self.tag[id*2+1]+=self.tag[id]
        self.val[id*2]+=self.tag[id]*ls#更新左儿子和右儿子的值
        self.val[id*2+1]+=self.tag[id]*rs
        self.tag[id]=0#当前节点标记设置为0

    def build(self,id,l,r,arr):
        self.tag[id]=0#建树前初始化标记为0
        if l==r:
            self.val[id]=arr[l]
            return
        mid=(l+r)//2
        self.build(id*2,l,mid,arr)
        self.build(id*2+1,mid+1,r,arr)
        self.val[id]=self.val[id*2]+self.val[id*2+1]

    def update(self,L,R,v,id,l,r):
        #L,R是要修改的区间,v是要加上的值,l,r是有效的边界
        if L>r or R<l:
            return
        if L<=l and r<=R:
            self.tag[id]+=v#将当前区间的tag加上v
            self.val[id]+=v*(r-l+1)#将当前区间的值更新
            return
        mid=(l+r)//2
        self.pushdown(id,mid-l+1,r-mid)#标记下传
        self.update(L,R,v,id*2,l,mid)
        self.update(L,R,v,id*2+1,mid+1,r)
        self.val[id]=self.val[id*2]+self.val[id*2+1]

    def query(self,L,R,id,l,r):
        #L,R是要查询的区间,l,r是有效的边界
        if L>r or R<l:
            return 0
        if L<=l and r<=R:
            return self.val[id]
        mid=(l+r)//2
        self.pushdown(id,mid-l+1,r-mid)#标记下传
        return self.query(L,R,id*2,l,mid)+self.query(L,R,id*2+1,mid+1,r)

    def init(self,arr):
        self.build(1,1,self.n,arr)

    def add(self,l,r,v):
        self.update(l,r,v,1,1,self.n)

    def get(self,l,r):
        return self.query(l,r,1,1,self.n)

import sys
input=sys.stdin.readline
mii=lambda:map(int,input().split())
N,Q=mii()
a=[0]+list(mii())
st=SegmentTree(N)
st.init(a)
for i in range(Q):
    list1=list(mii())
    if list1[0]==1:
        l,r,v=list1[1:]
        st.add(l,r,v)
    elif list1[0]==2:
        l,r=list1[1:]
        print(st.get(l,r))

之后是C++

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define MAXN 100010

ll n, m;
ll a[MAXN];

struct Node{
	ll val, tag;
}tree[MAXN * 4];

void build(ll id, ll l,ll r) {
	tree[id].tag = 0;
	if (l == r) {
		tree[id].val = a[l];
		return;
	}
	ll mid = (l + r) >> 1;
	build(id * 2, l, mid);
	build(id * 2 + 1, mid + 1, r);
	tree[id].val = tree[id * 2].val + tree[id * 2 + 1].val;
}

void pushdown(ll id, ll ls, ll rs) {
	if (tree[id].tag == 0) {
		return;
	}
	tree[id * 2].tag += tree[id].tag;
	tree[id * 2 + 1].tag += tree[id].tag;
	tree[id * 2].val += tree[id].tag * ls;
	tree[id * 2+1].val += tree[id].tag * rs;
	tree[id].tag = 0;
}

void update(ll L, ll R, ll v, ll l, ll r,ll id) {
	//L,R是操作区间,l,r是有效边界
	if (L > r || R < l){
		return;
	}
	if (L <= l && r <= R) {
		tree[id].tag += v;
		tree[id].val += v * (r - l + 1);
		return;
	}
	ll mid = (r + l) >> 1;
	pushdown(id, mid - l + 1, r - mid);
	update(L, R, v, l, mid, id * 2);
	update(L, R, v, mid + 1, r, id * 2 + 1);
	tree[id].val = tree[id * 2].val + tree[id * 2+1].val;

}

ll query(ll L, ll R ,ll l ,ll r ,ll id) {
	if (L > r || R < l) {
		return 0;
	}
	if (L <= l && r <= R) {
		return tree[id].val;
	}
	ll mid = (l + r) >> 1;
	pushdown(id, mid-l + 1, r - mid);
	return query(L, R, l, mid, id * 2) + query(L, R, mid + 1, r, id * 2 + 1);
}


int main() {
	cin >> n >> m;
	for (ll i = 1; i <= n; i++) {
		cin >> a[i];
	}

	build(1, 1, n);
	ll opt, l, r,k;
	for (ll i = 1; i <= m; i++) {
		cin >> opt;
		if (opt == 1) {
			cin >> l >> r >> k;
			update(l, r, k, 1, n, 1);
		}
		else if (opt == 2) {
			cin >> l >> r;
			cout << query(l, r, 1, n, 1) << endl;
		}	
	}
}

这里面我们就先看build这个函数,通常,在处理线段树的题目时,首先,我们通常会使用一个叫做id的标记来记录,这个是一个约定俗成的惯例(其实也是有规律的,可以参考二进制),对于一个节点,对于一个节点的左儿子,他的id为id*2,他的右儿子为id*2+1,那么这个时候看build函数,

build函数接受了3个参数,分别是id,l,r那么在递归后是对区间进行二分,那么便是假设中间的为mid=(l+r)//2(其实使用位运算左移和//2在C++11之后好像区别不是特别大了,不会因为这个把时间给卡了),然后他就被分成了两个区间,分别是[l,mid]和[mid+1,r],这样子就完成了一个线段树的建树

但是在build函数中出现了一个东西,tree[id].tag=0(在C++里面,python也有类似的),这个东西又是做什么的,这个便是设计到了线段树的区间修改和区间查询这两个重要功能的实现,也是线段树的精髓,叫做懒标记(lazytag)。

那么如果我要对一个区间进行修改,比如增加某一个特定的值,那么朴素的做法就是去将区间每一个值都增加一个value,假设区间的长度是m那么他所需的时间是m,这无疑是一个比较慢的做法,但是如果说我将他要增加某一个value的行为使用一个标记不断的将树的每一个节点给标记上,那么接下来,我每一次要查询的时候就随着递归不断地将标记给向下转移,这样子,就可以实现了一个区间上的修改。

那么来讲一下具体的代码实现,可以参考update函数和pushdown函数

    def pushdown(self,id,ls,rs):
        self.tag[id*2]+=self.tag[id]#标记传给左儿子和右儿子
        self.tag[id*2+1]+=self.tag[id]
        self.val[id*2]+=self.tag[id]*ls#更新左儿子和右儿子的值
        self.val[id*2+1]+=self.tag[id]*rs
        self.tag[id]=0#当前节点标记设置为0

    def update(self,L,R,v,id,l,r):
        #L,R是要修改的区间,v是要加上的值,l,r是有效的边界
        if L>r or R<l:
            return
        if L<=l and r<=R:
            self.tag[id]+=v#将当前区间的tag加上v
            self.val[id]+=v*(r-l+1)#将当前区间的值更新
            return
        mid=(l+r)//2
        self.pushdown(id,mid-l+1,r-mid)#标记下传
        self.update(L,R,v,id*2,l,mid)
        self.update(L,R,v,id*2+1,mid+1,r)
        self.val[id]=self.val[id*2]+self.val[id*2+1]

在这里pushdown函数就是用来实现标记下移的功能,先将左右儿子的标记作为下移,之后就对当前节点的左右儿子进行更新,去增加一个value*长度的值,之后,将当前位置的标记设置为0,这样子就清空了当前的节点的标记,

然后就是update函数,就是与pushdown函数进行结合,来使用,那么update也可以分为三种情况,分别是1)递归当前的位置与要修改的位置没有关系   2)递归到的当前位置在当前的修改位置范围内  3)递归到的当前位置在修改区间的部分范围  对于第一种情况,就直接return返回结果,而对于第二种情况,递归到当前修改位置的时候,这时就对区间的tag和value进行修改,而若是第三种情况,这进行标记下传并且进行向下递归

而下一步就是区间的查询操作,请看query函数

    def query(self,L,R,id,l,r):
        #L,R是要查询的区间,l,r是有效的边界
        if L>r or R<l:
            return 0
        if L<=l and r<=R:
            return self.val[id]
        mid=(l+r)//2
        self.pushdown(id,mid-l+1,r-mid)#标记下传
        return self.query(L,R,id*2,l,mid)+self.query(L,R,id*2+1,mid+1,r)

同样的,也是分为三种情况来进行讨论,原理一样,就不一一赘述了,只是这里面选择直接递归,和一般题解会去讨论mid是靠近左还是右的不一样,不过并没有什么关系就是了。

然后就是在最后就可以返回区间查询的值了,这道题就可以过了

(一开始还有点抵触线段树,然后用树状数组,结果MLE了)

然后再看后面的一些关于线段树的变式,这里的代码统一使用C++来演示

                                                                一

P4145 上帝造题的七分钟 2 / 花神游历各国 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

这个题其实和之前的线段树模版差不多,但是这里是涉及了开根号的处理方法,那么这里怎么处理呢?首先先对比一下前面的,我们发现使用懒标记去进行区间修改时,问题是比较明显的,他必须满足一个f(a+b)=f(a)+f(b)的关系式(f表示对应的操作,这里面表示满足结合律),那么这个开根号的情况肯定不满足的,因为^{\sqrt{a}}+^{\sqrt{b}}\neq \sqrt{a+b},所以这里没有办法使用懒标记,所以怎么办呢?

那就不用呗,直接修改也不是不行。

#include<bits/stdc++.h>
#define ll long long
#define MAXN (int)1e5+10
using namespace std;

struct {
	ll val,maxnum;
}tree[4*MAXN];

int m, n;
ll a[MAXN];
int k;
ll l, r;

void build(ll L, ll R, ll id) {
	if (L == R) {
		tree[id].val = a[L];
		return;
	}
	ll mid = (L + R) >> 1;
	build(L, mid, id * 2);
	build(mid + 1, R, id * 2 + 1);
	tree[id].val = tree[id * 2].val + tree[id * 2 + 1].val;
	tree[id].maxnum = max(tree[id * 2].maxnum, tree[id * 2 + 1].maxnum);
}


void update(ll L, ll R, ll id, ll l, ll r) {
	// l,r 是要更新的区间, L,R是递归到当前位置的区间;

	if (R < l || r < L ) {
		return;
	}

	if (L == R && l<=L && r>=R) {
		
		tree[id].val = floor(sqrt(tree[id].val));
		tree[id].maxnum = floor(sqrt(tree[id].val));
		return;
	}
	ll mid = (L + R) >> 1;
	if (tree[id * 2].val > 1) {
		update(L, mid, id * 2, l, r);
	}
	if (tree[id * 2 + 1].val > 1) {
		update(mid + 1, R, id * 2 + 1, l, r);
	}
	tree[id].val = tree[id * 2 + 1].val + tree[id * 2].val;
	tree[id].maxnum = max(tree[id * 2].maxnum, tree[id * 2 + 1].maxnum);
}

ll query(ll L, ll R, ll id, ll l, ll r) {
	if (R < l || r < L) {
		return 0;
	}
	if (l<=L && r>=R) {
		return tree[id].val;
	}
	ll mid = (L + R) >> 1;
	return query(L, mid, id * 2, l, r) + query(mid + 1, R, id * 2 + 1,l, r);
}

int main() {
	cin >> n;
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
	}
	build(1, n, 1);

	
	cin >> m ;
	for (int i = 1; i <= m; i++) {

		cin >> k >> l >> r;
		if (k == 0) {
			update(1, n, 1, l, r);
		}
		else if (k == 1) {
			cout << query(1, n, 1, l, r)<<endl;


		}
	}
}

就这样信心满满的交了一发,但是作为一个蒟蒻,我也不出所料的得到了这个结果

于是就开始优化,我于是就开始想想怎么优化,我发现如果进行开根号的操作,最后都会接近于1,所以若是我递归到当前位置之后发现当前为1之后,就不用往下继续处理了,于是就修改了一下,代码,,就AC了,下面是源代码


#include<bits/stdc++.h>
#define ll long long
#define MAXN (int)1e5+10
using namespace std;

struct {
	ll val,maxnum;
}tree[4*MAXN];

int m, n;
ll a[MAXN];
int k;
ll l, r;

ll int read() {
	ll x = 0;
	char c = getchar();
	while (!isdigit(c)) {
		c = getchar();
	}
	while (isdigit(c)) {
		x = (x << 3) + (x << 1) + c - '0';
		c = getchar();
	}
	return x;
}

void build(ll L, ll R, ll id) {
	if (L == R) {
		tree[id].val = a[L];
		tree[id].maxnum = a[L];
		return;
	}
	ll mid = (L + R) >> 1;
	build(L, mid, id * 2);
	build(mid + 1, R, id * 2 + 1);
	tree[id].val = tree[id * 2].val + tree[id * 2 + 1].val;
	tree[id].maxnum = max(tree[id * 2].maxnum, tree[id * 2 + 1].maxnum);
}


void update(ll L, ll R, ll id, ll l, ll r) {
	// l,r 是要更新的区间, L,R是递归到当前位置的区间;

	if (R < l || r < L ) {
		return;
	}
	if (L == R && l<=L && r>=R) {
		
		tree[id].val = floor(sqrt(tree[id].val));
		
		tree[id].maxnum = floor(sqrt(tree[id].maxnum));
		
		return;
	}
	ll mid = (L + R) >> 1;
	if (tree[id * 2].maxnum > 1) {
		update(L, mid, id * 2, l, r);
	}
	if (tree[id].maxnum > 1) {
		update(mid + 1, R, id * 2 + 1, l, r);
	}
	tree[id].val = tree[id * 2 + 1].val + tree[id * 2].val;
	tree[id].maxnum = max(tree[id * 2].maxnum, tree[id * 2 + 1].maxnum);
}

ll query(ll L, ll R, ll id, ll l, ll r) {
	if (R < l || r < L) {
		return 0;
	}
	if (l<=L && r>=R) {
		return tree[id].val;
	}
	ll mid = (L + R) >> 1;
	return query(L, mid, id * 2, l, r) + query(mid + 1, R, id * 2 + 1,l, r);
}

int main() {
	cin >> n;
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
	}
	build(1, n, 1);
	
	
	cin >> m ;
	for (int i = 1; i <= m; i++) {

		cin >> k >> l >> r;
		if (l > r)swap(l, r);
		if (k == 0) {
			update(1, n, 1, l, r);
		}
		else if (k == 1) {
			cout << query(1, n, 1, l, r)<<endl;

		}
	}
}

                                                                 二

P1471 方差 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

这个题的思路比较巧妙

他是一个对懒标记的应用,除此之外,还有就是线段树的结构在建树的时候也发生了改变,这里在建树的时候,会存在3个不同的value,分别是tag,value,还有square,这三个,所以我在建立线段树的时候会需要分别处理三个值

那么这时,他的build函数就会与之不同,像这样子

void pushup(ll id) {
	tree[id].square = tree[id * 2].square + tree[id * 2 + 1].square;
	tree[id].val = tree[id * 2].val + tree[id * 2 + 1].val;
}


void build(ll id, ll l, ll r) {
	tree[id].tag = 0;
	if (l == r) {
		tree[id].val = a[l];
		tree[id].square = pow(a[l], 2);
		return;
	}

	ll mid = (l + r) >> 1;
	build(id * 2, l, mid);
	build(id * 2 + 1, mid + 1, r);
	pushup(id);
}

其实和普通的线段树并没有太大的区别,之后就是他的pushdown函数,以及update函数,query函数

先看pushdown函数,pushdown函数他在这里的处理如下

void pushdown(ll id, ll ls, ll rs) {
	tree[id * 2].tag += tree[id].tag;
	tree[id * 2 + 1].tag += tree[id].tag;
	tree[id * 2].square += (2 * tree[id * 2].val * tree[id].tag + ls * pow(tree[id].tag, 2));
	tree[id * 2 + 1].square += (2 * tree[id * 2 + 1].val * tree[id].tag + rs * pow(tree[id].tag, 2));
	tree[id * 2].val += tree[id].tag * ls;
	tree[id * 2 + 1].val += tree[id].tag * rs;
	tree[id].tag = 0;
}

这里面除了更新tag之外,还要对square和value进行更新,在这里进行更新的时候要注意一定是先更新square在更新val,因为square的更新根据完全平方公式是需要使用val来进行维护的,所以说在这里一定要处理square之后再去处理val,

然后就是update函数

void update(ll L, ll R, ll id, ll l, ll r,ll v) {
	//L,R表示要查询的区间,l,r表示递归到的位置
	if (r<L || l>R) {
		return;
	}
	if (L <= l && r <= R) {
		tree[id].tag += v;
		tree[id].square += 2 * v * tree[id].val + pow(v, 2) * (r - l + 1);
		tree[id].val += v * (r - l + 1);
		return;
	}
	ll mid = (l + r) >> 1;
	pushdown(id, mid - l + 1, r - mid);
	update(L, R, id * 2, l, mid, v);
	update(L, R, id * 2 + 1, mid + 1, r, v);
	pushup(id);
}

这里也是一样,先处理square在处理val,别的没有太大的区别

最后就是query函数

这里会出现两个query函数,一个是用来处理query正常的val的,另一个是用来处理square的,这里和普通的query没有太多的区别

double query_val(ll L, ll R, ll id, ll l, ll r) {
	if (r<L || l>R) {
		return 0;
	}
	if (L <= l && r <= R) {
		return tree[id].val;
	}
	ll mid = (l + r) >> 1;
	pushdown(id, mid - l + 1, r - mid);
	return query_val(L, R, id * 2, l, mid) + query_val(L, R, id * 2 + 1, mid + 1, r);
}

double query_var(ll L, ll R, ll id, ll l, ll r) {
	if (r<L || l>R) {
		return 0;
	}
	if (L <= l && r <= R) {
		return tree[id].square;
	}
	ll mid = (l + r) >> 1;
	pushdown(id, mid - l + 1, r - mid);
	return query_var(L, R, id * 2, l, mid) + query_var(L, R, id * 2 + 1, mid + 1, r);

}

最后就是如何去求均值和方差,但是方差的公式需要变形,推理如下,可知

所以我们就可以通过这种方式去求出方差了

完整代码如下

#include<bits/stdc++.h>
#define ll long long
#define MAXN (int)1e6+10

using namespace std;
long double a[MAXN];
ll n, m;
ll opt, x, y;
long double k;
struct {
	long double tag, val, square;
}tree[MAXN*4];

void pushdown(ll id, ll ls, ll rs) {
	tree[id * 2].tag += tree[id].tag;
	tree[id * 2 + 1].tag += tree[id].tag;
	tree[id * 2].square += (2 * tree[id * 2].val * tree[id].tag + ls * pow(tree[id].tag, 2));
	tree[id * 2 + 1].square += (2 * tree[id * 2 + 1].val * tree[id].tag + rs * pow(tree[id].tag, 2));
	tree[id * 2].val += tree[id].tag * ls;
	tree[id * 2 + 1].val += tree[id].tag * rs;
	tree[id].tag = 0;
}

void pushup(ll id) {
	tree[id].square = tree[id * 2].square + tree[id * 2 + 1].square;
	tree[id].val = tree[id * 2].val + tree[id * 2 + 1].val;
}

void build(ll id, ll l, ll r) {
	tree[id].tag = 0;
	if (l == r) {
		tree[id].val = a[l];
		tree[id].square = pow(a[l], 2);
		return;
	}

	ll mid = (l + r) >> 1;
	build(id * 2, l, mid);
	build(id * 2 + 1, mid + 1, r);
	pushup(id);
}

void update(ll L, ll R, ll id, ll l, ll r,long double v) {
	//L,R表示要查询的区间,l,r表示递归到的位置
	if (r<L || l>R) {
		return;
	}
	if (L <= l && r <= R) {
		tree[id].tag += v;
		tree[id].square += 2 * v * tree[id].val + pow(v, 2) * (r - l + 1);
		tree[id].val += v * (r - l + 1);
		return;
	}
	ll mid = (l + r) >> 1;
	pushdown(id, mid - l + 1, r - mid);
	update(L, R, id * 2, l, mid, v);
	update(L, R, id * 2 + 1, mid + 1, r, v);
	pushup(id);
}

double query_val(ll L, ll R, ll id, ll l, ll r) {
	if (r<L || l>R) {
		return 0;
	}
	if (L <= l && r <= R) {
		return tree[id].val;
	}
	ll mid = (l + r) >> 1;
	pushdown(id, mid - l + 1, r - mid);
	return query_val(L, R, id * 2, l, mid) + query_val(L, R, id * 2 + 1, mid + 1, r);
}

double query_var(ll L, ll R, ll id, ll l, ll r) {
	if (r<L || l>R) {
		return 0;
	}
	if (L <= l && r <= R) {
		return tree[id].square;
	}
	ll mid = (l + r) >> 1;
	pushdown(id, mid - l + 1, r - mid);
	return query_var(L, R, id * 2, l, mid) + query_var(L, R, id * 2 + 1, mid + 1, r);

}

int main() {
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
	}
	build(1, 1, n);
	
	for (int i = 1; i <= m; i++) {
		cin >> opt;
		if (opt == 1) {
			cin >> x >> y >> k;
			update(x, y, 1, 1, n, k);
			
		}
		else if (opt == 2) {
			cin >> x >> y;
			double ans = query_val(x, y, 1, 1, n) / (y - x + 1);
			printf("%.4lf\n", ans);
		}
		else if (opt == 3) {
			cin >> x >> y;
			double ans1 = pow(query_val(x, y, 1, 1, n) / (y - x + 1), 2);
			double ans = -ans1 + query_var(x, y, 1, 1, n) / (y - x + 1);
			printf("%.4lf\n", ans);

		}
	}
	
}

ok,那就到这里就结束了

  • 30
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值