2022牛客#2 J,函数极值,线性回归求拟合直线

题意:

给定一个数组,每个数都可以修改一次,把他们修改成等差数列(可以是小数等差数列),若把 a i a_{i} ai修改为 a i ′ a'_{i} ai,代价是 ( a i − a i ′ ) 2 (a_{i}-a'_{i})^{2} (aiai)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(aikib)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+k2i=1ni2+nb22ki=1niai2bi=1nai+2bki=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=(xixˉ)2i=1n(xixˉ)(yiyˉ)

并且直线一定经过样本中心点,即 ( 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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值