首先膜拜 AK 了的神仙 xxy 学长。然后感觉还算是送温暖场,至少让我这样的菜鸡也能感受到温暖 。
T1
题意
对一个长度为 \(n\) 的排列进行冒泡排序,然后选出一个最大的子集 \(S\),满足在排序前序列中下标为 \(x,y\) 的两个数没有进行过互相交换操作 \(x,y\in S\),求 \(|S|\) 和一定在任意一种子集选取方案中的元素集合。
\(1\leq n\leq 10^5\)。
solution1
首先观察到序列中的一对数没有互相交换的充要条件是该数对是顺序对,那么第一问就等价于求序列中的最长上升子序列,随便 \(O(n\log n)\) 的 dp 搞搞。
然后对于第二问,设 \(f_i\) 可以由 \(f_j\) 转移而来,则连有向边 \((i,j)\),对于所有 \(f_i=|S|\) 连一条有向边到虚拟节点: \((s,i)\)。最后就等价于所有路径 \(s \to i(f_i=1)\) 的交集(必经点)。
观察到这个图是个 DAG,考虑支配树解决这个问题,暴力建出所有边再跑支配树的复杂度为 \(O(n^2)\),无法接受,考虑建立支配树的拓扑排序过程,这里的拓扑序是现成的(按下标倒序),\(i\) 在支配树上的父亲是 \(\operatorname{LCA}_{(j,i)\in E}j\) (所有连向 \(i\) 的点的 LCA),而这些点可以用动态开点线段树来维护,LCA 满足结合律,在线段树上直接上传即可。
增量 LCA 用倍增的话,总时间复杂度为 \(O(n\log^2n)\),空间为 \(O(n\log n)\)。可以用 Splay+LCT 做到一只 \(\log\) ,但常数大还难写。
solution2
上面那个做法是小丑,采用求必经点的另一种做法,用 HASH 表维护路径方案,若 \(cnt_{(s,i)}\times cnt_{(i,t)}=cnt_{(s,t)}\) 则 \(i\) 为必经点,正反两遍线段树就可以求出来,使用双模 HASH 即可稳过,时间复杂度 \(O(n\log n)\),比上面的支配树不知道高到哪里去了。
T2
题意
一颗 \(n\) 个节点的树,树上节点 \(i\) 的权值在区间 \([l_i,r_i]\) 中,且满足相邻两个节点的权值互质,求在所有方案中每个节点的权值之和。
\(1\leq n\leq 50,1\leq l_i\leq r_i\leq 5\times 10^4\)。
solution
首先考虑暴力 dp,设 \(f_{i,x}\) 表示节点 \(i\) 的权值为 \(x\) 时的子树 \(i\) 的方案数,有转移方程:
\[f_{i,x}=\prod_{j\in son_i}\sum_{y}f_{j,y}[\gcd(x,y)=1] \]我们收获了单次 dp 复杂度为 \(O(nv^2)\) 的做法,以每个点为根做一次,总复杂度为 \(O(n^2v^2)\)。
首先考虑优化转移,莫比乌斯反演:
\[f_{i,x}=\prod_{j\in son_i}\sum_{d|x}\mu(d)\sum_{d|y}f_{j,y} \]\(\sum_{d|y}f_{j,y}\) 可以预处理,复杂度为 \(O(nv\log v)\);\(\sum_{d|x}\mu(d)\) 暴力枚举,复杂度为 \(O(\sqrt v)\) ,总时间复杂度降为 \(O(n^2v\sqrt v)\)。
然后考虑换根 dp,从 \(x\) 转移到儿子 \(y\) 时,除去 \(subtree(y)\) 的贡献再乘上下面的一坨即可,时间复杂度降为 \(O(nv\sqrt v)\) (能过)。
有没有鸽鸽教我 \(O(nv\log v)\) 做法呀 /kel
T3
题意
给一个长度为 \(n\) 的括号串,\(m\) 次询问,每次询问一个区间内的最长合法括号子串。
\(1\leq n \leq 10^5,1\leq m\leq 4\times 10^6\)。
solution1
%%% xxy
考虑将所有询问离线,保存在右端点处,从前向后扫描,将当前的括号添加进去,同时维护 \(len_i\) 表示以 \(i\) 开头的最长合法括号子串,则对于右端点为当前节点(设为 \(x\))的所有询问答案为 \(\max_{l\leq i\leq x}\{len_i\}\)。
然后考虑这个 \(len\) 怎么维护。首先观察到左括号是没有影响的,直接丢到栈里面即可;如果是右括号,并且此时栈中有能匹配的左括号,那么弹出,开始计算新的 \(len\)。设当前这个右括号的下标为 \(x\),与其匹配的左括号下标为 \(y\);按照套路将括号看成 \(1\) 和 \(-1\),记 \(h_i\) 表示当前节点的高度(前缀和),设 \(pre_x\) 表示满足 \(h_z\geq h_x\) 的最小的 \(z\),那么加入这个新的右括号 \(x\) 将会把位于 \([pre_x,x]\) 中所有满足 \(h_i=h_x\) 的 \(len_i\) 变为 \(len_i+x-y+1\)。
然后口胡了一个假的线段树被 xxy D 了考虑分块维护,设块的大小为 \(S\),记 \(mxlen_{id,v}\) 表示块 \(id\) 中满足 \(h_i=v\) 的 \(len_i\) 的最大值,\(delta_{id,v}\) 表示块 \(id\) 中满足 \(h_i=v\) 的 \(len_i\) 需要加上的值(懒标记),\(mx_{id}\) 表示块 \(id\) 内所有 \(len_i\) 的最大值,然后整块打标记,散块下传标记并暴力更新即可,查询时同理,时间复杂度为 \(O((n+m)(S+\dfrac{n}{S}))\) ,显然过不了。
考虑一个小优化,每次将右端点相同的也按左端点从大到小排序,更新时还是一样更新,查询时则从后向前扫,维护后缀最小值,分析一些这样做复杂度为 \(O((n+m)S+\dfrac{n^2}{S}))\) ,取 \(S=50\) 最优。(这块长算出来还是整数,真的不是出题人想放分块过吗 /youl )
还有一个优化空间的 trick ,按上述暴力存储的空间复杂度为 \(O(\dfrac{n^2}{S})\),但是观察一块内的 \(h\) 值分布,我们发现是跨度不超过 \(S\) 的连续区间,那么我们记录下一块内的最小值 \(mn_{id}\),利用离散化的思想,令块内的 \(mxlen_{id,v}\) 中的 \(v\) 实际对应 \(v+mn_{id}\)(\(delta\) 同理),每块只需要存放 \(O(S)\) 级别的信息,空间复杂度降为 \(O(n)\)。
暂时不知道这个做法有没有 polylog 的维护方式。
solution2
nb 做法,就是出题人的 std 码风过于奇怪,看了半天才懂。
直接考虑 ST 表维护这东西,追求 \(O(n\log n +m)\) 的复杂度。
观察一下一个区间的最长括号子串可能是怎么来的,设 \(x,y\) 分别是区间 \([l,r]\) 内 \(h\) 最小值的最左边的一个和最右边一个,\(lenl_i\) 为以 \(i\) 开头在整个序列中最长的合法括号子串,\(lenr_i\) 为以 \(i\) 结尾在整个序列中最长的合法括号子串:
- 由 \([x,y]\) 构成。
- 由 \(max_{l\leq i<x}\{lenl_i\}\) 构成。
- 由 \(max_{y<i\leq r}\{lenr_i\}\) 构成。
为什么这里是在整个区间?如果 \(lenl_i\) 的右端点超过了 \(r\) 呢?实际上并不会,因为 \(x\) 和 \(y\) 是该段区间中的最小值,其它的 \(i\) 都满足 \(h_i>h_x=h_y\) 。合法括号串首尾 \(h\) 相同并且串内所有 \(h\) 都满足大于等于首尾的值,那么以满足 \(l\leq i< x\) 的 \(i\) 为端点的合法括号串一定不会跨过 \([x,y]\),也更不会跨过 \(r\) 了,\(lenr_i\) 的情况同理。
那么就可以快快乐乐地哈希表了,\(lenl\) 和 \(lenr\) 都可以用单调栈 \(O(n)\) 处理出来,区间 \(h,lenl,lenr\) 的最值使用 ST 表即可做到 \(O(n\log n)-O(1)\)。 用四毛子可以做到线性。
整体时间复杂度为 \(O(n\log n+m)\),并且还比分块好写。