SPOJ SP16809 EST - Estimation 题解
Description
给定一个长度为 \(n\) 的数组 \(a\) 和一个整数 \(k\),你需要创建另一个长度为 \(n\) 的数组 \(b\)。\(b\) 数组需要由 \(k\) 个段组成,并且每个段中的数全部相等。求 \(\sum\limits_{i=1}^{n}|a_i-b_i|\) 的最小值。
其中 \(n\le2000,k\le25\) 并且 \(k\le n\)。
Solution
首先考虑朴素的动态规划。设 \(f_{i,k}\) 表示 \(b\) 数组的前 \(i\) 个数分成 \(k\) 段能取到的最小值。显然有
其中 \(cost(j+1,i)= \min(\sum\limits_{l=j+1}^{i}|a_l-b_l|)\)。
考虑如何求 \(cost(j+1,i)\)。注意到从 \(b_{j+1}\) 到 \(b_i\) 是属于同一段的,因此都等于一个定值 \(x\)。于是求 \(cost(j+1,i)\) 等价于找一个 \(x\),使 \(\sum\limits_{l=j+1}^{i}|a_l-x|\) 最小。将所有 \(a_l\) 看成数轴上的点,则问题变成在数轴上找一个点,使这个点到一些给定的点的距离之和最小。容易证明这个点取在 \(a_{j+1},a_{j+2}\dots a_{i}\) 的中位数处是最优的。
这个结论画个图就很好理解了。假设 \(i-(j+1)+1\) 是偶数,当 \(x\) 取中位数时,如下如所示:
那么 \(\sum\limits_{l=j+1}^{i}|a_l-x|=|a_{j+4}-a_{j+3}|+|a_{j+5}-a_{j+2}|+|a_i-a_{j+1}|\)。如果将 \(x\) 的位置移动到 \(a_{j+4}\) 和 \(a_{j+5}\) 之间,如下图所示:
那么结果就比原来多了 \(|x-a_{j+4}|\) 这一部分,显然不是最优的。将 \(x\) 移到其它区间同理,只有当 \(x\) 在 \([a_{j+3},a_{j+4}]\) 这一区间时才是最优的,取中位数即可。
\(i-(j+1)+1\) 是奇数时的证明也同理。
可以先将 \(a_{j+1},a_{j+2}\dots a_i\) 存到一个新数组中并排序得到中位数 \(a_{mid}\),再扫描一遍统计 \(\sum\limits_{l=j+1}^{i}|a_l-a_{mid}|\),就可以在 \(n \log n\) 的时间内求出 \(cost(j+1,i)\) 了。
这样一来,我们就得到了一个 \(O(n^3k\log n)\) 的优秀做法,可以拿下 TLE。
接下来考虑如何优化。注意到 \(cost(j+1,i)\) 可能被用了很多次,因此可以预处理。设 \(g_{i,j}=cost(i+1,j)\)。但是预处理的复杂度仍然是 \(O(n^3\log n)\) 的,无法承受。复杂度瓶颈在于排序。
为了更快地求出 \(a_{i+1}\) 到 \(a_j\) 的中位数,我们可以使用“对顶堆”,建立一个大根堆和一个小根堆。设 \(cnt=j-(i+1)+1\),大根堆存前 \(\lfloor\frac{cnt}{2}\rfloor\) 小的数,小根堆存剩下的数。从 \(a_{i+1}\) 开始扫描,每次将当前扫描到的数 \(x\) 和大根堆堆顶元素比较,如果 \(x\) 比较小就将 \(x\) 插入大根堆,否则插入小根堆。每次插入后对两个堆进行维护,确保大根堆的大小为 \(\lfloor\frac{cnt}{2}\rfloor\)。这样,大根堆的堆顶元素就是 \(i+1\) 到 \((i+1)+cnt-1\) 的中位数了。设 \(a_{mid}\) 是当前 \(cnt\) 个数的中位数,想要求出 \(\sum\limits_{l=i+1}^{(i+1)+cnt-1}|a_l-a_{mid}|\),只需在扫描的时候同时维护大根堆和小根堆中每个元素的和即可。
每次插入操作的复杂度为 \(O(\log n)\),我们成功让预处理的时间复杂度降到了 \(O(n^2\log n)\)。总时间复杂度 \(O(n^2(k+\log n))\),本题时间限制为 6s,已经足够。
Code
#include<bits/stdc++.h>
using namespace std;
const int INF=0x7fffffff;
const int N=2010,M=30;
int a[N],f[N][M],g[N][N],b[N];
int n,m;
void init()
{
for(int i=1;i<=n;i++)
{
priority_queue<int> q1;//大根堆
priority_queue<int,vector<int>,greater<int>> q2;//小根堆
q1.push(-INF),q2.push(INF);
int sum1=0,sum2=0;
for(int j=i;j<=n;j++)
{
//插入
if(q1.empty()||a[j]<=q1.top()) q1.push(a[j]),sum1+=a[j];
else q2.push(a[j]),sum2+=a[j];
//维护
if(q1.size()>q2.size()+1)
{
int x=q1.top();
q2.push(x);
q1.pop();
sum1-=x,sum2+=x;
}
else if(q1.size()<q2.size())
{
int x=q2.top();
q1.push(x);
q2.pop();
sum2-=x,sum1+=x;
}
//求出g[i][j]
int x=q1.top();
g[i][j]=q1.size()*x-sum1+sum2-q2.size()*x;
}
}
}
int main()
{
while(scanf("%d%d",&n,&m)!=EOF&&(n||m))
{
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
init();
memset(f,0x3f,sizeof(f));
f[0][0]=0;
for(int i=1;i<=n;i++)
for(int k=1;k<=m;k++)
for(int j=0;j<i;j++)
f[i][k]=min(f[i][k],f[j][k-1]+g[j+1][i]);
printf("%d\n",f[n][m]);
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统
· 【译】Visual Studio 中新的强大生产力特性
· 2025年我用 Compose 写了一个 Todo App