Acwing 109. 天才ACM

推导

本题有一个重要的性质:如果 \(0<a<b<c<d\),那么 \((a-d)^2+(b-c)^2 > (a-c)^2+(b-d)^2\)

证明: 设不等式两端为1,2式;

\((a-d)^2+(b-c)^2 = a^2+b^2+c^2+d^2-2ad-2bc\)\((a-c)^2+(b-d)^2 = a^2+b^2+c^2+d^2-2ac-2bd\)

因为\(a*(c-d) > b*(c-d)\),所以\(-2ad-2bc > -2ac-2bd\)

所以\(a^2+b^2+c^2+d^2-2ad-2bc>a^2+b^2+c^2+d^2-2ac-2bd\)
所以\((a-d)^2+(b-c)^2 > (a-c)^2+(b-d)^2\)

暴力 ---> 二分

这道题的暴力很好写,每次拓宽边界,都排序一次,直到校验值超过T为止,进行下一次拓宽

但是最坏情况下为 \(O(n^3 log n)\),爆得不成人样。

对于拓宽边界,因为所有数都是非负整数,所以当右边界拓宽,校验值一定变大,所以图像上呈单调性。

既然如此,我们可以用二分查找右边界(注意右边界一定在左边界的右侧,所以边界值注意)

时间复杂度计算

理论上和暴力的复杂度推导一样,算出来应该是 \(O(n^2 log^2n)\),这也爆得很惨,但是能拿40pts其实是因为这个算法有很大误差,主要在每一次二分操作的值计算。

关于这个的解释,首推这篇题解
我懒得写了

最后舍去余值,最坏情况下的时间复杂度应该在\(O(n^2logn)\)

40 pts

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 500010;
int n, m, k;
int a[N];

int t[N];
bool get(int l,int r)
{
	int len = 0;
	for(int i = l; i <= r; i ++ )
		t[len ++ ] = a[i];
	sort(t, t + len);
	
	int res = 0;
	for(int i = 0; i < m && i < len; i ++ , len -- )
		res += (t[i] - t[len - 1])*(t[i] - t[len - 1]);
	
	//cout << res << endl;
	return res > k;
}

int main()
{
	freopen("in.txt", "r", stdin);
	freopen("out.txt", "w", stdout);
	
	int K; cin >> K;
	while(K -- )
	{
		cin >> n >> m >> k;
		for(int i = 0; i < n; i ++ ) cin >> a[i];
		
		int start = 0, cnt = 0;
		while(start < n)
		{
			int l = start, r = n;
			while(l < r)
			{
				int mid = l + r >> 1;
				if(get(start, mid)) r = mid;
				else l = mid + 1;
			}
			start = r;
			cnt ++;
		}
		cout << cnt << endl;
	}
	
	return 0;
}

二分 ---> 倍增

二分每次\(O(log (r-l))\)在剩余的区间内找合适的右边界,但倍增用\(O(log len_i)\)的时间复杂度解决一次拓宽。

因为\(\sum_{i=1}^{i<=m}{len_i} = n\),所以最坏情况下相加是\(O(nlog^2n)\)

90 pts

#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 500010;
ll n, m, k;
int a[N];

ll t[N];
ll get(int l, int r)
{
	int len = 0;
	for(int i = l; i <= r; i ++ )
		t[len ++ ] = a[i];
	sort(t, t + len);
	
	ll res = 0;
	for(int i = 0; i < m && i < len; i ++ , len -- )
		res += (t[i] - t[len - 1])*(t[i] - t[len - 1]);
	
	return res;
}

int main()
{
	freopen("in.txt", "r", stdin);
	freopen("out.txt", "w", stdout);
	
	int K; cin >> K;
	while(K -- )
	{
		cin >> n >> m >> k;
		for(int i = 0; i < n; i ++ ) cin >> a[i];
		
		int start = 0, end = 0, cnt = 0;
		while(end < n)
		{
			int len = 1;
			while(len)
			{
				if(end + len <= n && get(start, end + len - 1) <= k)
					end += len, len <<= 1;
				else len >>= 1;
			}
			start = end;
			cnt ++ ;
		}
		cout << cnt << endl;
	}
	
	return 0;
}

倍增 + 二路归并

因为每次倍增增加\(2^m\)的区间,而原来\(2^{k1}+2^{k2}+...+2^{kn}\)的区间是有序的,那么我们可以把新进入的区间排序,再与原来的有序表合并,就可以不用全部排序。

时间复杂度此时为最佳,\(O(nlogn)\)

100 pts

#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 500010;
ll n, m, k;
ll a[N];

ll t[N], tmp[N];
ll get(int l, int mid, int r)
{
	for(int i = mid; i < r; i ++ ) t[i] = a[i];
	int i = l, j = mid, len = 0;
	sort(t + mid , t + r);
	
	while(i < mid && j < r)
		if(t[i] <= t[j]) tmp[len ++ ] = t[i ++ ];
		else tmp[len ++ ] = t[j ++ ];
	while(i < mid) tmp[len ++ ] = t[i ++ ];
	while(j < r) tmp[len ++ ] = t[j ++ ];
	
	ll res = 0;
	for(int i = 0; i < m && i < len; i ++ , len -- )
		res += (tmp[i] - tmp[len - 1])*(tmp[i] - tmp[len - 1]);
	
	return res;
}

int main()
{
	freopen("in.txt", "r", stdin);
	freopen("out.txt", "w", stdout);
	
	int K; cin >> K;
	while(K -- )
	{
		cin >> n >> m >> k;
		for(int i = 0; i < n; i ++ ) cin >> a[i];
		
		int start = 0, end = 0, cnt = 0;
		while(end < n)
		{
			int len = 1;
			while(len)
			{
		        if(end + len <= n && get(start, end, end + len) <= k) //以end + len - 1结尾,注意这一点
				{
					end += len, len <<= 1;
					if(end == n) break;
					for(int i = start; i < end; i ++ ) t[i] = tmp[i - start];
				}
					
				else len >>= 1;
			}
			start = end;
			cnt ++ ;
		}
		cout << cnt << endl;
	}
	
	return 0;
}
posted @ 2023-01-26 23:34  Sankano  阅读(26)  评论(0编辑  收藏  举报