P4072 [SDOI2016] 征途

之前 CSDN LaTeX \LaTeX LATEX 寄了的时候就没发上来的东西。

P4072 [SDOI2016]征途

题目大意

将序列分成 m m m 段,求每一段的和的最小方差。

n , m ⩽ 3000 n,m\leqslant 3000 n,m3000

题解

为了方便,我们将下文所有 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=1m(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=1m(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=1m(aˉ2+ai22aˉ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=1mai22(i=1mai)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=1mai)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=1mai2(i=1mai)2

后面那一项 ( ∑ i = 1 m a i ) 2 \left(\sum\limits_{i=1}^ma_i\right)^2 (i=1mai)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=1mai2。可以得到状态转移方程如下:

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,j1+(fifk)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,j1+(fifk)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,j1+fi2+fk22fifk

发现有一项既跟 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,j1+fk2,x=fk,k=2fi,b=dpi,jfi2。(这里那个 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} wi1,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;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值