「SDOI2016」征途

原题面:LojLuogu

斜率优化,kixi 均单调,单调队列。

王 道 征 途
「そうですね…やっぱり僕は、王道を征く、ソープ系ですか」

简述

给定一列有序的 n 个物品,第 i 个物品的价值为 ai
给定参数 m,要求将一列物品分成 m 段,最小化每段长度之和的方差 v,输出 vm2
1nm3000(ai)3×104
1S,256MB。

分析

记划分出的 m 段的和分别为 b1bm,将 vm2 展开一下:

vm2=m2i=1m(bib¯)2m=mi=1m(bi22bib¯+b¯2)=mi=1m(bi22bii=1mbim+(i=1mbi)2m2)=mi=1mbi22(i=1mbi)2+(i=1mbi)=mi=1mbi2(i=1mbi)2

发现最后的式子中 (bi)2 等于 (ai)2,是一个定值,仅需最小化第一项即可。

按传统先写个暴力,设 fi,j 表示将前 i 个数划分为 j 段时 bi2 的最小值,转移时枚举段数 k 和上一段的结尾 j,则有:

fi,k=minj=0i1{fj,k1+(l=j+1ial)2}

预处理前缀和后暴力转移复杂度 O(n2m),考虑优化。
发现后一项是一个区间和的平方的形式,它同时与 i,j 有关。考虑对 a 做一个前缀和,设 si=i=1iai,将其代入原式:

fi,k=minj=0i1{fj,k1+(sisj)2}fi,k=minj=0i1{fj,k1+si22sisj+sj2}

考虑先通过枚举固定 k,对于每一个决策 fj,k,都有:

fi,ksj2=(fj,k1+sj2)2sisj

这是一个显然的斜率优化的形式,套路地设:

xi=siyi=fi,k1+si2ki=2sibi=fi,ksi2

对于上式,显然应最小化截距 bi 的值,易证最优决策点一定位于下凸包上。又 xjki 均随 i 的增加而增加,单调队列维护下凸包即可。注意一些初始化的小问题,详见代码。
总复杂度 O(nm) 级别。

代码

复制复制
//知识点:斜率优化
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
#define LD long double
const int kN = 3000 + 10;
//=============================================================
int n, m, h = 1, t, q[kN];
LL s[kN], f[kN][kN]; //代码中 f[j][i] 表示将前 i 个数划分为 j 段时 sum b_i^2 的最小值,与上述分析中相反。
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmin(LL &fir, LL sec) {
if (sec < fir) fir = sec;
}
LD X(int x_) {
return s[x_];
}
LD Y(int id_, int x_) {
return f[id_][x_] + s[x_] * s[x_];
}
LD K(int id_, int x_, int y_) {
if (X(x_) == X(y_)) return (Y(id_, y_) > Y(id_, x_) ? 1e18 : -1e18);
return (LD) ((Y(id_, y_) - Y(id_, x_)) / (X(y_) - X(x_)));
}
int Query(int id_, int now_) {
LD know = 2 * s[now_];
while (h < t && K(id_, q[h], q[h + 1]) <= know) ++ h;
return q[h];
}
void Insert(int id_, int now_) {
while (h < t && K(id_, q[t - 1], q[t]) >= K(id_, q[t - 1], now_)) -- t;
q[++ t] = now_;
}
//=============================================================
int main() {
n = read(), m = read();
for (int i = 1; i <= n; ++ i) s[i] = s[i - 1] + read();
memset(f, 63, sizeof(f));
f[0][0] = 0;
for (int k = 1; k <= m; ++ k) {
h = 1, t = 0;
Insert(k - 1, 0);
for (int i = 1; i <= n; ++ i) {
if (i >= k) {
int j = Query(k - 1, i);
f[k][i] = f[k - 1][j] + (s[i] - s[j]) * (s[i] - s[j]);
}
Insert(k - 1, i);
}
}
printf("%lld\n", 1ll * m * f[m][n] - 1ll * s[n] * s[n]);
return 0;
}
posted @   Luckyblock  阅读(109)  评论(0编辑  收藏  举报
编辑推荐:
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示