例1. [2019 美团春招实习笔试] 2. 染色格子数量
时间限制:C/C++语言 1000MS;其他语言 3000MS
内存限制:C/C++语言 65536KB;其他语言 589824KB
题目描述:
在二维平面上有一个无限的网格图形,初始状态下,所有的格子都是空白的。现在有n个操作,每个操作是选择一行或一列,并在这行或这列上选择两个端点网格,把以这两个网格为端点的区间内的所有网格染黑(包含这两个端点)。问经过n次操作之后,共有多少个格子被染黑,显然在众多操作中,很容易重复染色同一个格子,这个时候只计数一次。输入
输入第一行包含一个正整数n(1<=n<=10000).接下来n行,每行四个整数,x1,y1,x2,y2,分别表示一个操作的两端格子坐标。(-10^9<=x1,y1,x2,y2<=10^9),若x1=x2则是在一列上染色,若y1=y2,则是在一行上染色,保证每次操作是在一行或一列上染色。
输出
输出仅包含一个正整数,表示被染色的格子的数量。
样例输入
3
1 2 3 2
2 5 2 3
1 4 3 4
样例输出
8
算法:
矩形面积并,扫描线+离散化+线段树
参考链接:https://www.jianshu.com/p/d70f6d346913
图1. 扫描线示意图
思路:如图所示,假想一条扫描线从下向上移动,每次遇到矩形上底或者下底便计算一次扫过的矩形面积,并加到总和中去。如图所示,依次求出不同颜色矩形的面积,想加便可求得所有矩形覆盖的总面积。
扫描线的移动由程序来实现实质上是在各个矩形的底边按纵坐标从小到大跳动,每跳动一次需要计算一次扫过的矩形面积。
面积=跳动的长度*该段x方向被矩形覆盖的长度。
首先,我们需要将矩形的底边的纵坐标按从小到大进行排序,Y即为相邻两个底边的差值。同时,我们还需要记录底边的横坐标范围,以便每扫过一条底边后更新X(当扫瞄线经过一条底边时,意味着有一个矩形进入或者退出扫描区域)。
1. 扫描线
所以定义每条线段的结构体:
struct Line { int y; // 该线段的y轴坐标 int x1, x2; // 该线段的左右端点x坐标 int flag; // 该线段的标记, flag = 1 表示为矩形的底边, flag = -1 表示为矩形的顶边 }lines[maxn * 2 + 10];
flag值为什么要这样设置?
其中flag为lines[i]的标记符号,covered + flag记录x轴坐标的覆盖次数:
1)flag = 1,表示这条线为底部的线,扫描这条线则进入矩形区域,该线段上的所有横坐标覆盖次数 + 1,
2)flag = -1, 表示这条线为顶部的线,扫描这条线则退出矩形区域,该线段上的所有横坐标覆盖次数将 -1,
2. 离散化
2.1 离散化简介
离散化可以将大区间map到一个小区间上。举个例子:
对于数组 list[5] = {150, 900, 1, 50, 1000000}
如果要将其在一维坐标轴中表示的话,那涉及到的区间范围将会是[1, 1000000] 非常大;
离散化则将每个点用一个数字符号来标记,再通过一种map机制建立与真实值一一映射的关系,即可在小区间上表示大区间的值。比如
list_map = {1: 150, 2: 900, 3: 1, 4: 50, 5: 1000000}
这样我们只需要在一维坐标轴中用到区间[1, 5]即可,对于区间中的每一个点,通过list_map找到真实值,则保留了原来的信息的同时,大大缩小了所需空间。
2.2 本题目中的离散化
“ 输入
输入第一行包含一个正整数n(1<=n<=10000).
接下来n行,每行四个整数,x1,y1,x2,y2,分别表示一个操作的两端格子坐标。(-10^9<=x1,y1,x2,y2<=10^9)”
题目中的横纵坐标点变化范围非常大,根本没有办法采用数组等其他结构对其坐标直接存储。但是其操作数n在可接受范围内。我们可以考虑离散化,将x的真实区间范围,映射到x的点数范围内。
对于题目样例
3
1 2 3 2
2 5 2 3
1 4 3 4
输入了6个x的坐标值,分别为:[1, 3, 2, 2, 1, 3], 经过离散化后的x: {1: 1, 2: 2, 3: 3}, 即为用加粗的$i$表示数值$j$,建立了 $i \rightarrow j$的映射关系。
3. 线段树
3.1 线段树简介
线段树算法有两个关键点:区间修改和单点查询。
1)区间修改
线段树是一种用来快速进行区间修改的算法。
它不直接对区间上的每一个值进行修改,而是采用了一种巧妙的方式。
举个例子,对于数组list = [0, 1, 2, 3, 4, 5, 6, 7], 我们想将数组的第0至3位上的每一个数值+1。
对于naive的方法,则是遍历这个数组,对应位进行相应操作, 即进行一次操作的时间复杂度为$O(n)$
如果数组规模为n, 操作次数为m, 那么对该数组进行m次操作的时间复杂度为$O(m * n)$,这样对于需要进行多次操作的大数组,其时间复杂度很高。
我们可以构造一棵线段树:
图2.线段树示意图
图中每个节点有表示一个区间范围 [l, r), 对于要求“将数组的第0至3位上的每一个数值+1”,只需告知[0, 4)节点“+ 1” 这个操作即可,这表示该区间内所有点都将执行该操作。
线段树的时间复杂度降为了$O(m * log_2n)$.)
2)单点查询
在对一棵线段树进行了若干次操作后,给定一个数组list的index,如何快速计算list[index]的值是多少呢?
还是对于图2所示,修改后的list[2]的值是多少呢?只需将 从根节点到[2, 3)叶子节点的路径上的操作加成即可。
3) 注意:线段树开4N空间
证明:考虑非完全二叉树的情况。https://blog.csdn.net/gl486546/article/details/78243098
对于倒数第二行,假设除了一个节点表示的区间包含2个值以外,其余各节点均只包含一个值。那么该行中有(N - 2)个节点是唯一对应一个值的。有1个节点包含2个值。那么该节点需要再分。
看最后一行。在倒数第二行中的(N-2)个节点在最后一行被分为了(N-2) *个节点,这些叶子节点内部并不包含任何值。在倒数第二行中的那唯一一个包含2个值的节点在最后一行被分为了两个节点,每个节点包含一个值。
所以总的来看,这棵完全二叉树的叶子节点总数为 (N - 2) * 2 + 2 = 2N - 2. 则这棵完全二叉树的总结点个数为:(2N - 2) + (2N - 2 - 1) = 4N - 5.
3.2 本题目中的线段树
本题目中,当扫描线扫到lines[i]时,若lines[i].flag = 1, 表示这条线段为矩形的底边,扫描线开始进入该矩形区域,那么需要将该线段所覆盖的x区域covered数目 + 1, 这里涉及到区间修改操作,使用线段树可以提高效率。
树节点的定义如下:
struct TreeNode { int l, r; int real_l, real_r; int covered; // whether been covered double cnt; // covered length }segTree[maxn * 4];
1)l, r, real_l, real_r
其中,l, r表示该节点表示的区域的下界和上界,该上下界为离散化处理后的index。
real_l, real_r 表示该节点表示的区域的真实坐标的下界和上界。
在题目输入时,通过将所有线段左右端点存储到x[]数组中,并对x数组进行排序,则
1 2 ... i ... 2n - 1 2n
| | | | |
x[1] x[2] ...x[i] ... x[2n - 1] x[2n]
构建树时:
segTree[t].real_l = x[l];
segTree[t].real_r = x[r];
对于$ l <= i <= r $, 有 $x[i] = $ real_i。通过这种方式,保存了离散化后的index与真实值之间的一一映射关系。
2. covered
covered表示该节点区域是否被覆盖。
举个例子:
3. cnt
所以这里与3.1中介绍的线段树的修改的操作不同,3.1中是计算叶子节点的最终值,表示修改后的数组的值,需要从根节点走向叶子节点,每一个节点记录在该区域内的操作;
本题目中的线段树的节点的cnt表示在该区间范围内被覆盖的总长度,所以该节点的cnt的值,为其所有子节点的cnt的值的和。
每次计算当前退出的小矩形的面积时,$S_{tmp} = 当前覆盖的x的区间总长度 * (y_{退出} - y_{进入}) $。
那么线段树的根节点的cnt的值,综合了x可行域范围内的被覆盖的区间总长度,所以最终 $$S_{tmp} = segTree[1].cnt * (y_{退出} - y_{进入}) $$
4. 本题目与poj1151的不同
1. 本题目中,数值为整数;poj1151中数值为浮点值;
2. 本题目中,每一个整数坐标表示一个格子,每个单位格子面积为1;poj1151中每个浮点坐标表示一个点,点的面积为0;
在算法实现上,有以下变动
4.1 输入坐标的转换
在读入数据时,将格子坐标转成了点坐标,具体实现就是:
对于(x1, y1, x2, y2)的输入,其在坐标系中的端点坐标为:(x1 - 1, y1, x2, y2 - 1);并以此新坐标作为line的输入。
4.2 线段树的构造
在poj1151中,线段树的每个节点表示一段区间:节点[1, 5) 表示真实区间 [0, 4)等;
下面代码中,将左闭右开的区间表示,转换为左闭右闭的表示:节点[1, 4] 表示真实区间 [0, 3]等;
因而,poj1151中的叶子节点为 [l, r), 其中 r == l + 1; 而下面代码中的叶子节点为 [l, r], 其中 r == l;
4.3 线段树cnt的计算
每个节点的cnt的值,为其孩子节点cnt的值的加和,
在poj1151中,对于叶子节点,若被覆盖,则其cnt的值为segTree[t].real_r - segTree[t].real_l,表示这个左闭右开的区间长度;
在下面代码中,由于每个叶子节点只表示坐标系中的一个点,若其被cover,则该节点的cnt = 0.5。为什么设置为0.5呢?以本题目样例说明:
3
1 2 3 2
2 5 2 3
1 4 3 4
则转换后的各条扫描线分别为:
lines[1]: y = 1, [x1, x2] = [0, 3], flag = 1;
lines[2]: y = 2, [x1, x2] = [0, 3], flag = -1;
lines[3]: y = 2, [x1, x2] = [1, 2], flag = 1;
lines[4]: y = 3, [x1, x2] = [0, 3], flag = 1;
lines[5]: y = 4, [x1, x2] = [0, 3], flag = -1;
lines[6]: y = 5, [x1, x2] = [1, 2], flag = -1;
图2. 线段树
(图中矩形框中的[l ,r] 表示线段树节点的左右端点序号范围;每个矩形框右边的[real_l, real_r] 表示该节点对应的真实的左右端点x轴坐标范围。橙色标注的序号t表示线段树的第t个节点。)
对于lines[3], 该线段覆盖区域为[1, 2], 该区域长度为1;即上图中节点5, 6均被覆盖,则每个节点的cnt值应该设置为单位区间长度的一半,为0.5.
代码:
1 # include <bits/stdc++.h> 2 using namespace std; 3 4 const int maxn = 1e5; 5 int n; 6 set<int> x_set; 7 set<int>::iterator it; 8 int x[maxn * 2 + 10]; 9 10 struct TreeNode 11 { 12 int l, r; 13 int real_l, real_r; 14 int covered; // covered times 15 double cnt; // total covered length in this zone 16 }segTree[maxn * 4]; 17 18 struct Line 19 { 20 int y; 21 int x1, x2; 22 int flag; 23 }lines[maxn * 2 + 10]; 24 25 bool cmp(Line a, Line b) 26 { 27 return a.y < b.y; 28 } 29 30 void build(int t, int l, int r) 31 { 32 segTree[t].l = l; 33 segTree[t].r = r; 34 segTree[t].real_l = x[l]; 35 segTree[t].real_r = x[r]; 36 37 segTree[t].covered = 0; 38 segTree[t].cnt = 0; 39 40 if(l == r) return; 41 int mid = (l + r) >> 1; 42 build(t << 1, l, mid); 43 build(t << 1 | 1, mid + 1, r); 44 } 45 46 void calen(int t) 47 { 48 if(segTree[t].covered > 0) 49 { 50 double tmp = 0; 51 if(segTree[t].l == segTree[t].r) tmp = 0.5; 52 segTree[t].cnt = segTree[t].real_r - segTree[t].real_l + tmp; 53 return; 54 } 55 56 if(segTree[t].r - segTree[t].l == 0) segTree[t].cnt = 0 ; 57 else segTree[t].cnt = segTree[t << 1].cnt + segTree[t << 1 | 1].cnt; 58 } 59 60 void update(int t, Line line) 61 { 62 // line.x1 ~ line.x2 covered segTree[t].real_l ~ real_r => add 1 63 if(segTree[t].real_l >= line.x1 && segTree[t].real_r <= line.x2) 64 { 65 segTree[t].covered += line.flag; 66 calen(t); 67 return; 68 } 69 70 // line not in the segTree[t] covered line 71 if(segTree[t].real_l > line.x2 || segTree[t].real_r < line.x1) return; 72 73 // update the child node 74 update(t << 1, line); 75 update(t << 1 | 1, line); 76 calen(t); // 每个节点的cnt的值为其孩子节点cnt的值的加和, 更新完孩子节点cnt后要更新t节点cnt 77 } 78 79 80 int main() 81 { 82 int x1, x2, y1, y2; 83 while(cin >> n) 84 { 85 int t = 1; 86 for(int i = 1; i <= n; ++i) 87 { 88 scanf("%d%d%d%d", &x1, &y1, &x2, &y2); 89 lines[t].y = y1; 90 lines[t].x1 = x1 - 1; 91 lines[t].x2 = x2; 92 lines[t].flag = -1; 93 x_set.insert(x1 - 1); 94 ++t; 95 96 lines[t].y = y2 - 1; 97 lines[t].x1 = x1 - 1; 98 lines[t].x2 = x2; 99 lines[t].flag = 1; 100 x_set.insert(x2); 101 ++t; 102 } 103 sort(lines + 1, lines + t, cmp); 104 105 int x_index = 1; 106 for(it = x_set.begin(); it != x_set.end(); ++it) 107 { 108 x[x_index++] = *it; 109 } 110 sort(x + 1, x + x_index); 111 112 build(1, 1, x_index - 1); 113 114 int res = 0; 115 update(1, lines[1]); 116 for(int i = 2; i < t; ++i) 117 { 118 int tmp = (lines[i].y - lines[i - 1].y) * segTree[1].cnt; 119 res += tmp; 120 update(1, lines[i]); 121 } 122 123 cout << res << endl; 124 } 125 return 0; 126 }
例2. [2016华为] 最高分是多少
https://www.nowcoder.com/test/260145/summary
时间限制:1秒
空间限制:65536K
老师想知道从某某同学当中,分数最高的是多少,现在请你编程模拟老师的询问。当然,老师有时候需要更新某位同学的成绩.输入描述:
输入包括多组测试数据。 每组输入第一行是两个正整数N和M(0 < N <= 30000,0 < M < 5000),分别代表学生的数目和操作的数目。 学生ID编号从1编到N。 第二行包含N个整数,代表这N个学生的初始成绩,其中第i个数代表ID为i的学生的成绩 接下来又M行,每一行有一个字符C(只取‘Q’或‘U’),和两个正整数A,B,当C为'Q'的时候, 表示这是一条询问操作,他询问ID从A到B(包括A,B)的学生当中,成绩最高的是多少 当C为‘U’的时候,表示这是一条更新操作,要求把ID为A的学生的成绩更改为B。输出描述:
对于每一次询问操作,在一行里面输出最高成绩.
这道只涉及到线段树,相较于例1简单很多。
1 #include <bits/stdc++.h> 2 using namespace std; 3 int N, M; 4 const int maxn = 30000; 5 const int maxm = 5000; 6 int scores[maxn]; 7 8 struct Operation 9 { 10 char c; 11 int A; 12 int B; 13 }operations[maxm + 5]; 14 15 struct TreeNode 16 { 17 int l, r; 18 int max_score; 19 }segTree[maxn * 4 + 5]; 20 21 void buildTree(int t, int l, int r) 22 { 23 segTree[t].l = l; 24 segTree[t].r = r; 25 segTree[t].max_score = 0; 26 if(l + 1 == r) return; 27 int mid = (l + r) >> 1; 28 buildTree(t << 1, l, mid); 29 buildTree(t << 1 | 1, mid, r); 30 } 31 32 void update(int t, int *scores) 33 { 34 if(segTree[t].l + 1 == segTree[t].r) 35 { 36 int id = segTree[t].r; 37 segTree[t].max_score =scores[id]; 38 return; 39 } 40 if(segTree[t].l == segTree[t].r) 41 { 42 segTree[t].max_score = 0; 43 return; 44 } 45 update(t << 1, scores); 46 update(t << 1 | 1, scores); 47 segTree[t].max_score = max(segTree[t << 1].max_score, segTree[t << 1 | 1].max_score); 48 } 49 50 void query(int t, int A, int B, int &score) 51 { 52 if(segTree[t].l + 1 >= A && segTree[t].r <= B) { score = max(score, segTree[t].max_score); return;} 53 if(segTree[t].l >= B || segTree[t].r < A) return; 54 query(t << 1, A, B, score); 55 query(t << 1 | 1, A, B, score); 56 } 57 58 int loc(int t, int A) 59 { 60 if(segTree[t].l + 1 == segTree[t].r && segTree[t].r == A) 61 { 62 return t; 63 } 64 int mid = (segTree[t].l + segTree[t].r) >> 1; 65 if(mid >= A) return loc(t << 1, A); 66 else return loc(t << 1 | 1, A); 67 } 68 69 void changeSegTree(int A, int B) 70 { 71 scores[A] = B; 72 int t = loc(1, A); 73 segTree[t].max_score = B; 74 t /= 2; 75 int origin; 76 while(t >= 1) 77 { 78 origin = segTree[t].max_score; 79 segTree[t].max_score = max(segTree[t << 1].max_score, segTree[t << 1 | 1].max_score); 80 if(origin == segTree[t].max_score) break; 81 t /= 2; 82 } 83 } 84 85 int main() 86 { 87 while(cin >> N >> M) 88 { 89 int score = 0; 90 for(int i = 1; i <= N; ++i) 91 { 92 cin >> scores[i]; 93 } 94 int first, second; 95 for(int i = 1; i <= M; ++i) 96 { 97 cin >> operations[i].c; 98 cin >> first >> second; 99 if(operations[i].c == 'Q') 100 { 101 operations[i].A = min(first, second); 102 operations[i].B = max(first, second); 103 } 104 else 105 { 106 operations[i].A = first; 107 operations[i].B = second; 108 } 109 } 110 111 buildTree(1, 0, N); 112 update(1, scores); 113 114 for(int i = 1; i <= M; ++i) 115 { 116 if(operations[i].c == 'Q') 117 { 118 score = 0; 119 query(1, operations[i].A, operations[i].B, score); 120 cout << score << endl; 121 } 122 else if(operations[i].c == 'U') 123 { 124 changeSegTree(operations[i].A, operations[i].B); 125 } 126 } 127 128 } 129 return 0; 130 } 131 132 // segtree size: N * 4 133 // why have to be N * 4? pf: https://blog.csdn.net/gl486546/article/details/78243098 134 // Notice: 135 // A = B 136 // A > B when c == 'Q' : A < B; else keep the origin order
给一个测试用例:
// input: // 12 27 // 1 15 16 15 6 19 24 39 26 26 14 7 // U 8 80 // Q 1 3 // Q 5 3 // Q 1 3 // U 5 14 // Q 12 10 // Q 5 8 // U 8 62 // U 3 8 // U 2 47 // U 7 42 // U 2 1 // Q 3 4 // Q 1 10 // Q 4 10 // U 11 7 // Q 6 5 // Q 2 2 // Q 10 10 // U 7 12 // U 2 45 // U 10 51 // U 4 82 // U 7 9 // U 2 64 // U 10 3 // Q 11 5 // output: // 16 // 16 // 16 // 26 // 80 // 15 // 62 // 62 // 19 // 1 // 26 // 62