GDUT ACM2022寒假集训 专题五 C E(线段树)

一、线段树

线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为O(logN)。而未优化的空间复杂度为2N,实际应用时一般还要开4N的数组以免越界,因此有时需要离散化让空间压缩。——百度百科

线段树主要用于维护区间信息,并且支持加、乘、取模等多种操作,几乎是算法竞赛中最常用的数据结构,本文以区间加为例

对于一个数组a【1,5,4,2,3】,我们对他建立线段树并进行区间加操作

1、建树

对于这颗二叉树的每一个节点,我们定义一个结构体 tt ,其中的整数l,r用于存储每个点所统辖的区间的左右边界,pre为该点统辖的区间值的总和,la为一个特殊的懒标记,在区间修改和区间查询的时候会用到

struct tt
{
	int l, r;		   // l,r为这个点统率的区间长度
	long long la, pre; // la代表这个点的lazy值,pre代表这个点的存储的值
} t[400005];

对于建树函数build(),我们每次需要传入p,l,r三个参数,p为树的节点编号,l,r为p点所统辖的区间

我们先来看看建好的树长什么样子
btwx3j.png

其中,圈内数字表示节点编号,蓝色数字表示该点统辖的区间,红色数字表示叶子节点对应的原数组值

在建树时,我们从p=1开始建树,此时左右边界即为整个原数组的左右边界

建立左右儿子节点时,对应的编号 p 左 p_{左} p p ∗ 2 p*2 p2 , p 右 p_{右} p p ∗ 2 + 1 p*2+1 p2+1

对应的统率左右边界也应该从中平分,其中 p 左 r = ( p r + 1 ) / 2 p_ {左r}=(p_ {r}+1)/2 pr=(pr+1)/2,其他的由图易知

当某一点l=r的时候,说明这是一个叶子节点,无法再被分,将其按顺序赋值

因为采用递归建树,总是在完成左节点才返回建右节点,故赋值顺序不会改变

在每一个叶子节点的兄弟节点都被建立完成时,该叶子节点的父亲的值会修改为两个儿子的值之和,也就是建树完成之后,每个节点的值都是自己统率范围的和,例如编号为2的节点值为1+5+4=10

void build(int p, int l, int r) //建树,点p的统率长度为l-r
{
	t[p].l = l, t[p].r = r; //存储该点统率长度左端点和右端点
	if (l == r)				//叶子结点,直接赋值
	{
		t[p].pre = a[r];
		return;
	}
	int mid = (l + r) >> 1;						  //取中值
	build(p << 1, l, mid);						  //递归建左树
	build(p << 1 | 1, mid + 1, r);				  //递归建右树  p << 1 | 1  p乘2+1
	t[p].pre = t[p << 1].pre + t[p << 1 | 1].pre; //当前点的值就等于左子树的值+右子树的值
}

2、区间修改

例如对于区间2-3,进行加3操作

图示过程如下

bt48g0.png

再看一组例子,我们在上图的基础上再对区间3-5加3,如下图所示

bt5ZGR.png

我们发现,在右节点时,4-5完全覆盖了4-5,说明此时要修改的区间就是节点3统率的全部区间,所以直接将节点3的值加上 区间长度x修改值

同时给节点3标记一个la值,说明该区间的所有节点都要被修改

这样子的操作不会影响计算,因为节点3的值已经变为修改后的值

当需要使用节点6和7的值时,我们则进行下传lazy值操作来修改子节点值

注意:每次进行修改时,若某点的区间无法被完全覆盖,还需要执行一次下传lazy值操作,无法被完全覆盖说明还需要使用子节点值,故同上理需要下传lazy值操作

void lazy(int p) //下传懒标记一次,某个节点打上懒标记说明它统御的所有点都要更改
{
	if (t[p].la >= 1) //点p存在懒标记
	{
		t[p << 1].pre += t[p].la * (len(p << 1)); //点p左右节点的值加上懒标记值*左右节点的长度
		t[p << 1 | 1].pre += t[p].la * (len(p << 1 | 1));

		t[p << 1].la += t[p].la;
		t[p << 1 | 1].la += t[p].la; //下传懒标记

		t[p].la = 0; //清除当前懒标记
	}
}

void change(int p, int x, int y, int z) // p点,xy为区间,z为值  x-y区间更改z值
{//原理为重复递归直到xy超过点的统御区间,标记lazy
	if (t[p].l >= x && t[p].r <= y) //如果xy完全覆盖该点统率的范围的左右区间,即所有的点都需更改。同时也是递归出口
	{
		t[p].pre += (long long)z * (len(p)); //将该点的值修改为更改值乘该点区间长度,也就是统率的所有点都要加上该值
		t[p].la += z;						 //该点打上lazy值,表示统率的区间都要加上z,之后不再对子节点进行更改值,节省时间
		return;
	}
	lazy(p);//如果不是所有点都要更改则需要下传懒标记,若不存在懒标记,此步骤无效
	int mid = (t[p].l + t[p].r) >> 1; //求出该点区间中值
	//判断修改区间位置
	if (x <= mid)
		change(p << 1, x, y, z);
	if (y > mid)
		change(p << 1 | 1, x, y, z);
	t[p].pre = t[p << 1].pre + t[p << 1 | 1].pre;//找到了统御区间全部都要被修改的点,则将该点的父节点p值修改
}

3、区间查询

区间查询的代码其实和区间修改差不多

long long query(int p, int x, int y)//查询某区间上的和
{//和区间加差不多
	if (x <= t[p].l && y >= t[p].r)//涵盖了整个区间的话直接返回当前节点值,不需要取子节点值
		return t[p].pre;
	lazy(p);//因为并不是所有区间都要被查询,也就是需要使用子节点,存在懒标记,进行下传
	int mid = (t[p].l + t[p].r) >> 1;
	long long ans = 0;
	if (x <= mid)
		ans += query(p << 1, x, y);
	if (y > mid)
		ans += query(p << 1 | 1, x, y);
	return ans;
}

同理,先判断是否覆盖整个区间,覆盖则直接返回节点值不再计算子节点

不完全覆盖则和区间修改使用一样的方式在树中进行查找,不同的是查找不需要修改值,只需要将能够取到的值加和

例题一(线段树模板1)

原题链接:https://vjudge.net/contest/479523#problem/C

1、题干

如题,已知一个数列,你需要进行下面两种操作:

将某区间每一个数加上 k。
求出某区间每一个数的和。

2、输入格式

第一行包含两个整数 n, m,分别表示该数列数字的个数和操作的总个数。

第二行包含 n 个用空格分隔的整数,其中第 i 个数字表示数列第 i 项的初始值。

接下来 m 行每行包含 3 或 4 个整数,表示一个操作,具体如下:

1 x y k:将区间 [x, y]内每个数加上 k。
2 x y:输出区间 [x, y]内每个数的和。

3、输出格式

输出包含若干行整数,即为所有操作 2 的结果。

4、数据范围

对于 100%100% 的数据: 1 ≤ n , m ≤ 10 5 1 \le n, m \le {10}^5 1n,m105
保证任意时刻数列中任意元素的和在 [ − 2 63 , 2 63 ) [-2^{63}, 2^{63}) [263,263) 内。

5、样例

sample input 1

5 5
1 5 4 2 3
2 2 4
1 2 3 2
2 3 4
1 1 5 1
2 1 4

sample output1

11
8
20

例题一题解

1、分析

标准模板题,直接套

2、代码

#include <cstdio>
#include <iostream>
using namespace std;
struct tt
{
	int l, r;		   // l,r为这个点统率的区间长度
	long long la, pre; // la代表这个点的lazy值,pre代表这个点的存储的值
} t[400005];

int a[100005]; //存储最一开始的数字

int len(int a) { return t[a].r - t[a].l + 1; } //返回一个点维护的区间长度

void build(int p, int l, int r) //建树,点p的统率长度为l-r
{
	t[p].l = l, t[p].r = r; //存储该点统率长度左端点和右端点
	if (l == r)				//叶子结点,直接赋值
	{
		t[p].pre = a[r];
		return;
	}
	int mid = (l + r) >> 1;						  //取中值
	build(p << 1, l, mid);						  //递归建左树
	build(p << 1 | 1, mid + 1, r);				  //递归建右树  p << 1 | 1  p乘2+1
	t[p].pre = t[p << 1].pre + t[p << 1 | 1].pre; //当前点的值就等于左子树的值+右子树的值
}

void lazy(int p) //下传懒标记一次,某个节点打上懒标记说明它统御的所有点都要更改
{
	if (t[p].la >= 1) //点p存在懒标记
	{
		t[p << 1].pre += t[p].la * (len(p << 1)); //点p左右节点的值加上懒标记值*左右节点的长度
		t[p << 1 | 1].pre += t[p].la * (len(p << 1 | 1));

		t[p << 1].la += t[p].la;
		t[p << 1 | 1].la += t[p].la; //下传懒标记

		t[p].la = 0; //清除当前懒标记
	}
}

void change(int p, int x, int y, int z) // p点,xy为区间,z为值  x-y区间更改z值
{//原理为重复递归直到xy超过点的统御区间,标记lazy
	if (t[p].l >= x && t[p].r <= y) //如果xy完全覆盖该点统率的范围的左右区间,即所有的点都需更改。同时也是递归出口
	{
		t[p].pre += (long long)z * (len(p)); //将该点的值修改为更改值乘该点区间长度,也就是统率的所有点都要加上该值
		t[p].la += z;						 //该点打上lazy值,表示统率的区间都要加上z,之后不再对子节点进行更改值,节省时间
		return;
	}
	lazy(p);						  //如果不是所有点都要更改则需要下传懒标记,若不存在懒标记,忽略此步
	int mid = (t[p].l + t[p].r) >> 1; //求出该点区间中值
	//判断修改区间位置
	if (x <= mid)
		change(p << 1, x, y, z);
	if (y > mid)
		change(p << 1 | 1, x, y, z);
	t[p].pre = t[p << 1].pre + t[p << 1 | 1].pre;//找到了统御区间全部都要被修改的点,则将该点的父节点p值修改
}

long long query(int p, int x, int y)//查询某区间上的和
{//和区间加差不多
	if (x <= t[p].l && y >= t[p].r)//涵盖了整个区间的话直接返回当前节点值,不需要取子节点值
		return t[p].pre;
	lazy(p);//因为并不是所有区间都要被查询,也就是需要使用子节点,存在懒标记,进行下传
	int mid = (t[p].l + t[p].r) >> 1;
	long long ans = 0;
	if (x <= mid)
		ans += query(p << 1, x, y);
	if (y > mid)
		ans += query(p << 1 | 1, x, y);
	return ans;
}

int main()
{
	int n, m;
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i++)
		scanf("%d", &a[i]);
	build(1, 1, n); //建树
	//
	while (m--)
	{
		int d, x, y, k;
		scanf("%d", &d);
		if (d == 1)
		{
			scanf("%d%d%d", &x, &y, &k);
			change(1, x, y, k);
			// for(int i=1;i<=4*n;i++) cout<<i<<":"<<t[i].l<<" "<<t[i].r<<" "<<t[i].pre<<" "<<endl;
		}
		else
		{
			scanf("%d%d", &x, &y);
			cout << query(1, x, y) << endl;
		}
	}
}

例题二(Lost Cows)

原题链接:https://vjudge.net/contest/479523#problem/E

1、题意

给出n头牛前面有多少头比他编号少的数目,求出原来的牛的编号

2、输入格式

第一行,单个整数n
接下来n-1行,每行一个整数表示前面有多少头比该牛编号少的数目,第一头牛前面没有牛,故并未表出

3、输出格式

共n行,每行一个数字表示当前的奶牛排列

4、样例

sample input1

5
1
2
1
0

sample output1

2
4
5
3
1

例题二题解

1、分析

我们先从样例开始入手,0,1,2,1,0

bNNjK0.jpg

对于上图,我们先按照样例输出将牛的编号表示为方块的长度,将每头牛前面编号比他小的数量一一对应,也可以理解为是每个方块前面比他矮的数量

橙色的数字表示比每个方块矮的数量,蓝色的数组是用于判断牛是否已被选中,值为1表示没有,为0表示已被选中

我们从数组a的后面往前循环一次,对于从后面开始的每个方块,我们可以很容易从样例中得到

按顺序的第五头牛编号为1,比他小的牛数量为0,因此在蓝色数组中寻找a[5]+1 个 1,也就是0+1=1,寻找第1个数字1,我们发现它的编号是1,和样例对应,将其选中,并将蓝色数组中的第 a[5]+11改为0

其他的依次类推

我们再来看看怎么用线段树来构建这样一个蓝色数组,并方便查找第x个1

bNvBVA.png

上图为查找蓝色数组中第三个1并修改为0,其他的均按此法类推直到整个蓝色数组均为0

2、代码

#include <cstdio>
#include <iostream>
using namespace std;

struct tt
{
    int l, r;
    long long pre, la;
} t[400005];

int a[400000], ans[400000]; //存储题目所给的在前矮奶牛数量


void build(int p, int l, int r) //建树,点p的统率长度为l-r
{
    t[p].l = l, t[p].r = r; //存储该点统率长度左端点和右端点
    t[p].pre = r - l + 1;
    if (l == r) //叶子结点,直接赋值
    {
        return;
    }
    int mid = (l + r) >> 1;        //取中值
    build(p << 1, l, mid);         //递归建左树
    build(p << 1 | 1, mid + 1, r); //递归建右树  p << 1 | 1  p乘2+1
}
int query(int p, int k)
{
    t[p].pre--;//走过的点必然要-1
    if (t[p].l == t[p].r)//左右边界相同,必定为该值
        return t[p].l;
    int left_p = p << 1;      // 2*p
    int right_p = p << 1 | 1; // 2*p+1
    if (t[left_p].pre < k)//左区间的值不足k,则剩余在右区间取
        return query(right_p, k - t[left_p].pre);
    //左区间的数的个数 大于等于 k,那么就在左区间搜索即可
    else
        return query(left_p, k);
}

int main()
{
    int n;
    cin >> n;
    a[1] = 0;//第一头牛前面没有牛
    for (int i = 2; i <= n; ++i)
    {
        cin >> a[i];
    }
    build(1, 1, n);

    for (int i = n; i >= 1; i--)
    {
        ans[i] = query(1, a[i] + 1);
    }
    for (int i = 1; i <= n; i++)
    {
        printf("%d\n", ans[i]);
    }
    return 0;
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值