題目鏈接
Part A 式子化簡
首先題目要求的式子就是 \(n^2\) 乘上 \(\frac{1}{n}\sum_{i=1}^n(a_i-\bar a)^2\),其中 \(\bar a=\frac{1}{n}\sum_{i=1}^n a_i\)。
我們把這三合在一起也就是:
前面化簡一下,后面用完全平方公式展開:
拆開:
對於第二坨,把與 \(a_i\) 無關的抽出來:
把第二坨化簡一下:
把第二坨的后兩個寫成平方的形式:
看第三坨,發現沒有和 \(i\) 有關的項,之間把 \(\sum_{i=1}^n\) 變成乘 \(n\):
化簡一下第三坨:
然后我們發現二、三坨可以合並:
於是對於每種 \(a\) 我們都有直接算的方法了。
Part B 差分交換
現在讓我們考慮令一個問題。
對於相鄰的三個數 \(a_{i-1}\)、\(a_i\)、\(a_{i+1}\),我們計算相鄰兩數的差分別為 \(a_i-a_{i-1}\)、\(a_{i+1}-a_i\)。
先在我們把 \(a_i\) 變成 \(a_{i-1}+a_{i+1}-a_i\),現在就是 \(a_{i-1}\)、\(a_{i-1}+a_{i+1}-a_i\)、\(a_{i+1}\)。
然后我們再對相鄰兩數作差分別為:\(a_{i+1}-a_i\)、\(a_i-a_{i-1}\)。
發現了什么?
沒錯,我們對每個數進行交換,只不過是把相鄰兩項的差交換。
於是我們對 \(a\) 作差分序列 \(d\),這樣無論我們對 \(a\) 序列進行怎樣的變換,\(d\) 序列的元素始終不變,只是順序改變。
基於這個結論,我們可以枚舉 \(d\) 的順序,時間復雜度 \(O(n!)\)。
Part C 差分單谷性
這里我們可以引出一個結論:\(d\) 的排列必然是先從大到小,再從小到大。
這里可以感性理解一下,因為對於每個 \(a\) 只有不斷靠近平均數,方差才最小。
我們也可以大致運用反證法證明。(有誤請指出)

圖中 \(d_1>d_2\),我們發現 \(a_1\) 與 \(a_3\) 的值不變,圖2相比圖1中只有 \(a_2\) 變大了。
兩個 \(a_2\) 相比,顯然圖1中的 \(a_2\) 更靠近谷底,也就是 \(\bar a\)。按照開始的式子 \(\frac{1}{n}\sum_{i=1}^n(a_i-\bar a)^2\) 我們發現 \(a_2\) 取圖1的情況更優。
有了這個結論,我們可以把 \(d\) 按從大到小排序,對於 \(d_i\),要么放左邊,要么放右邊,通過此方法,我們就得到了一種 \(O(2^n)\) 的暴力。
48分Code
// Problem: P7962 [NOIP2021] 方差【民間數據】
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P7962
// Memory Limit: 512 MB
// Time Limit: 1000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include<bits/stdc++.h>
using namespace std;
// #define int long long
inline int read(){int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;
ch=getchar();}while(ch>='0'&&ch<='9'){x=(x<<1)+
(x<<3)+(ch^48);ch=getchar();}return x*f;}
#define N 410
//#define mo
int n, m, i, j, k;
int a[N], d[N], b[N];
int ans=0x7fffffff;
int sum, num;
bool cmp(int x, int y)
{
return x>y;
}
void dfs(int x, int l, int r, int s, int t)
{
if(l==r-1)
{
ans=min(ans, n*t-s*s);
return ;
}
int k;
k=a[l+1]=a[l]+d[x]; dfs(x+1, l+1, r, s+k, t+k*k);
k=a[r-1]=a[r]-d[x]; dfs(x+1, l, r-1, s+k, t+k*k);
}
signed main()
{
// freopen("tiaoshi.in","r",stdin);
// freopen("tiaoshi.out","w",stdout);
n=read();
for(i=1; i<=n; ++i) a[i]=read();
for(i=2; i<=n; ++i) d[i-1]=a[i]-a[i-1];
sort(d+1, d+n, cmp);
dfs(1, 1, n, a[1]+a[n], a[1]*a[1]+a[n]*a[n]);
printf("%d", ans);
return 0;
}
Part D 相對位置代替絕對位置
可以發現,我們在上面的方法中每次都把 \(a\) 的絕對位置求出來。但其實我們可以只求出其相對位置。
我們先對 \(d\) 從小到達排序,再一條一條放進去。此時 \(a_i\) 的位置是在這種放進去的方法下的 \(\sum_{j=1}^i d_j\)。
首先讓我們把之前的式子搬回出來:
我們要讓結果最小,就是要讓 \(\sum_{i=1}^n a_i\) 最大,\(\sum_{i=1}^n a_i^2\) 最小。
讓我們設 \(f(i, j)\) 表示已經決定的 \(i\) 個 \(a\) 里面,和為 \(j\) 的最小平方和。設 \(S\) 為當前的 \(i\) 的 \(\sum_{j=1}^i d_i\) 。
\(j\) 可以理解為 \(\sum_{i=1}^n a_i\),\(f(i, j)\) 可以理解為 \(\sum_{i=1}^n a_i^2\)。
現在對於 \(d_{i+1}\),要么放在左邊,要么放在右邊。
先說放在右邊,\(j\) 則會加上當前點的坐標,答案加上當前的平方,而當前點為 \(S+d_i\)。
放在左邊的話,就是整體 \(a\) 右移 \(d_{i+1}\),\(j\) 就明顯是加上 \((i+1)\times d_{i+1}\),至於 \(f\) 的話,這里要手推一下。
首先至少要加上這個點 \(d_i^2\),然后后面的和變成 \(\sum_{k=1}^i(a_k+d_i)^2\),而原先是 \(\sum_{k=1}^i a_k^2\),所以加上的是:
前面用完全平方公式拆一下:
最前面與最后面兩坨消去:
前面那坨把無關緊要的提出了:
把 \(j\) 套進去:
后面那坨直接把 \(\sum_{k=1}^i\) 變成乘 \(i\):
根據這個,我們可以把放左邊的dp推出了:
后面兩個合一下:
然后就行了。
轉移方程與最終代碼可能有一些細節的東西,改一改就行了。
可是按照這個方法打只有72分。
72分code
// Problem: P7962 [NOIP2021] 方差【民間數據】
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P7962
// Memory Limit: 512 MB
// Time Limit: 1000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include<bits/stdc++.h>
using namespace std;
#define int long long
inline int read(){int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;
ch=getchar();}while(ch>='0'&&ch<='9'){x=(x<<1)+
(x<<3)+(ch^48);ch=getchar();}return x*f;}
#define N 410
#define M 610
//#define mo
int n, m, i, j, k;
int a[N], d[N];
int ans=0x7fffffffff;
int sum, num;
int f[N][N*M];
signed main()
{
// freopen("tiaoshi.in","r",stdin);
// freopen("tiaoshi.out","w",stdout);
n=read();
for(i=1; i<=n; ++i) a[i]=read();
for(i=2; i<=n; ++i) d[i-1]=a[i]-a[i-1];
sort(d+1, d+n);
for(i=0; i<=n; ++i) for(j=0; j<=240000; ++j) f[i][j]=ans;
f[0][0]=0;
for(i=0; i<n-1; ++i)
{
sum+=d[i+1];
for(j=0; j<=240000; ++j)
{
f[i+1][j+sum]=min(f[i+1][j+sum], f[i][j]+sum*sum);
f[i+1][j+d[i+1]*(i+1)]=min(f[i+1][j+d[i+1]*(i+1)], f[i][j]+2*j*d[i+1]+(i+1)*d[i+1]*d[i+1]);
// if(f[i][j]!=ans)
// printf("f[%lld][%lld]=%lld\n", i, j, f[i][j]);
}
}
for(i=0; i<=24000; ++i)
{
// if(f[n-1][i]!=0x7fffffffff) printf("f[%lld][%lld]=%lld\n", n-1, i, f[n-1][i]);
// ans=min(ans, n*(f[n-1][i]+2*i*a[1]+(n-1)*a[1]*a[1]+a[1]*a[1])-(a[1]*n+i)*(a[1]*n+i));
ans=min(ans, n*f[n-1][i]-i*i);
}
printf("%lld", ans);
return 0;
}
Part E 小優化
我們發現主要是集中在MLE和TLE。
MLE方面,我們可以使用滾動數組。
TLE的話,對於 \(j\) 的循環范圍,我們可以動態分配。
提一句,最后加不加上 \(a_1\) 都行(實測可以)。
最后貼上AC code:
// Problem: P7962 [NOIP2021] 方差【民間數據】
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P7962
// Memory Limit: 512 MB
// Time Limit: 1000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include<bits/stdc++.h>
using namespace std;
#define int long long
inline int read(){int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;
ch=getchar();}while(ch>='0'&&ch<='9'){x=(x<<1)+
(x<<3)+(ch^48);ch=getchar();}return x*f;}
#define N 10010
#define M 610
//#define mo
int n, m, i, j, k;
int a[N], d[N];
int ans=0x7ffffffffff;
int sum, num, p, q;
int f[2][500010];
signed main()
{
// freopen("tiaoshi.in","r",stdin);
// freopen("tiaoshi.out","w",stdout);
n=read();
for(i=1; i<=n; ++i) a[i]=read();
for(i=2; i<=n; ++i) d[i-1]=a[i]-a[i-1];
sort(d+1, d+n);
for(j=0; j<=500000; ++j) f[1][j]=f[0][j]=ans;
f[0][0]=0;
for(i=0; i<n-1; ++i)
{
sum+=d[i+1];
p=(i+1)%2; q=i%2; num+=d[i+1]*(i+1);
for(j=0; j<=num; ++j) f[p][j]=ans;
for(j=0; j<=num; ++j)
{
if(j+sum<=num)
f[p][j+sum]=min(f[p][j+sum], f[q][j]+sum*sum);
if(j+d[i+1]*(i+1)<=num)
f[p][j+d[i+1]*(i+1)]=min(f[p][j+d[i+1]*(i+1)], f[q][j]+2*j*d[i+1]+(i+1)*d[i+1]*d[i+1]);
}
}
for(i=0; i<=500000; ++i)
{
//ans=min(ans, n*(f[(n-1)%2][i]+2*i*a[1]+(n-1)*a[1]*a[1]+a[1]*a[1])-(a[1]*n+i)*(a[1]*n+i));
//可寫可不寫,是是否加上a1的情況
ans=min(ans, n*f[n-1][i]-i*i);
}
printf("%lld", ans);
return 0;
}
