关于那些区间维护的数据结构

Posted by n0000000000o's Blog on August 16, 2018

题目  

P3372 【模板】线段树 Ⅰ
P3373 【模板】线段树 Ⅱ
P3374 【模板】树状数组 Ⅰ
P3368 【模板】树状数组 Ⅱ

最近学习了一个十分基础之前觉得很高大上的数据结构——没错,就是线段树。首先,在这篇博客(登上了洛谷日报)浅谈线段树(Segment Tree) 以及 $Sparky$大佬 的帮助下,我终于摸到了线段树的门,,,在此总结一下线段树的学习。

线段树 

基本概念

线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。对于线段树中的每一个非叶子节点[a,b],它的左儿子表示的区间为 $[a,\frac{a+b}{2}]$ ,右儿子表示的区间为 $ [\frac{a+b}{2} + 1, b] $。因此线段树是平衡二叉树,最后的子节点数目为N,即整个线段区间的长度。 使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为 $ O ( \log N)$ 。而未优化的空间复杂度为 $ 2N $,因此有时需要离散化让空间压缩。(来源百度)

当然,这都不重要,简言之,线段树是一种处理区间修改与查询的数据结构,他能解决形如

已知一个数列,你需要进行下面两种操作: 1.将某区间每一个数加上x 2.求出某区间每一个数的和

之类的问题

代码分析

开始胡乱分析,
在此以上述问题为例,分析一下线段树的基本代码组成

1.主函数

不多说直接上代码

int main()
{
	int n, m;
	cin >> n >> m;
	for (int i = 1; i <= n; i++)
		scanf ("%d", &a[i]);
		
	build (1, n, 1);//建树
	
	for (int i = 1; i <= m; i++)
	{
		int t;
		scanf ("%d", &t);
		if (t == 1)
		{
			int x, y, v;
			scanf ("%d %d %d", &x, &y, &v);
			modify (1, n, 1, x, y, v);//修改 
		}
		else
		{
			int x, y;
			scanf ("%d %d", &x, &y);
			printf("%lld\n", query (1, n, 1, x, y) );//查询
		}
	}
	return 0;
}
2.建树

通过递归实现自下而上的建树,返回条件是只有一个点的时候,该点的 $tree[ ]$ 就是该点的价值

void build (int l, int r, int rt)
{
	if (l == r)//返回条件:只有一个点时
	{
		tree[rt] = a[l];// a[] 就是读入的点的权值
		return;
	}
	int m = (l+r) >> 1;
	build (l, m, rt << 1);//左子树
	build (m+1, r, rt << 1 | 1);//右子树
	update (rt);//递归返回时处理,将叶结点上传 
}

$ update $ 函数的编写如下 (据说 $inline$ 能加速,我也不知道是不是真的 )

inline void update (int rt)
{
	tree[rt] = tree[rt << 1] + tree[rt << 1 | 1];//两个儿子的 tree 之和
}

另外,由于

	l, m, rt << 1       //左子树
	m+1, r, rt << 1 | 1 //右子树

之类的用法非常大量,在之后的程序以及下面的文章里,直接采用宏定义的手段简化代码

#define lson l, m, rt << 1         //左子树
#define rson m+1, r, rt << 1 | 1   //右子树
3.修改

将一个区间内所有的数增加一个值
修改一个大区间,如果修改的区间中包含了小区间,将小区间的 $tag[ ]$ 值增加 $v$ ,不然,将区间下放,直到访问到叶节点为止。(注意返回时还是要自下而上更新)

void modify (int l, int r, int rt, int b, int e, int v)
{
	if (l == r)//访问到最下层时返回
	{
		tree[rt] += v;
		return;
	}
	else 
	if (b <= l && r <= e)//当前区间 [l, r] 包含在目标修改区间 [b, e] 内
	{
		mark (l, r, rt, v);//修改区间 [l, r]
		return;
	}
	demark (l, r, rt);//标记下放
	
	int m = (l+r) >> 1;
	if (b <= m)//目标区间包含在当前区间左子树内
		modify (lson, b, e, v);//不要忘了之前的宏定义~~
	if (e > m)//目标区间包含在当前区间右子树内
		modify (rson, b, e, v);
	update (rt);//递推返回时上传
	return;
}

$ mark $ 与 $ demark $ 函数 如果为叶结点,直接赋值(不修改 $ tag [ ] $ ),不然直接修改 $ tag[ ] $ 以及 $ tree[ ] $,注意 $ tree[ ] $ (表示当前子树的值(某区间和)) 的修改要增加整个区间的值

//编号为 rt 的 [l, r] 的区间,整个增加 v
inline void mark (int l ,int r, int rt, long long v)
{
	if (l == r)//只为叶结点
		tree[rt] += v;
	else
	{
		tag[rt] += v;
		tree[rt] += (r - l + 1) * v; //注意增加整个区间的和
	}
} 

下放标记时,要注意将该节点的标记清零(若为乘法标记,要将值改为 $1$)

inline void demark (int l, int r, int rt)
{
	int m = (l+r) >> 1;
	mark (lson, tag[rt]);
	mark (rson, tag[rt]);
	tag[rt] = 0;//还原标记
}
4.询问

询问操作也通过递归实现,最后的返回也要将标记上传

long long query (int l, int r, int rt, int b, int e)//求和 
{
	//如果 所在区间[l, r] 被包含在 目标区间[b, e]内,直接返回
	if (b <= l && r <= e)
		return tree [rt];
	demark (l, r, rt);//标记下放
	int m = (l+r) >> 1;
	long long ans = 0;
	if (b <= m)
		ans += query (lson, b, e);
	if (e > m)
		ans += query (rson, b, e);
	update (rt);//返回时标记上传 
	return ans;
}

完整代码

//P3372 【模板】线段树 1
#include <cstdio>
#include <iostream>
using namespace std;
const int maxn = 100000;
long long a[maxn + 9], tree[maxn*4 + 9], tag[maxn*4 + 9];
#define lson l, m, rt << 1
#define rson m+1, r, rt << 1 | 1
void build (int l, int r, int rt);
void modify (int l, int r, int rt, int b, int e, int v);
long long query (int l, int r, int rt, int b, int e);//求和 
int main()
{
	int n, m;
	cin >> n >> m;
	for (int i = 1; i <= n; i++)
		scanf ("%d", &a[i]);
	build (1, n, 1);
	for (int i = 1; i <= m; i++)
	{
		int t;
		scanf ("%d", &t);
		if (t == 1)
		{
			int x, y, v;
			scanf ("%d %d %d", &x, &y, &v);
			modify (1, n, 1, x, y, v);//modify -- 修改 
		}
		else
		{
			int x, y;
			scanf ("%d %d", &x, &y);
			printf("%lld\n", query (1, n, 1, x, y) );
		}
	}
	return 0;
}
inline void update (int rt)
{
	tree[rt] = tree[rt << 1] + tree[rt << 1 | 1];
}
void build (int l, int r, int rt)
{
	if (l == r)
	{
		tree[rt] = a[l];
		return;
	}
	int m = (l+r) >> 1;
	build (lson);
	build (rson);
	update (rt);//递归返回时处理 
}
inline void mark (int l ,int r, int rt, long long v)
{
	if (l == r)
		tree[rt] += v;
	else
	{
		tag[rt] += v;
		tree[rt] += (r - l + 1) * v;//注意增加整个区间的和
	}
} 
inline void demark (int l, int r, int rt)
{
	int m = (l+r) >> 1;
	mark (lson, tag[rt]);
	mark (rson, tag[rt]);
	tag[rt] = 0;
}
long long query (int l, int r, int rt, int b, int e)//求和 
{
	//如果 所在区间[l, r] 被包含在 目标区间[b, e]内,直接返回
	if (b <= l && r <= e)
		return tree [rt];
	demark (l, r, rt);//标记下放
	int m = (l+r) >> 1;
	long long ans = 0;
	if (b <= m)
		ans += query (lson, b, e);
	if (e > m)
		ans += query (rson, b, e);
	update (rt);//返回时标记上传 
	return ans;
}
//编号为 rt 的 [l, r] 的区间,整个增加 v
void modify (int l, int r, int rt, int b, int e, int v)
{
	if (l == r)
	{
		tree[rt] += v;
		return;
	}
	else 
	if (b <= l && r <= e)
	{
		mark (l, r, rt, v);
		return;
	}
	demark (l, r, rt);//标记下放
	int m = (l+r) >> 1;
	if (b <= m)
		modify (lson, b, e, v);
	if (e > m)
		modify (rson, b, e, v);
	update (rt);
	return;
}

其他题目

P3373 【模板】线段树 Ⅱ

已知一个数列,你需要进行下面三种操作: 1.将某区间每一个数乘上x 2.将某区间每一个数加上x 3.求出某区间每一个数的和

注意本题要用双标记,标记更新时有一个原则:

乘法标记更新,加法标记和乘法标记都要乘上更新值 加法标记更新,加法标记加上更新值

相当于线段树要打两遍

//P3373 【模板】线段树 2
#include <bits/stdc++.h>
#define lson l, m, rt << 1
#define rson m+1, r, rt << 1 | 1
using namespace std;
const int maxn = 100000;
long long a[maxn + 9], tree[maxn*4 + 9], tag_add[maxn*4 + 9], tag_mul[maxn*4 + 9];
//l r 正在一步步缩小的区间  , b e 不变的需要查找的区间 
void build (int l, int r, int rt);
void modify_mul (int l, int r, int rt, int b, int e, long long k);
void modify_add (int l, int r, int rt, int b, int e, long long k);
long long query (int l, int r, int rt, int b, int e);//查询区间 [b, e] 
long long n, m, p;
int main()
{
	cin >> n>> p;
	for (int i = 1; i <= n; i++)
		scanf ("%lld", &a[i]);
		
	build (1, n, 1);
	cin >> m;
	for (int i = 1; i <= m; i++)
	{
		int t;
		scanf ("%d", &t);
		if (t == 1)
		{
			int x, y;
			long long k;
			scanf ("%d %d %lld", &x, &y, &k);
			modify_mul (1, n, 1, x, y, k);
		}
		else if (t == 2)
		{
			int x, y;
			long long k;
			scanf ("%d %d %lld", &x, &y, &k);
			modify_add (1, n, 1, x, y, k);
		}
		else if (t == 3)
		{
			int x, y;
			scanf ("%d %d", &x, &y);
			printf ("%lld\n", query(1, n, 1, x, y) % p );
		}
	}
}
void update (int rt)//上传操作 
{
	int ls = rt << 1;
	int rs = rt << 1 | 1;
//	int m = (l+r) >> 1;
	
	//乘法优先 
/*	tree[rt] = tree[ls] * mul[ls] + add[ls] * (m - l + 1)
			 + tree[rs] * mul[rs] + add[rs] * (r - m);*/
			 
	tree[rt] = (tree[ls] + tree[rs]) % p;
}
void build (int l, int r, int rt)
{
	tag_mul[rt] = 1;//乘法标记要更新值 1 
	if (l == r)
	{
		tree[rt] = a[l];
		return;
	}
	int m = (l+r) >> 1;
	build (lson);
	build (rson);
	update (rt);
}
void mark_mul (int l, int r, int rt, long long v)
{
	if (l == r)
		tree[rt] = (tree[rt] * v) % p;
	else
	{
		tag_mul[rt] = (tag_mul[rt] * v) % p;//两种标记以及本身的值都要乘 
		tag_add[rt] = (tag_add[rt] * v) % p;
		tree[rt] = (tree[rt] * v) % p; 
	}
}
void mark_add (int l, int r, int rt, long long v)
{
	if (l == r)
		tree[rt] = (tree[rt] + v) % p;
	else
	{
		tag_add[rt] = (tag_add[rt] + v) % p;//加法标记以及本身增加 
		tree[rt] = (tree[rt] + (r - l + 1) * v % p) % p;
	}
}
void demark(int l, int r, int rt)//将标记下放 
{
	int m = (l+r) >> 1;
	mark_mul (lson, tag_mul[rt]);
	mark_mul (rson, tag_mul[rt]);
	mark_add (lson, tag_add[rt]);
	mark_add (rson, tag_add[rt]);
	tag_mul[rt] = 1;//乘法标记要赋值1 
	tag_add[rt] = 0;
}
void modify_mul (int l, int r, int rt, int b, int e, long long v)
{
	if (l == r)
	{
		tree[rt] = (tree[rt] * v) % p;
		return;
	}
	else if (b <= l && r <= e)
	{
		mark_mul (l, r, rt, v); 
		return;
	}
	demark(l, r, rt);
	int m = (l+r) >> 1;
	if (b <= m)
		modify_mul (lson, b, e, v);
	if (e > m)
		modify_mul (rson, b, e, v);
	update (rt);
}
void modify_add (int l, int r, int rt, int b, int e, long long v)
{
	if (l == r)
	{
		tree[rt] = (tree[rt] + v) % p;
		return;
	}
	else if (b <= l && r <= e)
	{
		mark_add (l, r, rt, v);
		return;
	}
	demark (l, r, rt);//再次下放
	int m = (l+r) >> 1;
	if (b <= m)
		modify_add (lson, b, e, v); 
	if (e > m)
		modify_add (rson, b, e, v);
	update (rt);
}
long long query (int l, int r, int rt, int b, int e)//查询区间 b e 
{
	if (b <= l && r <= e)
		return tree[rt];
//	printf("-----%d %d %d %d\n", l, r, rt, tree[rt]);
	demark(l, r, rt);
	int m = (l+r) >> 1;
	long long ans = 0;
	if (b <= m)
		ans = (ans + query (lson, b, e)) % p;
	if (e > m)
		ans = (ans + query (rson, b, e)) % p;
	update (rt);
	return ans;
}
P3374 【模板】树状数组 Ⅰ

如题,已知一个数列,你需要进行下面两种操作: 1.将某一个数加上x 2.求出某区间每一个数的和

本题完全可以用线段树的解法来写,注意赋值操作不用再考虑区间,直接修改最下的叶结点。

#include <iostream>
#include <cstdio>
#define lson l, m, rt << 1
#define rson m+1, r, rt << 1 | 1 
using namespace std;
long long a[500000 + 9], tree[500000 * 4 + 9], tag[500000 * 4 + 9];
void build (int l, int r, int rt);
void modify_add (int l, int r, int rt, int p, int v);
long long query (int l, int r, int rt, int b, int e);
int main()
{
	int n, m;
	cin >> n >> m;
	for (int i = 1; i <= n; i++)
		scanf ("%lld", &a[i]);
	build (1, n, 1); 
	for (int i = 1; i <= m; i++)
	{
		int t, x, y;
		scanf ("%d %d %d", &t, &x, &y);
		if (t == 1)
			modify_add (1, n, 1, x, y);
		if (t == 2)
			printf ("%lld\n", query(1, n, 1, x, y) );
	}
}
void mark (int l, int r, int rt, long long v)
{
	if (l == r)
	{
		tree[rt] += v;
	}
	else
	{
		tag[rt] += v;
		tree[rt] += (r - l + 1) * v;
	}
}
void demark (int l, int r, int rt)
{
	int m = (l+r) >> 1;
	mark (lson, tag[rt]);
	mark (rson, tag[rt]);
	tag[rt] = 0;
}
void update (int rt)
{
	tree[rt] = tree[rt << 1] + tree[rt << 1 | 1];
}
void build (int l, int r, int rt)
{
	if (l == r)
	{
		tree[rt] = a[l];
		return;
	}
	int m = (l+r) >> 1;
	build (lson);
	build (rson);
	update (rt);
}
void modify_add (int l, int r, int rt, int p, int v)
{	 
	 if (l == r)//改为单点修改 ~~ 
	 {
	 	tree[rt] += v;
	 	return;
	 }
	 int m = (l+r) >> 1;
	 if (p <= m)
	 	modify_add (lson, p, v);
	 if (p >  m)
	 	modify_add (rson, p, v);
	update (rt);
}
long long query (int l, int r, int rt, int b, int e)
{
	if (b <= l && r <= e)
		return tree[rt];
	demark (l, r, rt);//下放 
	int m = (l+r) >> 1;
	long long ans = 0;
	if (b <= m)
		ans += query (lson, b, e);
	if (e >  m)
		ans += query (rson, b, e);
	update (rt); 
	return ans;
}
P3368 【模板】树状数组 Ⅱ

如题,已知一个数列,你需要进行下面两种操作: 1.将某区间每一个数数加上x 2.求出某一个数的和

和线段树相比,也只是修改一下查询函数

#include <iostream>
#include <cstdio>
#define lson l, m, rt << 1
#define rson m+1, r, rt << 1 | 1 
using namespace std;
long long a[500000 + 9], tree[500000 * 4 + 9], tag[500000 * 4 + 9];
void build (int l, int r, int rt);
void modify_add (int l, int r, int rt, int b, int e, int v);
long long query (int l, int r, int rt, int x);
int main()
{
	int n, m;
	cin >> n >> m;
	for (int i = 1; i <= n; i++)
		scanf ("%lld", &a[i]);
	build (1, n, 1); 
	for (int i = 1; i <= m; i++)
	{
		int t, x, y, k;
		scanf ("%d", &t);
		if (t == 1)
		{
			scanf ("%d %d %d", &x, &y, &k);
			modify_add (1, n, 1, x, y, k);
		}
		if (t == 2)
		{
			scanf ("%d", &x);
			printf ("%lld\n", query(1, n, 1, x) );
		}	
	}
}
void mark (int l, int r, int rt, long long v)
{
	if (l == r)
	{
		tree[rt] += v;
	}
	else
	{
		tag[rt] += v;
		tree[rt] += (r - l + 1) * v;
	}
}
void demark (int l, int r, int rt)
{
	int m = (l+r) >> 1;
	mark (lson, tag[rt]);
	mark (rson, tag[rt]);
	tag[rt] = 0;
}
void update (int rt)
{
	tree[rt] = tree[rt << 1] + tree[rt << 1 | 1];
}
void build (int l, int r, int rt)
{
	if (l == r)
	{
		tree[rt] = a[l];
		return;
	}
	int m = (l+r) >> 1;
	build (lson);
	build (rson);
	update (rt);
}
void modify_add (int l, int r, int rt, int b, int e, int v)
{	 
	 if (l == r)
	 {
	 	tree[rt] += v;
	 	return;
	 }
	 if (b <= l && r <= e)
	 {
		 mark (l, r, rt, v);
		 return ; 
	 }
	 int m = (l+r) >> 1;
	 if (b <= m)
	 	modify_add (lson, b, e, v);
	 if (e >  m)
	 	modify_add (rson, b, e, v);
	update (rt);
}
long long query (int l, int r, int rt, int x)
{
	if (l == r)
		return tree[rt];
	demark (l, r, rt);//下放 
	int m = (l+r) >> 1;
	long long ans = 0;
	if (x <= m)
		ans += query (lson, x);
	if (x >  m)
		ans += query (rson, x);
	update (rt); 
	return ans;
}

完结撒花~ ~ ~