【题解】A18747.眼红的同学

题目跳转:眼红的同学

题干信息很简单,看到数据量之后就不简单了。在数据量小的时候可以使用双层循环暴力的方法来求答案。显然对于这道题而言O(n^2)是完全过不去的。

前置知识:

  1. 使用树状数组求逆序对
  2. 会归并排序等分治算法。

如果想要了解跟多信息,可以自行在搜索引擎搜索有关CDQ分治的练习题。

考虑使用分治算法来解决,这是一道经典的三维偏序问题。对于这种坐标相关题目,一个很常见的方法是先对其中一个纬度进行排序。这样子控制一个纬度之后再去查找速度就会快很多。

例如如果控制语文成绩这个维度,按照从大到小的顺序排序后可以得出以下结论:

第i位同学的嫉妒值一定只会被前i-1位同学所影响,排在i后面的所有学生都不会贡献为第i为同学贡献嫉妒值。

接下来的做法跟分治逆序对很像,每次将所有的数字分成两半,以任意一个k为分界线,保证第k个学生的语文成绩低于第k+1个学生的成绩即可。直至区间的长度为1。

由于前半部分的x肯定比后半部分的x要大,可以按照数学成绩再分别被前半部分和后半部分的学生进行排序。这样子依然也不会损失单调性,只要保证前半部分x都比后半部分x大即可。接下来我们就可以遍历 后半部分,在遍历的时候计算单个点所获得的贡献(这些贡献来自于前半部分)。

截至目前已经对两个纬度进行排序了,最后一个纬度可以使用树状数组进行维护。这部分可以借用树状数组求逆序对的思想(请自行查阅)。

时间复杂度也比较好计算,分治的时间复杂度为 O ( l o g ( n ) × n ) O(log(n) \times n) O(log(n)×n)。但是树状数组的单点修改和区间查询也分别是 O ( log ⁡ ( n ) ) O(\log(n)) O(log(n))级别的。综合下来时间复杂度在 O ( n × log ⁡ ( n ) 2 ) O(n \times \log(n)^2) O(n×log(n)2)附近。因为题输入数据比较大,注意常数优化。

参考代码如下:

#include <iostream>
#include <algorithm>
#define int long long
using namespace std;

const int MAXN = 3e5 + 5;
int maximum;
int n, ans[MAXN];
int L[MAXN], R[MAXN];
int bit[MAXN << 1];
struct student{
    int x, y, z;
    int id, ans, val;
} arr[MAXN];

bool cmp1(student a, student b){
    if (a.x != b.x) return a.x > b.x;
    if (a.y != b.y) return a.y > b.y;
    return a.z > b.z;
}

bool cmp2(student a, student b){
    if (a.y != b.y) return a.y > b.y;
    if (a.z != b.z) return a.z > b.z;
    return a.x > b.x;
}

inline int max(int a, int b, int c){
    return max(a, max(b, c));
}

inline int lowbit(int k) {
    return k & (-k);
}

void add(int x, int val){
    for (int i=x; i<=maximum; i+=lowbit(i))
        bit[i] += val;
    return ;
}

int query(int x){
    int ans = 0;
    for (int i=x; i; i-=lowbit(i)) 
        ans += bit[i];
    return ans;
}

// 分治闭区间[L,R]
void cdq(int l, int r){
    if (l >= r || L[r] == L[l]) return ;
    int mid = L[(l + r) >> 1];
    if (arr[mid].x == arr[mid+1].x) mid--;
    if (mid < l) {
        // 往右边寻找中点
        mid = R[(l + r) >> 1];
        if (mid + 1 > r) return ;
    }
    cdq(l, mid); cdq(mid+1, r);
    sort(arr+l, arr+mid+1, cmp2);
    sort(arr+mid+1, arr+r+1, cmp2);
    int j = l;
    for (int i=mid+1; i<=r; i++){
        while (j <= mid && arr[j].y > arr[i].y){
            add(arr[j].z, arr[j].val);
            j++;
        }
        arr[i].ans += query(maximum) -  query(arr[i].z);
    }
    // 恢复树状数组
    for (int i=l; i<j; i++)
        add(arr[i].z, -arr[i].val);
    return ;
}
  • 15
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
动态分区分配方式是一种通过划分内存块来满足进程的内存需求的方法。这种分配方式的实现需要使用空闲分区表来记录内存的空闲情况,并根据进程的大小找到合适的空闲分区进行分配。 对于a.5中的题目,我们可以使用一个动态分区分配方式的模拟答案来解答。假设有以下内存空间和进程需求: 内存空间: 1. 起始地址:0,大小:100 2. 起始地址:100,大小:50 3. 起始地址:150,大小:75 4. 起始地址:225,大小:200 进程需求: 1. 进程A,大小:130 2. 进程B,大小:70 3. 进程C,大小:90 初始状态下,空闲分区表中只有一个分区,起始地址为0,大小为400。按照从小到大的顺序,我们可以将进程依次分配到空闲分区中。首先,将进程A分配到起始地址为0,大小为130的分区中,更新空闲分区表为: 1. 起始地址:130,大小:370 然后,将进程B分配到起始地址为130,大小为70的分区中,更新空闲分区表为: 1. 起始地址:200,大小:300 最后,将进程C分配到起始地址为200,大小为90的分区中,更新空闲分区表为: 1. 起始地址:290,大小:210 经过分配后,空闲分区表中只有一个分区,它的起始地址为290,大小为210。 这样,我们利用动态分区分配方式将进程分配到内存空间,并更新了空闲分区表的内容。通过这个模拟答案,我们可以更好地理解动态分区分配方式的工作原理。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值