D2. Red-Blue Operations (Hard Version)
题意
给定一个长度为\(n\)的数列和\(q\)次询问,初始所有数的颜色都是红色,每次询问给一个\(k\),要求你做恰好\(k\)次操作,对于第\(i\)次操作,你可以任选数列中的一个数
- 如果它是红色,则将它的值加上\(i\),并将它变成蓝色
- 如果它是蓝色,则将它的值减去\(i\),并将它变为红色
对于每个询问,求\(k\)次操作后数列最小值的最大值,\(1 \leq n,q \leq 1e5\),\(1 \leq a_i \leq 1e9\),\(1 \leq k_i \leq 1e9\)
题解
如果\(k \leq n\),显然,此时我们可以只加不减,且最优策略显然是把最大的数加到最小的数身上,次大的数加到次小的数身上,以此类推
如果\(k>n\)
我们不妨先考虑\(k-n\)是偶数的情况
首先证明这\(k\)次操作一定是\(+-+-+-+-...+++++\)这样的序列(加减在哪一个数上还不一定)
证明:我们交换序列中的任一对\(+-\),合法的交换一定是减法后移,加法前移(因为一开始所有数都是红色,所以对于序列的任一前缀,加法的数量必须不少于减法),在任一种分配策略下,我们这样交换一定会导致某些数减的更多或加的更少了,一定是不更优的
现在我们考虑这些操作分配给了哪些数,对于目前的最优序列,可以将\(k\)次操作分为两部分,后\(n\)次全为加法,前\(k-n\)次可以打包成\((k-n)/2\)次操作,每次操作相当于给任意一个数减去\(1\),由于操作的顺序不影响最后答案,我们不妨先做后\(n\)次操作
之前已经说明,如果\(k \leq n\),贪心即可,现在我们考虑后面还有若干次减法时,还能不能这样贪心
证明:考虑减法操作结束后,答案为\(min(原数列最小值,新数列和的平均值)\),如果我们在\(n\)次加法中不按最优策略来,原数列最小值一定不会更大,无论采取什么操作,新数列和的平均值一定,所以其它方案不会比贪心更优
所以,答案为\(min(n次操作后数列最小值,新数列和的平均值)\),直接计算单组询问复杂度是\(O(n)\)的,会\(TLE\),考虑如何优化
首先考虑\(k \leq n\)时,将原数列从小到大排序,我们本质上求的是\(min(\underset{1 \leq i \leq k}{min}a_i+k+1-i,\underset{k+1 \leq i \leq n}{min}a_i)\)
显然\(\underset{k+1 \leq i \leq n}{min}a_i=a_{k+1}\),这里我们可以令\(b_i=a_i-i+1\),令\(mi_i=\underset{1 \leq j \leq i}{min}b_i\),可以把上述式子变成求\(min(mi_k+k,a_{k+1})\),通过\(O(n)\)的预处理,我们可以\(O(1)\)的回答每组询问
求新数列的和,通过求和公式,显然也可以做到\(O(1)\),一点小细节时要注意\(k-n\)分奇偶讨论,具体公式见代码
代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll INF = 1e18;
const int N = 2e5 + 7;
ll n, q, sum, a[N], mi[N];
ll solve(ll k) {
if (k <= n) {
return min(mi[k] + k, a[k + 1]);
}
else if ((k - n) % 2) {
ll op = (k - n + 1) / 2, presum = (k - n + 2 + k) * (n - 1) / 2;
return min(min(mi[n - 1] + k, a[n]), (sum + presum - op) / n);
}
else {
ll op = (k - n) / 2, presum = (k - n + 1 + k) * n / 2;
return min(mi[n] + k, (sum + presum - op) / n);
}
}
int main() {
scanf("%lld%lld", &n, &q);
for (ll i = 1; i <= n; ++i) {
scanf("%lld", &a[i]);
sum += a[i];
}
sort(a + 1, a + 1 + n);
a[n + 1] = mi[0] = INF;
for (ll i = 1; i <= n; ++i) {
mi[i] = min(mi[i - 1], a[i] - i + 1);
}
while (q--) {
ll k;
scanf("%lld", &k);
printf("%lld ", solve(k));
}
return 0;
}