三道 Petrozavodsk 和 Open Cup
Different Summands Counng
反套路题。介绍一下 O(m3logn) 的卡常做法和 O(m3) 的标算。
我们考虑对于一个数,算出它恰好没有出现过的方案数,然后用总方案减去这个就是出现过的方案数量。前面这个可以方便地容斥成钦定,而如果是钦定且这题是有序拆分就可以用插板来安排剩下的数。
n∑a=1((n−1m−1)−min(m,n/x)∑k=0(−1)k(mk)(n−ka−1m−k−1))=n∑a=1(min(m,n/x)∑k=1(−1)k+1(mk)(n−ka−1m−k−1))=m∑k=1(−1)k+1(mk)n/m∑a=1(n−ka−1m−k−1)这个东西看起来比较难算。我们考虑第一层直接枚举,怎么计算后面这个东西。
我们倍增计算出所有的 (n−ak−1j) 。具体来说,我们先计算出
F(j)=n/2∑a=0(n−ak−1j)然后考虑怎么去计算前半部分。
由于一个组合恒等式,有
(n−ak−1j)=∑t(n−ak−1−nk2j−t)(nk2t)所以可以 O(m2) 转移出所有的 F(j) 。复杂度 O(m3logn) ,转移时可以 NTT 做到 O(m2logmlogn),写着很难受而且很不优秀。
标算做法非常反套路。具体来说,我们把 a 看作 x ,于是会发现 (n−kx−1m−k−1) 这个玩意是个 O(m) 次项的多项式。所以说我们直接把多项式暴力算出来(这部分可以暴力或者 NTT),然后去求一个自然数幂和来算这个多项式的 ∑F(a) 。这样复杂度仅仅是 O(m3) 或者 O(m2logm) ,非常优秀。
第一种做法是当 n 很大的时候比较容易想到的套路做法,也就是去分治,第二种是一种优秀但是比较特殊的做法。
1 |
|
B Process with Constant Sum
我可能比较擅长这种题。
我们来手玩一下样例以及手捏的数据,会发现最后一段区间操作完后一定会成为一段 0 再拼一段 1 再拼一个 x 。具体来说,我们可以这样操作:首先对每个数 mod2 ,把所有 ≥2 的数用操作把 x−xmod2 送到结尾。然后找到倒数第二段 1 ,如果它和最后一段之间隔了奇数个 0 ,会发现你可以使用操作把最后两段互相削除掉。如果它和最后一段之间隔了偶数个0 ,会发现可以使用操作把这两段拼起来(这两部分需要仔细想想,有点细节)。而且会发现先拼 / 削任意两段是不会有区别的,也就是操作顺序其实不影响。
然后就是一些套路操作了,因为顺序不影响,我们把一个区间会得到的最终状态放到线段树上,在一个区间维护前面 0 的长度,中间 1 的长度,最后 x 是多少。合并就是上面说的,需要想一想,细节可以参考代码。修改就是正常的线段树单点修改。
复杂度 O(nlogn) 。
1 |
|
C Interest
神仙题
会发现这是以前 DLS round 的第三题,AUOJ 459。
我们考虑 O(n2) 暴力怎么做。可以看出选出两条边不相交路径可以直接看成一个网络流问题,也就是求出 1 到点 u 的大小为 2 的最小费用流,且每条边容量为 1 ,然后就可以类似环游那个题真正的跑 2 次单路增广。
首先第一次流其实可以非常简单的得出,也就是当我们建出最短路树后,第一次流一定是从 1 在最短路树上流到 u ,可以一次 dijkstra
实现。我们考虑能否通过一次 dijkstra
做完第二次流。但是在流了一次并反向后会得到负权边,所以我们可以对最开始的图做一次 primal dual
,也就是设到每个点最短路为 du ,那么把 u→v 边权为 w 的边的边权设置为 w′=w−dv+du 。不难发现这样操作后最短路树上的边一定全部变成了 0 ,并且对于一条 1→k 的路径,实际长度一定正好是 dk+dis 。并且此时,如果我们对 1→u 的路径全部反向,所有边的长度仍然是 ≥0 的!
于是我们考虑做一个 dijkstra
的过程,也就是我们设 d′u 为在 1→u 的边被反向时,1→u 的最短路。然后我们仍然考虑类 dijkstra
的转移,也就是每次拿出堆顶来更新其他点。考虑当前堆顶为 u ,那么转移有两种。首先,这个点自己连出去的边肯定可以转移,对于一条非树边 (u,v,w) ,一定可以用 du+w 来更新 dv 。
然后考虑还可以用 du 更新什么,会发现,其实 1→u 的边被反向后等价于当前的根换成了 u ,我们考虑对于一对其他的边 (a,b,w) ,我们可以用 du+w 来更新 b 当且仅当 a,b 不在 u 的同一棵子树。因为如果把 1→b 的路径反向其实等价于在这个图上把 u→b 的路径反向,然后如果 a,b 不在同一个子树,那么把 1→b 反向后仍然可以从 u 以 0 的代价走到 a ,否则一定走不到。所以可以这样更新。
但是直接这样更新需要枚举 u 的所有子树,这是不优秀的。但是考虑 u 的某个子树中如果存在某个点 v ,而 dv<du ,即 v 已经作为堆顶过了,那么 v 的子树一定是不用更新的。因为 v 子树内部一定不能更新, v 子树向外已经被 v 更新过了。所以我们更新完一个点 u 后其实可以直接把 u 删去,也就是做一个类似点分的过程。
但是这种启发式点分也很容易被卡到 O(n2) 。我们考虑在更新一个点 u 时,其实可以不用枚举这个点的最大的子树。我们可以对每个点存下这个点的所有出边即入边,然后枚举所有非最大的子树以及它们的出入边即可。我们可以证明这样每个点被枚举的次数是不超过 O(logn) 的,因为每次它作为轻子树出现,就说明这次点分之前它所在子树大小会翻倍。
然后还需要考虑最后一个问题,怎么判断哪个子树是最大的。考虑维护一个当前最大的子树 mx 。考虑用 O(min(szu,szv)) 的复杂度比大小。具体来说,可以倍增一个上界 lim ,然后在两个子树分别 dfs
lim 次,如果其中某一个没 dfs
到这么多次即可直接比较,否则倍增 lim 。可以发现这样实际上只会用 O(min(sz)) 次。所以,每次枚举一个子树后如果它比当前子树大,那么我们去把当前最大子树给 dfs
一次枚举所有出入边,然后把最大的子树修改成当前子树,否则对当前子树做即可。
前面说明了,这样的复杂度是 O(mlogn) 的。
1 |
|