征途[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_1sim a_n) ,平均数是 (v) ,那么它的方差就是 (dfrac{sumlimits_{i=1}^{n}(a_i)^2}{n}-v^2)

所以原问题就变成:将 (n) 个数分为 (m) 段,假设第 (i) 段内的所有数之和为 (b_i) ,求 (sumlimits_{i=1}^{m} (b_i)^2) 的最小值

(f_{i,j}) 表示将前 (j) 个数分成 (i) 段的最小平方和

容易得到转移方程: (f_{i,j} = minlimits_{0le 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
", f[m&1][n] * m - a[n] * a[n]);
	return 0;
}
原文地址:https://www.cnblogs.com/ak-dream/p/AK_DREAM115.html