L - D CodeForces - 668D 树状数组

本文详细解析 CodeForces 668D 题目,介绍如何利用树状数组和 map 实现高效的时间轴操作,包括在特定时刻向容器添加或移除元素,以及查询特定元素在某一时刻的数量。通过离散化和前缀和计算,解决了大规模数据范围的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目链接:http://codeforces.com/problemset/problem/668/D

题意: 有一个时间轴一个multiset容器,执行n(1 ~ 1e5)次操作:

操作1 是  在第a时刻 向容器中加入一个数b,

操作2 是  在第a时刻 从容器中取出一个数b,

操作3 时 输出在第a时刻 容器中b的数量。

思路:刚开始看到这题就像线段树或主席树,后面发现其实就只是计算个前缀和,以时间的前缀和,但这里的数据很大1e9的时间,b的范围也是1e9,但是总的操作次数只有1e5所以我们可以开个map的数组记录时间轴,再用一个map来存每一个出现的数字b这样map的数组大小就可以只开1e5,因为最多存1e5个不同的数字,用树状数组来计算前缀和,这样map数组中的数字最多也就记录log(1e9),也就是数组的每个map元素记录时间不超过30个,内存就妥妥的了。

接下来就是实现了:

#include <cstdio>
#include <queue>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <map>
#include <set>
#include <string>
#include <cmath>
#include <stack>
#define ll long long
#define ull unsigned long long
#define exp 0.00000001
using namespace std;
const int mxx = 1e6+5;
const int mx = 1e9+5;
map<int, int>ma[mxx];///ma[每个数字b(第几个数)][时间轴];
map<int, int>mma;///每个数字b离散为第几个没出现的数
///树状数组模板。。。
int bit(int x)
{
    return x&(-x);
}

void up(int x, int y, int k)
{
    while (x < mx)
    {
        ma[y][x] += k;///k为1时数字进来,-1时代表出去。。。。
        x += bit(x);
    }
    return;
}

int qry(int x, int y)
{
    int ans = 0;
    while (x)
    {
        ans += ma[y][x];
        x -= bit(x);
    }
    return ans;
}
int main()
{
    int n, t, a, b, cnt = 1;
    scanf("%d", &n);
    for (int i = 0; i < n; ++i)
    {
        scanf("%d%d%d", &t, &a, &b);
        if (!mma[b] && t == 1) mma[b] = cnt++;///每种数字,这就算是简略的离散了
        if (t == 1)
            up(a, mma[b], 1);
        else if (t == 2)
            up(a, mma[b], -1);
        else printf("%d\n", qry(a, mma[b]));
    }
   return 0;
}

 

<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),用于存储树状数组和离散化映射。 ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值