1 条题解
-
0
思路与算法
令相邻差 。一次操作恰好交换 ,因此所有差的任意排列都能到达。整体平移不影响方差,可固定第一个数为0。设前缀坐标的和为 、平方和为 ,答案就是 。
最优差序可取V形,即先非增、再非减。因此按差值从小到大处理,把每组相同差分配到当前序列左右两端。零差先合并:若有 个零差,初态为 个重合点,跨度 ,。状态 记录固定当前点数 、跨度 时的最小平方和 ;相同 下只保留最小 ,未来变换中其他部分只依赖 ,所以此压缩安全。
考虑差值 出现 次,左端放 次、右端放 次。定义 ,,, 。 直接累加新点坐标可得 , 。
若 ,当前必为单个点且没有已处理差;这一组产生唯一等距点集,直接初始化 。其余情况 。令 ,消去 得 。 对于固定余数 ,新坐标 的候选旧坐标为 。维护评分 的长度 滑动窗口最小值即可,一次处理整组,无须对每个旧状态再枚举全部 。处理完后更新 。最后枚举所有有限状态,取 的最小值。
正确性说明
操作与差排列。 修改 后,左差变为原右差、右差变为原左差,其他差不变。相邻交换生成任意排列;所有差非负,故整个过程中数列仍单调且端点不变。平移不改方差,目标展开确为 。
V形性质。 假设全局最优差序包含内峰:连续 个相等正差 ,两边的差为 。交换左边界相邻差,只把一个前缀坐标 增加 ;交换右边界,只把另一个前缀坐标 减少 。这两个前缀相距 。若一个坐标 改变 ,目标的变化为 。最优性要求两次交换的变化都非负,于是 , 。 相减得到 ,整理为 ,与 矛盾。因此不存在内峰或高平台。没有内峰平台的序列必为先非增后非减的V形。
状态覆盖与分组。 V形差序中,所有最小差位于中央;每加入一个更大差,只能放到已处理差序两端。同组相同差放左 个,其余放右,恰好覆盖所有V形排列。零差均聚在中央,初态处理不漏解。对固定 ,较大 在任何后续分组中仍会保持较大,故只保留最小平方和不丢失最优。
滑窗等价。 新序列中旧点整体右移 ,新增左点坐标为 ,新增右点为 ,求和即得两条分组公式。它们消去 后得到评分恒等式。固定余数时, 等价于旧坐标落在长度 的等步长窗口中,所以单调队列返回的最小评分与逐个枚举 完全一致。由组数归纳,最终DP覆盖全部最优差序,取目标最小值即为答案。
复杂度
记总跨度 ,正差不同值数为 。因为这些不同正整数之和不超过 ,有 ,从而 。初始化读入和差值计数为 。第 组所有余数链扫描的总长度为 ,其中 为当前跨度,单调队列每个位置至多入队、出队一次。因此总时间 ,可宽松写为 ;零差无论多少都只形成一个初态,不额外增加组数。空间为两个DP数组,,另含常数大小差值计数与一条余数链的队列。所有目标、评分和中间乘积用64位有符号整数;按 的全局上界,最大中间量小于 。
易错点
不能直接枚举所有差排列,也不能只对小 搜索后把大数据当作已解决。V形性质需要交换证明,不能凭经验交替摆放。零差造成大量重复坐标,必须计入点数 ,不能删掉这些点。 时不能做余数或除法;该组用等距点集直接初始化。评分变换中的 与 不能混用,窗口过期条件是旧坐标小于 。不可达状态应跳过,不能把无穷大带入乘法。答案是 ,不是 。题面方差定义的求和下标排版有误,按 个数的定义解释。
参考代码(C++11)
#include <bits/stdc++.h> using namespace std; using ll = long long; const ll INF = 4000000000000000000LL; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; if (!(cin >> n)) return 0; vector<int> count(600); int previous; cin >> previous; for (int i = 1; i < n; ++i) { int a; cin >> a; ++count[a - previous]; previous = a; } int c = count[0] + 1, L = 0, lo = 0, hi = 0; vector<ll> dp(1, 0); for (int x = 1; x < 600; ++x) if (count[x]) { int q = count[x]; ll H = 1LL * (c - 1) * x - L; ll C = 1LL * q * L + 1LL * x * q * (q + 1) / 2; ll A = 1LL * L * L + 1LL * L * x; ll D = 1LL * q * L * L + 1LL * L * x * q * (q + 1) + 1LL * x * x * q * (q + 1) * (2 * q + 1) / 6; if (H == 0) { lo = hi = (int)C; dp.assign(hi + 1, INF); dp[hi] = D; c += q; L += q * x; continue; } int zmax = hi + q * H; vector<ll> next((int)C + zmax + 1, INF); vector<int> queue((hi - lo) / H + q + 4); vector<ll> val(queue.size()); int nlo = INT_MAX, nhi = 0; for (int r = 0; r < H && r <= zmax; ++r) { int head = 0, tail = 0; int start = r; if (start < lo) start += (lo - start + H - 1) / H * H; for (int z = start; z <= zmax; z += H) { while (head < tail && queue[head] < z - q * H) ++head; if (z <= hi && dp[z] != INF) { ll score = H * dp[z] - 1LL * x * z * z + A * z; while (head < tail && val[tail - 1] >= score) --tail; queue[tail] = z; val[tail++] = score; } if (head < tail) { int p = C + z; ll Q = (val[head] + H * D + 1LL * x * z * z - A * z) / H; next[p] = Q; nlo = min(nlo, p); nhi = max(nhi, p); } } } next.resize(nhi + 1); dp.swap(next); lo = nlo; hi = nhi; c += q; L += q * x; } ll answer = INF; for (int p = lo; p <= hi; ++p) if (dp[p] != INF) answer = min(answer, 1LL * n * dp[p] - 1LL * p * p); cout << answer << '\n'; }
- 1