洛谷 3648 bzoj 3675 [APIO2014]序列分割 题解

博客探讨了一道关于序列分割的问题,通过数学证明和斜率优化方法解决O(n^2)的时间复杂度,实现最大分数的计算。文章详细解释了如何使用前缀和与斜率优化技巧来优化动态规划,同时提供了实现时需要注意的空间优化策略。

博客观赏效果更佳

题意简述

给定一个长度为 n ( < = 1 e 5 ) n(<=1e5) n(<=1e5)的序列 a a a,和一个 k k k。将 a a a k k k次,划分成 k + 1 k+1 k+1个段,每切一次产生分数就是姓切出来的两个段的和的乘积。请你最大化分数(还要记录在哪里切的)。

思路

式子 j j j [ 0 , i ] [0,i] [0,i]之间。

(矢量图,随便放大)斜率优化一下顺便记录答案即珂。

具体思路

先证明分数和切的顺序没有关系。

设原序列珂以分为三个段,和为 x , y , z x,y,z x,y,z

  1. 先切 x , y x,y x,y,再切 y , z y,z y,z x ( y + z ) + y z = x y + y z + x z x(y+z)+yz=xy+yz+xz x(y+z)+yz=xy+yz+xz
  2. 先切 y , z y,z y,z,再切 x , y x,y x,y ( x + y ) z + x y = x y + y z + x z (x+y)z+xy=xy+yz+xz (x+y)z+xy=xy+yz+xz

所以是一样的。显然这个结论有珂扩展性,即如果原序列划分成更多的块,也能三个三个的合并,并得到同样的结果。证毕。

d p [ i ] [ k ] dp[i][k] dp[i][k]表示前 i i i个数切 k k k刀的最大分数,然后枚举上一次分开的位置 j j j,用前缀和维护一下就能得到上面那个方程了。但是很显然这个是 O ( n 2 ) O(n^2) O(n2)的,过不去。

考虑斜率优化。由于 d p [ × ] [ k ] dp[\times][k] dp[×][k]只和 d p [ × ] [ k − 1 ] dp[\times][k-1] dp[×][k1]有关,所以设 f i f_i fi表示 d p [ i ] [ k ] dp[i][k] dp[i][k] g i g_i gi表示 d p [ i ] [ k − 1 ] dp[i][k-1] dp[i][k1]。然后:
f i = m a x { g j + s u m [ j ] × s u m [ i ] − s u m [ j ] 2 } f_i=max\{g_j+sum[j]\times sum[i]-sum[j]^2\} fi=max{gj+sum[j]×sum[i]sum[j]2}

考虑两个转移点 j , k j,k j,k。如果 j j j k k k优,那么:

$
g_j+sum[j]\times sum[i]-sum[j]^2>=g_k+sum[k]\times sum[i]-sum[k]^2\
\frac{(g_j-sum[j]2)-(g_k-sum[k]2)}{-sum[j]-(-sum[k])}<=sum[i]
$
把每个点 j j j看作是 P ( − s u m [ j ] , g [ j ] − s u m [ j ] 2 ) P(-sum[j],g[j]-sum[j]^2) P(sum[j],g[j]sum[j]2),然后我们发现,由于 s u m [ i ] sum[i] sum[i]是单调递增的,所以这个 p p p的斜率也是单调递增的(不明白去找斜率优化的笔记看看。。。)

然后对于队首的元素,如果斜率超过了 s u m [ i ] sum[i] sum[i],也就出队了。维护一下即珂。

实现注意

  1. 记录答案:设 p r e [ i ] [ k ] pre[i][k] pre[i][k]表示 d p [ i ] [ k ] dp[i][k] dp[i][k]时选择的最优的 j j j。然后不断往前跳即珂输出答案。
  2. 空间开不下,记得用滚动数组维护 d p dp dp(或者只需要一个 f f f和一个 g g g即珂)
  3. l o n g l o n g long long longlong勿忘

代码:

#include<bits/stdc++.h>
using namespace std;
namespace Flandre_Scarlet
{
    #define int long long 
    #define real double
    #define EPS 1e-7
    #define N 155555
    #define F(i,l,r) for(int i=l;i<=r;++i)
    #define D(i,r,l) for(int i=r;i>=l;--i)
    #define Fs(i,l,r,c) for(int i=l;i<=r;c)
    #define Ds(i,r,l,c) for(int i=r;i>=l;c)
    #define Tra(i,u) for(int i=G.Start(u),__v=G.To(i);~i;i=G.Next(i),__v=G.To(i))
    #define MEM(x,a) memset(x,a,sizeof(x))
    #define FK(x) MEM(x,0)
    void R1(int &x)
    {
        x=0;char c=getchar();int f=1;
        while(c<'0' or c>'9') f=(c=='-')?-1:1,c=getchar();
        while(c>='0' and c<='9') x=(x<<1)+(x<<3)+(c^48),c=getchar();
        x=(f==1)?x:-x;
    }
    int n,k,a[N];
    int s[N];
    void Input()
    {
        R1(n),R1(k);
        F(i,1,n) 
        {
            R1(a[i]);
            s[i]=s[i-1]+a[i];
        }
    }

    int f[N],g[N];
    int pre[N][201];
    int x(int i){return g[i]-s[i]*s[i];}
    int y(int i){return -s[i];}
    //抽象成点
    double slope(int a,int b)
    {
        real dx=x(a)-x(b);
        real dy=y(a)-y(b);
        return (fabs(dy)<EPS)?-1e18:(dx/dy);
    }//计算斜率
    int Q[N],head,tail;
    void Soviet()
    {
        head=1,tail=1;
        Q[tail]=0;
        F(p,1,k)
        {
            FK(Q);
            FK(f);
            head=tail=1;
            Q[tail]=0;
            
            F(i,1,n)
            {
                while(head<tail and slope(Q[head],Q[head+1])<=s[i]) ++head;//斜率<=s[i]的出去
                int j=Q[head];
                f[i]=g[j]+s[j]*(s[i]-s[j]);
                pre[i][p]=j;//记录从哪里转移的
                while(head<tail and slope(Q[tail-1],Q[tail])>=slope(Q[tail],i)) --tail;
                Q[++tail]=i;//加入队列
            }
            F(i,1,n) g[i]=f[i];//别忘了滚动
        }

        printf("%lld\n",f[n]);
        int cur=n;
        D(i,k,1) cur=pre[cur][i],printf("%lld%c",cur," \n"[i==1]);
    }
    void IsMyWife()
    {
        Input();
        Soviet();
    }
    #undef int //long long 
}
int main()
{
    Flandre_Scarlet::IsMyWife();
    getchar();getchar();
    return 0;
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值