【倍增/归并】天才ACM

天才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,A2AN

输出格式

对于每组测试数据,输出其答案,每个答案占一行。

数据范围

1 ≤ K ≤ 12 1≤K≤12 1K12,
1 ≤ N , M ≤ 500000 1≤N,M≤500000 1N,M500000,
0 ≤ T ≤ 1 0 18 0≤T≤10^{18} 0T1018,
0 ≤ A i ≤ 2 20 0≤A_i≤2^{20} 0Ai220

输入样例:
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,如果是时间复杂度计算练习,我建议还是算上。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值