P7962 [NOIP2021] 方差()

[NOIP2021] 方差

时隔一年。我又回来做这个题了。。。

我们通过观察是可以发现这里的操作实际上就是交换相邻差分,但是差分 \(c_1\) 不可被交换。

然后如果要求方差最小的话我们可以想到这个差分应该是单谷的。当然,实际上这就是一个直觉的结论。

想要证明的话可以从调整法入手,我们交换这个单谷的两个差分使其变成非单谷的状态,会发现方差一定会更大。

所以说我们把第一个差分数组固定后,剩下的差分数组从小到大排列,要么放在现有数列的最前面,要么放在最后面。

这个单谷的形态不可考察,我们有必要用 DP 来寻找最佳。

定义 \(f(i,s)\) 表示考虑了前 \(i\) 个差分数组(排序后),此时序列的 \(\sum a_i = s\) 时的最小的 \(\sum a _ i ^ 2\)。

考虑转移:

我们知道每次转移是 \(O(na)\) 的,那么直接从头至尾做就是 \(O( n ^ 2 a)\) 的。

考虑到 \(a\) 的范围不大,实际上 $c_i = 0 $ 的情况很多,而 \(c_i \ne 0\) 的情况至多只有 \(O(a)\) 的数量级。

而 \(c_i=0\) 都排在前面,我们提前铺开处理掉就可以了。

这样复杂度就是 \(O(na^2)\) 的,数据范围大概印证了这个方法就是相当的正解。

代码:

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<cstdlib>
#define ll long long
using namespace std;
namespace Ehnaev{
  inline ll read() {
    ll ret=0,f=1;char ch=getchar();
    while(ch<48||ch>57) {if(ch==45) f=-f;ch=getchar();}
    while(ch>=48&&ch<=57) {ret=(ret<<3)+(ret<<1)+ch-48;ch=getchar();}
    return ret*f;
  }
  inline void write(ll x) {
    static char buf[22];static ll len=-1;
    if(x>=0) {do{buf[++len]=x%10+48;x/=10;}while(x);}
    else {putchar(45);do{buf[++len]=-(x%10)+48;x/=10;}while(x);}
    while(len>=0) putchar(buf[len--]);
  }
}using Ehnaev::read;using Ehnaev::write;

const ll N=1e6,M=1e6,inf=1e16;

ll n,m,sum,now,ans;
ll a[N+5],c[N+5],g[2][M+5];

int main() {

  n=read();
  for(ll i=1;i<=n;i++) {
    a[i]=read();c[i]=a[i]-a[i-1];m=max(m,a[i]);
  }

  sort(c+2,c+n+1);

  m=m*n;sum=c[1];
  for(ll i=1;i<=m;i++) g[now][i]=inf;
  g[now][sum]=sum*sum;

  for(ll i=2;i<=n;i++) {
    sum+=c[i];
    if(c[i]!=0) {
      for(ll j=1;j<=m;j++) {g[1-now][j]=inf;}
      for(ll j=1;j<=m;j++) {
        g[1-now][j+sum]=min(g[1-now][j+sum],g[now][j]+sum*sum);
        g[1-now][j+c[i]*(i-1)+c[1]]=min(g[1-now][j+c[i]*(i-1)+c[1]]
        ,g[now][j]+(i-1)*c[i]*c[i]+2*c[i]*j+c[1]*c[1]);
      }
      now=1-now;
    }
    else {
      g[now][sum*(i-1)]=inf;g[now][sum*i]=sum*sum*i;
      continue;
    }
    // for(ll j=1;j<=m;j++) {
    //   printf("Test: %lld %lld\n",j,g[now][j]);
    // }
  }

  ans=inf;
  for(ll i=1;i<=m;i++) {
    // printf("Test: %lld %lld\n",i,g[now][i]);
    ans=min(ans,n*g[now][i]-i*i);
  }

  write(ans);

  return 0;
}
————————

[NOIP2021] 方差

时隔一年。我又回来做这个题了。。。

我们通过观察是可以发现这里的操作实际上就是交换相邻差分,但是差分 \(c_1\) 不可被交换。

然后如果要求方差最小的话我们可以想到这个差分应该是单谷的。当然,实际上这就是一个直觉的结论。

想要证明的话可以从调整法入手,我们交换这个单谷的两个差分使其变成非单谷的状态,会发现方差一定会更大。

所以说我们把第一个差分数组固定后,剩下的差分数组从小到大排列,要么放在现有数列的最前面,要么放在最后面。

这个单谷的形态不可考察,我们有必要用 DP 来寻找最佳。

定义 \(f(i,s)\) 表示考虑了前 \(i\) 个差分数组(排序后),此时序列的 \(\sum a_i = s\) 时的最小的 \(\sum a _ i ^ 2\)。

考虑转移:

我们知道每次转移是 \(O(na)\) 的,那么直接从头至尾做就是 \(O( n ^ 2 a)\) 的。

考虑到 \(a\) 的范围不大,实际上 $c_i = 0 $ 的情况很多,而 \(c_i \ne 0\) 的情况至多只有 \(O(a)\) 的数量级。

而 \(c_i=0\) 都排在前面,我们提前铺开处理掉就可以了。

这样复杂度就是 \(O(na^2)\) 的,数据范围大概印证了这个方法就是相当的正解。

代码:

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<cstdlib>
#define ll long long
using namespace std;
namespace Ehnaev{
  inline ll read() {
    ll ret=0,f=1;char ch=getchar();
    while(ch<48||ch>57) {if(ch==45) f=-f;ch=getchar();}
    while(ch>=48&&ch<=57) {ret=(ret<<3)+(ret<<1)+ch-48;ch=getchar();}
    return ret*f;
  }
  inline void write(ll x) {
    static char buf[22];static ll len=-1;
    if(x>=0) {do{buf[++len]=x%10+48;x/=10;}while(x);}
    else {putchar(45);do{buf[++len]=-(x%10)+48;x/=10;}while(x);}
    while(len>=0) putchar(buf[len--]);
  }
}using Ehnaev::read;using Ehnaev::write;

const ll N=1e6,M=1e6,inf=1e16;

ll n,m,sum,now,ans;
ll a[N+5],c[N+5],g[2][M+5];

int main() {

  n=read();
  for(ll i=1;i<=n;i++) {
    a[i]=read();c[i]=a[i]-a[i-1];m=max(m,a[i]);
  }

  sort(c+2,c+n+1);

  m=m*n;sum=c[1];
  for(ll i=1;i<=m;i++) g[now][i]=inf;
  g[now][sum]=sum*sum;

  for(ll i=2;i<=n;i++) {
    sum+=c[i];
    if(c[i]!=0) {
      for(ll j=1;j<=m;j++) {g[1-now][j]=inf;}
      for(ll j=1;j<=m;j++) {
        g[1-now][j+sum]=min(g[1-now][j+sum],g[now][j]+sum*sum);
        g[1-now][j+c[i]*(i-1)+c[1]]=min(g[1-now][j+c[i]*(i-1)+c[1]]
        ,g[now][j]+(i-1)*c[i]*c[i]+2*c[i]*j+c[1]*c[1]);
      }
      now=1-now;
    }
    else {
      g[now][sum*(i-1)]=inf;g[now][sum*i]=sum*sum*i;
      continue;
    }
    // for(ll j=1;j<=m;j++) {
    //   printf("Test: %lld %lld\n",j,g[now][j]);
    // }
  }

  ans=inf;
  for(ll i=1;i<=m;i++) {
    // printf("Test: %lld %lld\n",i,g[now][i]);
    ans=min(ans,n*g[now][i]-i*i);
  }

  write(ans);

  return 0;
}