UOJ Logo WuHongxun的博客

博客

UOJ NOI Round #3 Day1 题解

2018-07-13 13:50:00 By WuHongxun

鸽子固定器

from AprilGrimoire, 题解 by AprilGrimoire

这次的题目顺序真是十分抱歉QwQ A题可能是三道题中AC人数最少的了……

然而这题std才50行,放B和C显得不太合适哇QwQ

子任务一

n的范围只有10,所以随便怎么暴力都可以哇QwQ

时间复杂度:Ω(poly(n))

期望得分:5

子任务二

容易发现,如果我们确定了选择的固定器大小的范围,那么一定会选择这些固定器中牢固度最大的至多m个。所以我们可以把固定器按大小排序,枚举选择的右端点,然后将左端点向左扫一遍。在扫的过程中,用一个小根堆来维护选择的固定器的牢固度。

时间复杂度:O(n2logm)

期望得分:10

子任务三

ds=dv=1的时候,首先把鸽子固定器按照大小排序。设fi,j表示在前i个固定器中选择j个,且必须选择第j个的价值的最大值。则 fi,1=vi fi,j=max1k<jfk,j1(sisk)+vi=visi+max1k<jfk,j1+sk 只要记录前缀最大值进行DP,就可以在O(nm)的时间内解决子任务三。

时间复杂度:O(nm)

期望得分:10

结合子任务二的做法可以获得20分。

子任务四

考虑子任务二的一个优化:在扫描过程中,如果发现右端点被出堆了就终止扫描。这样做显然是对的,因为右端点被出堆后的方案可以通过简单地左移右端点来使答案变优,一定不是最优值。然而,对于构造的数据,这个优化可能并没有什么用:

200000 50 2 2
1 1
2 2
3 3
...

对于这样的数据,这个优化并没有什么用。然而对于随机数据,它还是跑得挺快的。让我们进行一波有理有据的分析:对于第i大的固定器,随机找另一个固定器,有大约in的概率找到一个比它大的固定器。也就是说,期望扫描nmi个固定器,它就会被出堆。对于所有固定器,扫描所需要的总期望时间是 i=1nnmi=O(nmlogn)

时间复杂度:O(nmlogn)

期望得分:25

结合前三个子任务的做法可以获得45分。

子任务五

假设以i为右端点向左扫描时,j号点始终没有被入堆,那么以i+1为右端点时,它肯定也不会被入堆。这启示我们大胆猜想:对于右端点来说,左端点有决策单调性!取区间上前m大的点可以用主席树在O(logn)的时间内完成,再算上决策单调性二分的O(logn),我们就可以在O(nlog2n)的时间内解决

.

.

.

dv2的情况。为什么当dv=2时没有决策单调性呢?考虑这样的数据:

200000 50 2 2
1 5000
2 1
3 1
...
199999 1
200000 10000000

对于中间的大部分点,最优决策都是选连续的m个固定器。而对于最后一个点,最优策略是选择最后一个点,第一个点和中间任意48个点。非线性的东西往往能够破坏决策单调性。

这个做法实际上可以通过前五个子任务,因为小数据不太好卡,对于随机数据正确性又很高。

实际上冷静一下就能发现决策单调性是假的:如果复杂度和m无关,为啥m这么小呢?

时间复杂度:O(nlog2n)

期望得分:70

子任务六

考虑优化子任务四的做法。这个做法会被价值递增的数据给卡掉。那么,如果我们二分ST表来找下一个会被入堆的点呢?事实上,这个算法是能通过全部数据的。

证明:假设在以i为右端点向左扫描时j被入堆了,就记录有序对(j,i)。我们把有序对(a,b)分成两类:vbva的叫做第一类有序对,vb<va的叫做第二类有序对。 1. 对于第一类有序对(a,b),一个b最多对应ma。(否则b会被出堆) 2. 对于第二类有序对(a,b),一个a最多对应mb。(否则在扫描到a的时候,堆里至少有m个比a大的数,也就是说,a不会被入堆)

入堆操作最多会被执行O(nm)次,每次需要logn的时间来二分ST表和logm的时间来入堆,因此,这个做法能够在nmlogn的时间内通过所有子任务。

时间复杂度:O(nmlogn)

期望得分:100

神奇的做法

在比赛时,发现了这样一种神奇的做法: 按照坚固度从小到大考虑所有固定器。 1. 如果当前固定器被选中了,那么选中的固定器集合一定是一个区间。(否则可以把当前固定器换掉来获得更优的答案。) 2. 如果当前固定器没被选中,我们可以把它删掉。

用链表维护剩下的固定器。包含当前最小固定器的区间最多只有m个,所以这个算法的复杂度是O(nm)的。

子任务六的两个优化

  1. 用链表维护当前可能被入堆的点集。如果在从右往左扫的过程中一个点没有被入堆,那么我们可以把它从点集里删掉,因为它以后也永远不会入堆。这样就能避免二分ST表,只有O(nmlogm)的复杂度。(logm是因为需要用堆维护当前被选的固定器。)
  2. 堆也不是必要的,因为每次可能入堆的点只会增加一个,所以可以线性维护当前被选中的点集。这样就能做到O(nm)

To Do Tree

对于大多数OIer来说,这题应该是个大胆猜结论题。

算法零:构造

看过题的同学就知道,subtask3就是送分的,每星期只能肝一个ddl,搞什么都行嘛……只要输出格式没有问题,大家应该都能得到这6分。

算法一:搜索

直接枚举每星期肝的ddl集合搜索,期望得分30,可以通过subtask1。

结合算法零可以得到36分。

算法二:子集dp

dpS表示肝掉集合S中的所有ddl所需要的最少的时间,然后枚举下一次肝的集合转移即可。

时间复杂度O(3n),期望得分60分,可以通过subtask1,subtask2。

结合算法零可以得到66分。

算法三:构造

这个题看起来不可做又看起来很可做,有哪些选手能证明一下自己的算法是最优的呢?

先说出题人Mike给出的一种构造算法和证明。

首先对把依赖关系翻转一下,对于任意一个任务i,执行i之前必须先完成i的所有子节点。这个问题的最优解把执行顺序反转一下就是原问题的最优解了。

直接说结论:每次选择最深的叶节点执行,若有多个则随意执行一个即可。

证明如下:

考虑答案的两个显然的下界,tmaxiT(depi)tnm

显然前面那个是机器数较多的情况,后面那个是机器数较少的情况。

然后将节点按深度分两块,任取一个深度α,有

tα1+i=αnpim

显然最早在时刻i=αnpim时深度不小于α的节点才能被处理完,此时至少会剩下一个深度为α1的节点,至少还需要α1时间才能完成,因此这是总用时的一个下界。(显然对于α=1时也成立,但上面的叙述有问题)

下面证明tmaxα(α1+i=αnpim)是一个紧下界。

考虑最大值时α取到的β,如果有多个则取最小的,则有

d<β,i=dβ1piβd<m

上式显然成立,否则αd的函数值不比αβ时小。

根据当前树的深度是否小于β,我们按时间序将算法分为前半部分和后半部分。

下面证明,假设深度不小于β的节点全部被完成后,剩下的节点只需要β1时间就能完成。

首先归纳证明,算法执行过程中,任意时刻树中深度最大的任意k层节点,每层的节点均值都小于m

形式化的表述是,设树的深度为h,则有i=dhpihd+1<m(1)

也就是和上面一样的形式。

采用归纳法,反证,假设经过了1时间后深度至少为d的部分不满足条件,取最大的d

根据假设,显然有pdm,否则深度至少为d+1的部分也满足条件,与d最大矛盾。

考虑此时深度为d的这些点构成的子树,显然每个子树至少有一个叶节点,因此当前深度不小于d的叶节点有至少m个。

删掉一个节点后,深度不小于d的节点个数单调不减,因此上一次深度不小于d的叶节点个数不小于m,根据算法流程可知上次删掉的节点深度至少为d

则上次深度不小于d的节点有i=dhpi+m个,h是当前树的深度,pi是当前树中深度为i的节点个数。

i=dhpim(hd+1)

i=dhpi+mm(hd+2)

即上次深度不小于d的部分也不满足条件,与归纳条件矛盾,因此命题(1)得证。

由命题(1)可得,算法执行后半部分的任意时刻,深度最大的节点个数不超过m,因此只需要树的深度时间就能够执行完所有深度小于β的节点。

下面证明删掉深度不小于β的节点只需要i=βnpim时间。

β的定义,可以得到如下结论:

dβ,i=βdpidβ+1m

否则αd的函数值比αβ时大。

下面证明,算法执行前半部分的任意时刻,设当前树的高度为h,深度为i的节点有pi个,则对于任意的βd<h,满足上述条件

βd<hi=βdpidβ+1m(2)

使用归纳法,反证,假设经过1时间后深度为βd的部分不满足条件,取满足条件最小的d

这个单位时间内显然删掉了至少一个深度为d的节点。(上一时刻时这一部分还满足条件)

这说明之前深度大于d的部分叶节点不足m个。

dh,则当前的d不在定义域中,自然不影响成立性。

d<h,则此时这一时刻内一定删掉了所有深度为h+1的点。

由于深度大于d的叶节点个数随时间单调不减,且上一时刻深度大于d叶节点个数不足m,所以只需要hd时间就能将所有深度大于d的部分删去,因此在上一时刻有

i=βdpi<(dβ+1)m+D

其中D为删掉的深度为d的节点个数。

深度不少于d+1的叶节点至少有pd+1个,因此Dmpd+1

因此得到

i=βd+1pi<(dβ+2)m

d<h,因此上一时刻取d=d+1时条件不满足,与归纳基础矛盾,因此命题(2)得证。

因此当深度大于β时恒有pβm,即深度不小于β的叶节点个数m,每次都能取满m个。在深度为β处至多一次取不满m,因此前半部分可以在i=βnpim内完成。

综上,算法正确性和用时下界都得到了证明。

这个算法可以做到O(n)的,想必大家都会,也不是重点,于是就没设分。

按深度贪心也是正确的,证明主要思路采用调整法;按大小贪心也是正确的,证明主要思路采用反证法。证明繁杂赛后我在uoj博客发一下证明。

配对树

from C_SUNSHINE, 题解 by C_SUNSHINE

子任务 1

我们可以暴力枚举ddl区间,然后在树上做树形DP来计算最小匹配的大小,如何DP呢?

fi,j 表示 i 的子树内还有 j 个点没匹配的匹配大小,然后计算转移边需要被覆盖几次。这个想法明显太傻了,我们不可能让子树内的点拉到子树外面配对的,如果存在两个点 x,y 他们在子树内都没有配对,那么假设他们分别和 x,y 配对,显然 改为 x,y 配对 x,y 配对更优,所以 j 只能是 0 或1。

接着发现其实 j 的奇偶性和子树内ddl数目的奇偶性相同,于是直接dp即可。

时间复杂度 O(m2n)

子任务 2

我们接着观察,对于一个确定的ddl区间,一条树边被匹配包含了当且仅当把它删去后树被分成两个包含奇数个ddl的块。若我们令 1 为根做dfs,则一个非根点父向边被包含当且仅当其子树内有奇数个ddl。

我们可以换一种方式思考,计算一条树边被多少个偶区间覆盖。

枚举一条树边,把它子树内的ddl全部在序列上标为 1 其余标为 0,来把序列转成 01 串,于是问题变成了统计 01 串中包含偶数个 1 的子串数目。

01 串做前缀和得到 si,则一个区间 [l,r] 要被统计当且仅当 srsl11(mod2) 由于 l1r(mod2),我们可以对 si(0im) 按照下标奇偶性分别维护,维护对 imod2=k=01 分别计算 si 中奇数的数目 cntk,1 和偶数的数目 cntk,0。那么包含这条树边的区间数目就是 cnt0,0cnt0,1+cnt1,0cnt1,1

把序列转化成 01 串需要 n+m 的时间,统计 cnt 需要 O(m) 的时间,于是时间复杂度为 O(n2+nm)

子任务 3

这个直接把不超过 100 个点的虚树建出来之后暴力即可。

子任务 4

我们可以考虑使用线段树来维护序列转化成的 01 串,可以发现,我们只需要在线段树区间上维护区间内的 cntk,0/1,通过左半边内 1 的数目来决定是否对右边进行反转,就可以 O(1) 时间合并左右区间,最终需要的 cnt 的值会统计在根节点上。

既然单次修改 01 序列是 O(logm) 的,我们可以枚举每条边,然后只加入其子树,总修改次数为 O(n2),于是复杂度为 O(n2logm)

子任务 5

这条件其实没什么特别的用处,只是让点的分步均匀一些。

子任务 6

树是随机的,我们依旧暴力枚举树边然后只添加子树,可以发现每个点会被添加恰好深度次,而深度的期望是 O(logn) 的,所以总修改次数的期望也是 O(mlogn) 的,总复杂度就是 O(mlognlogm) 的。

子任务 7

在本来的做法中,我们对子树做完后,清空线段树再吧子树内的 ddl 重新加入,可以发现这个过程中有重复计算。考虑对于每个点 x 我们在 dfs 完其最后一个孩子之后,不清空 dfs 时的修改,而是把其他孩子子树内的 ddl 直接补上,令 x 的最后一个孩子为 sonxxsonx 的边称为特殊边,可以发现,一个 ddl 被添加次数为其到跟的路径上非特殊边的数目。

到这里其实算法已经很显然了,对树轻重链剖分,sonx 就是 x 的重孩子,一个 ddl 被添加的次数就是其到根的路径上轻边的数目是 O(logn) 的,采用线段树维护的话复杂度就是 O(mlognlogm),这个算法可以通过本题。提交记录点这里

因为NOI之前信心赛大家开心就好,所以两个 log 是可以通过的。

这题我本来出出来准备当个前两题的难度的,但不知道哪个管理员就把它挂到 C 题上了,我给他题解的时候还把这段话删了,所以两个 log 是可以通过的。

想到用线段树维护后,本题有个较为直接的做法即采用线段树合并,每个点的线段树可以看成其所有子树并起来再插入其上的 ddl,我们知道 m 个只有一个元素的线段树合并的复杂度为 mlogm,所以直接采用线段树合并可以做到 O(n+mlogm) 的复杂度,只不过空间是 Θ(mlogm) 的。提交记录点这里 不过采用 Splay 做合并的话,空间可以做到 O(n)

你要问我为啥就是个 Splay 合并却写这么长题解,我只能说……既然管理员钦点了,得装的像个 C 题的样子(逃

评论

前排
前排 前排
前排
中排
waiting for the guguguing problemsetter
后排纪念,话说T2居然茶不掉
评论回复
Lin1043:T1子任务三的设fi,j表示在前i个固定器中选择j个是不是写错了不然的话似乎跟下面fi,1=vi矛盾了
后排
T1 nmlogn 2.1s.. 被卡爆了
评论回复
kczno1:nm好想+好写
peehs_moorhsum:回复 @kczno1:QwQ强大啊
kczno1:回复 @peehs_moorhsum:nmlog咋搞的啊
peehs_moorhsum:回复 @kczno1:就强行分治一下..?
kczno1:分治之后然后咋搞啊
后排~
t1 ds,dv搞反了,成功拿到45分
评论回复
kczno1:我读题时间花的太少了..
TLEbyc:AK.kczno1 太强啦
第二题的作者没写
后排~
我好像没有看到第三题取模
评论回复
bzh:然后也没有算longlong的事
bzh:回复 @bzh:第一题那个极大无比的答案不用取模,然后就记成第三题了
有的低水平选手已经不会二分st表了。。 以及t3可以 空间O(n) 时间O(n+mlogm)
评论回复
Marco_L_Tsien:可能这就是神仙吧
star_magic_young:萌新瑟瑟发抖
有没有小哥哥证一下我的暴力或者叉一下啊。。。 做法大概是枚举r对每个l维护前m大的固定值。。。相邻两个如果相同了就合并。。。
考试的时候想到了二分+st表 但是并没有想到那个子任务四的优化 GG
评论回复
negiizhao:我刚好相反 想到了任务四 然而没想起来二分+st表((
ridiculos:回复 @negiizhao:是不是我们一起打就A了(雾
我来编一下我T3做法。。(表达能力非常差,可能有点混乱) 首先题解已经说了 最优方案的路径显然是边不相交的(否则能得到更优的方案),且所有最优方案会产生贡献的边的集合是确定的。考虑增加两个点,显然新的边集是 原边集 和 增加的两个点之间的路径 的对称差。 再考虑枚举子串的右端点,对所有这样的子串统计对答案的贡献。我们可以维护每条边产生了几次贡献,右端点移动到下一个位置时,相当于所有子串增加了两个点,那么 这两个点之间的路径上的边的贡献次数 变为 子串数-原贡献次数。
受到影响的只有这条路径上的边,那么只需要重新统计这条路径上的边的贡献即可得到新的完成时间之和,可以用lct之类的东西快速维护。 空间是线性的,时间的话 直接lct是O(mlogn),建出虚树再跑的话是O(n+mlogm) http://uoj.ac/submission/266685 这是我的提交(以为这个号报名了于是就错过了。。所以是用另一个号打的)
评论回复
kczno1:把树链剖分换成了lct吗
LadyLex:我考场上也是这样想的……然后意识模糊没有打出线段树标记维护的正确姿势 然后就GG了
LadyLex:您的标记维护真的很巧妙
OldDriverTree:我觉得论文哥的数据结构已经无敌了
后两题的validator咕咕了?
哇QwQ. 场上迷迷糊糊打了个A的正解硬是把剪枝注释掉了. 还把ds, dv读入反了. 快乐10分

发表评论

可以用@mike来提到mike这个用户,mike会被高亮显示。如果你真的想打“@”这个字符,请用“@@”。