APIO真题选做
[APIO2020] 粉刷墙壁
将长度为 \(n\) 的墙壁涂色,共有 \(k\) 种颜色,第 \(i\) 段墙壁期望的颜色为 \(c_i\) 。有 \(m\) 个粉刷公司,第 \(i\) 个粉刷公司可以涂 \(a_i\) 种颜色(是给定的)。现在可以提出若干个形如 \(x\;y\;(0\le x<m,0\le y\le n-m)\) 的要求,只有当对于所有的 \(0\le i< m\) ,第 \((x+i)\,mod\,m\) 个公司可以涂第 \(y+i\) 段墙壁,则会将这些墙壁全部涂上色。每一段墙壁可以被涂色多次,但每次必须都是它所期望的颜色 \(c_i\) 。最小化提出的要求数,若无法满足期望颜色输出 -1。
\(1\le n\le 10^5\;\;1\le m\le min(n,5\times 10^4)\;\;1\le k\le 10^5\)
设 \(f_i\) 表示可以涂第 \(i\) 种颜色的公司数量,则 \(\sum{f_i}^2\le 4\times 10^5\)
我们先考虑如何统计答案,我们发现一次请求对应的就是涂上区间 \([y,y+m-1]\) ,那么发现这就变成了一个简单的区间覆盖问题,直接贪心地不断扩展右端点即可。那我们现在要做的就是如何找到这些区间,也就是找到那个对应的 \(x\) 。我们先考虑一个朴素的 \(dp\) ,设 \(f_{i,j}\) 表示从第 \(i\) 段墙壁第 \(j\) 个公司开始匹配最多能涂几段墙,转移显然。若能涂 \(f_{i,j}=f_{i+1,(j+1)\,mod\,m}+1\) ,否则 \(f_{i,j}=0\) 。若 \(f_{i,j}>=m\) 说明 \([i,i+m-1]\) 为一个区间。哦那个是滚动数组优化一下,此时时间复杂度为 \(O(nm)\) ,考虑优化。发现我们转移时只需要能涂的,所以我们可以变为枚举可以涂第 \(i\) 段墙壁的公司直接进行转移,由于 \(\sum{f_i}^2\le 4\times 10^5\) ,所以时间复杂度有保证。
[APIO2020] 交换城市
给定 \(n\) 个点和 \(m\) 条边的无向图,每条边有边权。有 \(Q\) 次询问,每次询问 \(u,v\) 两点是否可以互达,要求不能相遇,若能互达最小化路径上边权的最大值,否则输出 -1,强制在线。
\(2\le n\le 10^5\;\;m\le 2\times 10^5\;\;Q\le 2\times10^5\)
前置知识: $Kruskal $ 重构树
我们在 \(Kruskal\) 求最小生成树的过程中,对于边 \((u,v)\) ,当我们合并两个连通块\(fa_u,fa_v\) 的时候,我们新建一个点 \(x\) ,其点权为新加的那条边的权值,然后连边 \((x,fa_u),(x,fa_v)\) ,再让 \(fa_u=fa_v=x\) 即可。时间复杂度为 \(O(n\log n+m\log m)\) 。
性质:共有 \(2n-1\) 个节点。其中叶子节点都是原树上的节点,其余节点表示某条边的边权。原图中两个点之间的路径上最大边权的最小值为 \(Kruskal\) 重构树上两个节点的 \(LCA\) 的点权。(若求两个节点的最小边权的最大值跑最大生成树即可)
我们先考虑几个简单的性质,当 \(u,v\) 所在连通块是一条链的时候显然无解,当其连通块有一个点的度数 \(\ge 3\) 时必定有解,或者它就是一个环的时候也有解。
发现这道题我们额外做的就是在做 \(Kruskal\) 重构树的时候去维护这些连通块的信息,我们主要考虑去处理链的情况。假如我们要处理边 \((u,v,w)\) ,再设 \(fa_u=p,fa_v=q\) 。
当 \(u,v\) 已经连通的时候,我们看一下这个连通块是不是一条链,也就是说我们还需要维护一个数组表示连通块是否还是一条链的形态,如果是一条链,它再加一条边必然就不是链了,所以我们修改一下标记。此时我们有一个神奇的操作,当链变为非链时,我们在重构树上新建节点,点权为 \(w\) ,让它向所有原来链上的点连一条边,再将这个连通块的 \(root\) 设为该点。显然这不影响重构树用 \(LCA\) 求解的性质,而且可以使得求出的一定为非链的情况。
当 \(u,v\) 不连通时,我们发现,若其中有一个为非链,那么合并完也必然非链,所以我们直接对为链的那个连通块(也可能没有)进行连边操作,合并两个连通块并将标记设为非链形态。但是也有可能两个都是链,那我们还需要分类讨论一下。若点 \(u,v\) 都是两条链的某个端点,那它合并完仍然是一条链,所以我们直接修改新链的 \(p1,p2\) 两端点即可。否则,它合并后一定为非链,直接将两个连通块全进行加边操作即可。
此时我们就完美用 \(Kruskal\) 重构树解决了询问,最后我们只需要求两点 \(LCA\) 即可,其点权就是答案了。注意最后建出的是一颗森林,则无解当且仅当两个点不在同一棵树上,这个 \(dfs\) 的时候直接处理一下即可。总时间复杂度为 \(O(n\log n+Q\log n)\) 。
[APIO2021] 封闭道路
给定一棵 \(n\) 个结点的树,每条边有边权。对于 \(0\le x<n\) ,删掉一些边使每个结点的度数不大于 \(x\) ,求删掉的边的权值和最小值。
\(n\le 250000\)
我们先从暴力入手,我们设 \(f_{u,0/1}\) 表示 \(u\) 与父亲的边删/不删时以 \(u\) 为根的子树满足所有节点度数 \(\le x\) 时的答案。设 \(d_i\) 表示节点 \(i\) 的度数,我们先不考虑点 \(u\) 的度数限制,此时答案显然为 \(\sum_{v\in son_u}min(f_{v,0},f_{v,1}+w)\) 。我们考虑上点 \(u\) 的度数,我们假设满足 \(min(f_{v,0},f_{v,1}+w)=f_{v,1}+w\) 的儿子有 \(cnt\) 个,此时它的度数为 \(d_u-cnt\) ,还需要删 \(d_u-cnt-x\) 个儿子。我们可以想到维护一个小根堆,将所有不满足 \(min(f_{v,0},f_{v,1}+w)=f_{v,1}+w\) 的儿子 \(f_{v,1}+w-f_{v,0}\) 的值扔进去,取出来前 \(d_u-cnt-x\) 个即可,设和为 \(sum\) ,这样答案为 \(sum+\sum_{v\in son_u}f_{v,0}\) ,单次时间复杂度为 \(O(n\log n)\) ,总时间复杂度为 \(O(n^2\log n)\) 。
这个暴力将是我们正解的重要基础。我们枚举 \(x\) 的时候,必然会有很多信息是重复计算的,我们考虑从小到大枚举 \(x\) ,若某个点的 \(d_u\le x\) ,容易发现在后面的计算中这个点已经没用了,我们定义这样的点为无用点。所以我们再求解的时候,可以只对有用点进行遍历,将无用点当成叶子去处理,但无用点必然还是会对父亲有影响的,我们来考虑计算无用点的贡献,由于它度数已经满足要求了,所以其 \(f\) 值均为 \(0\) ,所以贡献就是边权 \(w\) ,扔到堆里。然后再考虑那些有用点,它们的贡献计算方法与暴力相同。当我们处理好堆中的元素时,我们直接暴力取需要的元素即可。而且容易发现无用点的计算是永久的,而有用点的计算可能只是临时的。所以我们可以在计算结束后撤销对有用点的贡献,同时我们在求答案的时候可能会删掉无用点的贡献,所以我们还需要撤销对无用点的删除操作。那么此时我们就需要一个支持可撤销添加或删除操作的堆,我们可以用两个堆去维护,一个堆维护插入的元素,另一个堆维护删除的元素,\(size\) 就是两个堆的 \(size\) 之差,求堆顶要先把两个堆相同的元素弹出去再取插入的堆的 \(top\) 即可。算是一个可撤销堆的 \(trick\) 吧。对于时间复杂度,每个点最多会成为有用点 \(d_i\) 次, \(\sum d_i=2n-2\) ,所以时间复杂度可以控制在 \(O(n\log n)\) ,实现细节较多。
[APIO2021] 雨林跳跃
给定一个长度为 \(n\) 的序列 \(a\) 。有 \(Q\) 次询问,每次询问给定四个正整数 \(A,B,C,D\) ,求能否从 \([A,B]\) 中的任意一个点出发到达 \([C,D]\) 中的某个点。移动方式为:假设现在在位置 \(x\) ,一次移动可以到左面或右面比 \(a_x\) 大的第一个位置。若能到达输出最小移动步数,否则输出 -1。
\(2\le n\le 2\times 10^5\;\;1\le Q\le 10^5\)
我们先预处理出每个点向左右跳一次可以到达的点 \(L_i,R_i\) ,这个显然可以单调栈求出。然后我们逐渐地考虑这个问题。
我们先考虑 \(A=B\) 且 \(C=D\) 的情况,发现也就是询问从 \(A\) 出发能否到 \(C\) 。先考虑无解的情况,发现当 \([A,C-1]\) 中的最大值 \(a_{pos}\) 大于 \(a_C\) 时无解,因为无论怎么移动 \(pos\) 都会拦住你到 \(C\) ,而你一旦到达 \(pos\) 后只能往更大的位置移动也就永远到不了 \(C\) 了。此时发现 \(pos\) 这个位置似乎很关键,我们从这里入手解决有解的情况,因为从 \(pos\) 是可以一步向右跳到 \(C\) 的,所以我们考虑怎么能最小步数到达 \(pos\) 。很容易发现我们只需不断跳左右两边的较大值,直到较大值大于等于 \(a_{pos}\) 了。假如左边大于 \(a_{pos}\) ,我们需要不断往右跳,若是右边显然较大值就是 \(a_{pos}\) 了,直接跳过去即可,综上我们直接再往右跳即可。但是也很容易举出反例,就是我们跳到左边一个比 \(a_{pos}\) 更大的点,再直接跳到 \(C\) ,这是可行的。但我们可以发现,当 \(a_{L_{pos}}<a_C\) 时, 它向左跳到 \(L_{pos}\) 是最优的,因为 \(L_{pos}\) 到 \(C\) 的值都小于 \(a_{pos}\) ,所以 \(L_{pos}\) 是 \(A\) 左边最靠右的可以一步跳到 \(C\) 的位置,但记住前提是 \(a_{L_{pos}}<a_C\) 。所以将这两种情况取个 \(min\) 即可。我们举个例子,假如序列为 \(8,7,4,3,5,6,9,11,10,13\) ,\(A=4,C=10\) ,以下用位置的值来代表这个位置。发现 \(pos\) 为 \(11\) ,按照第一种类型,我们从 \(3\) 出发,先跳到 \(5\) ,再跳到 \(7\) ,再跳到 \(9\) ,再跳到 \(11\) ,最后跳到 \(13\) 。再以 \(4,1,2,3,5\) 为例,\(A=1,C=5\) ,发现 \(pos\) 为 \(3\) ,我们直接跳到 \(L_{pos}\) \(4\) 再直接跳到 \(5\) 。
我们再考虑只有 \(C=D\) 的情况,首先无解情况显然与上面相同。发现我们就是要找一个出发点。我们沿用上面的两种情况,如果我们要经过 \(pos\) ,我们的出发点显然要在 \(L_{pos}\) 右面,否则它往右跳一定会到 \(L_{pos}\) 进而到不了 \(pos\) ,同时贪心地想,显然还要以值最大的那个位置为起点,然后再按照第一种情况处理即可。我们再考虑不经过 \(pos\) 的情况,前提仍然是 \(a_{L_{pos}}<a_C\) 。事实上就是找到能到 \(L_{pos}\) 的点,如果 \(L_{pos}\) 就在 \([A,B]\) 中直接以它为起点就好,否则 \(L_{pos}\) 一定在 \(A\) 的左面,那么我们可以贪心地选取 \([A,B]\) 中值最大的点,然后以左右选较大值的方式跳到 \(L_{pos}\) 再一步到 \(C\) 。同理两种情况取个 \(min\) 即可。
最后我们来考虑没有其它限制的情况,发现我们只需要考虑能否到达 \([C,D]\) 中的某个位置。无解情况类似,就是 \([C,D]\) 的最大值比 \(a_{pos}\) 要小。我们再按照两种情况分析。如果我们要跳到 \(pos\) ,发现它就一定可以跳到 \([C,D]\) 中了,因为 \([C,D]\) 中一定存在某个位置值大于 \(a_{pos}\) ,这就和上面一样了。然后我们考虑利用 \(L_{pos}\) 的情况,发现与上面的情况几乎一模一样,只有前提条件变为 \(a_{L_{pos}}\) 小于 \([C,D]\) 中的最大值,也就是要保证从 \(L_{pos}\) 能跳过去。
最后跳的这个过程怎么求解,发现直接倍增即可。时间复杂度为 \(O(n\log n+Q\log n)\) 。
[APIO2022] 游戏
有 \(n\) 个点,编号为 \(0\) 到 \(n-1\) 。编号为 \(0\) 到 \(k-1\) 为特殊点。对于 \(0\le i\le k-2\) ,初始有有向边 \((i,i+1)\) 。接下来给 \(T\) 个操作,每个操作会给定 \(u\) \(v\) ,表示新加一个从 \(u\) 指向 \(v\) 的有向边(添加后这条边会一直存在)。若在加入这条边后,出现了一个包含特殊点的环,输出 \(1\) 并结束程序,否则输出 \(0\) 并接着读入。
\(1\le n\le 3\times 10^5\;\;1\le T\le 5\times 10^5\;\;1\le k\le n\)
神仙题目。考虑设 \(l_u\) 表示可以到达点 \(u\) 的编号最大的特殊点,\(r_u\) 表示 \(u\) 可以到达的编号最小的特殊点。如果不存在值分别为 \(-1,k\) 。那么容易发现存在环当且仅当存在点 \(u\) 其 \(l_u\ge r_u\) 。所以我们就有了一个暴力的思路,每次加边之后直接暴力 \(dfs\) 正图和反图去维护 \(l\) 和 \(r\) 数组。然后我们考虑去怎么优化这个过程,我们可以把 \([l_u,r_u]\) 看成区间,直接上一棵权值线段树,根节点的区间为 \([-1,k]\) 。再没有环的时候它们就可以构成合法区间。初始时显然特殊点都是叶子节点,非特殊点都是根节点。我们考虑只记录能包含区间 \(l_u,r_u\) 的最小区间对应的线段树节点 \(tr_u\) ,也就是说,我们的 \(l\) 和 \(r\) 数组只会记录到对应的线段树节点的区间。首先发现如果有有向边边 \((u,v)\) ,那么 \(l_v=max(l_u,l_v),r_u=min(r_u,r_v)\) 。基于这个性质,我们对 \(tr_u\) 和 \(tr_v\) 进行处理。我们来对 \(tr_u\) 和 \(tr_v\) 的关系进行分类。
若 \(tr_u\) 与 \(tr_v\) 相离,显然只有 \(r_v<l_u\) 时,更新后会出现 \(r_v<l_u\le l_v,r_u\le r_v<l_u\) ,也就是都变成不合法区间形成环了,那我们直接判断即可。同时这也就是判断能否成环的最基本方法。
若 \(tr_u\) 和 \(tr_v\) 是同一个节点(非叶子节点),此时 \(l_u=l_v,r_u=r_v\) ,显然不会成环,直接 \(return\) 即可。(这里我们进行一个正确性证明,假如我们现在 \(l\) 和 \(r\) 表示真实值,那么它们的 \(tr\) 在同一个节点说明两个区间有交,手模一下发现更新后也不会出现不合法区间,所以不影响正确性)。
若 \(tr_u\) 和 \(tr_v\) 有包含关系,也就是说它们有父子关系。我们先设 \(mid_u\) 和 \(mid_v\) 分别表示 \([l_u,r_u]\) 和 \([l_v,r_v]\) 的中点。我们有四种情况:一是 \(v\) 在 \(u\) 的左儿子内,即 \(r_v\le mid_u\) ,这样更新后 \(r_u\) 就会变为 \(r_v\) ,\(tr_u\) 也会随之改边,但是按照我们的做法,我们只需把 \(r_u\) 先改为 \(mid_u\) 即可。二是 \(v\) 在 \(u\) 的右儿子内,即 \(l_v>mid_u\) ,但此时 \(tr_u\) 是不会改变的。三是 \(u\) 在 \(v\) 的左儿子内,即 \(r_u\le mid_v\) ,此时 \(tr_v\) 也不会改变。最后是 \(u\) 在 \(v\) 的右儿子内,即 \(mid<l_u\) ,此时 \(l_v\) 会变成 \(l_u\) ,同情况一我们只需要将 \(l_v\) 变成 \(mid_v+1\) 即可。我们发现情况二和情况三不会改变 \(tr\) 的值,所以我们不用处理它。然后我们单独看情况一和情况四,由于它们的 \(tr\) 发生了改变,那么按照判断环的方式,它们是有可能成环的,所以我们需要处理。我们直接遍历 \(tr\) 改变的那个点的所有邻边再判断是否成环,别忘了还要遍历反图中的邻边,不断这样递归下去即可。只要出现环直接 \(return\) 即可。由于每个 \(tr\) 只会变化 \(\log k\) 次,同时每次还会遍历邻边,所以总时间复杂度为 \(O(m\log k)\) 。
[APIO2022] 排列
构造一个含有 \(k\) 个上升子序列的排列(从 \(0\) 开始,包括空序列)。要求序列长度 \(\le 90\) 。
\(2\le k\le 10^{18}\)
先说一个很无脑的想法,直接构造一个长度为 \(k\) 的递减序列。可以获得 \(10\) 分的成绩。
我们考虑 \(1\) 到 \(x\) 的一个递增序列,发现其任意一个子序列都是递增的,所以有 \(2^x\) 个上升子序列。由此我们可以想到将 \(k\) 二进制拆分,先假设其最大项为 \(2^x\) ,那么我们可以先来一个 \(0\) 到 \(x-1\) 。每出现一个 \(2^x\) ,我们就在当前序列的第 \(x\) 项后面添加 \(1\) 个严格大于当前序列中所有数的数。举个例子,现在 \(k=2^4+2^2+2^0\) ,初始序列为 \(0,1,2,3\) ,我们要出现一个 \(2^2\) ,那我们在 \(1\) 的后面加一个 \(4\) ,变成 \(0,1,4,2,3\) ,这样 \(4\) 就可以产生 \(2^2\) 个上升子序列,对于 \(2^0\) ,我们在最前面放一个 \(5\) 即可。最终得到 \(5,0,1,4,2,3\) 。按照这样最后序列的长度为 \(\log_2 k+popcount(k)-1\) 。但发现 \(10^{18}<2^{60}\) ,那么对于 \(2^{59}-1\) ,它就需要 \(58+58-1=115\) 个数,还是超出了限制,我们继续考虑优化。
发现最开始的那 \(\log _2k\) 个数的骨架是很难减掉了。发现 \(90=1.5\times 60\) ,我们就有一个非常妙的想法:能不能把两个 \(1\) 尝试用一个 \(1\) 表示出来。发现这个想法是可行的!我们以序列 \(0,4,1,6,2,5,3\) 为例,初始序列是 \(0,1,2,3\) ,产生 \(2^4\) 个上升子序列。然后我们加了一个 \(5\) ,产生了 \(2^3\) 个子序列,然后又加了一个 \(6\) ,产生了 \(2^2\) 个子序列,然后我们又加了一个 \(4\) ,它可以和 \(0\) 以及后面的 \(6,5\) 共产生 \(2^0+2^0\times 2=2^0+2^1\) 个子序列。这样这个 \(4\) 就起到了用一个表示两个的作用。一般地,在我们得到初始序列后,我们按二进制位从大到小先找到两个 \(1\) 作为这个操作的基础,并添加两个递减的数,记录下最大值和次大值的位置。然后我们继续往后找 \(1\) ,每找到两个连续的 \(1\) ,我们就可以按照刚刚例子的操作,先把那两个最大值和次大值加 \(1\) ,然后在对应的位置添加一个原来作为次大值的值,这压根就可以产生 \(3\) 倍的贡献。而如果不是两个连续的 \(1\) ,我们就按照上面的方案,直接插入一个比当前最大值加 \(1\) 的数,并修改最大值和次大值位置即可。这样序列长度就可以控制在 \(1.5\log k\) 了,可以得到满分。
[APIO2018] 新家
在一条数轴上会出现 \(n\) 个点,每个点有颜色、位置、出现时间和消失时间,共有 \(k\) 种颜色。接下来有 \(Q\) 个询问,每次询问为:在时间 \(t\) 对于位置 \(pos\) ,每种颜色的点到 \(pos\) 的最小距离的最大值。若在时间 \(t\) 并不存在所有 \(k\) 种颜色的点,输出 -1。
\(1\le n,Q\le 3\times 10^5\;\;1\le k\le n\) 位置和时间的值域范围为 \([1,10^8]\) 。
我们先考虑怎么处理时间的限制,这很容易想到将所有点的出现时间、消失时间以及询问一起离线下来排个序。然后我们考虑这种区间数颜色的操作,经典的操作是对于每个位置记录与其颜色种类相同的上一个位置,而这里由于每个位置上可能出现多个点,所以我们每个位置(也就是叶子节点)开一个 \(multiset\) 维护该位置上所有颜色的点对应的前驱,区间直接维护其前驱最小值。同时我们对于每种颜色的点还要开一个 \(multiset\) 记录其出现的位置,还要先插入两个哨兵商店,这样可以很方便的查找其前驱后继。具体地,若新加一个点位置为 \(pos\) ,我们找到该点的前驱 \(a\) 和后继 \(b\) ,我们将 \(b\) 的前驱修改为它自己,也就是将位置 \(b\) 的 \(multiset\) 删去 \(a\) 而插入 \(pos\) ,再将 \(pos\) 的前驱修改为 \(a\) ,再看其 \(size\) 是否为 \(2\) ,若是 \(2\) 则说明该种颜色的点原先没有而现在有了,所以该时刻的颜色种类数要加一,同时对应颜色的 \(multiset\) 还要插入 \(pos\) 。删除的处理与插入类似。对于询问,我们可以通过二分长度 \(len\) ,若 \([x-len,x+len]\) 包含了所有的 \(k\) 种颜色,那么 \(len\) 合法。怎么判断是否包含所有颜色呢,我们利用记录的前驱最小值,若 \(r+1\) 往后的位置前驱最小值小于 \(l\) ,说明一定有某种颜色没有在 \([l,r]\) 内出现。这样单次时间复杂度为 \(\log ^2n\) 。我们可以直接线段树上二分位置,这样时间复杂度可以做到 \(O(n\log n+Q\log n)\) 。
[APIO2018] 铁人两项
给定一张 \(n\) 个点 \(m\) 条边的无向图,问有多少三元组 \((a,b,c)\) 满足存在一条简单路径,从 \(a\) 出发经过 \(b\) 到达 \(c\) 。
\(n\le 10^5\;\;m\le 2\times 10^5\)
我们发现当 \(a\) 和 \(c\) 确认时 \(b\) 的数量就是 \(a\) 到 \(c\) 的所有简单路径的并的大小减二(减去 \(a\) 和 \(c\) 两个点)。此时可以用到点双连通分量的一个性质:对于同一个点双中的两个点其简单路径的并集恰好为整个点双。所以 \(a\) 到 \(c\) 的简单路径的并还等于 \(a\) 到 \(c\) 经过的所有点双的并集。我们直接建出原图的圆方树,这样两点间路径就和其在圆方树上的路径经过的圆点和方点有关。这里我们运用一个小技巧,对圆方树上的点赋上点权。我们在这里将方点点权设为点双的大小,圆点点权设为 -1。手模一下发现此时两点间简单路径的并的大小减二就等于圆方树上两点路径上经过的点的权值和。此时问题就转化为统计圆方树上两圆点间路径的权值和。这是经典问题,我们可以考虑每个点对答案的贡献,直接 \(dfs\) 即可。时间复杂度为 \(O(n+m)\) 。