征途[SDOI2016]
题目描述
Pine 开始了从 \(S\) 地到 \(T\) 地的征途。
从 \(S\) 地到 \(T\) 地的路可以划分成 \(h\) 段,相邻两段路的分界点设有休息站。
Pine 计划用 \(m\) 天到达 \(T\) 地。除第 \(n\) 天外,每一天晚上 Pine 都必须在休息站过夜。所以,一段路必须在同一天中走完。
Pine 希望每一天走的路长度尽可能相近,所以他希望每一天走的路的长度的方差尽可能小。
帮助 Pine 求出最小方差是多少。
设方差是 \(v\) ,可以证明,\(v*m^2\) 是一个整数。为了避免精度误差,输出结果时输出 \(v*m^2\)。
输入格式
第一行两个数 \(n\) 、\(m\) 。
第二行 \(n\) 个数,表示 \(n\) 段路的长度。
题解
CCF不考DP了(悲)
假设有一个序列 \(a_1\sim a_n\) ,平均数是 \(v\) ,那么它的方差就是 \(\dfrac{\sum\limits_{i=1}^{n}(a_i)^2}{n}-v^2\)
所以原问题就变成:将 \(n\) 个数分为 \(m\) 段,假设第 \(i\) 段内的所有数之和为 \(b_i\) ,求 \(\sum\limits_{i=1}^{m} (b_i)^2\) 的最小值
设 \(f_{i,j}\) 表示将前 \(j\) 个数分成 \(i\) 段的最小平方和
容易得到转移方程: \(f_{i,j} = \min\limits_{0\le k<j} (f_{i-1,k} + (S_j-S_k)^2)\) , \(S\) 表示前缀和
变换一下得到 \(f_{i-1,k}+(S_k)^2=2*S_j*S_k+(f_{i,j}-(S_j)^2)\)
这样就可以进行斜率优化,维护 \(f_{i-1}\) 的凸壳来转移出 \(f_i\)
由于 \(S\) 数组单调递增,所以直接用单调队列维护下凸壳即可
时间复杂度 \(O(nm)\)
代码
#include <bits/stdc++.h>
#define N 5005
using namespace std;
typedef long long ll;
template<typename T>
inline void read(T &num) {
T x = 0, f = 1; char ch = getchar();
for (; ch > '9' || ch < '0'; ch = getchar()) if (ch == '-') f = -1;
for (; ch <= '9' && ch >= '0'; ch = getchar()) x = (x << 3) + (x << 1) + (ch ^ '0');
num = x * f;
}
int n, m, head, tail, q[N];
ll a[N], f[2][N], x[N], y[N];
inline double slope(int p, int q) {
if (x[p] == x[q]) return 1e15;
else return 1.0 * (y[p] - y[q]) / (x[p] - x[q]);
}
int main() {
read(n); read(m);
for (int i = 1; i <= n; i++) {
read(a[i]); a[i] += a[i-1];
}
int o = 0;
for (int i = 1; i <= m; i++) {
o = i & 1;
head = 1; tail = 0;
for (int j = 0; j <= n; j++) {
if (i > 1 || j == 0) {
while (head < tail && slope(q[tail-1], q[tail]) > slope(q[tail], j)) tail--;
q[++tail] = j;
}
while (head < tail && slope(q[head], q[head+1]) < (double)a[j]) head++;
int k = q[head];
f[o][j] = f[!o][k] + (a[j]-a[k]) * (a[j]-a[k]);
}
for (int j = 0; j <= n; j++) {
f[!o][j] = 0;
x[j] = 2 * a[j]; y[j] = f[o][j] + a[j] * a[j];
}
}
printf("%lld\n", f[m&1][n] * m - a[n] * a[n]);
return 0;
}