[BZOJ3675][Apio2014]序列分割 n*log 做法(斜率优化dp+带权二分)

Address

BZOJ 3675
O(nk) O ( n k ) 斜率优化题解

Solution

假设我们把 k k 加强到 [1,n1] 而不是 [1,min(n1,200)] [ 1 , min ( n − 1 , 200 ) ]
结论:最终的得分与分割顺序无关,并且最终得分为:

(ni=1ai)2k+1i=1b2i2 ( ∑ i = 1 n a i ) 2 − ∑ i = 1 k + 1 b i 2 2

其中 bi b i 为第 i i 段的总分。
(具体分析见 O(nk) 斜率优化题解)
于是我们要最小化每一段的平方之和。
dp 模型: f[i][j] f [ i ] [ j ] 表示前 i i 个数分成 j 段的最小平方和。
转移显然:
f[i][j]=mink=0i1{f[k][j1]+(sum[i]sum[k])2} f [ i ] [ j ] = min k = 0 i − 1 { f [ k ] [ j − 1 ] + ( s u m [ i ] − s u m [ k ] ) 2 }

复杂度 O(n2k) O ( n 2 k ) 。经过斜率优化之后复杂度 O(nk) O ( n k ) ,在 kn1 k ≤ n − 1 时无法通过。
我们考虑能否去掉第二维。
感性地理解一下,可以得到对于任意的 i,j i , j 都有 f[i][j]f[i][j+1] f [ i ] [ j ] ≥ f [ i ] [ j + 1 ]
f[i][j]f[i][j+1]f[i][j+1]f[i][j+2] f [ i ] [ j ] − f [ i ] [ j + 1 ] ≥ f [ i ] [ j + 1 ] − f [ i ] [ j + 2 ]
不妨给每一段一个代价 C C ,表示分成 j 段的额外代价是 j×C j × C ,并去掉 f f 的第二维。
根据上面的结论可以得到推论:设 f[n] 取得最小值时分成 最多 q(C) q ( C ) 段,那么 q q C 的增大而单调不增。 C=0 C = 0 时最优方案是分成 n n 段每段一个数, C+ 时最优方案是只分一段。
于是我们考虑二分 C C ,也就是找到满足 q(C)k+1 的最大的 C C
f 的转移:
f[i]=maxj=0i1{f[j]+(sum[i]sum[j])2}+C f [ i ] = max j = 0 i − 1 { f [ j ] + ( s u m [ i ] − s u m [ j ] ) 2 } + C

是一个简单的斜率优化 dp 。
但我们二分判定 C C 时要求 q(C) ,故还要记下 g[i] g [ i ] 表示前 i i 个数在代价最小的前提下分成的最大段数。取最优决策时,如果最优决策三点共线,则要取出 g 最大的点进行转移。
这种二分称为 带权二分凸优化
复杂度 O(nlogC) O ( n log ⁡ C )

Code

判断斜率是否递增时不能写成叉积的形式,否则会爆 long long

#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define For(i, a, b) for (i = a; i <= b; i++)
using namespace std;
inline int read()
{
    int res = 0; bool bo = 0; char c;
    while (((c = getchar()) < '0' || c > '9') && c != '-');
    if (c == '-') bo = 1; else res = c - 48;
    while ((c = getchar()) >= '0' && c <= '9')
        res = (res << 3) + (res << 1) + (c - 48);
    return bo ? ~res + 1 : res; 
}

template <class T>
T Max(T a, T b) {return a > b ? a : b;}

typedef long long ll;

const int N = 1e5 + 5;
int n, k, g[N], H, T, Q[N];
ll f[N], X[N], Y[N], sum[N];

ll calc(int j, int i)
{
    return Y[j] - X[j] * sum[i] * 2;
}

bool slope(int k, int j, int i)
{
    return X[j] == X[i] ? 1.0 * (X[j] - X[k]) / (Y[j] - Y[k])
        <= 1.0 * (X[i] - X[k]) / (Y[i] - Y[k]) :
        1.0 * (X[j] - X[k]) / (Y[j] - Y[k])
        < 1.0 * (X[i] - X[k]) / (Y[i] - Y[k]);
}

void slopedp(ll mid)
{
    int i;
    f[0] = 0; g[0] = 0;
    X[0] = Y[0] = 0;
    Q[H = T = 1] = 0;
    For (i, 1, n)
    {
        while (H < T && calc(Q[H], i) >= calc(Q[H + 1], i))
        {
            if (calc(Q[H], i) == calc(Q[H + 1], i))
                g[Q[H + 1]] = Max(g[Q[H + 1]], g[Q[H]]);
            H++;
        }
        f[i] = calc(Q[H], i) + sum[i] * sum[i] + mid;
        g[i] = g[Q[H]] + 1;
        X[i] = sum[i]; Y[i] = f[i] + sum[i] * sum[i];
        while (H < T && slope(Q[T - 1], Q[T], i)) T--;
        Q[++T] = i;
    }
}

int main()
{
    int i;
    n = read(); k = read();
    For (i, 1, n) sum[i] = sum[i - 1] + read();
    ll l = 0, r = 1e18;
    while (l <= r)
    {
        ll mid = l + r >> 1;
        if (slopedp(mid), g[n] >= k + 1) l = mid + 1;
        else r = mid - 1;
    }
    slopedp(r);
    cout << (sum[n] * sum[n] - f[n] + r * (k + 1) >> 1) << endl;
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值