NOI2018
Day1
归程
-
算是 Kruskal 重构树的裸题,感觉就是在卡科技
-
知道了 Kruskal 重构树后就没什么难度了
-
先建好重构树,那么每次询问其实就是在树上的某个点跳到深度最浅且海拔超过 p 的点所在子树中到 1 节点的最短路的最短长度
-
那么预处理出每个点到 1 的最短路,树上每个点子树中的最短距离
-
每次询问倍增地在树上跳就行了
总结
-
Kruskal 重构树 + 最短路 + 倍增
-
注意倍增的顺序是先枚举 2 的次方,我就是没注意到这个调了半天
- 首先讨论关于 68 分的做法
想法一
-
不知道对不对,恳请指出错误
-
大概就是对于所有 T 离线下来建立广义 SAM ,然后用 S 在这个广义 SAM 上面先跑一遍,预处理出哪些子串在 S 中出现过,复杂度为 \(O(n)\)
-
然后对于每个 T ,就是询问在 S 中出现过多少本质不同的子串,那么我们找到每个 T 的每个前缀所在的节点,那么实际上就是求在 parent 树上 \(n\) 个点到根的链合并算贡献
-
那么就考虑经典套路按照 dfs 序排序,然后算每个点到根的贡献和,减去相邻点的 LCA 到根的贡献和,这里复杂度因为有 LCA ,所以是 \(O(n\log n)\)
-
所以总的复杂度就是 \(O(n\log n)\)
-
这是我最初的想法,然后你会发现没办法优化了
想法二
-
考虑还是对于每个 T 单独处理
-
对于 T 的每个前缀先求出在 S 中出现的最长后缀的长度
-
这个可以先对 S 建立 SAM ,然后让 T 在 S 上面跑,如果对于 las 有相应的匹配,那么就直接匹配,否则就一直跳 parent 树直到可以匹配
-
注意到这里的复杂度为什么是正确的,考虑只算向前匹配的复杂度是 \(O(|T|)\) ,而向后撤的复杂度不会超过 \(O(|T|)\) ,所以总的复杂度仍然是 \(O(|T|)\)
-
然后对于 T 建立 SAM,对于 SAM 上的每个节点求出从 \(\operatorname{endpos}\) 往前最长出现在 S 中的长度,然后就可以算贡献了
-
总的复杂度为 \(O(|T|+|S|)\)
正解做法
-
如果知道了 S 在区间的后缀自动机,那么就可以直接做上面的东西了
-
对于 S ,我们实际上做的是:
- 找到一个存在出现范围在 \((l,r)\) 的节点转移
- 对于当前节点求出范围在 \((l,r)\) 的最长长度
-
那么我们考虑对于每个 S 上的节点用线段树维护每个位置在范围 \((1,pos)\) 中 \(\operatorname{endpos}\) 最大的位置
-
那么对于每个终止值一开始就会有 \(O(|S|)\) 个元素,总的复杂度可以证明就是 \(O(|S|\log |S|)\)
-
注意到之前的线段树合并一般是以 dfs 的形式,但是这里的线段树合并需要记录每个线段树大小,所以每次不能直接用指针,而是需要新建一个节点
-
对于 T 做匹配的时候每次都要做询问,所以复杂度带一支 \(\log\)
-
总的复杂度就是 \(O((|T|+|S|)\log |S|)\)
总结
-
SAM + 线段树合并
-
首先要想出部分分的做法,然后再在部分分的做法上思考范围的限制
-
但是你如果像我一样想的部分分的做法和正解没什么关系,那就......
-
代码是真的难写,主要是细节难调,真是佩服考场能写出来的人
-
感觉 68 分第一种想法细节没那么多,考场上的最佳策略应该是迅速拿 68 分走人