[lnsyoj2209/luoguP10455]Genius Acm
来源
原题链接
2024.7.25 校内测验 T2
题意
给定序列 \(P\),将 \(P\) 分为连续的 \(l\) 段,使得对于任意的长度为 \(x\) 的一段,其中任意 \(min\{m, \lfloor \frac{x}{2} \rfloor\}\) 对数的差的平方和不超过 \(k\),求最小的 \(l\)。
赛时 0PTS
sol
首先,对于 \(4\) 个数 \(a<b<c<d\),可以证明 \((d-a)^2 + (c-b)^2 \ge (d-b)^2 + (c-a)^2\)
Prove
\[d^2 - 2 ad + a ^ 2 + c^2 - 2bc + b^2 \ge d^2 - 2bd + b^2 + c^2 - 2ac + a^2
\]
\[-2ad - 2bc + 2bd + 2ac \ge 0
\]
\[(d-c)(b-a)\ge 0
\]
因此每次取最大和最小数作差平方即可。
由于分段是连续且完全覆盖序列 \(P\) 的,因此前一个区间右端点 \(r_{i-1}\) 确认之后,当前区间的左端点即已确定为 \(r_{i-1}+1\),我们可以根据贪心,计算出一个最右的右端点,可以证明这样计算出的答案一定为最优解。
由于 \(n,m\) 的数据范围达到了 \(5\times 10^5\) ,因此我们每次计算的复杂度必须为 \(O(n^2)\) 以下。
为了降低复杂度,我们考虑倍增:
每次从 \(1\) 开始向上加,并进行一次判断,判断时如果这一段满足要求,下一次加就加倍,否则下一次加就减半
判断时,可以对序列进行归并排序,使得取第 \(k\) 大值的时间复杂度达到 \(O(1)\),若满足要求,将这一段复制到原序列即可。
代码
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long LL;
const int N = 500005;
int T;
int n, m;
LL k;
LL v[N];
LL tmp[N], temp[N];
bool check(int l, int r, int len){
int mid = r - len + 1;
for (int i = l; i <= r; i ++ ) tmp[i] = v[i];
sort(tmp + mid, tmp + r + 1);
int i, j, idx;
for (i = l, j = mid, idx = 0; i < mid && j <= r; ){
while (i < mid && tmp[i] <= tmp[j]) temp[ ++ idx] = tmp[i ++ ];
while (j <= r && tmp[j] <= tmp[i]) temp[ ++ idx] = tmp[j ++ ];
}
while (i < mid) temp[ ++ idx] = tmp[i ++ ];
while (j <= r) temp[ ++ idx] = tmp[j ++ ];
int mm = min(idx / 2, m);
LL res = 0;
for (int i = 1, j = idx; i <= mm; i ++ , j -- ) res += (temp[j] - temp[i]) * (temp[j] - temp[i]);
return res <= k;
}
int find(int x){
int l = x - 1, p = 1;
while (p){
if (l + p <= n && check(x, l + p, p)){
for (int i = x; i <= l + p; i ++ ) v[i] = temp[i - x + 1];
l += p;
p <<= 1;
}
else p >>= 1;
}
return l;
}
int main(){
scanf("%d", &T);
while (T -- ){
scanf("%d%d%lld", &n, &m, &k);
for (int i = 1; i <= n; i ++ ) scanf("%lld", &v[i]);
int pos = 1, ans = 0;
while (pos <= n) pos = find(pos) + 1, ans ++ ;
printf("%d\n", ans);
}
return 0;
}