题目大意:给你一个数组,让你把这个数组变成一个等差数列,问最小的操作次数是多少。
操作一次是指使一个数加1或者减1。
思路:刚看到这道题的时候,很容易想到用直线去拟合数组,但是如果去枚举可能的斜率看到会超时,因为斜率的取值范围为-1e13到1e13,如果用枚举两点直接连线的斜率很显然也是会超时的(O())。如果我们令f(x)为当斜率为x时的最小操作次数,不难发现f(x)是一个凹凸性不变的函数,因此可以使用三分来枚举斜率。
如何判断这个斜率下的最少操作次数?不难发现,当数组表示的点均匀分布在直线两侧时,操作总次数最少。所以我们假设使用第一个元素作为直线上的点,枚举所有元素到直线的垂直距离,把结果存在a数组里面,所有垂直距离的和就是这个直线在取当前截距的操作次数。因此,我们只需要找到a数组的中位数作为基准点进行求解即可。
注意:由于三分的时间复杂度要高于二分,因此求中位数禁止使用sort(实测在PTA平台提交会TLE),请使用快速选择算法或者部分快速排序,下面的代码使用了北京大学提供的题解上使用的nth_element函数求解。还有由于数据范围过大(1e13*2e5),在运算过程中可能会爆long long,请使用128位整数__int128,或者手写高精度,不过应该没人这么干。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e6+5;
const int mod=998244353;
ostream& operator<<(ostream& os, __int128 t) {
if (t==0) return os << "0";
if (t<0) {
os<<"-";
t=-t;
}
int a[50],ai=0;
memset(a,0,sizeof a);
while (t!=0){
a[ai++]=t%10;
t/=10;
}
for (int i=1;i<=ai;i++) os<<abs(a[ai-i]);
return os<<"";
}
int n;
__int128 a[N];
long long c[N];
__int128 check(__int128 num){
a[1]=0;
int tb=c[1];
for(int i=2;i<=n;i++){
a[i]=c[i]-tb-num*(i-1);
}
nth_element(a+1,a+(n+1)/2,a+n+1);
__int128 temp=a[((n+1)>>1)];
__int128 res=0;
for(int i=1;i<=n;i++){
if(temp-a[i]>0)res+=temp-a[i];
else res-=temp-a[i];
}
return res;
}
void io() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
}
signed main(){
io();
cin>>n;
for(int i=1;i<=n;i++){
cin>>c[i];
}
__int128 l=-1e13;
__int128 r=1e13;
while(l<r){
__int128 midl=l+(r-l)/3;
__int128 midr=r-(r-l)/3;
if(check(midl)>check(midr)){
l=midl+1;
}else{
r=midr-1;
}
}
cout<<check(l)<<endl;
return 0;
}