集合操作题解+小结
1.前言
我考试时把全部时间都交给这道题(T3)了,结果还没调出来,最后发现 自己是傻逼 自己又出现了傻逼错误,我真的是傻逼。为了警戒自己注意细节,于是写下了这篇题解
2.题解
现将 a a a 数组排序,可以考虑二分。二分什么呢?答曰:二分答案。
当然,不是集合中的每个元素都要二分答案,我们只二分原来还没有开始操作时最大的元素( a [ n ] a[n] a[n])最后的大小。
我们假设 a [ n ] a[n] a[n] 最后的大小排名第 i d x idx idx,不妨假设原 [ i d x , n ) [idx, n) [idx,n) 的元素最后的排名分布在 ( i d x , n ] (idx, n] (idx,n]。
容易证明,当 a [ n ] a[n] a[n] 最后的大小越小,需要操作的次数越多。
c h e c k check check 函数就很好写了
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时满足要求
}
但还没完,我们还需要对它进行一定的调整,因为 a [ i ] ( i ∈ [ i d x , n ) ) a[i] (i \in [idx, n)) a[i](i∈[idx,n)) 最后的大小不一定大于 a [ n ] a[n] a[n] 最后的大小。于是我们让操作次数为最小的大于 k k k 的值,最后再向前回溯。容易证明回溯的次数不超过 O ( n ) O(n) O(n)。
一些细节就写在注释里面了。
参考代码(细节很多)
#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;
}
3.小结
平时太依赖于评测机和数据了,一会改不出来就去下数据,平时的散漫就导致考试时要求有限的时间时,就不能很快的跳出来。
之后我一定要改正,不然就会像今天这样,有思路但是打不出来,细节考虑不到,令人唏嘘,后悔不尽。