【题目描述】
原题来自:Ural 1028
天空中有一些星星,这些星星都在不同的位置,每个星星有个坐标。如果一个星星的左下方(包含正左和正下)有 k 颗星星,就说这颗星星是 k 级的。

例如,上图中星星 5 是 3 级的(1,2,4 在它左下),星星 2,4 是 1 级的。例图中有 1 个 0 级,2 个 1 级,1 个 2 级,1 个 3 级的星星。
给定星星的位置,输出各级星星的数目。
一句话题意:给定 n 个点,定义每个点的等级是在该点左下方(含正左、正下)的点的数目,试统计每个等级有多少个点。
【输入】
第一行一个整数 N,表示星星的数目;
接下来 N 行给出每颗星星的坐标,坐标用两个整数 x,y表示;
不会有星星重叠。星星按 y 坐标增序给出,y 坐标相同的按 x 坐标增序给出。
【输出】
N 行,每行一个整数,分别是 0 级,1 级,2 级,......,N−1 级的星星的数目。
【输入样例】
5
1 1
5 1
7 1
3 3
5 5
【输出样例】
1
2
1
1
0
【提示】
数据范围与提示:
对于全部数据,1≤N≤1.5×10^4,0≤x,y≤3.2×10^4 。
在信息学奥赛的数据结构模块中,树状数组往往以"单点修改、区间查询"的差分形态登场。但今天我们要解析的这道经典名题------数星星 ,将向大家展示树状数组的另一层高端形态:权值树状数组(频次桶的前缀和)。
一、 题目分析
【题目模型】 给定平面直角坐标系上的N颗星星的坐标。规定一颗星星的"等级"为:在它左边及下边(包含正左和正下)的星星总数。求等级为0到N-1的星星分别有多少颗。
【隐藏的条件】 题目中通常会带有一句极其关键的输入说明:星星的坐标按 Y 坐标升序给出,Y 坐标相同时按 X 坐标升序给出。 这个输入顺序,是我们解题的关键
二、 思考过程(从暴力到降维)
-
萌新的做法(暴力枚举): 开个双重
for循环,对于每一颗星星,都去和前面所有的星星比对一遍X和Y坐标。 结局: 时间复杂度高达O(N^2),在 N≤32000 的数据量下,运算量破亿,注定超时。 -
大神的凝视(利用单调性): 既然输入数据已经按 Y 坐标排好序了 ,这就意味着:当我们读入第
i颗星星时,前面读入的i-1颗星星的Y坐标必定全都小于等于当前星星, 既然Y坐标的条件天然满足,我们还需要管Y吗?完全不需要!问题瞬间被降维成了一维问题:在我们已经读入的星星中,有多少颗星星的X坐标小于等于当前星星的X坐标?
三、 解题思路
既然是求"小于等于某个 X 的数量",这不就是求频次数组的前缀和吗?
-
建立频次桶(权值树状数组): 我们不再用树状数组的下标表示"第几个元素",而是用下标表示真实的X坐标。 树状数组c底层维护的数组a
[i](实际并没有开这个数组),记录的是"横坐标为i的位置上有几颗星星"。 -
操作流转(边读边算):
-
每读入一颗星星的横坐标
x。 -
先查询: 统计目前横坐标在
x及x以内的星星总数,即前缀和。因为当前这颗星星准备加进去但还没加,这个前缀和就是它的"等级"。(如果先更新再查询,查出来的结果减去 1 也是一样的逻辑)。 -
后更新: 把当前星星的横坐标加入树状数组,即该位置的频次+1。
-
记答案: 将对应等级的星星总数数组
cnt[level]增加1。
-
四、 时空复杂度分析
-
时间复杂度: 每读入一颗星星,只需执行一次查询和一次更新。树状数组单次操作复杂度为
O(logW)(W为坐标最大值 32000)。总计N颗星星,总时间复杂度为O(NlogW),很快。
-
空间复杂度: 仅需开辟大小为坐标上限的树状数组
c和答案数组cnt,空间复杂度为O(W+N),极其节省内存。
五、 易错点总结
这段代码看似简单,但实际做题中很多同学容易写错。以下三个易错点必须避开:
-
死循环(原点): 题目中X坐标是可以为0的,但树状数组的下标绝对不能为0 (因为
lowbit(0)=0会导致update陷入死循环)。 破解法: 读入所有X坐标后,强制执行x++,把整个坐标系向右平移1个单位。 -
越界: 既然我们将所有的X坐标加了1,那么原本最大为32000的坐标就会变成 32001。此时如果树状数组更新循环的边界还是写
<=32000,这颗极限边缘的星星就会被吞掉,导致由于访问负数下标而引发的RE或WA。 解决方法**:** 循环边界多开一点余量,写成i<=32005。 -
概念混淆: 请注意,这里的树状数组底层维护的是频次桶,而不再是前一道模板题里的"差分数组"。切勿将两者的物理意义搞混。
六、 完整代码
cpp
//单点修改 区间查询
#include <iostream>
using namespace std;
int n;
//树状数组本体,下标代表X坐标,存储的是该坐标区间内的星星频次
int c[32100];//树状数组(每个横坐标)
int cnt[32010];//记录每个等级星星数目
//返回x的最右边的最低位的1所代表的整数
int lowbit(int x){
return x&(-x);
}
//单点修改 在横坐标为x的位置增加val颗星星
void update(int x,int val){
//注意边界:因为平移了坐标,上限必须大于32000,这里取32005
for(int i=x;i<=32005;i+=lowbit(i)){
c[i]+=val;
}
}
//区间查询 把截至目前所有横坐标小于等于x的点的总数加出来
int query(int x){
int ret=0;//记录前缀和
while(x>0){
ret+=c[x];
x-=lowbit(x);
}
return ret;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cin>>n;
for(int i=1;i<=n;i++){
int x,y;
cin>>x>>y;
//x不能为0,不然会死循环 整体向右平移一个单位
//横坐标为x的点加一
update(x+1,1);
//查询当前星星的等级
//因为前面已经把自己update进去了,所以当前的前缀和包含了自己
// 减去1才是真正的"左下角星星数量"
//query(x+1)-1 代表当前这个坐标的等级
//-1是因为要剪掉自己
cnt[query(x+1)-1]++;
}
// 输出从 0 级到 n-1 级各有多少颗星星
for(int i=0;i<n;i++) cout<<cnt[i]<<"\n";
return 0;
}