题解 | #集合操作#

集合操作

https://ac.nowcoder.com/acm/contest/11173/C

集合操作(C) 题解

先将 数组排序,可以考虑二分。二分什么呢?答曰:二分答案。

当然,不是集合中的每个元素都要二分答案,我们只二分原来还没有开始操作时最大的元素()最后的大小。

我们假设 最后的大小排名第 ,不妨假设原 的元素最后的排名分布在 ,且尽可能小。

容易证明,当 最后的大小越小,需要操作的次数越多。

函数就很好写了

bool check (LL x) {
    LL temp = a[n] - x * p;//最后a[n]的大小
    LL sum = 0;//统计需要操作多少次
    for (int i = 1; i <= n; i++) {
        if (a[i] <= temp) continue;//排名在 [idx, n]
        sum += (a[i] - temp) / p;
    }
    return sum >= k;//当操作次数大于k时满足要求 
}

但还没完,我们还需要对它进行一定的调整,因为 最后的大小不一定大于 最后的大小。于是我们让操作次数为:最小的大于 的值,最后再向前回溯。容易证明回溯的次数不超过

一些细节就写在注释里面了。

参考代码(细节很多)

#include <map>
#include <queue>
#include <cstdio>
#include <vector>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;

#define int long long
#define LL long long
template <typename T>
void read (T &x) {
    x = 0; T f = 1;
    char ch = getchar ();
    while (ch < '0' || ch > '9') {
        if (ch == '-') f = -1;
        ch = getchar ();
    }
    while (ch >= '0' && ch <= '9') {
        x = (x << 1) + (x << 3) + ch - '0';
        ch = getchar ();
    }
    x *= f;
}
template <typename T>
void write (T x) {
    if (x < 0) {
        putchar ('-');
        x = -x;
    }
    if (x < 10) {
        putchar (x + '0');
        return;
    }
    write (x / 10);
    putchar (x % 10 + '0');
}
template <typename T> T Abs (T x) { return x > 0 ? x : -x; }

const int Maxn = 1e6;
const LL Maxr = 1e18;

int n;
LL k, p;
LL a[Maxn + 5], tema[Maxn + 5];
//tema用于存储原数组 

bool check (LL x) {
    LL temp = a[n] - x * p;//最后a[n]的大小
    LL sum = 0;//统计需要操作多少次
    for (int i = 1; i <= n; i++) {
        if (a[i] <= temp) continue;//排名在 [idx, n]
        sum += (a[i] - temp) / p;
    }
    return sum >= k;//当操作次数大于k时满足要求 
}
struct Node {
    int idx;
    LL val;
    Node () {}
    Node (int IDX, LL VAL) {
        idx = IDX;
        val = VAL;
    }
};
bool operator < (Node x, Node y) {
    return x.val > y.val;
}
priority_queue <Node> q;
void solve (LL x) {
    LL temp = a[n] - x * p;//原a[n]最后的大小 
    LL sum = 0;//需要的操作次数 
    for (int i = 1; i <= n; i++) {
        if (a[i] < temp) continue;
        //元素的最后排名在 [idx, n) 里我们才会统计操作次数并且才可能会回溯 
        sum += (a[i] - temp) / p;
        a[i] -= (a[i] - temp) / p * p;
        q.push (Node (i, a[i]));
    }
    for (int i = 1; i <= sum - k; i++) {
        Node tem = q.top (); q.pop ();
        while (a[tem.idx] + p > tema[tem.idx]) {
            //有两点细节 
            //1.(83~92)行不能改成下面的代码
            //if (a[tem.idx] + p > tema[tem.idx]) continue
            //因为continue之后回溯的步数就会少1(不妨手推一下) 
            //(我考试时就是这里没调出来) 
            //2.tem.idx也不是可以无限加下去的,Ta不能超过原元素的大小 
            //(这个点让我对拍+调试了1h+) 
            tem = q.top (); q.pop ();
        }
        a[tem.idx] += p; 
        tem.val += p;
        q.push (tem);
        //(93~95)行都是回溯 
    }
    sort (a + 1, a + 1 + n);
    for (int i = 1; i <= n; i++) {
        write (a[i]); putchar (' ');
    }
}

signed main () {
    read (n); read (k); read (p);
    for (int i = 1; i <= n; i++) {
        read (a[i]);
    }
    sort (a + 1, a + 1 + n);
    memcpy (tema, a, sizeof a);

    if (p == 0) {//等于0时,除数就为0了,需要特判 
        for (int i = 1; i <= n; i++)
            printf ("%lld ", a[i]);
        return 0;
    }

    LL l = 0, r = Maxr * 2 / p;
    //二分,最后的 a[n] 为 a[n] - mid * p 
    while (l + 1 < r) {
        LL mid = l + r >> 1;
        if (check (mid)) r = mid;
        else l = mid;
    }
    if (check (l)) solve (l);
    else solve (r);
    return 0;
}
全部评论
不用二分吧( 大约是后缀和找个均值,往前重新算贡献即可。 短码,37 lines
1 回复 分享
发布于 2021-05-22 11:52

相关推荐

ArisRobert:统一解释一下,第4点的意思是,公司按需通知员工,没被通知到的员工是没法去上班的,所以只要没被通知到,就自动离职。就是一种比较抽象的裁员。
点赞 评论 收藏
分享
评论
10
收藏
分享
牛客网
牛客企业服务