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
*/