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;
}
本文来自博客园,作者:{三季野花},转载请注明原文链接:https://www.cnblogs.com/SanGarden/articles/17067936.html