P7962-[NOIP2021]方差【dp,差分】
正题
题目链接:https://www.luogu.com.cn/problem/P7962
题目大意
给出一个长度为\(n\)的序列\(a\),你每次可以让一个\(a_i(1<i<n)=a_{i-1}+a_{i+1}-a_i\),求能变出的最小方差。
\(1\leq n\leq 400,1\leq a_i\leq 600\)或\(1\leq n\leq 10^4,1\leq a_i\leq 50\)
解题思路
民间数据过了就当过了吧
这个式子显然我们可以差分之后变成交换相邻的数,让所有的数都减去\(a_1\)这样方差不变并且第一个保证为\(0\),然后我们记差分数组\(b_i=a_{i+1}-a_i\),那么化一下答案的式子就是
这个式子乍一眼我们看不出什么,但是我们可以得到每个\(b_i\)对之间乘积的权重记为\(f_{i,j}\),会发现\(f_{i,j}\)是按照\(i/j\)相互之间越接近/越接近中间而递增的,但是依然会出现一些边边的靠近数对的情况比中间的不那么靠近数对的权重要高。
一个直观的想法是类似于一个山谷之类的填法,打几个表之后不难发现确实是从一个点往两边递增的规律。
考虑在此基础上进行\(dp\),考虑在一个填好的序列的前面/后面加上一个数字会产生的变化,记\(ans\)为原来的答案,记\(L\)为题目中给出的\(n\)(因为目前我们还没有放完\(n\)个数字,所以此时的\(n\)不一样),\(x\)为插入的数组。
- 插在前面:
- 插在后面:
然后会发现我们很难知道\(\sum_{i=1}^{n-1}b_i(n-i)\)这个东西,所以考虑放进\(dp\)数组里面维护,但是如果丢进去维护了后面那个东西就完全没有必要了,所以可以删掉很多复杂的部分。
那么设\(f_{i,j}\)表示目前填了\(i\)个\(\sum_{i=1}^{n-1}b_i(n-i)=j\)时的最小方差,然后就可以\(O(1)\)转移了。
这样的时间复杂度是\(O(n^2a_i)\)的,可以拿到\(88\)分,我在考场上就止步于此了。
现在来分析一下最后一个点\(a_i\leq 50\)的性质,也就是说差分数组里面最多只有\(50\)个数是有值的,所以有一堆\(0\),直接动态更新\(dp\)枚举的上界就过了。
时间复杂度:\(O(na_i\min\{a_i,n\})\)
\(\color{white}{好不容易那么接近一次正解,你却让我输的那么彻底,焯!}\)
code
#include<cstdio>
#include<cstring>
#include<algorithm>
#define ll long long
using namespace std;
const ll N=5e5+10,inf=1e18;
ll n,L,a[N],s[N],f[2][N],ans;
signed main()
{
scanf("%lld",&n);
if(n==1)return puts("0");
for(ll i=1;i<=n;i++)scanf("%lld",&a[i]);
L=n*a[n];ans=inf;
for(ll i=1;i<n;i++)a[i]=a[i+1]-a[i];
n--;sort(a+1,a+1+n);L=a[1];
for(ll i=1;i<=n;i++)s[i]=s[i-1]+a[i];
for(ll i=0;i<=L;i++)f[1][i]=inf;
f[1][a[1]]=a[1]*a[1];
for(ll k=2;k<=n;k++){
int R=s[k]*k;
for(ll i=0;i<=R;i++)f[k&1][i]=inf;
for(ll i=0;i<=L;i++){
if(f[~k&1][i]!=inf){
f[k&1][i+a[k]*k]=min(f[k&1][i+a[k]*k],f[~k&1][i]+k*a[k]*a[k]+2ll*i*a[k]);
f[k&1][i+s[k]]=min(f[k&1][i+s[k]],f[~k&1][i]+s[k]*s[k]);
}
}
L=R;
}
for(ll i=0;i<=L;i++)
if(f[n&1][i]!=inf)
ans=min(ans,f[n&1][i]*(n+1)-i*i);
printf("%lld\n",ans);
return 0;
}
/*
10
6 19 34 35 56 63 82 82 83 99
*/