「SDOI2016」征途 - 斜率优化 DP

Pine 开始了从 地到 地的征途。 从 地到 地的路可以划分成 段,相邻两段路的分界点设有休息站。 Pine 计划用 天到达 地。除第 天外,每一天晚上 Pine 都必须在休息站过夜。所以,一段路必须在同一天中走完。 Pine 希望每一天走的路长度尽可能相近,所以他希望每一天走的路的长度的方差尽可能小。 帮助 Pine 求出最小方差是多少。

设方差是 ,可以证明, 是一个整数。为了避免精度误差,输出结果时输出

链接

COGS 2225
BZOJ 4518

题解

为每一天的路程,,题目要求即为最小化

将上式展开,整理得

因为 是个常数, 是个常数,所以只要最小化

即可

表示前 段路,分成 天的最优方案对应上式的值,则有

直接以这个式子进行划分DP,状态数为 ,时间复杂度为 ,预计得分 60 分。

尝试进行优化。首先,二维的状态存储,显然第一维是可以滚动的,设 。考虑 的两个取值 ),若 优,则有

左边是一个斜率的式子,分子和分母都是单调的,右边也是单调的。使用一个单调队列来存储一些决策点,使得从前到后每一个决策点都比下一个决策点更优,需要满足的条件是:

  1. 较后面的一对点组成的斜率比较前面一对点大;
  2. 个点与第 个点()组成的斜率大于 (如果小于,说明较靠后的 点更优)。

因为斜率是单调递增的,所以第 2 条只需要使前两个元素满足条件即可。

枚举 ,不需要枚举 ,而是从单调队列中寻找最优决策点。首先检查队首元素,使其满足条件 2,此时队首即为最优解;然后将当前决策点作为新的 加入到队列尾部,需要先删除一些决策点使得条件 1 被满足。因为每个决策点最多会被添加、删除各一次,所以状态转移的代价为均摊 ,总时间复杂度降为 ,可以通过本题。

注意正无穷的取值,合理取值可以避免特判。

代码

#include <cstdio>
#include <cstring>
#include <climits>
#include <algorithm>

const int MAXN = 3000;
const int MAXM = 3000;

int n, m, a[MAXN + 1], s[MAXN + 1];
long long f[MAXN + 1], g[MAXN + 1];

template <typename T>
inline T sqr(const T x) { return x * x; }

inline double slope(const int a, const int b) {
    // printf("K(%d, %d)\n", a, b);
    return (double)(g[a] - g[b] + sqr(s[a]) - sqr(s[b])) / (double)(s[a] - s[b]);
}

int main() {
    freopen("menci_journey.in", "r", stdin);
    freopen("menci_journey.out", "w", stdout);

    scanf("%d %d", &n, &m);
    for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
    for (int i = 1; i <= n; i++) s[i] = s[i - 1] + a[i];

    for (int i = 1; i <= n; i++) f[i] = INT_MAX;
    for (int j = 1; j <= m; j++) {
        memcpy(g, f, sizeof(f));
        memset(f, 0, sizeof(f));

        static int q[MAXN];
        int l = 0, r = -1;
        q[++r] = 0;

        for (int i = 1; i <= n; i++) {
            while (l < r && slope(q[l + 1], q[l]) < 2 * s[i]) l++;

            int t = q[l];
            f[i] = g[t] + sqr(s[i] - s[t]);

            while (l < r && slope(q[r], q[r - 1]) > slope(q[r], i)) r--;

            q[++r] = i;
        }
    }

    // for (int i = 1; i <= n; i++) printf("s[%d] = %lld\n", i, s[i]);
    printf("%d\n", (int)(f[n] * m - sqr(s[n])));

    fclose(stdin);
    fclose(stdout);

    return 0;
}