之前 CSDN LaTeX \LaTeX LATEX 寄了的时候就没发上来的东西。
P4072 [SDOI2016]征途
题目大意
将序列分成 m m m 段,求每一段的和的最小方差。
n , m ⩽ 3000 n,m\leqslant 3000 n,m⩽3000。
题解
为了方便,我们将下文所有 a i a_i ai 记为每一段的总和,不是他最开始的意义。记 f i f_i fi 表示到 i i i 的前缀和。
考虑先转化方差的式子:
s = 1 m ∑ i = 1 m ( a ˉ − a i ) 2 s=\dfrac{1}{m}\sum\limits_{i=1}^m(\bar{a}-a_i)^2 s=m1i=1∑m(aˉ−ai)2
按照题目要求左右同时乘上 m 2 m^2 m2:
s m 2 = m ∑ i = 1 m ( a ˉ − a i ) 2 sm^2=m\sum\limits_{i=1}^m(\bar{a}-a_i)^2 sm2=mi=1∑m(aˉ−ai)2
直接拆开平方:
s m 2 = m ∑ i = 1 m ( a ˉ 2 + a i 2 − 2 a ˉ a i ) sm^2=m\sum\limits_{i=1}^m(\bar{a}^2+a_i^2-2\bar{a}a_i) sm2=mi=1∑m(aˉ2+ai2−2aˉai)
接着拆掉求和:
s m 2 = m 2 a ˉ 2 + m ∑ i = 1 m a i 2 − 2 ( ∑ i = 1 m a i ) 2 sm^2=m^2\bar{a}^2+m\sum\limits_{i=1}^ma_i^2-2\left(\sum\limits_{i=1}^ma_i\right)^2 sm2=m2aˉ2+mi=1∑mai2−2(i=1∑mai)2
发现 m 2 a ˉ 2 m^2\bar{a}^2 m2aˉ2 与 − 2 ( ∑ i = 1 m a i ) 2 -2\left(\sum\limits_{i=1}^ma_i\right)^2 −2(i=1∑mai)2 是可以解决掉的:
s m 2 = m ∑ i = 1 m a i 2 − ( ∑ i = 1 m a i ) 2 sm^2=m\sum\limits_{i=1}^ma_i^2-\left(\sum\limits_{i=1}^ma_i\right)^2 sm2=mi=1∑mai2−(i=1∑mai)2
后面那一项 ( ∑ i = 1 m a i ) 2 \left(\sum\limits_{i=1}^ma_i\right)^2 (i=1∑mai)2 等价于序列中所有数的和的平方,是一个常数,不用考虑他。
设 d p i , j dp_{i,j} dpi,j 表示前 i i i 个数分成 j j j 段的最小 ∑ i = 1 m a i 2 \sum\limits_{i=1}^ma_i^2 i=1∑mai2。可以得到状态转移方程如下:
d p i , j = min ( d p k , j − 1 + ( f i − f k ) 2 ) dp_{i,j}=\min(dp_{k,j-1}+(f_i-f_k)^2) dpi,j=min(dpk,j−1+(fi−fk)2)
解法一:斜率优化
根据套路必须先去掉 min \min min:
d p i , j = d p k , j − 1 + ( f i − f k ) 2 dp_{i,j}=dp_{k,j-1}+(f_i-f_k)^2 dpi,j=dpk,j−1+(fi−fk)2
把括号拆开,分离各项:
d p i , j = d p k , j − 1 + f i 2 + f k 2 − 2 f i f k dp_{i,j}=dp_{k,j-1}+f_i^2+f_k^2-2f_if_k dpi,j=dpk,j−1+fi2+fk2−2fifk
发现有一项既跟 i i i 有关,也跟 k k k 有关。令 y = d p k , j − 1 + f k 2 , x = f k , k = 2 f i , b = d p i , j − f i 2 y=dp_{k,j-1}+f_{k}^2,x=f_k,k=2f_i,b=dp_{i,j}-f_i^2 y=dpk,j−1+fk2,x=fk,k=2fi,b=dpi,j−fi2。(这里那个 k k k 显然是斜率)维护一个上凸包就行。使用斜率优化的条件是 x , k x,k x,k 递增,我们发现确实如此,因此是可以斜率优化的。
在此基础上我们发现第二维 j j j 用过一次不会再用了,于是我们可以再利用一下滚动数组优化,这是好的。(四边形不等式解法同理)
代码不细讲了,见 这个大佬的博客。
这里放一个 HACK:
4 1
1 1 1 5
输出显然 0 0 0。
解法二:四边形不等式优化
不会严谨证明。但是呢我们可以控制 i i i 相同或 j j j 相同证明他的单调性。一个技巧,如果这个 DP 数组横着单调竖着也单调那他就满足四边形不等式,省去你的证明时间。(可能不是很严谨但是确实很好用)
先控制 j j j 相同,一堆正整数的和的平方显然加上一个数后只能越来越大,单调了。
再控制 i i i 相同,发现分的组越多,我们的每一个数产生的贡献越少,形象一点的说,原本 a i a_i ai 要和 a i + 1 , a i + 2 , a i + 3 a_{i+1},a_{i+2},a_{i+3} ai+1,ai+2,ai+3 一组,产生一部分贡献是 2 a i a i + 1 2a_{i}a_{i+1} 2aiai+1 等等。但是现在我们把组分的更多了,假定 a i + 3 a_{i+3} ai+3 不在这一组了,那么我们就会少一项 2 a i a i + 3 2a_{i}a_{i+3} 2aiai+3。所以分的组越多答案越小,也单调。
所以这个 DP 是满足四边形不等式的。由于决策点由 w i − 1 , j w_{i-1,j} wi−1,j 和 w i , j + 1 w_{i,j+1} wi,j+1 决定,我们要倒序枚举一下,要不然决策点还没出来。
最后加上滚动数组优化,时间复杂度 O ( n m ) \mathcal{O}(nm) O(nm),空间复杂度 O ( n ) \mathcal{O}(n) O(n)。
请大家忽略我代码前面那一大堆东西。
//四边形不等式
#include<bits/stdc++.h>
#define mid ((l+r)>>1)
#define fir first
#define sec second
#define lowbit(i) (i&(-i))
using namespace std;
const int N=3e3+5;
const int inf=1e18;
struct edge{int to,nxt,l;};
inline int read(){
char op=getchar();
int w=0,s=1;
while(op<'0'||op>'9'){
if(op=='-') s=-1;
op=getchar();
}
while(op>='0'&&op<='9'){
w=(w<<1)+(w<<3)+op-'0';
op=getchar();
}
return w*s;
}
//数论部分
const double pi=acos(-1);
const int mod=1e9+7;
int Mul(int a,int b){return (a%mod*b%mod)%mod;}
int Add(int a,int b){return (a+b)%mod;}
int Dec(int a,int b){return (a-b+mod)%mod;}
int Pow(int a,int k){
int ans=1;
while(k){
if(k&1) ans=Mul(ans,a);
a=Mul(a,a);
k>>=1;
}
return ans;
}
int gcd(int x,int y){return y==0?x:gcd(y,x%y);}
int lcm(int x,int y){return x/gcd(x,y)*y;}
int inv(int x){return Pow(x,mod-2);}
void exgcd(int a,int b,int &x,int &y){
if(b==0){
x=1,y=0;
return;
}
exgcd(b,a%b,x,y);
int t=x;
x=y;
y=t-a/b*y;
}
int dp[2][N],sum[N],a[N],w[2][N];
signed main(){
int n=read(),m=read();
for(register int i=1;i<=n;i++){
a[i]=read();
sum[i]=sum[i-1]+a[i];
}
memset(dp,0x3f,sizeof(dp));
dp[0][0]=0;
int now=1;
for(register int len=1;len<=m;len++){
w[now][n+1]=n-1;
for(register int i=n;i>=len;i--){
for(register int k=w[now^1][i];k<=w[now][i+1];k++){
if(dp[now^1][k]+(sum[i]-sum[k])*(sum[i]-sum[k])<dp[now][i]){
dp[now][i]=dp[now^1][k]+(sum[i]-sum[k])*(sum[i]-sum[k]);
w[now][i]=k;
}
}
}
for(register int i=0;i<=n+1;i++) dp[now^1][i]=(int)1e9,w[now^1][i]=0;
if(len==m) printf("%d",dp[now][n]*m-sum[n]*sum[n]);
now^=1;
}
}