NOI2018

Day1

归程

  • 算是 Kruskal 重构树的裸题,感觉就是在卡科技

  • 知道了 Kruskal 重构树后就没什么难度了

  • 先建好重构树,那么每次询问其实就是在树上的某个点跳到深度最浅且海拔超过 p 的点所在子树中到 1 节点的最短路的最短长度

  • 那么预处理出每个点到 1 的最短路,树上每个点子树中的最短距离

  • 每次询问倍增地在树上跳就行了

  • 参考代码

总结

  • Kruskal 重构树 + 最短路 + 倍增

  • 注意倍增的顺序是先枚举 2 的次方,我就是没注意到这个调了半天

P4770 [NOI2018] 你的名字

  • 首先讨论关于 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 分走人

posted @ 2022-06-04 11:33  Kzos_017  阅读(30)  评论(0编辑  收藏  举报