再次温习线段树的一些思路

  |  

写在前面

因为之前4月份打了将近一个月的个人赛,有些以前学习过的算法已经忘了,所以重新再温习一遍。

关于线段树

这里提一下完全二叉树。完全二叉树是叶节点只能出现在最下层和次下层,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树。若设二叉树的深度为h,除第h层外,其它各层(1~h-1)的结点数都达到最大个数,第h层所有的结点都连续集中在最左边。那么线段树就是完全二叉树,一定条件下成为满二叉树。线段树的主要思想是二分,也就是通过二分的方法来查找节点。假设有编号从1到n的n个点,每个点都存了一些信息,用[L,R]表示下标从L到R的这些点。线段树的用处就是,对编号连续的一些点进行修改或者统计操作,修改和统计的复杂度都是O(log2(n))。

线段树的原理,就是,将[1,n]分解成若干特定的子区间(数量不超过4*n),然后,将每个区间[L,R]都分解为少量特定的子区间,通过对这些少量子区间的修改或者统计,来实现快速对[L,R]的修改或者统计。由此看出,用线段树统计的东西,必须符合区间加法,否则,不可能通过分成的子区间来得到[L,R]的统计结果。

符合区间加法的例子:

  • 数字之和——总数字之和 = 左区间数字之和 + 右区间数字之和

  • 最大公因数(GCD)——总GCD = gcd(左区间GCD,右区间GCD)

  • 最大值——总最大值=max(左区间最大值,右区间最大值)

不符合区间加法的例子:

  • 众数——只知道左右区间的众数,没法求总区间的众数

  • 01序列的最长连续零——只知道左右区间的最长连续零,没法知道总的最长连续零

一个问题,只要能化成对一些连续点的修改和统计问题,基本就可以用线段树来解决。

线段树的原理

(注:由于线段树的每个节点代表一个区间,以下叙述中不区分节点和区间,只是根据语境需要,选择合适的词)

线段树本质上是维护下标为1,2,..,n的n个按顺序排列的数的信息,所以,其实是“点树”,是维护n的点的信息,至于每个点的数据的含义可以有很多,
在对线段操作的线段树中,每个点代表一条线段,在用线段树维护数列信息的时候,每个点代表一个数,但本质上都是每个点代表一个数。以下,在讨论线段树的时候,区间[L,R]指的是下标从L到R的这(R-L+1)个数,而不是指一条连续的线段。只是有时候这些数代表实际上一条线段的统计结果而已。

线段树是将每个区间[L,R]分解成[L,M]和[M+1,R] (其中M=(L+R)/2 这里的除法是整数除法,即对结果下取整)直到 L==R 为止。

开始时是区间[1,n] ,通过递归来逐步分解,假设根的高度为1的话,树的最大高度为tupian1(n>1)。

线段树对于每个n的分解是唯一的,所以n相同的线段树结构相同,这也是实现可持久化线段树的基础。

下图展示了区间[1,13]的分解过程:

tupian2

上图中,每个区间都是一个节点,每个节点存自己对应的区间的统计信息。

线段树的点修改

假设要修改[5]的值,可以发现,每层只有一个节点包含[5],所以修改了[5]之后,只需要每层更新一个节点就可以线段树每个节点的信息都是正确的,所以修改次数的最大值为层数tupian3

复杂度O(log2(n))

线段树的区间修改

在写线段树的区间修改的时候,我们需要先掌握一个知识点,就是懒惰标记。

什么是懒惰标记?

试想,我们在操作的时候有可能有这样的操作。首先进行区间修改,修改了800次,然后再进行一次查询。这样,如果我们每次都将整个线段树的数据进行更新,实际上是非常慢的,如果我们能用一段空间,来记录修改数据,只有在使用的时候,一次性更新,就非常的方便。

所以这也是懒惰标记的作用。可以先对修改的数据进行储存,只有在使用的时候才更新线段树。那么,理论上我们应该建一个跟线段树同样大小的数组,称为懒惰数组,表示了每个节点的懒惰标记。有这样的操作:

  • 1.修改数据的时候,每次递归到某节点,修改数据以后将数据的变化添加到数组中。

  • 2.当使用到这个节点的时候,发现对应的懒惰标记存在,那么就应该更新该节点,以及以下的所有节点的数据,方便使用。

总之,就是不使用的时候就一直在积累,在使用的时候再统一更新。

那么懒惰数组的更新非常简单,对线段树更新的时候就可以添加到懒惰标记,但是在使用的时候,我们需要用一个函数来完成懒惰标记的下推操作,也就是更新积累的值。代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void Pushdown(int rt, int l, int r) //下推标记的函数。l,r为左子树,右子树的数字数量
{
if (lz[rt])
{
//下推标记
lz[rt << 1] += lz[rt];
lz[rt << 1 | 1] += lz[rt];
//修改子节点的Sum使之与对应的lz相对应
//int mid=(l+r)>>1;与下面Update_tree_interval函数中Pushdown(rt,l,r)对应
Sum[rt << 1] += lz[rt] * l; //Sum[rt<<1]+=(mid-l+1)*lz[rt];与下面Update_tree_interval函数中Pushdown(rt,l,r)对应
Sum[rt << 1 | 1] += lz[rt] * r; //Sum[rt<<1|1]+=(r-mid)*lz[rt];与下面Update_tree_interval函数中Pushdown(rt,l,r)对应
//清除本节点标记
lz[rt] = 0;
}
}

lz数组,即lazy,就是懒惰标记数组。可以看出,当lz[rt]存在值的时候,就说明现在我在使用这个节点,而这个节点以及其下的节点需要更新了,所以就利用二叉树的性质向下传递更新数据,同时更新线段树中的数据。最终,要将该节点的懒惰标记清零。

注意,下推的时候不是一直更新到叶子节点,而是只更新当前节点以及2个子树,因为实际操作的时候,只要碰到对某节点的操作就要调用Pushdown()函数,所以每次只用下推一层即可。

Pushdown()函数的使用需要在下面的区间操作中添加。

区间修改:

单点更新类似二分查找,更新的时候对经过的路径进行操作就可以了。但是区间更新需要考虑整个区间。线段树除了叶子节点,都表示了一段区间的值,那么就要配合懒惰标记在整个区间上进行操作。

线段树的区间查询

区间查询的原理跟区间更新基本一样,也是看结点表示的数据范围有不同的操作。同样,在到某个结点的时候一定要调用Pushdown()。不同点在于跟数据操作无关,但是需要一个sum来储存收集到的区间数据,同时最后return。这样在递归完成以后最后返回的就是区间和了。理解区间修改后,区间查询就容易的多了。

线段树的储存结构

线段树是用数组来模拟树形结构,对于每一个节点R ,左子节点为 2 R (一般写作R<<1)右子节点为 2 R+1(一般写作R<<1|1)

然后以1为根节点,所以,整体的统计信息是存在节点1中的。

这么表示的原因看下图就很明白了,左子树的节点标号都是根节点的两倍,右子树的节点标号都是左子树+1:

tupian4

线段树需要的数组元素个数很大,一般都开4倍空间,比如: int Sum[maxx<<2];

脱离lazy数组

lazy数组的使用在很大程度将降低了解决问题所耗费的时间,但是也增加了对模板的修改难度。诚然,lazy很实用,但是在一些问题的构造上并不容易修改。我们平常的区间修改,整个区间的数值变化是统一的,所以我们能够在数学上提前好多个结点先算出来更改情况。但是有很多问题并不是这样的。

例如:hdu4027。11年上海网络赛,要求区间内对每个节点的数值取其二次根。那么再考虑lazy数组就是徒生烦恼。如果我们抛弃lazy数组,直接每次都更新到叶子结点,同时考虑剪枝,速度也并不慢(500ms)。所以,在区间操作不平衡,同时可以剪枝的情况下,完全可以抛弃lazy数组,从而修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
boolean cleck(int rt,int l,int r){
// 剪枝条件
}

void Update_tree_interval(int rt, int l, int r, int L, int R) {
if (L == R) {
Sum[rt] = 1; // 更新方式
return;
}
int mid = (L + R) / 2;
if (mid >= l && cleck(rt << 1,L,mid))
Update_tree_interval(rt << 1, l, r, L, mid);
if (mid < r && cleck(rt << 1 | 1,mid+1,R))
Update_tree_interval(rt << 1 | 1, l, r, mid + 1, R);
Pushup(rt);
}

线段树实现代码

首先:定义

1
2
3
4
5
6
7
8
#include <bits/stdc++.h>
typedef long long ll;
const int maxx = 10010;
const int inf = 0x3f3f3f3f;
using namespace std;
int A[maxx]; //存原数组数据下标[1,n]
int Sum[maxx << 2]; //Sum求和
int lz[maxx << 2]; //lz为懒惰标记

初始化

1
2
3
4
void init()//初始化
{
memset(Sum,0,sizeof(Sum));
}

建树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void Pushup(int rt) //Pushup函数更新节点
{
Sum[rt] = Sum[rt << 1] + Sum[rt << 1 | 1];
}
void Build_tree(int l, int r, int rt) //建树,l,r表示当前节点区间,rt表示当前节点编号
{
if (l == r) //若到达叶节点
{
Sum[rt] = A[l];//cin>>Sum[rt];
return;
}
int mid = (l + r) >> 1;
Build_tree(l, mid, rt << 1);
Build_tree(mid + 1, r, rt << 1 | 1);
Pushup(rt);
}

这样的建树,是一个递归的过程。l与r分别表示区间,结合上面的图,当l==r的时候说明递归已经遍历到叶子节点了,而这个节点rt也是二叉树的节点编号。对应了数组的下标。所以进行赋值。然后直接return进行回溯。那么在正常递归的时候,我们需要利用二叉树的性质,即对于rt编号的节点而言,左子树编号为rt<<1,右子树为rt<<1|1。同样,由于二分的性质,利用mid = (l+r)/2,就可以获取下一个子树的区间范围。

在回溯的时候,是从树的最下层开始向最上层回溯,那么同样利用二叉树的性质,我们可以轻松将子树的数据加到父节点上。这样,当函数完成的时候,我们就可以利用数组来构建了一个线段树。

懒惰标记,下推操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void Pushdown(int rt, int l, int r) //下推标记的函数。l,r为左子树,右子树的数字数量
{
if (lz[rt])
{
//下推标记
lz[rt << 1] += lz[rt];
lz[rt << 1 | 1] += lz[rt];
//修改子节点的Sum使之与对应的lz相对应
//int mid=(l+r)>>1;与下面Update_tree_interval函数中Pushdown(rt,l,r)对应
Sum[rt << 1] += lz[rt] * l; //Sum[rt<<1]+=(mid-l+1)*lz[rt];与下面Update_tree_interval函数中Pushdown(rt,l,r)对应
Sum[rt << 1 | 1] += lz[rt] * r; //Sum[rt<<1|1]+=(r-mid)*lz[rt];与下面Update_tree_interval函数中Pushdown(rt,l,r)对应
//清除本节点标记
lz[rt] = 0;
}
}

单点修改

线段树并不必须要进行区间的操作,如果是对单点进行操作,完全可以用更快的方法来实现。而对于单点修改而言,其实相比区间修改的代码要简单很多(因为lazy数组的存在),所以能用针对单点的修改最好不要用区间修改。单点更新非常类似二分查找,通过递归找到更新点的位置,在回溯的时候更新所有节点的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void Update_tree_point(int l, int r, int rt, int a, int b) //点修改,假设A[a]=b,l,r表示当前节点区间,rt表示当前节点编号,a为更新点,b为更新值
{
if (l == r) //到叶节点,修改
{
A[a] = b;
Sum[rt] = b;
return;
}
int mid = (l + r) >> 1;
//根据条件判断往左子树调用还是往右
// Pushdown(rt,mid-l+1,r-mid); 若既有点更新又有区间更新,需要这句话
if (a <= mid)
Update_tree_point(l, mid, rt << 1, a, b);
else
Update_tree_point(mid + 1, r, rt << 1 | 1, a, b);
Pushup(rt);
}

区间修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void Update_tree_interval(int L, int R, int l, int r, int rt, int b) //区间修改,假设A[L,R]=b,L,R表示操作区间,l,r表示当前节点区间,rt表示当前节点编号,b为更新值
{
if (L <= l && r <= R) //如果本区间完全在操作区间[L,R]以内
{
Sum[rt] += b * (r - l + 1);
lz[rt] += b; //增加lz标记,表示本区间的Sum正确,子区间的Sum仍需要根据lz的值来调整
return;
}
int mid = (l + r) >> 1;
Pushdown(rt, mid - l + 1, r - mid); //Pushdown(rt,l,r);//下推标记
//这里判断左右子树跟[L,R]有无交集,有交集才递归
if (L <= mid)
Update_tree_interval(L, R, l, mid, rt << 1, b);
if (R > mid)
Update_tree_interval(L, R, mid + 1, r, rt << 1 | 1, b);
Pushup(rt); //更新本节点信息
}

区间查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ll Query_tree(int L, int R, int l, int r, int rt) //区间查询,L,R表示操作区间,l,r表示当前节点区间,rt表示当前节点编号
{
if (L <= l && r <= R)
{
//在区间内,直接返回
return Sum[rt];
}
int mid = (l + r) >> 1;
//下推标记,否则Sum可能不正确
Pushdown(rt, mid - l + 1, r - mid); //Pushdown(rt,l,r);
ll sum = 0; //累计答案
if (L <= mid)
sum += Query_tree(L, R, l, mid, rt << 1);
if (R > mid)
sum += Query_tree(L, R, mid + 1, r, rt << 1 | 1);
return sum;
}

线段树模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
#include <bits/stdc++.h>
typedef long long ll;
const int maxx = 10010;
const int inf = 0x3f3f3f3f;
using namespace std;
int A[maxx]; //存原数组数据下标[1,n]
int Sum[maxx << 2]; //Sum求和
int lz[maxx << 2]; //lz为懒惰标记

void init()//初始化
{
memset(Sum,0,sizeof(Sum));
}

void Pushup(int rt) //Pushup函数更新节点
{
Sum[rt] = Sum[rt << 1] + Sum[rt << 1 | 1];
}

void Pushdown(int rt, int l, int r) //下推标记的函数。l,r为左子树,右子树的数字数量
{
if (lz[rt])
{
//下推标记
lz[rt << 1] += lz[rt];
lz[rt << 1 | 1] += lz[rt];
//修改子节点的Sum使之与对应的lz相对应
//int mid=(l+r)>>1;与下面Update_tree_interval函数中Pushdown(rt,l,r)对应
Sum[rt << 1] += lz[rt] * l; //Sum[rt<<1]+=(mid-l+1)*lz[rt];与下面Update_tree_interval函数中Pushdown(rt,l,r)对应
Sum[rt << 1 | 1] += lz[rt] * r; //Sum[rt<<1|1]+=(r-mid)*lz[rt];与下面Update_tree_interval函数中Pushdown(rt,l,r)对应
//清除本节点标记
lz[rt] = 0;
}
}

void Build_tree(int l, int r, int rt) //建树,l,r表示当前节点区间,rt表示当前节点编号
{
if (l == r) //若到达叶节点
{
Sum[rt] = A[l];//cin>>Sum[rt];
return;
}
int mid = (l + r) >> 1;
Build_tree(l, mid, rt << 1);
Build_tree(mid + 1, r, rt << 1 | 1);
Pushup(rt);
}

void Update_tree_point(int l, int r, int rt, int a, int b) //点修改,假设A[a]=b,l,r表示当前节点区间,rt表示当前节点编号,a为更新点,b为更新值
{
if (l == r) //到叶节点,修改
{
A[a] = b;
Sum[rt] = b;
return;
}
int mid = (l + r) >> 1;
//根据条件判断往左子树调用还是往右
// Pushdown(rt,mid-l+1,r-mid); 若既有点更新又有区间更新,需要这句话
if (a <= mid)
Update_tree_point(l, mid, rt << 1, a, b);
else
Update_tree_point(mid + 1, r, rt << 1 | 1, a, b);
Pushup(rt);
}

void Update_tree_interval(int L, int R, int l, int r, int rt, int b) //区间修改,假设A[L,R]=b,L,R表示操作区间,l,r表示当前节点区间,rt表示当前节点编号,b为更新值
{
if (L <= l && r <= R) //如果本区间完全在操作区间[L,R]以内
{
Sum[rt] += b * (r - l + 1);
lz[rt] += b; //增加lz标记,表示本区间的Sum正确,子区间的Sum仍需要根据lz的值来调整
return;
}
int mid = (l + r) >> 1;
Pushdown(rt, mid - l + 1, r - mid); //Pushdown(rt,l,r);//下推标记
//这里判断左右子树跟[L,R]有无交集,有交集才递归
if (L <= mid)
Update_tree_interval(L, R, l, mid, rt << 1, b);
if (R > mid)
Update_tree_interval(L, R, mid + 1, r, rt << 1 | 1, b);
Pushup(rt); //更新本节点信息
}

ll Query_tree(int L, int R, int l, int r, int rt) //区间查询,L,R表示操作区间,l,r表示当前节点区间,rt表示当前节点编号
{
if (L <= l && r <= R)
{
//在区间内,直接返回
return Sum[rt];
}
int mid = (l + r) >> 1;
//下推标记,否则Sum可能不正确
Pushdown(rt, mid - l + 1, r - mid); //Pushdown(rt,l,r);
ll sum = 0; //累计答案
if (L <= mid)
sum += Query_tree(L, R, l, mid, rt << 1);
if (R > mid)
sum += Query_tree(L, R, mid + 1, r, rt << 1 | 1);
return sum;
}

例题:HDU-1166

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#include <bits/stdc++.h>
typedef long long ll;
const int maxx = 50010;
const int inf = 0x3f3f3f3f;
using namespace std;
int A[maxx]; //存原数组数据下标[1,n]
int Sum[maxx << 2]; //Sum求和
int lz[maxx << 2]; //lz为懒惰标记
void Pushup(int rt) //Pushup函数更新节点
{
Sum[rt] = Sum[rt << 1] + Sum[rt << 1 | 1];
}
void Build_tree(int l, int r, int rt) //建树,l,r表示当前节点区间,rt表示当前节点编号
{
if (l == r) //若到达叶节点
{
Sum[rt] = A[l];// //cin>>Sum[rt];
return;
}
int mid = (l + r) >> 1;
Build_tree(l, mid, rt << 1);
Build_tree(mid + 1, r, rt << 1 | 1);
Pushup(rt);
}
void Update_tree_point(int l, int r, int rt, int a, int b) //点修改,假设A[a]=b,l,r表示当前节点区间,rt表示当前节点编号,a为更新点,b为更新值
{
if (l == r) //到叶节点,修改
{
//A[a] = b;
Sum[rt] += b;
return;
}
int mid = (l + r) >> 1;
//根据条件判断往左子树调用还是往右
if (a <= mid)
Update_tree_point(l, mid, rt << 1, a, b);
else
Update_tree_point(mid + 1, r, rt << 1 | 1, a, b);
Pushup(rt);
}
ll Query_tree(int L, int R, int l, int r, int rt) //区间查询,L,R表示操作区间,l,r表示当前节点区间,rt表示当前节点编号
{
if (L <= l && r <= R)
{
//在区间内,直接返回
return Sum[rt];
}
int mid = (l + r) >> 1;
ll sum = 0; //累计答案
if (L <= mid)
sum += Query_tree(L, R, l, mid, rt << 1);
if (R > mid)
sum += Query_tree(L, R, mid + 1, r, rt << 1 | 1);
return sum;
}
int main()
{
int t;
scanf("%d", &t);
string s;
int cnt = 0;
while (t--)
{
++cnt;
printf("Case %d:\n", cnt);
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%d", &A[i]);
Build_tree(1, n, 1);
int x, y;
cin >> s;
while (s != "End")
{
scanf("%d%d", &x, &y);
if (s == "Query")
{
printf("%d\n", Query_tree(x, y, 1, n, 1));
}
else if (s == "Add")
{
Update_tree_point(1, n, 1, x, y);
}
else if (s == "Sub")
{
Update_tree_point(1, n, 1, x, -y);
}
cin >> s;
}
}
return 0;
}

线段树应用:扫描线求矩形面积并

分析:

1.矩形比较多,坐标也很大,所以横坐标需要离散化(纵坐标不需要),熟悉离散化后这个步骤不难,所以这里不详细讲解了,不明白的还请百度。

2.重点:扫描线法:假想有一条扫描线,从左往右(从右往左),或者从下往上(从上往下)扫描过整个多边形(或者说畸形。。多个矩形叠加后的那个图形)。如果是竖直方向上扫描,则是离散化横坐标,如果是水平方向上扫描,则是离散化纵坐标。下面的分析都是离散化横坐标的,并且从下往上扫描的。

扫描之前还需要做一个工作,就是保存好所有矩形的上下边,并且按照它们所处的高度进行排序,另外如果是上边我们给他一个值-1,下边给他一个值1,我们用一个结构体来保存所有的上下边。

1
2
3
4
5
struct segment
{
double l,r,h; //l,r表示这条上下边的左右坐标,h是这条边所处的高度
int f; //所赋的值,1或-1
}

接着扫描线从下往上扫描,每遇到一条上下边就停下来,将这条线段投影到总区间上 (总区间就是整个多边形横跨的长度) ,这个投影对应的其实是个插入和删除线段操作。 还记得给他们赋的值1或-1吗,下边是1,扫描到下边的话相当于往总区间插入一条线段,上边-1,扫描到上边相当于在总区间删除一条线段 (如果说插入删除比较抽象,那么就直白说,扫描到下边,投影到总区间,对应的那一段的值都要增1,扫描到上边对应的那一段的值都要减1, 如果总区间某一段的值为0,说明其实没有线段覆盖到它,为正数则有,那会不会为负数呢?是不可能的, 可以自己思考一下)。

每扫描到一条上下边后并投影到总区间后,就判断总区间现在被覆盖的总长度,然后用下一条边的高度减去当前这条边的高度,乘上总区间被覆盖的长度,就能得到一块面积,并依此做下去,就能得到最后的面积

(这个过程其实一点都不难,只是看文字较难体会,建议纸上画图,一画即可明白,下面献上一图希望有帮助)

tupian6

扫描线求矩形面积并的例题:POJ-1151

题目给了n个矩形,每个矩形给了左下角和右上角的坐标,矩形可能会重叠,求的是矩形最后的面积。因为变化范围比较大,我们要用到离散化,离散化就不说了,重点说一说扫描线的过程:

下面有个矩形

tupian7

现在假设我们有一根线,从下往上开始扫描

tupian8

tupian9

tupian10

tupian11

tupian12

tupian13

如图所示,我们可以把整个矩形分成如图各个颜色不同的小矩形,那么这个小矩形的高就是我们扫过的距离,那么剩下了一个变量,那就是矩形的长一直在变化。

我们的线段树就是为了维护矩形的长,我们给每一个矩形的上下边进行标记,下面的边标记为1,上面的边标记为-1,每遇到一个矩形时,我们知道了标记为1的边,我们就加进来这一条矩形的长,等到扫描到-1时,证明这一条边需要删除,就删去,利用1和-1可以轻松的到这种状态。

还要注意这里的线段树指的并不是线段的一个端点,而指的是一个区间,所以我们要计算的时候r+1和r-1

再提一下离散化,离散化就是把一段很大的区间映射到一个小区间内,这样会节省大量空间,要进行离散化,我们先对端点进行排序,然后去重,然后二分找值就可以了
具体的请结合代码分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#include <cstdio>
#include <cstring>
#include <cctype>
#include <string>
#include <set>
#include <iostream>
#include <stack>
#include <cmath>
#include <queue>
#include <vector>
#include <algorithm>
#define mem(a,b) memset(a,b,sizeof(a))
#define inf 0x3f3f3f3f
#define N 220
#define ll long long
using namespace std;
#define lson l,m,rt<<1
#define rson m+1,r,rt<<1|1
struct Seg
{
double l,r,h;
int f;
Seg() {}
Seg(double a,double b,double c,int d):l(a),r(b),h(c),f(d) {}
bool operator < (const Seg &cmp) const
{
return h<cmp.h;
}
} e[N];
struct node
{
int cnt;
double len;
} t[N<<2];
double X[N];
void pushdown(int l,int r,int rt)
{
if(t[rt].cnt)//当前的边被标记,就把当前的长度加上
t[rt].len=X[r+1]-X[l];
else if(l==r)//当为一个点的时候长度为0
t[rt].len=0;
else//其他情况把左右两个区间的值加上
t[rt].len=t[rt<<1].len+t[rt<<1|1].len;
}
void update(int L,int R,int l,int r,int rt,int val)
{
if(L<=l&&r<=R)
{
t[rt].cnt+=val;//加上标记的值
pushdown(l,r,rt);//像下更新节点
return;
}
int m=(l+r)>>1;
if(L<=m) update(L,R,lson,val);
if(R>m) update(L,R,rson,val);
pushdown(l,r,rt);
}
int main()
{
int n,q=1;
double a,b,c,d;
while(~scanf("%d",&n)&&n)
{
mem(t,0);
int num=0;
for(int i=0; i<n; i++)
{
scanf("%lf%lf%lf%lf",&a,&b,&c,&d);
X[num]=a;
e[num++]=Seg(a,c,b,1);//矩形下面用1来标记吗
X[num]=c;
e[num++]=Seg(a,c,d,-1);//上面用-1来标记
}
sort(X,X+num);//用于离散化
sort(e,e+num);//把矩形的边的纵坐标从小到大排序
int m=unique(X,X+num)-X;
double ans=0;
for(int i=0; i<num; i++)
{
int l=lower_bound(X,X+m,e[i].l)-X;//找出离散化以后的值
int r=lower_bound(X,X+m,e[i].r)-X-1;
update(l,r,0,m,1,e[i].f);
ans+=t[1].len*(e[i+1].h-e[i].h);
}
printf("Test case #%d\nTotal explored area: %.2lf\n\n",q++,ans);
}
return 0;
}

×

纯属好玩

扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦

文章目录
  1. 1. 写在前面
  2. 2. 关于线段树
  3. 3. 线段树的原理
    1. 3.1. 线段树的点修改
    2. 3.2. 线段树的区间修改
    3. 3.3. 线段树的区间查询
    4. 3.4. 线段树的储存结构
    5. 3.5. 脱离lazy数组
  4. 4. 线段树实现代码
    1. 4.1. 首先:定义
    2. 4.2. 初始化
    3. 4.3. 建树
    4. 4.4. 懒惰标记,下推操作
    5. 4.5. 单点修改
    6. 4.6. 区间修改
    7. 4.7. 区间查询
  5. 5. 线段树模板
  6. 6. 例题:HDU-1166
  7. 7. 线段树应用:扫描线求矩形面积并
  8. 8. 扫描线求矩形面积并的例题:POJ-1151
,
字数统计:87.6k 载入天数...载入时分秒...