<think>题目要求:给定n个点,每个点有两个属性(a, b)。对于点i和点j,如果满足(a_i > a_j) 或者 (b_i > b_j),则称i可以KO j。并且这种KO关系具有传递性(即如果i KO j, j KO k,那么i KO k)。要求输出每个点能够KO的点的数量(即每个点能到达的点的数量)。
注意:KO关系是传递的,所以实际上我们要求的是在传递闭包中每个点能到达的点的数量。但是n最大为1e5,直接求传递闭包(O(n^2))是不可行的。
观察:KO关系由两个维度决定。我们可以将问题转化为:在二维平面上有n个点,如果点i的两个坐标都大于等于点j,并且至少有一个严格大于,那么i可以KO j。但是注意,题目中KO关系是“或”的关系:即只要一个坐标大于,就可以KO。而且传递闭包会将所有间接的KO关系也包含进来。
实际上,这种关系是偏序关系(具体是乘积序)。但是注意,由于是“或”的关系,它并不是一个全序,而且传递闭包可能很大。
然而,我们可以利用二维偏序的性质来优化。具体思路如下:
1. 首先,我们注意到,如果点i在a和b两个属性上都大于等于点j(且至少有一个严格大于),那么i可以KO j。但是,题目中KO关系只需要一个属性大于即可,所以实际上,如果点i在a属性上大于点j,那么i就可以KO j,无论b属性如何。同理,如果b属性大于,也可以KO。因此,KO关系实际上是:i可以KO j当且仅当(a_i>a_j)或(b_i>b_j)。
2. 传递闭包:由于传递性,如果存在一条路径i->j(即通过若干中间点),那么i就可以KO j。
3. 我们要求每个点能KO多少个点(包括直接和间接)。
但是,直接计算传递闭包是O(n^2)的,无法承受。
另一种思路:考虑KO关系实际上是一个有向图,图中存在边i->j当且仅当(a_i>a_j)或者(b_i>b_j)。然后求每个点能到达的点的数量。
然而,这个图的边数可能达到O(n^2),因此不能显式建图。
我们需要更高效的方法。
注意到:这个图实际上可以由两个完全图(分别按a和按b排序)组合而成。具体来说,如果按a排序,那么每个点会向所有a值比它小的点连边(按a属性连的边),同理按b排序,每个点会向所有b值比它小的点连边。但是,由于是“或”的关系,边是这两种边的并集。
那么,传递闭包怎么求?实际上,我们可以利用排序和树状数组/线段树来优化。
但是,有一个重要的性质:如果我们定义点i支配点j当且仅当a_i>a_j且b_i>b_j(即二维偏序中的严格支配),那么显然i可以KO j。但是,反过来不成立:因为可能a_i>a_j而b_i<=b_j,这时i也可以KO j(直接KO),但j可能通过其他路径KO一些点,然后i再通过传递性KO那些点。
实际上,我们可以将点按照a降序排序,然后考虑b。但是,传递闭包在图上很难直接计算。
参考:在DAG中,求每个点能到达的点的个数,一般用拓扑排序+状态压缩(bitset)来求传递闭包,但是n=1e5,bitset空间O(n^2)需要12.5GB,不可行。
因此,我们需要更巧妙的办法。
观察:实际上,如果点i的a和b都很大,那么它能KO的点就很多。而且,如果一个点i的a和b都不是很大,但是存在一个点j,使得j的a大于i的a,且j的b大于i的b,那么i能KO的点j都能KO(因为i能KO的点j都能直接KO,而且j还能KO更多)。所以,实际上,我们只需要考虑那些“极大点”(即不被其他任何点支配的点)?但是,题目要求每个点能KO的点的数量,包括间接KO。
另一种思路:考虑KO关系的逆否关系:如果i不能KO j,那么必须满足a_i<=a_j且b_i<=b_j。所以,i不能KO j当且仅当j在二维偏序上“不小于”i(注意,题目中同一网站没有相同值,所以不会出现相等)。那么,i能KO的点的数量 = n - 1 - (满足a_j>=a_i且b_j>=b_j的j的个数)?不对,因为即使j满足a_j>=a_i且b_j>=b_i,i仍然可能通过其他路径KO j(传递性)。
例如:有3个点:A(1,3), B(2,2), C(3,1)。则A不能直接KO C(因为1<3且3>1,所以不满足a_A>a_C且b_A>b_C?实际上,A可以直接KO C吗?注意题目:只要有一个属性大于即可。A的b属性3>1,所以A可以KO C。所以,A能KO C。因此,上面的逆否命题不成立。
实际上,i不能KO j当且仅当a_i<=a_j且b_i<=b_j?不对,因为如果a_i<=a_j且b_i<=b_j,那么i肯定不能KO j(因为两个属性都不大于)。但是,反过来,如果i不能KO j,那么一定有a_i<=a_j且b_i<=b_j吗?是的,因为如果有一个属性大于,那么i就可以KO j。所以,i不能KO j当且仅当a_i<=a_j且b_i<=b_j。
因此,每个点i能KO的点的数量就是:总数n - 1(减去自己)减去(满足a_j>=a_i且b_j>=b_i的点的个数)?注意,这里包括自己(因为自己满足a_i>=a_i且b_i>=b_i),所以减去(满足a_j>=a_i且b_j>=b_i的点的个数)后,还要加回自己(因为自己不应该被减去)?不对,因为自己不能KO自己(题目中没有说可以KO自己,而且自己KO自己也不符合定义)。所以,实际上,i能KO的点的个数 = n - 1 - (满足a_j>=a_i且b_j>=b_i的点的个数) + ?注意,我们减去的点包括i自己,但是i自己不在不能KO的集合中(因为不能KO自己,但这里我们考虑的是其他点),所以实际上,满足a_j>=a_i且b_j>=b_i的点的个数(包括i自己),那么i不能KO的点的个数就是这个集合的大小减去1(减去自己)。所以:
i能KO的点的个数 = n - 1 - [ (满足a_j>=a_i且b_j>=b_i的点的个数) - 1 ]
= n - (满足a_j>=a_i且b_j>=b_i的点的个数)
但是,这个结论对吗?我们看上面的例子:点A(1,3),那么满足a_j>=1且b_j>=3的点有几个?只有A自己(因为B(2,2)的b=2<3,C(3,1)的b=1<3)。所以A能KO的点数=3-1=2。实际上,A能KO B(因为b=3>2)和C(因为b=3>1),所以确实KO了2个。点B(2,2):满足a_j>=2且b_j>=2的点:只有B自己(因为A的b=3>=2,但是a=1<2,所以不满足;C的a=3>=2,但b=1<2,所以不满足)。所以B能KO的点数=3-1=2。实际上,B能KO A(因为a=2>1)和C(因为a=2<3?不对,B的a=2,C的a=3,所以a_B<a_C,但是b_B=2>1,所以B可以KO C。同时,B也可以KO A(a_B=2>1)。所以KO了2个。点C(3,1):满足a_j>=3且b_j>=1的点:有C自己,还有A?A的a=1<3,不满足;B的a=2<3,不满足。所以只有1个。所以C能KO的点数=3-1=2?不对,实际上C能KO谁?C的a=3最大,所以可以KO所有a小于3的点,即A和B(因为A的a=1<3,B的a=2<3)。所以C能KO2个。但是我们的公式计算出来是2,但题目要求输出的是3(因为样例输出是2,2,0,3,这个样例是4个点)?不对,我们重新看样例输入。
重新看Sample Input:
4
2 3
3 2
1 1
4 5
按照公式:
点1(2,3):满足a_j>=2且b_j>=3的点:点1(2,3)和点4(4,5) -> 2个,所以能KO的点数=4-2=2。
点2(3,2):满足a_j>=3且b_j>=2的点:点2(3,2)和点4(4,5) -> 2个,所以能KO的点数=4-2=2。
点3(1,1):满足a_j>=1且b_j>=1的点:所有点(4个),所以能KO的点数=4-4=0。
点4(4,5):满足a_j>=4且b_j>=5的点:只有点4 ->1个,所以能KO的点数=4-1=3。
所以输出:2,2,0,3 符合样例。
因此,问题转化为:对于每个点i,求满足a_j>=a_i且b_j>=b_i的点的个数(包括自己)。
这就是一个二维偏序问题(求右上角的点的个数),可以用排序+树状数组解决。
具体步骤:
1. 将点按a从小到大排序,如果a相同,按b从小到大排序(注意题目同一网站没有相同值,所以a和b都不会相同?但是不同点可能a相同吗?题目没有说,所以假设a和b都可能相同?题目说:同一网站中,没有人做题数量一样。所以同一个网站的值互不相同。但是两个网站之间没有限制,所以可能一个点的a等于另一个点的b,但不会出现两个点的a相同,也不会出现两个点的b相同。所以a值互不相同,b值互不相同?不对,题目只保证同一网站中,没有相同的值。所以a值互不相同,b值可能相同?不对,题目说“第二个数表示小孩在cf上的做题数量”,也是同一网站,所以b值也互不相同。所以每个点的a和b都是唯一的,但是a和b之间可能相同(比如一个点的a等于另一个点的b)?但是不影响。
所以,我们按a从小到大排序,然后按b值做树状数组。但是注意,我们要求的是右上角(即a_j>=a_i且b_j>=b_i),所以我们可以按a从大到小排序,然后求b的逆序数(但这里是求大于等于当前b的个数,所以用树状数组求后缀和?或者将b取负转化为求前缀和)。
具体:
- 将点按a从大到小排序(a相同的,按b从大到小排序?但是a不会相同,所以无所谓)。
- 然后按顺序遍历,对于当前点,我们要求之前遍历过的点中b>=当前点b的个数(因为按a从大到小,所以之前遍历过的点a都大于等于当前点,所以只需要看b)。
- 但是注意,a相等的情况不会出现,所以按a降序排序后,当前点之前的点a都大于等于当前点(因为a都不同,所以都是大于)。那么,我们只需要统计b>=当前点b的点的个数。
但是,我们要求的是右上角(包括自己),所以自己也会被统计到。所以我们在遍历时,每处理一个点,就将它的b插入树状数组,然后查询当前b到最大b之间的点数(即后缀和)。
但是,树状数组一般用于前缀和,我们可以将b坐标离散化,然后从大到小排序,然后树状数组维护从大到小的b值(即大的b值在树状数组的前面)。或者,我们可以将b取负数,然后转化为求前缀和。
步骤:
1. 离散化b:将b值离散化到1~m(m为不同b的个数)。
2. 将点按a降序排序(从大到小)。
3. 初始化树状数组(大小为m)。
4. 按排序后的顺序遍历每个点:
在当前点,查询树状数组中b值大于等于当前点b的点的个数(即后缀和)。由于树状数组只能求前缀和,我们可以将b离散化后,用m+1减去当前b的排名,然后求前缀和。或者,我们改变树状数组的更新方向:从大到小更新,然后查询后缀和?树状数组可以支持后缀和,但通常我们使用前缀和。
另一种方法:将b值取负,然后从小到大排序,这样b值大的取负后小,然后按a降序排序,再按取负后的b升序排序(相当于原b的降序),然后树状数组维护取负后的b值(即原b值大的,取负后小,在树状数组的前面)。这样,我们查询小于等于当前取负b的个数(即原b>=当前b的个数)就是前缀和。
但是,为了简单,我们可以将b从大到小排序,然后离散化,然后树状数组维护后缀和?或者,我们直接按b降序排序,然后树状数组维护前缀和(但需要按a降序,同时b降序,这样相同的a不会出现,但b降序后,我们插入时按b降序,那么之前插入的点的b都大于等于当前点?不对,因为按a降序,b降序,那么当前点之前的点a都大于等于当前点,b都大于等于当前点?不对,因为a降序,b降序,那么当前点之前的点a都大于等于当前点,b都大于等于当前点(因为排序后,前面的点a更大,b更大)。所以,当我们遍历到当前点时,树状数组中已经插入的点都是a大于等于当前点的(因为按a降序,所以已经插入的点的a都大于等于当前点),并且b都大于等于当前点(因为b降序,所以当前点之前的点b都大于等于当前点)?不对,按b降序排序后,当前点之前的点b都大于等于当前点,但是a不一定大于等于当前点?因为排序是首先按a降序,然后按b降序。所以,在a不同的情况下,a大的排在前面,a小的排在后面。当a相同时,按b降序(但a不会相同)。所以,当前点之前的点a都大于当前点(因为a都不同,且从大到小),所以b值可以任意。因此,我们无法保证b都大于等于当前点。
所以,我们只能按a降序排序,然后对b值进行统计。具体做法是:在按a降序排序后,我们按照这个顺序将每个点的b值插入树状数组,同时查询当前树状数组中b值大于等于当前点b的点的个数。但是树状数组如何查询后缀和?我们可以转化为:总插入点数 - 小于当前b的点数。但是,我们要求的是大于等于当前b的点的个数,包括当前点自己(因为当前点已经插入?我们在查询前插入还是查询后插入?)
注意:为了避免当前点自己影响,我们可以在查询后插入。具体:
1. 按a从大到小排序(a相同不会出现,所以不用考虑)。
2. 遍历每个点:
查询树状数组中b值大于等于当前点b的点的个数(即已经插入的点中,b值大于等于当前b的个数)。注意,此时当前点还没有插入,所以已经插入的点都是a大于当前点a的(因为a严格大于,因为a都不同),或者a相等(但不会出现)?所以已经插入的点a都大于当前点,那么这些点中b值大于等于当前点b的个数,就是我们要的?不对,我们要的是所有a>=当前点a且b>=当前点b的点的个数(包括当前点自己)。但是,这里我们还没有插入当前点,所以已经插入的点都是a大于当前点的(因为a降序),所以这些点中b>=当前点b的个数,再加上当前点自己(因为自己也算)?不对,我们要求的是整个集合(包括当前点)中满足条件的点的个数。所以,在遍历到当前点时,还没有插入当前点,那么查询的结果是a大于当前点的点中b>=当前点b的个数。然后我们插入当前点,然后当前点自己也算一个,所以总数应该是查询结果+1(当前点)?但是,注意,我们要求的是所有a>=当前点a(包括自己)且b>=当前点b(包括自己)的点的个数。所以,在插入当前点后,这个值就是查询结果(已经插入的点中b>=当前点b的个数,因为已经插入的点都是a>=当前点a的(按a降序,当前点之前的点a都大于等于当前点,当前点自己还没插入,所以查询结果就是a大于当前点的点中b>=当前点b的个数)加上当前点自己(插入后,当前点自己满足条件,所以总数=查询结果+1)。但是,注意,在按a降序排序时,当前点之前的点a都大于当前点(因为a都不同),所以a大于当前点的点就是之前插入的所有点。因此,总数=查询结果(已经插入的点中b>=当前点b的个数)+1(当前点)。
但是,这样计算的是当前点所在的位置的“右上角”点数(包括自己)。然后,我们记录这个值cnt[i](对于点i,满足a_j>=a_i且b_j>=b_i的点的个数为cnt[i])。
然后,点i的答案就是:n - cnt[i]
但是,注意:在树状数组查询时,我们要求的是已经插入的点中b值大于等于当前点b的个数。树状数组可以这样查询:如果b的取值范围是1~m(离散化后),那么查询[b_i, m]区间的和。
步骤:
1. 离散化b:将所有b值收集起来,排序,然后离散化(因为b值互不相同,所以离散化后每个b值对应一个不同的rank,从1到n)。
2. 将点按a降序排序(从大到小)。
3. 初始化树状数组(大小为n)。
4. 遍历每个点(按排序后的顺序):
rank_b = 离散化后当前点b的排名(注意,我们希望b值大的对应的rank小?还是大?因为树状数组我们一般用前缀和,所以如果我们希望查询[b_i, m]的区间和,那么我们可以用树状数组的后缀和,或者用总点数减去[1, b_i-1]?但是树状数组通常维护前缀和,所以我们可以:
令c[i]表示离散化后b值为i的点的个数(初始为0),树状数组维护前缀和。
那么,查询大于等于当前b的点的个数 = 总插入点数(即已经插入的点数) - 小于当前b的点的个数(即查询[1, rank_b-1])?不对,因为我们要大于等于,所以包括等于,所以应该是:
总插入点数 - 小于当前b的点的个数(即[1, rank_b-1])?但是这样不包括等于当前b的点?不对,等于当前b的点在[rank_b, rank_b]区间,所以我们需要的是[rank_b, m]区间的和。
树状数组可以支持区间查询,但通常用前缀和相减:sum(m) - sum(rank_b-1) 就是[rank_b, m]的和。
但是,我们还没有插入当前点,所以当前点的b还没有在树状数组中。所以,我们查询[rank_b, m]的和,就是已经插入的点中b值大于等于当前b的点的个数。
然后,我们插入当前点:将树状数组中位置rank_b加1。
5. 然后,当前点的cnt[i] = 查询结果(即已经插入的点中b>=当前点b的个数) + 1(当前点自己)。
6. 然后,答案ans[i] = n - cnt[i]。
但是,注意:在按a降序排序时,a相同的点?题目中a不会相同,所以没问题。
但是,这里有一个问题:当前点自己还没有插入,所以查询结果中不包含自己,然后我们加上自己(+1)就是所有a大于等于当前点a(因为a大于等于的只有当前点自己和之前插入的点,而之前插入的点a都大于当前点)且b大于等于当前点b的点的个数。
然后,我们记录cnt[i] = 查询结果+1。
最后,答案就是n - cnt[i]。
但是,我们还要注意:在遍历顺序中,我们按a降序,所以当a相等时(但题目不会相等),需要特殊处理。这里a不会相等。
所以,代码步骤:
1. 读入n,n个点,每个点(a,b)。
2. 将b值离散化:将所有的b收集起来,排序,然后去重(因为b互不相同,所以去重后大小还是n),然后建立映射:b值->rank(1~n)。
3. 将点按a从大到小排序(降序)。
4. 初始化树状数组(大小为n)。
5. 创建一个数组cnt,长度n。
6. 按排序后的顺序遍历每个点i(原编号需要记录,因为输出按原编号):
令x = 当前点的b的离散化后的值(rank)。
查询树状数组中区间[x, n]的和(即大于等于当前b的点的个数,注意,此时树状数组中只有a比当前点大的点,因为按a降序,之前插入的点a都大于当前点)。
令cnt[i] = 查询结果 + 1 (加上自己)
然后,将树状数组位置x加1(插入当前点)。
7. 然后,对于每个点,答案ans[原编号] = n - cnt[i]。
8. 按原编号顺序输出ans(即第一个点对应原编号0,注意输入顺序:第i行输入的是第i个小孩)。
但是,注意:在树状数组中,我们查询的是区间[x, n]的和,我们可以用树状数组的区间查询:sum(n)-sum(x-1)。但是,树状数组通常维护前缀和,所以我们可以先求前缀和,然后做差。
树状数组模板:
```cpp
int tree[N];
int lowbit(int x) { return x & -x; }
void update(int x, int d) {
while (x <= n) {
tree[x] += d;
x += lowbit(x);
}
}
int sum(int x) { // 前缀和[1,x]
int res = 0;
while (x) {
res += tree[x];
x -= lowbit(x);
}
return res;
}
// 查询区间[l, r] = sum(r)-sum(l-1)
```
所以,查询[x, n] = sum(n) - sum(x-1)
注意:n是树状数组的大小(离散化后b的个数,即n)。
但是,树状数组的大小是离散化后的b的个数,即n,所以树状数组下标从1到n。
但是,我们离散化后,b的排名就是1~n。
所以,我们可以这样查询。
但是,注意:树状数组的n是固定的,就是离散化后的最大值(即n)。
然后,在遍历过程中,我们按a降序排序,然后对每个点,先查询,再更新。
最后,每个点的答案就是n - cnt[i](cnt[i]是满足a_j>=a_i且b_j>=b_i的点的个数)。
注意:这里n是总点数,所以答案就是n - cnt[i]。
但是,我们还要注意,同一个点可能被重复计算?不会,因为每个点只处理一次。
另外,树状数组的更新和查询顺序:先查询再更新,这样查询结果中不包含当前点。
最后,输出时,要按输入顺序输出。所以我们在排序前需要记录原始编号。
代码步骤:
1. 读入n,然后n行,每行两个整数a,b。
2. 将每个点存储为:id, a, b。id从0到n-1(或1到n)。
3. 收集所有b值,排序,然后离散化(用map或者数组映射)。
4. 将点按a降序排序(如果a相同,按b降序?但是a不会相同,所以按a降序即可)。
5. 初始化树状数组(大小为n)。
6. 遍历排序后的每个点:
得到当前点的离散化后的b值:x = rank(b)
查询区间[x, n]的和:s = sum(n) - sum(x-1)
cnt = s + 1 (因为当前点自己还没加入,所以加上自己)
记录这个点的结果:ans[原id] = n - cnt
更新树状数组:update(x, 1)
7. 按原id顺序(0到n-1)输出ans[i]。
但是,注意:树状数组的sum(n)是求[1,n]的和,即当前已经插入的点的总数(在树状数组中的点)。但是,在查询时,我们还没有插入当前点,所以已经插入的点数是当前遍历的序号减1(即i-1)。那么,查询的区间[x, n]的和就是已经插入的点中b值大于等于当前b的点的个数。
然后,当前点自己满足条件的个数就是s+1。
所以,ans = n - (s+1)
但是,我们输出的是每个点能KO的点的个数,这个公式正确。
注意:树状数组的sum函数,当x-1=0时,sum(0)=0,所以查询[x,n]就是sum(n)-sum(0)?不对,应该是sum(n)-sum(x-1),当x=1时,sum(0)不存在,所以我们在树状数组中,下标从1开始,所以x>=1,那么x-1>=0,但是树状数组的sum(x)要求x>=0,所以x-1可能为0,而我们的树状数组下标从1开始,所以sum(0)=0。
所以,查询[x, n] = sum(n) - sum(x-1)
但是,注意:树状数组的sum函数中,x不能为0(因为下标从1开始),所以当x=1时,sum(x-1)=sum(0)=0。
所以,代码实现:
```cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <cstring>
using namespace std;
const int MAXN = 100010;
struct Point {
int id, a, b;
} points[MAXN];
int n;
int tree[MAXN];
int ans[MAXN];
bool cmpA(const Point& x, const Point& y) {
if (x.a != y.a)
return x.a > y.a; // a从大到小
// 题目a不会相同,所以可以不加b的比较,但加上以防万一
return x.b > y.b;
}
inline int lowbit(int x) {
return x & -x;
}
void update(int x, int d) {
while (x <= n) {
tree[x] += d;
x += lowbit(x);
}
}
int sum(int x) {
int res = 0;
while (x) {
res += tree[x];
x -= lowbit(x);
}
return res;
}
int main() {
scanf("%d", &n);
vector<int> all_b;
for (int i = 0; i < n; ++i) {
scanf("%d%d", &points[i].a, &points[i].b);
points[i].id = i;
all_b.push_back(points[i].b);
}
// 离散化b
sort(all_b.begin(), all_b.end());
all_b.erase(unique(all_b.begin(), all_b.end()), all_b.end());
// 建立映射:b值 -> 排名(从1开始)
for (int i = 0; i < n; ++i) {
// 注意,离散化后的排名:在all_b中的位置+1
int pos = lower_bound(all_b.begin(), all_b.end(), points[i].b) - all_b.begin();
points[i].b = pos + 1; // 将b值替换为离散化后的值(1~n)
}
// 按a降序排序
sort(points, points + n, cmpA);
// 初始化树状数组
memset(tree, 0, sizeof(tree));
// 遍历
for (int i = 0; i < n; ++i) {
int x = points[i].b;
// 查询区间[x, n]的和
int s = sum(n) - sum(x-1); // 已经插入的点中b>=x的个数
// 当前点满足条件的点个数 = s(已经插入的点中满足的) + 1(自己)
int cnt = s + 1;
// 答案:n - cnt
ans[points[i].id] = n - cnt;
// 更新树状数组
update(x, 1);
}
// 输出
for (int i = 0; i < n; ++i) {
printf("%d\n", ans[i]);
}
return 0;
}
```
注意:树状数组的大小是n(离散化后b的最大值,为n),所以update和sum时,x的范围是1~n。
但是,有一个问题:在离散化b时,我们将b值映射到1~n,然后树状数组下标从1到n,所以没有问题。
但是,在查询时,当x=1时,x-1=0,sum(0)=0,所以查询[x, n] = sum(n) - sum(0) = sum(n) - 0,正确。
当x>1时,也正确。
测试样例:
4
2 3 -> 点0: a=2, b=3 -> 离散化后b: 3在all_b排序后:1,2,3,5 -> 排序后all_b: [1,2,3,5] -> 3的排名是2(从0开始)-> 离散化后b=3(排名2+1=3)? 不对,我们重新算:
输入b值:3,2,1,5 -> 排序后:1,2,3,5 -> 所以:
点0: b=3 -> 排名2(下标从0开始),所以离散化后b=3(2+1=3)?
点1: b=2 -> 排名1,离散化后b=2
点2: b=1 -> 排名0,离散化后b=1
点3: b=5 -> 排名3,离散化后b=4
然后按a降序排序:
点3: (4,5) -> a=4最大 -> 离散化后b=4
点1: (3,2) -> a=3 -> 离散化后b=2
点0: (2,3) -> a=2 -> 离散化后b=3
点2: (1,1) -> a=1 -> 离散化后b=1
遍历:
点3: x=4
查询[4,4]的和:当前树状数组为空,s=0
cnt=0+1=1
ans[3]=4-1=3
更新:tree[4]=1
点1: x=2
查询[2,4]的和 = sum(4)-sum(1) = (tree[4]+tree[2]? 但是目前只有tree[4]=1,而树状数组的前缀和:sum(4)=1, sum(1)=0(因为tree[1..3]都是0)? 所以s=1-0=1
cnt=1+1=2
ans[1]=4-2=2
更新:tree[2]加上1,所以tree[2]=1, tree[4]=1(树状数组更新:update(2,1) -> 然后tree[2]加1,然后2+lowbit(2)=2+2=4,所以tree[4]加1,变成2)
点0: x=3
查询[3,4]的和 = sum(4)-sum(2) = (tree[4]+tree[3]? 但是tree[3]还没更新,所以sum(4)= tree[4](下标4)的值+ tree[3]的值?树状数组的结构:tree[4]=2(因为更新了两次:点3和点1更新时都更新了tree[4]?不对,树状数组的更新:点3更新位置4:tree[4]=1;点1更新位置2:tree[2]=1,然后因为2+lowbit(2)=4,所以tree[4]+=1 -> tree[4]=2。然后sum(4)= tree[4] + tree[3]? 不对,树状数组的前缀和:
sum(4) = tree[4] = 2
sum(2) = tree[2] = 1
所以[3,4]的和 = 2-1=1
cnt=1+1=2
ans[0]=4-2=2
然后更新位置3:update(3,1) -> tree[3]=1,然后3+lowbit(3)=3+1=4,所以tree[4]加1,变成3。
点2: x=1
查询[1,4]的和 = sum(4)-sum(0)=3-0=3
cnt=3+1=4
ans[2]=4-4=0
输出:ans[0]=2, ans[1]=2, ans[2]=0, ans[3]=3 -> 按原id顺序:id0:2, id1:2, id2:0, id3:3 -> 输出:
2
2
0
3
符合样例。
所以,代码正确。
注意:树状数组更新时,注意下标范围。
时间复杂度:O(n log n),满足n=1e5。
注意:树状数组的sum(n)可以提前计算,但是这里我们每次都要计算,所以每次查询是O(log n),总O(n log n)。
空间复杂度:O(n)。
提交时注意:内存128MB,树状数组大小1e5,可以接受。
代码中使用scanf和printf,避免超时。
注意:离散化时,b值在int范围内,所以用vector排序离散化。
完整代码:</think>### 问题分析
题目要求计算每个小孩能在机房里KO其他小孩的数量。KO关系的定义是:如果小孩A在任意一个网站(gdgzoi或codeforces)上的做题数量比小孩B多,则A可以KO B,且KO关系具有传递性。我们需要计算每个小孩通过直接或间接关系能够KO的其他小孩总数。
### 优化思路
1. **问题转换**:
- 每个小孩有两个属性(a, b),分别表示gdgzoi和codeforces的做题数量。
- 小孩i能KO小孩j的条件是:`a_i > a_j` 或 `b_i > b_j`。
- 传递闭包意味着我们需要计算所有可达点的数量。
2. **关键观察**:
- 小孩i **不能KO** 小孩j当且仅当 `a_i ≤ a_j` 且 `b_i ≤ b_j`。
- 因此,小孩i能KO的小孩数 = `n - 1 - (满足 a_j ≥ a_i 且 b_j ≥ b_i 的 j 的数量)`。
3. **二维偏序求解**:
- 使用树状数组高效计算满足 `a_j ≥ a_i` 且 `b_j ≥ b_i` 的点的数量。
- 步骤:
1. 离散化b属性值。
2. 按a属性降序排序。
3. 遍历每个点,查询树状数组中b值≥当前点b值的数量。
4. 更新树状数组。
### 完整代码(C++)
```cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <cstring>
using namespace std;
const int MAXN = 100010;
struct Child {
int id, a, b;
} children[MAXN];
int n;
int tree[MAXN];
int ans[MAXN];
bool cmpA(const Child& x, const Child& y) {
return x.a > y.a;
}
inline int lowbit(int x) {
return x & -x;
}
void update(int x, int d) {
while (x <= n) {
tree[x] += d;
x += lowbit(x);
}
}
int query(int x) {
int res = 0;
while (x) {
res += tree[x];
x -= lowbit(x);
}
return res;
}
int main() {
scanf("%d", &n);
vector<int> all_b;
for (int i = 0; i < n; ++i) {
scanf("%d%d", &children[i].a, &children[i].b);
children[i].id = i;
all_b.push_back(children[i].b);
}
// 离散化b属性
sort(all_b.begin(), all_b.end());
all_b.erase(unique(all_b.begin(), all_b.end()), all_b.end());
for (int i = 0; i < n; ++i) {
int pos = lower_bound(all_b.begin(), all_b.end(), children[i].b) - all_b.begin();
children[i].b = pos + 1;
}
// 按a属性降序排序
sort(children, children + n, cmpA);
// 初始化树状数组
memset(tree, 0, sizeof(tree));
// 处理每个点
for (int i = 0; i < n; ++i) {
int b_val = children[i].b;
// 查询[b_val, n]区间的点数
int count = query(n) - query(b_val - 1);
// 结果 = n - (右上角点数)
ans[children[i].id] = n - (count + 1);
// 更新树状数组
update(b_val, 1);
}
// 输出结果
for (int i = 0; i < n; ++i) {
printf("%d\n", ans[i]);
}
return 0;
}
```
### 代码解释
1. **数据结构**:
- `struct Child`: 存储小孩的ID和两个网站的做题数量。
- `tree[]`: 树状数组,用于高效查询区间和。
2. **离散化b属性**:
- 收集所有b值,排序去重后映射到1~n的整数,减少树状数组空间。
3. **排序**:
- 按a属性降序排序,确保处理每个点时,已处理的点a值都更大。
4. **树状数组操作**:
- **查询**:计算当前点右上角的点数(`b_val`到最大值区间的点数)。
- **更新**:将当前点的b值加入树状数组。
5. **结果计算**:
- 每个小孩能KO的数量 = `n - 1 - (右上角点数 - 1)` = `n - 右上角点数`。
### 复杂度分析
- **时间复杂度**:O(n log n),主要来自排序和树状数组操作。
- **空间复杂度**:O(n),用于存储树状数组和离散化映射。
###