视频讲解I:https://www.bilibili.com/video/av625268851
视频讲解II:https://www.bilibili.com/video/BV1Tk4y1m7VM?p=31
一、题目大意
给出\(n\)个点坐标, 按照\(y\)升序的顺序, 若\(y\)相同, 则按照\(x\)升序的顺序. (不用我们自己排序,是\(y,x\)由小到大的顺序给出的坐标)
一个点坐标小于另一个点坐标的含义是, 横纵坐标都不大于另一个点坐标(保证没有两个点坐标完全相同).
对于给出的\(n\)个点中, 定义该点等级为: 小于该点的所有坐标之和. (左下角星星的个数)
问: 对于\(0 \sim n-1\)的所有等级, 输出有多少个点坐标为该等级.
直接暴力作法是不行的,因为\(x,y\)都是\(32000\),如果创建一个二维的数组就是\(32000*32000\),直接内存就爆炸了~
因为每个输入的\(y\)是升序的,就像是在运用扫描线的思想,在\(y\)轴上有一条扫描线在不断上移。现在就成了不断的查询\(y\)在当前的限制条件下,已经录入了多少个点。
- 为什么不能是扫描线+前缀和呢?
这是因为前缀和在查询是非常优秀,可以做到\(O(1)\),但此题是一边修改(加入点),一边查询统计前缀和(就是前面所有位置节点的数量),前缀和修改的时间复杂度是\(O(N)\),如果有多次就是\(O(N^2)\),现在\(N=32000\),前缀和就是没用的,需要找一个修改和统计时间复杂度都是\(O(logN)\)的方法,这时时间复杂度为:\(O(N*LogN^2)\),才是可以\(AC\)的。
当然,树状数组可以做的题,线段树肯定是可以做的,但树状数组更简单,代码更短。
这题是典型的 二维偏序问题. 由于按照\(y\)的升序给出, 所以我们可以在\(x\)轴上建立树状数组,其内记录对于\(index\)位置,
有多少个点的\(x\)值 <= \(index\),可以快速查询出区间内的前缀和。
二、实现代码
#include <stdio.h>
#include <string.h>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 32010;
int level[N >> 1];
int t[N];
int lowbit(int x) {
return x & -x;
}
//树状数组,一般用于单点修改,区间和查询
//如果是区间修改,它就不是啥办法了,这时需要用到线段树了
//从x开始增加一个数字k,这个就比普通的前缀和数组牛X了,不需要O(N)次修改了
void add(int x, int k) {
for (int i = x; i < N; i += lowbit(i)) t[i] += k;
}
//查询序列前x个数的和,这个比起前缀和就比较LOW了,因为人家是O(1)的,它是O(logN)的
//但是,如果是一边修改,一边查询的话,就是 N*logN*logN < N * N,这划算多了~
int sum(int x) {
int sum = 0;
for (int i = x; i; i -= lowbit(i)) sum += t[i];
return sum;
}
/**
5
1 1
5 1
7 1
3 3
5 5
1 2 1 1 0
*/
int main() {
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
int x, y;
scanf("%d %d", &x, &y); //这里的y没有用到,想想也是,因为是扫描线是从下向上的,与y的具体值无关
x++; //树状数组存储从1开始, 所有x映射都+1。是因为前缀和思想吗?
// 索引i 1 2 3 4 5 6 7 8
// a[i] 2 3 0 5 4 4 3 2
// 树状数组
//树状数组维护的内容其实是前缀和数组
add(x, 1);
//查询在x之前有多少个数字,也就是有多少个星星个数
int cnt = sum(x);
level[cnt]++; //找到了一个左下角有五个星星的,那么五这个桶计数++,这是题意要求的
}
//输出桶,计算所有等级星星的个数
for (int i = 1; i <= n; i++) printf("%d\n", level[i]);
return 0;
}