水题生成器
from taorunz
算法一
对于前6个数据
当然,由于
期望得分:30分
算法二
对于前14个数据
我们可以将本问题看成一个背包问题来解。
时间复杂度是
期望得分:70分
算法三
本题与阶乘紧密相关,说到把不超过
然而在本题里,用阶乘进位制分解得到的数不一定是
怎么办呢? 把阶乘进位制倒过来!
我们令新的进位制的第
然后将
这样
期望得分:100分
算法四
不知道阶乘进位制?没关系!我们可以直接贪心!
我们先算出
有趣的是,用这种方法,一定可以在不超过
为什么呢?我们参考算法三,对于当前剩余要造题的难度值之和
期望得分:100分
水题出题人
from saffah 大家好我是萌萌哒水题出题人saffah!
这道题目的idea来源是众所周知的APIO2013 tasksauthor。我们将在接下来的分析当中一点点发现这道NOIP好题是如何演变成现在这样的丧病题目的。
子任务1
归并排序卡计数排序,
注意到计数排序的用时与序列的最大元素有关,那么我们只需令
子任务2
冒泡排序卡选择排序,
这两个算法的时间都是平方级的,而且大概都受到逆序对数的加成。
分别往两个程序里面扔升序序列和降序序列,发现要想冒泡排序卡选择排序,必须是升序,这样能卡一个很小的常数。
其实升序序列和常数序列在这里没什么区别,那么我们往里扔一堆
注意到此时选择排序的用时是
我们人肉二分这个
子任务3
归并排序卡快速排序,
也就是说如果想AC这个子任务,快速排序要比归并排序慢大概20倍,这显然不是卡常数而是卡复杂度。(因为向归并排序和快速排序扔一个随机序列会发现归并排序的常数确实比较大)
注意这个快速排序的基准元素并不是随机选的,而是选择了当前区间的中间元素。根据快速排序的性质,只需使它每次选取的中间元素都是当前区间的最大值或最小值,就能把快速排序卡到平方级。
python代码:
a = range(n)
for i in range(n):
j = i / 2
a[i], a[j] = a[j], a[i]
其中的
总之这样就可以轻松加愉快地通过这个subtask啦!18分到手!
子任务4
计数排序卡冒泡排序,
既然是卡冒泡排序,第一个想到的就是给一个降序序列,到处都是逆序对。
但是这样跑出来以后发现
设序列中所有的数都小于
考虑已知
设这个序列中数
一个巧妙的方法,已知
def gen(n, x):
a = []
for i in range(n):
a = [i * x / n] + a
return a
说了这么多,总结一下就是:枚举
19分愉快到手!
子任务5
选择排序卡冒泡排序,
按照上一个子任务的思路直接给一个降序序列,发现
直接套上一个程序跑这个子任务,发现能跑出
我们出现了两位怒虐标答的选手:alpq654321的533650和xywmmd的529079!
根据规则,我们将发给xywmmd UOJ抱枕一个!
在这里只分享一个alpq654321的最开始的做法QAQ……(策爷做法看不懂嘛)
————————以下是alpq654321的神做法————————
要构造一个序列,使得冒泡排序慢而选择排序快,考虑这样构造:
def solve(l , r):
if l > r:
return []
mid = (l + r) / 2
return [mid] + solve(mid + 1, r) + solve(l, mid - 1)
我们神奇地发现这个序列的性质很好,因为从每个位置开始最小值的变化次数都不会很多,而且逆序对依然是平方级别的。
这样就可以愉快地AC+拿抱枕了。当然,alpq654321还有更神的最终版做法,我们请他亲自来讲……
————————以上是alpq654321的神做法————————
(以下是saffah的手玩历程)
但是我们怎能满足于10分?注意到此时的冒泡用时为
人肉枚举要减哪个数,发现如果把第一个
然后我们接着减,直到冒泡的时间只超出
(以下是vfleaking的爬山历程)
但是我们怎能满足于10分?我们像上一个subtask一样,把一个序列用每个数出现次数的数组
跑若干分钟就可以跑出能拿20分的方案啦!
子任务6
Bogo排序卡快速排序,
“啥?FML?我没看错题吧?用最不靠谱的排序卡最靠谱的排序?而且
且慢……我们先看看这个Bogo排序的大体思路。
首先它实现了一个线性同余法的随机数生成器。读入数据以后,init了一下随机参数,并用输入的
看起来很靠谱嘛!
稍加观察就可以发现,所有的模数都是
再看初始参数
妈呀!这两个数在
现在的Bogo排序中的“随机打乱”实际上就变成了这个样子:
for i in range(n):
j = (i + seed + 1) % n
a[i], a[j] = a[j], a[i]
其中
注意这个性质只能持续到第一次更换随机参数之前,所以我们必须一次打乱就将序列排好序。依次代入
为了使这个“随机打乱”能够一下子排好序,我们构造输入序列的过程就必然是:
n = 4096
a = range(n)
for i in reversed(range(n)):
j = (i + seed + 1) % n
a[i], a[j] = a[j], a[i]
我们只需一个
最后的问题是,给出一个序列,要求不改变它的相对大小顺序而使由它产生的
如果无视“不改变它的相对大小顺序”这一条就很好做了。直接算出由前
上述算法在
呼~这样就可以……不是很愉快地拿到30分啦!
这样6个子任务就都解决啦,就能拿100分啦!
什么,你还不满足?
子任务5 EXT
妈妈我想要UOJ抱枕怎么办!
怎么办呢……我们只需要像虐标答的神犇们一样神就可以了……
————————以下是saffah的逗比历程————————
我们观察一下爬山爬出来的方案,稍微好一些的方案当中,
等等,对称的话,似乎两种排序算法的时间都很好算呀?
对于冒泡排序,只需知道所有
对于选择排序……一个乱七八糟的序列的选择排序用时是很不好算的,但是由于
选择排序最难算的就是
根据这个次数就容易算出它的选择排序代价了。
这个触发次数似乎可能会比较大?但是冒泡需要算的平方和看起来并不大,之前的方案都在
考虑动态规划。设
状态转移:由于
嗯,剩下的事就是买一个大内存的电脑了。
靠谱点的做法呢?
滚动数组?我们好像还要记录方案……套用范爷题做法?各种不想写……
注意到,如果
如果
我们只需要做三个八分之一DP就可以把这个问题解决掉啦!
saffah表示最优跑出了
任何人有任何感觉更靠谱的想法都欢迎在这里讨论呀~
水题走四方
from Picks
Picks 给的题面,但是他自己并不会做。其它的事情都是 vfleaking 干的。
算法一
对于子任务 1,
我们可以记下当前两个地卜师在什么位置,以及每个结点是否被访问过,然后 DP 就可以了。
可以获得
算法二
对于子任务 2,
记
大错特错!有可能剩一棵子树和一个叶子没访问过,而如果往这棵子树走,将会经过很长一段只有一个儿子的结点的路。那么我们此时可以让这两个地卜师一起往下走,一个走到叶子停下,另一个走到那棵子树中往下第一个有不止一个儿子的位置停下。如果是先到达了分叉点,就等一等那个往叶子走的地卜师走到后瞬移过来。
显然,我们可以枚举最后剩下的子树和叶子分别是哪个,然后 DP 算算代价。当然你也可以注意到,如果已知选哪个子树,显然选最深的那个叶子更优。
我不知道你们有没有一开始想错了,至少我一开始想错了,还以为是水题 = =……
然后你会发现这个算法他过不了样例三!
我觉得大家这时候应该写个算法一然后对拍,就能知道为什么了。但是为啥大家都弃疗了呢?(我好像是
那么究竟是怎么回事呢 = =……
我们考虑一下,假如有两根很长很长的筷子,我们把用于吃饭的那一头粘起来并作为根结点。那么答案显然是一根筷子的长度。但是如果这两根筷子是劣质一次性筷子,在两根筷子的侧边长了一点点毛刺,这些毛刺比较靠近吃饭的那一端。那么最优解是什么呢?
最优解是:先派个地卜师下去把毛刺都去掉,然后再两个地卜师一起下去分别遍历两根筷子的主干部分。这样大约就只需要一根筷子的长度。而如果用刚才那个算法,会算出来是大约两根筷子的长度。于是这个算法又是错的。
孩子算法老是错,一定是姿势不对。
我们来重新考虑问题。现在有两个地卜师,一个是本体一个是分身,在树上乱逛。注意如果两个地卜师站在一起,在我们眼前一晃其实我们是分不清楚也不用分清楚谁是谁的,我们可以根据自己的需要调换两人的身份。所以对于任意一组移动方案,我们一定可以把这组方案调整成本体从来没有瞬移过的方案。
然后我们考虑本体,他一定是往下走几步,停几步,再走几步,再停几步。如果一组方案最后本体没有停在某个叶子,我们肯定可以让本体追随分身的脚步停在某个叶子。
好,那么现在本体是从根走到了某个叶子
这是因为,每个支出去的子树的叶子都是要访问的,访问肯定对应着“溜出去”和“瞬移回来”这两个过程,溜出去的位置到该叶子的距离就是时间。显然如果已知你只能选择哪几个地方(即关键点)之一溜出去,那么选择最近的那个最好。(其实不太显然,从一个关键点到另一个关键点,最后一个访问的叶子也可能节约时间,但是也是好证的)
对于“最后一个访问的叶子”,显然取最深的叶子最优。
所以我们用 DP 求出
假设已经知道是哪条主链,设
其中
然后你 dfs 一遍边 dfs 边 DP 就可以了。
使用暴力就能做到
算法三
我们仔细观察这个行为:“当还剩下一个叶子的时候,本体和分身一起往下,本体去下一个关键点,分身去叶子。分身到了叶子处就瞬移到本体所在位置。” 这里有一种情况是:关键点到叶子的距离比到下一个关键点短,那么分身瞬移到本体位置后,就会一起向下走。因为最后一次走的叶子一定两个关键点间最深的叶子,所以重逢的位置到下一个关键点一定是没有支出去的子树的。我们不妨把这个重逢的地方也设为关键点。
这样,我们 DP 的时候就可以考虑两种情况:
的父亲只有 这一个儿子,用父亲的 加 更新 。- 考虑一个
的祖先 ,且 ,用“一坨式子”更新。
考虑情况二的不等式:
于是为什么我们不把这个分开的位置作为关键点呢?答案显然不会更差 —— 首先一起走的部分还是一起走了,而“叶子到关键点距离和”又减少了。所以,我们只用考虑那些
注意到,一个
时间复杂度
什么?你想起了 NOI2014 的购票去优化 DP?感觉
算法四
其实你只要接下来注意到,情况二中你只用考虑在
因为假设
所以,其实只要你把暴力DP想清楚了 = =……这题真的就顺流直下,沦为了一个大水题……但是你不多想想,就成了恶心斜率优化题。
你可以 dfs 一下,维护一个
时间复杂度
算法五
你只要黑科技弹栈就能线性了。
观察题目性质,你每次 push 一个
所以你就干脆把最大值暴力 push 进去递归非最深子树,然后再把次大值暴力 push 进去递归最深子树。这样看上去只是个骗分,但是经过分析可以知道是线性的算法。(窝就不分析啦!你看看算法六对比下就大概知道了)
算法六
还有别的线性做法。我们考虑一种树的链剖分,每个儿子选一个自己最深的子树,把它和这个子树间的边染黑。
那么你在 dfs 的时候,沿着黑边走就要 push 次大值,否则 push 最大值。
考虑一条黑链,不管你怎么 push 最大值或次大值,总不可能把这条黑链上方那条黑链 push 进来的东西弹掉 —— 因为那个更深嘛。
这时就很明显了 —— 往黑链外走,push 最大值是什么行为?会把栈 pop 到 dfs 到这条黑链起始端的那个时候的栈的状态,因为之前 push 的次大值都比最大值小。我们 dfs 时记录下链起始端的那个时候的栈的栈顶指针就行了。
而 push 次小值呢?显然可以暴力嘛。沿着一条黑链 push 次小值之后,其实 pop 次数是不会超过 push 进来的次数,也就是黑链的长度。所有黑链的总长度为
算法七
楼上的算法都太复杂了怎么办?没关系!有个更 naive 的线性做法。
我们按逆 dfs 序,把子树信息一个个合并到父亲上去。子树信息就是假设整棵树就只有这一棵子树时,每个结点最近的祖先使得有伸出去的子树深度超过自己(叫做up祖先好了)。
现在这个子树跟它的父亲接了起来,就拿父亲下现在已经有的子树跟新加进来的子树互相更新一下还没找到up祖先的结点。这个我们只用记录一个链表存下每个子树中所有还没找到up祖先的结点,合并时搞搞就行了。
代码短,常数小,速度快,萌萌哒
UPD: 我后来试了一下,需要开邻接表还要dfs的做法都有点被卡内存危险。