例題
長度為n的正整數序列分為m段,求每段和的方差乘以m^2的最小值。——《SDOI2016 征途》
例題-solve(fake)
設分為\(m\)段每段長度為\(x_i\),此時的答案為
\[m^2\frac{\sum_i(x_i-\bar x)^2}{m}=m\sum_ix_i^2-2m\bar x\sum_ix_i+m^2\bar x^2=m\sum_ix_i^2-(\sum_ix_i)^2 \]
所以實際上是要最小化每段的平方和。然後可以寫暴力了,得出一個\(O(n^2m)\)的優秀算法。
帶權二分
或稱斜率凸優化、WQS二分,用於解決總共恰好有k次決策的最優化問題,設G(x)恰好x次的答案,G(x)呈現凸性(G'(x)單增或單減)。
簡單理解“帶權二分“,所謂“帶權”就是對決策附加一個代價-c,然後計算沒有次數限制的最優解g及取到最優解所需的最少決策次數t(這一問題需要能快速求解),所謂”二分“則是通過二分所帶權值使t等於所要求的k。
不太直觀?我們考慮凸函數G(x)與y=cx+b相切時的切點橫座標x0,這實際上是函數G(x)-cx的最大值(最小值)點的橫座標。也能從導數方面解釋:切點處G'(x0)=c,而G'(x)單調,故(G(x)-cx)'=G'(x)-c存在零點x0。所以帶權c有即二分切線斜率,此時的最優解所需決策次數t正是切點的橫座標x0。
這樣,我們就在函數圖像方面
?
認識了這一過程。
再談二分
斜率/權值c的範圍是什麼?實際上c的範圍只要能包含所有的G(x+1)-G(x)/1就行了。當如果G(x+1)-G(x)/1是全是整數時,我們甚至可以在整數域二分。
而實際上每次球的時最少決策次數,這可能永遠不會等於k,實際答案為g+mc而非g+tc。
凸性的判定
大概題目的凸性都不太好證明吧。因此可以採用
打表法
意會法,例如例題中,顯然G(x)比G(x+1)是不優的(因為\((a+b)^2>a^2+b^2\))並且G(x)比G(x+1)不優的程度因大於G(x+1)比G(x+2)不優的程度,推廣開就可以認為G(x)是有凸性的了。
例題-solve
整數域上二分權值c,設f[i]表示前i個分為若干段的最小平方和,顯然有f[0]=0,f[i]=f[j]+(s[i]-s[j])^2+c。而這是一個斜率優化可以解決的;設g[i]表示產生f[i]的最小決策次數,顯然g[i]單調不降。這意味着轉移i時如存在j1,j2都能取到較優解時,因考慮從g[j1]處更新g[i]。
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N=3e3+10;
int n,m,q[N];
ll s[N],f[N],g[N];
inline double slope(int x,int y) {
return (double)(f[y]+s[y]*s[y]-f[x]-s[x]*s[x])/(s[y]-s[x]);
}
inline void check(int C) {
static int h,t;
q[h=t=0]=f[0]=g[0]=0;
for(int i=1; i<=n; ++i) {
while(h<t&&slope(q[h],q[h+1])<2*s[i]) ++h;
f[i]=f[q[h]]-C+(s[i]-s[q[h]])*(s[i]-s[q[h]]);
g[i]=g[q[h]]+1;
while(h<t&&slope(q[t-1],q[t])>slope(q[t],i)) --t;
q[++t]=i;
}
}
int main() {
scanf("%d%d",&n,&m);
for(int i=1; i<=n; ++i) {
scanf("%lld",s+i);
s[i]+=s[i-1];
}
int l=-s[n]*s[n],r=0,mid,C=0;
while(l<=r) {
mid=l+(r-l)/2; check(mid);
if(g[n]<=m) l=mid+1,C=mid;
else r=mid-1;
}
check(C);
printf("%lld\n",(f[n]+m*C)*m-s[n]*s[n]);
return 0;
}
事實上,帶權二分還能做一些非dp的最優化問題,例如 BZOJ2654,基本方法始終是不變的。
沒想到吧,這竟然還是一篇講義 2333。