[扫描线] 数据结构测试(2025.3.22)
哈哈哈暴力大赛,赛时暴力打满(差40pts打满。我太菜了暴力都不会呜呜)喜提80pts,(T1没想到暴力。
难度:T2<T1<T3.
T1
第1题 团队 查看测评数据信息
有n个工人,第i个工人的能力是v[i], 他只与能力在L[i]到R[i]之间的人在一起工作,问最多能选出多少人在一起工作。
输入格式
第一行,一个整数n, 1 <= n <= 100000。
接下来有n行,第i行是: L[i]、v[i]、R[i], 其中1<= L[i] <= v[i] <= R[i] <= 300000。
【提示】
有40%数据n<=100。
输出格式
一个整数。
输入/输出例子1
输入:
4
2 8 9
1 4 7
3 6 8
5 8 10
输出:
3
输入/输出例子2
输入:
6
3 5 16
1 6 11
4 8 12
7 9 16
2 10 14
8 13 15
输出:
4
样例解释
样列一解释: 可以选择第1,第3,第4这三人。
样列二解释: 可以选择第1,第2,第3,第5这四人。
先看40%的分:
首先得想到,最终选了一些人,所组成的区间一定是一个交集 [L, R]。
这点我没想到,导致整题拜拜。如果正面想,想如何去求答案这个过程很难,不妨考虑从答案结果出发,然后根据结果直接反推,看哪些过程符合条件,就避免了过程的枚举。因为答案确定了过程也确定了。
然后就容易了,枚举L, 枚举R,再枚举n个人。看看有几个人满足在[L, R]内。其中L,R只会是每个人的区间端点值。
O(n^3)
100%的分:提供两种方法。
方法一
L,R看成x,y轴上的一个数。这样[L, R]变成一个坐标轴上的点(L, R)。区间->坐标轴上的点。
转化:区间有哪些人->考虑人对区间的贡献。我们就看什么时候人会对区间造成贡献,或者说人对哪些区间有贡献。
此时这个区间代表选了一些人所组成的区间。
假设 人:[L, v, R](是已知的)。某个区间:[L2, R2](是未知的)
只要 L<=L2<=v && v<=R2<=R,即人完全包含区间,就是人对区间有贡献。
可以根据题意理解:因为你只和能力值在你这个区间范围内的人工作,所以你一定要包含 [L, R] 这个区间。
其次,[L, R]是选了某些人后组成区间,这些人只和他们的能力范围并集内的人合作,所以 v 一定在 [L, R] 内。
L,R,L2,R2,v,再次转回坐标轴就是一个矩形。矩形内每个点都是原来的区间。
即:x 轴上有 L,v;y 轴上有 v, R。
转化思想:一个区间可以转成坐标轴上的点,多个区间合并在一起取值(形如 x<=L<=y<=R<=z)可以转换成坐标轴上的矩形。
想要找一个区间[L, R]内,工作的人最多,就转化成:找一个点,使得这个点被矩形覆盖的次数最多->扫描线(其实就是扫描线里,线段树存一个最大值)。
考虑问题的次序思想:不同顺序考虑问题难易程度将不一样,如此题是先考虑区间再考虑人会很难,先考虑人再考虑区间是简单的
一些细节:
1.不同于平常的扫描线,这里线段树叶结点存的是一个点,而非线段。
2.注意排序的时候,加法操作要放前面,不然最大值计算会少算。
#include <bits/stdc++.h> using namespace std; const int N=300005; int n; long long ans=0; struct node { int L, v, R; }b[N]; struct Tree { int Max, add; }tr[N<<4]; struct smx { int x, ay, by, add; }; vector<smx> a; vector<int> p; bool cmp(smx a, smx b) { if (a.x!=b.x) return a.x<b.x; return a.add>b.add; } void push_up(int id, int L, int R) { tr[id].Max=max(tr[id*2].Max, tr[id*2+1].Max); } void push_down(int id) { tr[id*2].add+=tr[id].add; tr[id*2+1].add+=tr[id].add; tr[id*2].Max+=tr[id].add; tr[id*2+1].Max+=tr[id].add; tr[id].add=0; } void add(int id, int L, int R, int qL, int qR, int k) { if (qL<=p[L] && p[R]<=qR) { tr[id].Max+=k; tr[id].add+=k; return ; } // if (L+1==R) return ; push_down(id); int mid=L+R>>1; if (qL<=p[mid]) add(id*2, L, mid, qL, qR, k); if (qR>=p[mid+1]) add(id*2+1, mid+1, R, qL, qR, k); push_up(id, L, R); } int main() { scanf("%d", &n); for (int i=1; i<=n; i++) scanf("%d%d%d", &b[i].L, &b[i].v, &b[i].R); for (int i=1; i<=n; i++) { int ax=b[i].L, ay=b[i].v, bx=b[i].v, by=b[i].R; a.push_back({ax, ay, by, 1}); a.push_back({bx, ay, by, -1}); p.push_back(ay), p.push_back(by); } sort(a.begin(), a.end(), cmp); sort(p.begin(), p.end()); p.erase(unique(p.begin(), p.end()), p.end()); for (int i=0; i<a.size(); i++) { if (i>0) ans=max(ans, 1ll*tr[1].Max); add(1, 0, p.size()-1, a[i].ay, a[i].by, a[i].add); } printf("%lld", ans); return 0; } /* 4 2 8 9 1 4 7 3 6 8 5 8 10 */
方法二
相比方法一少了套路,多了思考。
首先得想到,最终选了一些人的区间一定是一个交集 [L, R]
对于一类区间问题,可以考虑枚举 L,维护 R。
假设有人[L2, v, R2] 。即满足:L2<v<R2
先分析一下L。如果L2>L,肯定不会在这个区间,这样一定有L=L2,因为是交集嘛。
所以把人按 L2 从小到大排序。每个L2一定满足, L2<=L
考虑人的贡献。也就是考虑R的答案,怎么取值R才可能让 [L2, v, R2] 对 [L, R] 有贡献?
v<=R<=R2 就可以贡献了。分析同 L,因为是交集。
这里用线段树维护 R,L=L2时,[v, R2]+1。然后怎么知道对右端点有贡献有多少人?查询[v, R2] Max。
当L>v时,[v, R2]-1.
此处无代码。但是可以看看:[题解]CF377D Developing Game - 洛谷专栏
T2
第2题 好区间 查看测评数据信息
有n个整数从左往右排成一行,构成序列A[1...n]。
对于1 <= i <= j <= n, 子序列A[i...j]被称为“优质子序列”当且仅当:存在某个整数x,使得x在子序列A[i...j]中恰好出现过一次。
问序列A总共有多少个不同的子序列。
输入格式
第一行,一个整数n, 其中2 <= n <= 200000。
第二行,n个整数,第i个整数是A[i], 1 <= A[i] <= n。
【提示】
有40%的数据, n <= 1000。
输出格式
一个整数。
输入/输出例子1
输入:
10
1 2 1 4 3 3 3 2 2 4
输出:
47
输入/输出例子2
输入:
5
2 2 1 2 1
输出:
12
样例解释
样例二解释:
40%:枚举L,R,判断是否满足条件。
100%:
暴力发现不好操作。次序原因,因为先想区间再判区间里的特殊数比较麻烦。反过来想。先想特殊数(即区间内出现一次的数),再想区间。
考虑每个特殊数对区间的贡献。
一个trict:如果有“只出现一次的数“,都可以考虑记录:pre[a[i]], nxt[a[i]]。分别表示a[i]前面最先出现的数,和后面最先出现的数。
假设被贡献的区间是[L, R]
满足 pre[a[i]]+1<=L<=pos[a[i]] && pos[a[i]]<=R<=lst[a[i]]+1.
又是很多区间。放回二维坐标系。就是一个矩形。
然后转换成矩形面积并集。
#include <bits/stdc++.h> using namespace std; const int N=200005; int n, b[N], t[N], nxt[N], pre[N]; long long ans=0; struct Tree { int cover, len; }tr[N<<4]; struct node { int x, ay, by, add; }; vector<node> a; vector<int> p; bool cmp(node a, node b) { return a.x<b.x; } void push_up(int id, int L, int R) { if (tr[id].cover>=1) tr[id].len=p[R]-p[L]; else tr[id].len=tr[id*2].len+tr[id*2+1].len; } void add(int id, int L, int R, int qL, int qR, int k) { if (qL<=p[L] && p[R]<=qR) { tr[id].cover+=k; push_up(id, L, R); return ; } if (L+1==R) return ; int mid=L+R>>1; if (qL<=p[mid]) add(id*2, L, mid, qL, qR, k); if (qR>=p[mid]) add(id*2+1, mid, R, qL, qR, k); push_up(id, L, R); } int main() { scanf("%d", &n); for (int i=1; i<=n; i++) scanf("%d", &b[i]); for (int i=1; i<=n; i++) { if (t[b[i]]) nxt[t[b[i]]]=i; pre[i]=t[b[i]]; nxt[i]=n+1, t[b[i]]=i; } for (int i=1; i<=n; i++) { int ax=pre[i], ay=i, bx=i, by=nxt[i]; a.push_back({ax, ay, by, 1}); a.push_back({bx, ay, by, -1}); p.push_back(ay), p.push_back(by); } sort(a.begin(), a.end(), cmp); sort(p.begin(), p.end()); p.erase(unique(p.begin(), p.end()), p.end()); for (int i=0; i<a.size(); i++) { if (i>0) ans+=1ll*tr[1].len*(a[i].x-a[i-1].x); add(1, 0, p.size()-1, a[i].ay, a[i].by, a[i].add); } printf("%lld", ans); return 0; }
T3
第3题 连续子区间 查看测评数据信息
有一个1至n的排列P ,如果排列P中的第L个至第R个数是连续的,那么我们称[L,R]是好区间。例如,[1, 3, 2, 5, 4]中的好区间有:
[1,1], [1, 3], [1, 5], [2, 2], [2, 3], [2, 5], [3, 3], [4, 4], [4, 5], [5, 5]。
有q次询问,每次问[L,R]内,有多少子区间是好的?
输入格式
第一行,一个整数n , 1 <= n <= 120000。
第二行,n个整数,第i个整数表示P[i], 所有 1 <= P[i] <= n, 且P[i]互异。
第三行, 一个整数q表示询问的次数, 1 <= q <= 120000。
接下来有q行,每行两个整数:L和R,表示询问区间[L,R]有多少个子区间是好的。
【提示】
有40%数据,1 <= n,q <= 1000。
输出格式
共q行,每行一个整数。
输入/输出例子1
输入:
5
1 3 2 5 4
15
1 1
1 2
1 3
1 4
1 5
2 2
2 3
2 4
2 5
3 3
3 4
3 5
4 4
4 5
5 5
输出:
1
2
5
6
10
1
3
4
7
1
2
4
1
3
1
样例解释
无
搬运一下题解,写的还挺好: