【BZOJ4518】[SDOI2016] 征途(重拾斜率优化DP)

点此看题面

大致题意: 让你把一个长度为\(n\)的序列划分成\(m\)块,求每块数总和的最小方差乘\(m^2\)的值。

转化方差

首先方差显然是一个比较复杂的东西,需要进行一定转化。

\(p_i\)为第\(i\)块数总和;\(s_i\)为原序列的前缀和,即\(s_i=\sum_{i=1}^ia_i\)\(\bar p\)\(p_i\)的平均值,即\(\bar{p}=\frac{\sum_{i=1}^mp_i}m=\frac{s_n}m\)

然后推式子:

\[m^2*\frac{\sum_{i=1}^m(p_i-\bar{p})^2}m=m\sum_{i=1}^m(p_i^2-2p_i\bar{p}+\bar{p}^2)=m\sum_{i=1}^mp_i^2-2m\bar{p}\sum_{i=1}^mp_i+m^2\bar{p}^2\]

其中\(\sum_{i=1}^mp_i\)显然就是\(s_n\),同时我们把\(\bar{p}\)的值代入得到:

\[m\sum_{i=1}^mp_i^2-2s_n^2+s_n^2=m\sum_{i=1}^mp_i^2-s_n^2\]

动态规划

考虑上面这个式子,其中\(m,s_n^2\)都是常数,因此我们只需要最小化\(p_i\)平方和。

可以考虑动态规划

\(f_{i,j}\)表示当前第\(i\)位,已划分出\(j\)块时的\(p_i\)平方和的最小值。

显然暴力转移只需枚举一个转移点:

\[f_{i,j}=\min_{x=1}^{i-1}(f_{x,j-1}+(s_i-s_x)^2)\]

这就是\(O(n^3)\)的做法了。

斜率优化

显然上面的\(DP\)还不够优,需要优化。

这里我们考虑斜率优化。

设当前为\(i\),比较对于\(a\)\(b\)两个转移点,若我们选择\(a\)进行转移,则需要满足:

\[f_{a,j-1}+(s_i-s_a)^2<f_{b,j-1}+(s_i-s_b)^2\]

拆平方并移项:

\[2s_i(s_b-s_a)<(f_{b,j-1}+s_b^2)-(f_{a,j-1}+s_a^2)\]

两边同除以\(s_b-s_a\)得:

\[2s_i<\frac{(f_{b,j-1}+s_b^2)-(f_{a,j-1}+s_a^2)}{s_b-s_a}\]

\(A(x)=s_x,B(x)=f_{x,j-1}+s_x^2\),则上面的式子就相当于:

\[2s_i<\frac{B(b)-B(a)}{A(b)-A(a)}\]

这是一个斜率的形式。

那么我们就可以开一个单调队列维护一个斜率逐渐上升的序列。

然后每次转移之前,将队首斜率小于\(2s_i\)的几项弹掉再转移即可。

代码

#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 3000
#define INF 1e9
using namespace std;
int n,m,a[N+5],s[N+5],q[N+5],f[N+5][N+5];
int main()
{
    RI i,j,H,T;for(scanf("%d%d",&n,&m),i=1;i<=n;++i) scanf("%d",a+i),s[i]=s[i-1]+a[i];//读入+初始化前缀和
    #define A(x) (s[x])
    #define B(x) (f[x][j-1]+s[x]*s[x])
    #define S(x,y) (1.0*(B(y)-B(x))/(A(y)-A(x)))
    #define Slope (2.0*s[i])
    for(i=1;i<=n;++i) f[i][0]=INF;//初始化
    for(j=1;j<=m;++j) for(q[H=T=1]=0,i=1;i<=n;++i)//注意要先枚j
    {
        W(H<T&&Slope>=S(q[H],q[H+1])) ++H;//弹掉不合法队首
        f[i][j]=f[q[H]][j-1]+(s[i]-s[q[H]])*(s[i]-s[q[H]]);//转移
        W(H<T&&S(q[T-1],q[T])>=S(q[T-1],i)) --T;q[++T]=i;//保证单调递增,放入队尾
    }return printf("%lld",1LL*m*f[n][m]-1LL*s[n]*s[n]),0;//输出答案
}
posted @ 2019-08-08 12:21  TheLostWeak  阅读(...)  评论(... 编辑 收藏