题意:
给定一个数组,每个数都可以修改一次,把他们修改成等差数列(可以是小数等差数列),若把 a i a_{i} ai修改为 a i ′ a'_{i} ai′,代价是 ( a i − a i ′ ) 2 (a_{i}-a'_{i})^{2} (ai−ai′)2,求能修改成等差数列的最小总代价
方法:
等差数列一定落在一条直线上,设直线 y = k x + b y=kx+b y=kx+b,那么代价和为 ∑ i = 1 n ( a i − k i − b ) 2 \sum_{i=1}^{n}(a_{i}-ki-b)^2 ∑i=1n(ai−ki−b)2
展开化简,原式即 ∑ i = 1 n a i 2 + k 2 ∑ i = 1 n i 2 + n b 2 − 2 k ∑ i = 1 n i a i − 2 b ∑ i = 1 n a i + 2 b k ∑ i = 1 n i \sum_{i=1}^{n}a_{i}^2+k^{2}\sum_{i=1}^{n}i^{2}+nb^{2}-2k\sum_{i=1}^{n}ia_{i}-2b\sum_{i=1}^{n}a_{i}+2bk\sum_{i=1}^{n}i ∑i=1nai2+k2∑i=1ni2+nb2−2k∑i=1niai−2b∑i=1nai+2bk∑i=1ni
可以发现,这是一个关于 k , b k,b k,b的二元函数,并且固定 k k k,发现是一个关于 b b b的开口向上的二次函数,固定 b b b,是一个关于 k k k的二次函数,因此这个函数存在一个极小值,可以三分套三分求这个极值,但是精度不够,后面才注意到第一次三分固定 k k k后,关于 b b b的函数可以用数学方式求,但可惜,精度仍然不够
三分套三分WA代码
#include<bits/stdc++.h>
#define ll long long
#define double long double
using namespace std;
int read()
{
int ret=0,base=1;
char ch=getchar();
while(!isdigit(ch))
{
if(ch=='-') base=-1;
ch=getchar();
}
while(isdigit(ch))
{
ret=(ret<<3)+(ret<<1)+ch-48;
ch=getchar();
}
return ret*base;
}
int n;
const double max1=1e13;
const double eps=1e-6;
double a[1000005],suma2,sumi2,sumiai,sumai,sumi;
double f(double k,double b)
{
return suma2+k*k*sumi2+(double)n*b*b-2*k*sumiai-2*b*sumai+2*b*k*sumi;
}
double solve(double k)
{
double l=-max1,r=max1,ret=1e300;
while(r-l>eps)
{
// printf("2:l=%lf,r=%lf\n",l,r);
double midl=(2*l+r)/3,midr=(l+2*r)/3;
double vl=f(k,midl),vr=f(k,midr);
ret=min(ret,vl);ret=min(ret,vr);
if(vl<vr-eps) r=midr-eps;
else l=midl+eps;
}
return ret;
}
void work()
{
n=read();
suma2=sumi2=sumiai=sumai=sumi=0;
for(int i=1;i<=n;i++) a[i]=(double)read();
for(int i=1;i<=n;i++)
{
suma2+=a[i]*a[i];
sumi2+=i*i;
sumiai+=i*a[i];
sumai+=a[i];
sumi+=i;
}
// printf("%lf,%lf,%lf,%lf,%lf\n",suma2,sumi2,sumiai,sumai,sumi);
double l=-max1,r=max1,ans=1e300;
// printf("%lf\n",ans);
while(r-l>eps)
{
// printf("1:l=%lf,r=%lf\n",l,r);
double midl=(2*l+r)/3,midr=(l+2*r)/3;
double min1=solve(midl),min2=solve(midr);
ans=min(ans,min1);ans=min(ans,min2);
if(min1<min2-eps) r=midr-eps;
else l=midl+eps;
}
printf("%.12Lf\n",ans);
}
int main()
{
int t=read();
while(t--) work();
return 0;
}
这题要求一条直线,使平均距离最近,实际上这是给定 n n n个点拟合直线的过程,是高中的线性回归
最小二乘法拟合直线 y = k x + b y=kx+b y=kx+b,公式为
k = ∑ i = 1 n ( x i − x ˉ ) ( y i − y ˉ ) ( x i − x ˉ ) 2 k=\frac{\sum_{i=1}^{n}(x_{i}-\bar{x})(y_{i}-\bar{y})}{(x_{i}-\bar{x})^{2}} k=(xi−xˉ)2∑i=1n(xi−xˉ)(yi−yˉ)
并且直线一定经过样本中心点,即 ( x ˉ , y ˉ ) (\bar{x},\bar{y}) (xˉ,yˉ)
所以 y ˉ = k x ˉ + b \bar{y}=k\bar{x}+b yˉ=kxˉ+b
解出公式,计算距离即可
AC代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
int read()
{
int ret=0,base=1;
char ch=getchar();
while(!isdigit(ch))
{
if(ch=='-') base=-1;
ch=getchar();
}
while(isdigit(ch))
{
ret=(ret<<3)+(ret<<1)+ch-48;
ch=getchar();
}
return ret*base;
}
int n;
double ave_x,ave_y,ans1,ans2,k,b,ans;
double a[1000005];
inline double f(double x){return k*x+b;}
void work()
{
n=read();
ave_x=ave_y=ans1=ans2=k=b=ans=0;
for(int i=1;i<=n;i++)
{
a[i]=(double)read();
ave_y+=a[i];
}
ave_x=(1.0+n)/2;
ave_y/=n;
for(int i=1;i<=n;i++)
{
ans1+=(i-ave_x)*(a[i]-ave_y);
ans2+=(i-ave_x)*(i-ave_x);
}
k=ans1/ans2;
b=ave_y-k*ave_x;
for(int i=1;i<=n;i++) ans+=(f((double)i)-a[i])*(f((double)i)-a[i]);
printf("%.10lf\n",ans);
}
int main()
{
int t=read();
while(t--) work();
return 0;
}