天才ACM
给定一个整数 M M M,对于任意一个整数集合 S S S,定义“校验值”如下:
从集合 S S S 中取出 M M M 对数(即 2 × M 2×M 2×M 个数,不能重复使用集合中的数,如果 S S S 中的整数不够 M M M 对,则取到不能取为止),使得“每对数的差的平方”之和最大,这个最大值就称为集合 S S S 的“校验值”。
现在给定一个长度为 N N N 的数列 A A A 以及一个整数 T T T。
我们要把 A A A 分成若干段,使得每一段的“校验值”都不超过 T T T。
求最少需要分成几段。
输入格式
第一行输入整数 K K K,代表有 K K K 组测试数据。
对于每组测试数据,第一行包含三个整数 N , M , T N,M,T N,M,T 。
第二行包含 N N N 个整数,表示数列 A 1 , A 2 … A N A_1,A_2…A_N A1,A2…AN。
输出格式
对于每组测试数据,输出其答案,每个答案占一行。
数据范围
1
≤
K
≤
12
1≤K≤12
1≤K≤12,
1
≤
N
,
M
≤
500000
1≤N,M≤500000
1≤N,M≤500000,
0
≤
T
≤
1
0
18
0≤T≤10^{18}
0≤T≤1018,
0
≤
A
i
≤
2
20
0≤A_i≤2^{20}
0≤Ai≤220
输入样例:
2
5 1 49
8 2 1 7 9
5 1 64
8 2 1 7 9
输出样例:
2
1
解析
一眼题,类似 最佳牛围栏 ,只是二分判定的方式不同,我们需要确定左区间 l l l,再二分找右区间 r r r,为了使区间 [ l , r ] [l,r] [l,r] 满足题意中的 每对数的差的平方最大,只需要尽可能的让最小的减去最大的,那么我们将 [ l , r ] [l, r] [l,r] 中的数排序后依次处理就好。如果满足,就将 r r r 再扩大,不满足就缩小 r r r ,直到 r r r 无法继续扩大为止,此时答案数+1,再将左区间变为 r + 1 r + 1 r+1 继续二分就好。
代码很好写,但是这道题不止考了思路,也考了你关于时间复杂度的计算,先将代码写出,我们对照代码来计算下时间复杂度。
二分法代码
#include <cstdio>
#include <algorithm>
const int N = 500005;
long long arr_a[N], arr_b[N];
int m;
long long t;
bool cheak(int l, int r) { //判定函数
for (int i = l; i <= r; i++) {
arr_b[i] = arr_a[i];
}
std::sort(arr_b + l, arr_b + 1 + r);
long long tnt = 0;
for (int i = 0; i < m; i++) {
if(l + i >= r - i) break;
tnt += (arr_b[l + i] - arr_b[r - i]) * (arr_b[l + i] - arr_b[r - i]);
}
if(tnt > t) return false;
else return true;
}
int Bisection(int l, int r) { //二分
int cl = l;
while(l < r) {
int mid = (l + r + 1) >> 1;
if(cheak(cl, mid)) l = mid;
else r = mid - 1;
} return l;
}
int main() {
int K;
scanf("%d", &K);
while(K--) {
int n;
scanf("%d %d %lld", &n, &m, &t);
for (int i = 1; i <= n; i++) {
scanf("%lld", &arr_a[i]);
arr_b[i] = arr_a[i];
}
int l = 1, r = n, ans = 0;
while(l <= r) {
l = Bisection(l, r) + 1;
ans++;
}
printf("%d\n", ans);
} return 0;
}
二分的最坏情况是 O ( l o g n ) O(logn) O(logn) 每一个数都是一个合法区间,而且要调用 n n n 次,即复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)
判定函数的最坏时间复杂度为 O ( n + n l o g n + m ) O(n + nlogn + m) O(n+nlogn+m) 因为 n , m n,m n,m 的数值范围一样 即得 O ( 2 n + n l o g n ) O(2n + nlogn) O(2n+nlogn)
可得二分的最坏情况为 O ( 2 n 2 l o g n + n 2 l o g 2 n ) O(2n^2logn + n^2log^2n) O(2n2logn+n2log2n)。实际复杂度可能会少很多常数,因为我们如果每次二分都是最坏情况,那么反而判定函数的时间复杂度到不了最坏情况。
但即使少很多常数,10s内 [ 1 , 500000 ] [1,500000] [1,500000] 也不是 n 2 n^2 n2 的算法能够解决的,于是很显然 这道题用二分会超时
继续思考,如何优化这个解法。首先是二分的最坏情况,因为二分的最坏情况满足条件是每一次折半,即为 n , n 2 , n 4 , n 8 , . . . . . . . . . , n 2 x n,\frac{n}{2},\frac{n}{4},\frac{n}{8},.........,\frac{n}{2^x} n,2n,4n,8n,.........,2xn,最后一个数一定等于一,所以有 n 2 x = 1 \frac{n}{2^x} = 1 2xn=1 变形为 x = l o g 2 n x = log_2n x=log2n。
引入一个新算法 倍增
倍增算法可以用一句话简单的概括,确定左区间,每次以成倍的成长值加长,如果此次加长违法,那么成长值就会在原来的基础上减半,直到成长值为 0 0 0,此次倍增结束
我们可以使用倍增,因为对于倍增而言,二分的最坏情况就是倍增的最优情况,但倍增的最坏情况仍然有 O ( l o g n ) O(logn) O(logn),不要慌,对于这道题而言,倍增如果到了最坏情况,那么调用次数反而会变成 1 1 1 次,而倍增如果到了最优情况,调用次数才会变成 n n n 次。
如果是最坏情况 用倍增的复杂度就变成了 O ( l o g n ) O(logn) O(logn),判定的复杂度没变,总复杂度就变成了 O ( 2 n l o g n + n l o g 2 n ) O(2nlogn + nlog^2n) O(2nlogn+nlog2n),这道题就可以通过了
如果是最优情况 判定的复杂度会变成 O ( k ) O(k) O(k),k为一个小常数,那么就只剩了调用次数的复杂度 O ( n ) O(n) O(n),反而更快。
代码如下
倍增法代码
#include <cstdio>
#include <algorithm>
const int N = 500005;
long long arr_a[N], arr_b[N];
int m;
long long t;
bool cheak(int l, int r) { //和二分法的判定函数相同
for (int i = l; i <= r; i++) {
arr_b[i] = arr_a[i];
}
std::sort(arr_b + l, arr_b + 1 + r);
long long tnt = 0;
for (int i = 0; i < m; i++) {
if(l + i >= r - i) break;
tnt += (arr_b[l + i] - arr_b[r - i]) * (arr_b[l + i] - arr_b[r - i]);
}
if(tnt > t) return false;
else return true;
}
//变的只有这里
int BinaryL(int l, int r) { //倍增过程 成长值 p,左区间 l,右区间 k,数组边界 r
int p = 1, k = l;
while(1) {
if(p == 0) return k;
else {
if(k + p <= r && cheak(l, k + p)) k += p, p *= 2; //合法就加倍
else p /= 2; //不合法就减半
}
}
}
int main() {
int K;
scanf("%d", &K);
while(K--) {
int n;
scanf("%d %d %lld", &n, &m, &t);
for (int i = 1; i <= n; i++) {
scanf("%lld", &arr_a[i]);
arr_b[i] = arr_a[i];
}
int l = 1, r = n, ans = 0;
while(l <= r) {
l = BinaryL(l, r) + 1;
ans++;
}
printf("%d\n", ans);
} return 0;
}
题解结束,剩下是补充内容
虽然可以AC,但是这道题还是可以继续优化,因为我们倍增的过程中, [ l , k ] [l,k] [l,k] 与 [ k + 1 , k + p ] [k + 1, k + p] [k+1,k+p] 的序列里,只有后者没有排序,前者已经排序过了,我们只需要将后者排序,再用归并排序将两个序列合并即可,归并排序将两个有序序列合并的复杂度为 O ( n ) O(n) O(n),这样又会省下一个 l o g log log
最坏情况的总复杂度变为了 O ( 2 n + n l o g n ) O(2n + nlogn) O(2n+nlogn)
代码如下
倍增法+归并排序代码
#include <cstdio>
#include <algorithm>
const int N = 500005;
long long arr_a[N], arr_b[N], f[N];
int n, m;
long long t;
void mergesort(int l, int mid, int r) {
int i = l, j = mid + 1;
for (int k = l; k <= r; k++) {
if(j > r || i <= mid && arr_b[i] <= arr_b[j]) f[k] = arr_b[i++];
else f[k] = arr_b[j++];
}
}
bool cheak(int l, int mid, int r) {
for (int i = mid + 1; i <= r; i++) arr_b[i] = arr_a[i];
std::sort(arr_b + 1 + mid, arr_b + 1 + r);
mergesort(l, mid, r);
long long tnt = 0;
for (int i = 0; i < m; i++) {
if(l + i >= r - i) break;
tnt += (f[l + i] - f[r - i]) * (f[l + i] - f[r - i]);
}
if(tnt > t) return false;
else return true;
}
int BinaryL(int l, int r) {
int p = 1, k = l;
while(1) {
if(p == 0) return k;
else {
if(k + p <= r && cheak(l, k, k + p)) {
k += p, p *= 2;
for (int i = l; i <= k; i++) arr_b[i] = f[i]; //判定合法了再导入
} else p /= 2;
}
}
}
int main() {
int K;
scanf("%d", &K);
while(K--) {
int n;
scanf("%d %d %lld", &n, &m, &t);
for (int i = 1; i <= n; i++) {
scanf("%lld", &arr_a[i]);
arr_b[i] = arr_a[i];
}
int l = 1, r = n, ans = 0;
while(l <= r) {
l = BinaryL(l, r) + 1;
ans++;
}
printf("%d\n", ans);
} return 0;
}
快了6倍,而且对归并排序的理解变化运用更加自如,看到这里就推荐你去试试
以上的时间复杂度我都没乘 K K K,如果是时间复杂度计算练习,我建议还是算上。