黄源河《左偏树的应用》——数字序列(Baltic 2004)
这道题哪里都找不到。
[问题描述]
给定一个整数序列a1, a2, … , an,求一个不下降序列b1 ≤ b2 ≤ … ≤ bn,使得数列{ai}和{bi}的各项之差的绝对值之和 |a1 - b1| + |a2 - b2| + … + |an - bn| 最小。
[数据规模] 1 ≤ n ≤ 106, 0 ≤ ai ≤ 2*109
这道题很有趣,值得一做。
我们对于这种题目,要有有效的思维方式。
①:考虑最终的答案数列B,它能够看成是很多段相同的数段连接而成的,构成一个不下降的序列。(比如说:111 2 33333 4444 5)
②:猜想如何获得B序列,根据B序列的性质(①),我们猜测某两个连续的区间最优的答案(规定这两段序列分别是同一个值)可以快速合并。下面来"伪"证一下猜想。两段在位置上是相邻的,设左半边的最优值为U,右半边为V,若U≤V,则两段答案可以共存。不然,则需要继续探讨:(这时两段的最优解是不能共存的,因为这不能构成一个不下降的B数列),因此需要找到一个新的答案,为B[1~n(并上)n+1~m](其中原B[1~n]为U,原B[n+1~m]为V),先挖掘一下性质:1)B[n]<U,2)B[n+1]>V。这两点的正确性是显然的。假设我们现在已经知道B[n]应该是哪个数,那么B[n-1]我是希望尽量大的(因为B[n-1]≤B[n]<U,U是左半段的最优解,贪心的策略就是让B[n-1]的值尽量靠近U,所以B[n-1]=B[n]才是最理想的,继续考虑B[n-2]的值,B[n-2]=B[n-1],B[n-3]=B[n-2]……,最终得出左半段是一个值,同理右半段也是一个值,那左右是否就是同一个值呢?(不是的话这题还能怎么做呢?)假设新的答案为U1和V1,U1≤V1,若U1<V1,则贪心的,我让U1等于V1,则答案会更优,所以U1必定等于V1。太棒了,我现在用一堆弱弱的证明解决了大部分问题,还要考虑的就是U1的求法。
③:由于是同一个值,是否直接用中位数就好了?让人欣喜若狂的是,这是显然的(我都懒得证了)。为了编程方便,用排序后的A数组下标为floor(M/2)也是对的。
④:可以得到这样一个算法:
from 1 to m
新建一个答案区间i~i,值为A[i]
if(A[i]小于它左边区间的答案)
合并两个区间
获取"中位数"作为新区间的答案
endif
else continue
⑤:如何合并?考虑平衡树?太麻烦了,考虑用主席树或可并堆吧。主席树就是求区间第K大,几乎模板;而可并堆的做法复杂一些
好感人,至此这道题应该就没有什么疑点了吧。
我觉得做这道题要靠运气,很多地方要先猜到再证明,没有绝对的实力是很痛苦的,希望在不久的将来我能有绝对的实力。
这是黄源河大牛的题解。(简直没得比)
[初步分析]
我们先来看看两个最特殊的情况:
- a[1]≤a[2]≤…≤a[n],在这种情况下,显然最优解为b[i]=a[i];
- a[1]≥a[2]≥…≥a[n],这时,最优解为b[i]=x,其中x是数列a的中位数。
于是我们可以初步建立起这样一个思路:
把1…n划分成m个区间:[q[1],q[2]-1],[q[2],q[3]-1],……,[q[m],q[m+1]-1]。每个区间对应一个解,b[q[i]] = b[q[i]+1] = … = b[q[i+1]-1] = w[i],其中w[i] 为a[q[i]], a[q[i]+1], ... , a[q[i+1]-1] 的中位数。
显然,在上面第一种情况下m=n,q[i]=i;在第二种情况下m=1,q[1]=1。
这样的想法究竟对不对呢?应该怎样实现?
若某序列前半部分a[1], a[2], … , a[n] 的最优解为 (u,u,…,u),后半部分a[n+1], a[n+2], ... , a[m] 的最优解为 (v,v,…,v),那么整个序列的最优解是什么呢?若u≤v,显然整个序列的最优解为 (u,u,…,u,v,v,…,v)。否则,设整个序列的最优解为 (b[1],b[2],…,b[m]),则显然b[n]≤u(否则我们把前半部分的解 (b[1],b[2],…,b[n]) 改为 (u,u,…,u),由题设知整个序列的解不会变坏),同理b[n+1]≥v。接下来,我们将看到下面这个事实:
对于任意一个序列a[1],a[2],…,a[n],如果最优解为 (u,u,…,u),那么在满足u≤u′≤b[1] 或b[n]≤u′≤u的情况下,(b[1],b[2],…,b[n]) 不会比 (u′,u′,…,u′) 更优。
我们用归纳法证明u≤u′≤b[1] 的情况,b[n]≤u′≤u的情况可以类似证明。
当n=1时,u=a[1],命题显然成立。
当n>1时,假设对于任意长度小于n的序列命题都成立,现在证明对于长度为n的序列命题也成立。首先把 (b[1], b[2], … b[n]) 改为 (b[1], b[1], … b[1]),这一改动将不会导致解变坏,因为如果解变坏了,由归纳假设可知a[2],a[3],…,a[n]的中位数w>u,这样的话,最优解就应该为(u,u,…,u,w,w,…,w),矛盾。然后我们再把(b[1],b[1],…,b[1])改为 (u′,u′,…,u′),由于| a[1]- x | + | a[2]- x | + … + | a[n]- x | 的几何意义为数轴上点x到点a[1], a[2], … a[n]的距离之和,且u≤u′≤b[1],显然点u′到各点的距离之和不会比点b[1]到各点的距离之和大,也就是说,(b[1],b[1],…,b[n]) 不会比 (v,v,…,v) 更优。(证毕)
再回到之前的论述,由于b[n]≤u,作为上述事实的结论,我们可以得知,将(b[1],b[2],…,b[n])改为(b[n],b[n],…,b[n]),再将(b[n+1],b[n+2],…,b[m])改为(b[n+1],b[n+1],…,b[n+1]),并不会使解变坏。也就是说,整个序列的最优解为(b[n],b[n],…,b[n],b[n+1],b[n+1],…,b[n+1])。再考虑一下该解的几何意义,设整个序列的中位数为w,则显然令b[n]=b[n+1]=w将得到整个序列的最优解,即最优解为 (w,w,…,w)。
分析到这里,我们一开始的想法已经有了理论依据,算法也不难构思了。
[算法描述]
延续我们一开始的思路,假设我们已经找到前k个数a[1], a[2], … , a[k] (k<n) 的最优解,得到m个区间组成的队列,对应的解为 (w[1],w[2],…,w[m]),现在要加入a[k+1],并求出前k+1个数的最优解。首先我们把a[k+1]作为一个新区间直接加入队尾,令w[m+1]=a[k+1],然后不断检查队尾两个区间的解w[m]和w[m+1],如果w[m]>w[m+1],我们需要将最后两个区间合并,并找出新区间的最优解(也就是序列a中,下标在这个新区间内的各项的中位数)。重复这个合并过程,直至w[1]≤w[2]≤…≤w[m]时结束,然后继续处理下一个数。
这个算法的正确性前面已经论证过了,现在我们需要考虑一下数据结构的选取。算法中涉及到以下两种操作:合并两个有序集以及查询某个有序集内的中位数。能较高效地支持这两种操作的数据结构有不少,一个比较明显的例子是二叉检索树(BST),它的询问操作复杂度是O(logn),但合并操作不甚理想,采用启发式合并,总时间复杂度为O(nlog2n)。
有没有更好的选择呢?通过进一步分析,我们发现,只有当某一区间内的中位数比后一区间内的中位数大时,合并操作才会发生,也就是说,任一区间与后面的区间合并后,该区间内的中位数不会变大。于是我们可以用最大堆来维护每个区间内的中位数,当堆中的元素大于该区间内元素的一半时,删除堆顶元素,这样堆中的元素始终为区间内较小的一半元素,堆顶元素即为该区间内的中位数。考虑到我们必须高效地完成合并操作,左偏树是一个理想的选择。左偏树的询问操作时间复杂度为O(1),删除和合并操作时间复杂度都是O(logn),而询问操作和合并操作少于n次,删除操作不超过n/2次(因为删除操作只会在合并两个元素个数为奇数的堆时发生),因此用左偏树实现,可以把算法的时间复杂度降为O(nlogn)。
[小结]
这道题的解题过程对我们颇有启示。在应用左偏树解题时,我们往往会觉得题目无从下手,甚至与左偏树毫无关系,但只要我们对题目深入分析,加以适当的转化,问题终究会迎刃而解。这需要我们具有敏捷的思维以及良好的题感。
用左偏树解本题,相比较于前面BST的解法,时间复杂度和编程复杂度更低,这使我们不得不感叹于左偏树的神奇威力。这不是说左偏树就一定是最好的解法,就本题来说,解法有很多种,光是可并堆的解法,就可以用多种数据结构来实现,但左偏树相对于它们,还是有一定的优势的,这将在下一章详细讨论。
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
using namespace std;
const int maxn=;
int B[maxn];
int q[maxn],l[maxn],r[maxn],num[maxn],dep[maxn],tot;
int ch[maxn][],key[maxn],n;
int Merge(int x,int y){
if(!x||!y)return x|y;
if(key[x]<key[y])swap(x,y);
ch[x][]=Merge(ch[x][],y);
if(dep[ch[x][]]>dep[ch[x][]])
swap(ch[x][],ch[x][]);
dep[x]=dep[ch[x][]]+;
return x;
} void Delete(int i){
q[i]=Merge(ch[q[i]][],ch[q[i]][]);
} int main(){
dep[]=-;
scanf("%d",&n);
for(int i=;i<=n;i++)
scanf("%d",&key[i]);
for(int i=;i<=n;i++){
q[++tot]=i;l[tot]=r[tot]=i;num[tot]=;
while(tot!=&&key[q[tot]]<key[q[tot-]]){
q[tot-]=Merge(q[tot],q[tot-]);
num[tot-]+=num[tot];
r[tot-]=r[tot];tot--;
int sum=(r[tot]-l[tot]+)/;
while(num[tot]>sum){
Delete(tot);
num[tot]-=;
}
}
} for(int i=;i<=tot;i++)
for(int j=l[i];j<=r[i];j++)
B[j]=key[q[i]]; int ans=;
for(int i=;i<=n;i++)
ans+=abs(key[i]-B[i]);
printf("%d\n",ans);
return ;
}
黄源河《左偏树的应用》——数字序列(Baltic 2004)的更多相关文章
- 洛谷P4331 [BOI2004] Sequence 数字序列 [左偏树]
题目传送门 数字序列 题目描述 给定一个整数序列 a1,a2,⋅⋅⋅,an ,求出一个递增序列 b1<b2<⋅⋅⋅<bn ,使得序列 ai 和 bi 的各项之差的绝对 ...
- [BOI2004]Sequence 数字序列(左偏树)
PS:参考了黄源河的论文<左偏树的特点及其应用> 题目描述:给定一个整数序列\(a_1, a_2, - , a_n\),求一个递增序列\(b_1 < b_2 < - < ...
- 2021.08.01 P4311 数字序列(左偏树)
2021.08.01 P4311 数字序列(左偏树) [P4331 BalticOI 2004]Sequence 数字序列 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 重点: 1 ...
- Luogu P4331 [BOI2004]Sequence 数字序列 (左偏树论文题)
清晰明了%%% Fairycastle的博客 个人习惯把size什么的存在左偏树结点内,这样在外面好写,在里面就是模板(只用修改update). 可以对比一下代码(好像也差不多-) MY CODE # ...
- 洛谷$P4331\ [BOI2004]\ Sequence$ 数字序列 左偏树
正解:左偏树 解题报告: 传送门$QwQ$ 开始看到的时候$jio$得长得很像之前做的一个$dp$,,, 但是$dp$那题是说不严格这里是严格? 不难想到我们可以让$a_{i},b_{i}$同时减去$ ...
- POJ3666-Making the Grade(左偏树 or DP)
左偏树 炒鸡棒的论文<左偏树的特点及其应用> 虽然题目要求比论文多了一个条件,但是……只需要求非递减就可以AC……数据好弱…… 虽然还没想明白为什么,但是应该觉得应该是这样——求非递减用大 ...
- HDU 1512 Monkey King(左偏树+并查集)
[题目链接] http://acm.hdu.edu.cn/showproblem.php?pid=1512 [题目大意] 现在有 一群互不认识的猴子,每个猴子有一个能力值,每次选择两个猴子,挑出他们所 ...
- HDU3031 To Be Or Not To Be 左偏树 可并堆
欢迎访问~原文出处——博客园-zhouzhendong 去博客园看该题解 题目传送门 - HDU3031 题意概括 喜羊羊和灰太狼要比赛. 有R次比赛. 对于每次比赛,首先输入n,m,n表示喜羊羊和灰 ...
- HDU5818 Joint Stacks 左偏树,可并堆
欢迎访问~原文出处——博客园-zhouzhendong 去博客园看该题解 题目传送门 - HDU5818 题意概括 有两个栈,有3种操作. 第一种是往其中一个栈加入一个数: 第二种是取出其中一个栈的顶 ...
随机推荐
- JavaScript核心
JavaScript核心 arguments对象 Array对象 Boolean对象 Date对象 Error对象 Function对象 Global对象 Math对象 Number对象 Object ...
- VS2015升级Update2之后Cordova程序提示:此应用程序无法在此电脑上运行
VS2015在升级到Update2之后,有可能出现如下异常,在运行Cordova项目时提示: 查看输出面板会有乱码错误信息: 出现此问题的原因是在于npm程序损坏了.vs调用的npm程序并不是在nod ...
- oracle 导出导入数据
在window的运行中输出cmd,然后执行下面的一行代码, imp blmp/blmp@orcl full=y file=D:\blmp.dmp OK,问题解决.如果报找不到该blmp.dmp文件,就 ...
- hadoop-0.20-集群搭建___实体机通过SSH访问基于VM安装的Linux
不得不说LZ在最开始搭建hadoop的时候,由于VM中的网段配置和本地IP地址没有配置好, 所以一直都在使用 VM的共享文件夹的功能, 以至于集群搭建好之后,只有namenode主机可以实现共享的功能 ...
- 后台线程,优先级,sleep,yield
1.后台线程,是指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可获取的部分.当所有非后台线程结束时,程序也就 终止了,同时会杀死进程中所有后台线程.main()是一个非后 ...
- Java study 2:The note of studying Socket which based TCP
TCP concept: 传输控制协议(Transmission Control Protocol, TCP)是一种面向连接(连接导向)的.可靠的.基于字节流的运输层(Transport layer) ...
- 1. mybatis批量插入数据
通过list <insert id="saveByList" useGeneratedKeys="true" parameterType="ja ...
- HTML TAG FROM MDN
A <a> <abbr> <acronym> <address> <applet> <area> <article> ...
- NLP相关资源
一 NLP相关资源站点 Rouchester大学NLP/CL会议列表 一个非常好的会议时间信息网站,将自然语言处理和计算语言学领域的会议,按照时间月份顺序列出. NLPerJP 一个日本友好人士维护的 ...
- 如何执行一个mysql的sql脚本文件
sql脚本是包含一到多个sql命令的sql语句,我们可以将这些sql脚本放在一个文本文件中(我们称之为“sql脚本文件”),然后通过相关的命令执行这个sql脚本文件.基本步骤如下:一.创建包含sql命 ...