【题目描述】

Pine开始了从S地到T地的征途。 从S地到T地的路可以划分成n段,相邻两段路的分界点设有休息站。 Pine计划用m天到达T地。除第m天外,每一天晚上Pine都必须在休息站过夜。所以,一段路必须在同一天中走完。 Pine希望每一天走的路长度尽可能相近,所以他希望每一天走的路的长度的方差尽可能小。 帮助Pine求出最小方差是多少。 设方差是$ v $,可以证明,$ vm^2 $是一个整数。为了避免精度误差,输出结果时输出$ vm^2 $。

【题目链接】

BZOJ 4518 征途 【SDOI 2016】

【解题思路】

首先简单化下式子可得, $$ \begin{align*} v &= \frac 1 m \sum (x_i - \bar x )^2 \\ &= \frac 1 m \sum x_i^2 - \bar x^2 \end{align*} $$ (注意那个$\bar x^2$是在求和外面的哈)

把平方项展开下就可以得到这样的式子,注意到后面$ \bar x^2 $一项是固定的,因此要想最小化方差只需最小化平方和就可以了。

我们设$ f[i][j] $表示前$ i $天,走到$ j $位置的最小平方和,另以$ s_i $表示从原点到$ i $的距离,考虑是从前一天的$ k $位置走过来,转移显而易见, $$ f[i][j] = \min\{\ f[i - 1][k] + (s_i - s_j)^2, \ k \in [0, i] \} $$

直接做是 $ O(n^3) $的,可以过60分。

而正解就是斜率优化啦。

展开平方项,将$ k_1 < k_2$时,$ k_2 $优于$ k_1 $的充要条件写成斜率形式为,

$$ Slope(k_1, k_2) = \frac {g(k_1) - g(k_2)} {s_{k_1} - s_{k_2}} < 2s_j$$

其中,

$$ g(k) = f[i - 1][k] + s_k^2 $$

那么借助一个队列维护一系列有价值的决策,使得任意$ Slope(k_t, k_{t+1}) > 2s_j$,故最优决策在队首。

另维护斜率单调减。

【AC代码】

#include <cstdio>
#include <climits>
#include <algorithm>
 
const int MAXN = 3000;
const int MAXM = 3000;
 
typedef long long int64;
 
int n, m;
int64 sum[MAXN + 1];
 
inline int64 sqr(int64 x){
    return x * x;
}
 
inline void updateMin(int64 &x, int64 y){
    x = std::min(x, y);
}
 
inline int64 g(int64 *f, int k){
    return f[k] + sqr(sum[k]);
}
 
inline double slope(int64 *f, int i, int j){
    return (double)(g(f, i) - g(f, j)) / (sum[i] - sum[j]);
}
 
int64 f[MAXM + 1][MAXN + 1];
inline void dp(){
    for(int j = 0; j <= n; j++) f[1][j] = sqr(sum[j]);
 
    static int Q[MAXN + 1];
    for(int i = 2; i <= m; i++){
        int l = 0, r = -1;
 
        for(int j = 0; j <= n; j++){
            while(l < r && slope(f[i - 1], Q[r - 1], Q[r]) > slope(f[i - 1], Q[r], j)) r--;
            Q[++r] = j;
            while(l < r && slope(f[i - 1], Q[l], Q[l + 1]) < 2 * sum[j]) l++;
            
            int k = Q[l];
            f[i][j] = f[i - 1][k] + sqr(sum[j] - sum[k]);
        }
    }
}
 
int main(){
    scanf("%d%d", &n, &m);
 
    sum[0] = 0;
    for(int i = 1; i <= n; i++) scanf("%lld", sum + i), sum[i] += sum[i - 1];
 
    dp();
 
    printf("%lld\n", f[m][n] * m - sqr(sum[n]));
 
    return 0;
}

就是这样啦