点击查看代码(不含图片)
\documentclass[table]{beamer}
\usepackage{fontspec, xunicode, xltxtra}
\usepackage{xeCJKfntef}
\usepackage{ctex}
\usepackage[UTF8, noindent]{ctexcap}
\usepackage{graphicx}
\usepackage{tikz}
\usepackage{amsmath, amssymb}
\usepackage{multicol, multirow, makecell, tabu, dcolumn}
\usepackage{setspace}
\usepackage{ulem}
\usepackage{geometry}
\usepackage{algorithm}
\usepackage{listings}
\usepackage{hyperref}
\usepackage{xcolor}
\hypersetup{CJKbookmarks=true}
\usefonttheme[math]{serif}
\usetheme{Boadilla}
\usecolortheme{whale}
% \setbeamertemplate{background}{
% \begin{tikzpicture}
% \node[opacity = 0.3,inner sep = 0pt] at (0,0){\includegraphics[width=\paperwidth,height=\paperheight]{bkg.jpg}};
% \end{tikzpicture}
% }
\title{高级数据结构}
\author{紊莫}
\AtBeginSection[]{
\begin{frame}
\frametitle{目录}
\tableofcontents[currentsection]
\end{frame}
}
\begin{document}
\frame{\titlepage}
\section{可并堆(左偏树)}
\begin{frame}{可并堆(左偏树)}
\begin{itemize}
\item 左偏树是一种可并堆,具有堆的性质,并且可以快速合并。
\end{itemize}
\end{frame}
\begin{frame}{左偏树的定义和性质}
\begin{itemize}[<+->]
\item 对于一棵二叉树,我们定义外节点为子节点数小于两个的节点。
\item 定义一个节点的 $\mathrm{dist}$ 为其到子树中最近的外节点所经过的边的数量。
\item 空节点的 $\mathrm{dist}$ 为 $0$。
\item 左偏树是一棵二叉树,它不仅具有堆的性质,并且是\textbf{左偏}的:每个节点左儿子的 $\mathrm{dist}$ 都大于等于右儿子的 $\mathrm{dist}$。
\item 因此,左偏树每个节点的 $\mathrm{dist}$ 都等于其右儿子的 $\mathrm{dist}$ 加一。
\item 需要注意的是,$\mathrm{dist}$ 不是深度,左偏树的深度没有保证,一条向左的链也符合左偏树的定义。
\end{itemize}
\end{frame}
\begin{frame}{合并操作}
\begin{itemize}[<+->]
\item 假设要合并 $x, y$ 两个堆(以小根堆为例)。
\item 首先设 $\mathrm{val}(x) < \mathrm{val}(y)$,那么将 $x$ 作为新堆的根,其左儿子不变,递归的合并右儿子和 $y$ 即可。
\item 返回上来的时候维护左偏的性质,交换两个儿子并且更新 $\mathrm{dist}$ 即可。
\item 这样的复杂度是 $\mathcal{O}(\log n)$ 的。
\end{itemize}
\end{frame}
\begin{frame}{复杂度证明}
\begin{proof}
根据性质:左偏树每个节点的 $\mathrm{dist}$ 都等于其右儿子的 $\mathrm{dist}$ 加一。
每次递归都会使得 $\mathrm{dist}$ 减一,而根的 $\mathrm{dist}$ 是 $\mathcal{O}(\log n)$ 的。
一棵根的 $\mathrm{dist} = x$ 的二叉树至少有 $x-1$ 层是满二叉树,那么就至少有 $2^x-1$ 个节点。
所以 $n$ 个点的树 $\mathrm{dist}$ 是 $O(\log n)$ 级别的。
\end{proof}
\end{frame}
\begin{frame}{随机交换法}
\begin{itemize}[<+->]
\item 你可以采用每次以 $\dfrac{1}{2}$ 的概率交换两个儿子的方式,就不用维护 $\mathrm{dist}$ 了。
\item 复杂度为平均 $\mathcal{O}(\log n)$,实测常数差距不大。
\item \href{https://cp-algorithms.com/data_structures/randomized_heap.html}{想看证明的点我}。
\end{itemize}
\end{frame}
\begin{frame}{常规操作}
\begin{itemize}[<+->]
\item 加入点:将原堆和一个大小为 $1$ 的可并堆合并。
\item 删除最小值:合并根的左右儿子。
\item 像一些常见数据结构一样打打 tag,做点整体操作。
\end{itemize}
\end{frame}
\begin{frame}{\href{https://www.luogu.com.cn/problem/P1456}{P1456 Monkey King}}
\begin{itemize}
\item 有 $n$ 个集合,一开始只有一个元素 $s_i$。
\item 有 $m$ 次操作,每次将两个集合内元素最大值减半,然后合并这两个集合。
\item 输出每次操作完后最终集合内的最大值。
\end{itemize}
\end{frame}
\begin{frame}{代码}
\begin{figure}[htb]
\centering
\includegraphics[width=\linewidth]{可并堆1.png}
\end{figure}
\end{frame}
\begin{frame}{\href{https://www.luogu.com.cn/problem/P3642}{P3642 [APIO2016] 烟火表演}}
被要求加入的一个题。\\~\\
\begin{itemize}
\item 给出一棵大小为 $n$ 的树,边有边权,要求改变每一条边的边权,将边权从 $x$ 改为 $y$ 的代价为 $|x-y|$。
\item 使得根节点到每个叶子的距离相同,求最小代价。
\end{itemize}
要求复杂度 $\mathcal{O}(n \log n)$。
\end{frame}
\begin{frame}{解法}
首先设出朴素的 DP,$f_{u,i}$ 表示 $u$ 到 $u$ 子树内的叶子节点路径长度均为 $i$ 的最小代价。\\~\\
转移有 $f_{u,i} = \sum\limits_{v\in \mathrm{son}_u}\min\limits_{j=0}^{i}(|w-j|+f_{v,i-j})$,不妨考虑逐个儿子合并。\\~\\
因为绝对值函数是下凸的,运用归纳法容易得到 $f_i$ 也是下凸的。\\~\\
然后可以用上 Slope Trick 经典的维护方法,维护这个下凸函数的拐点。
\end{frame}
\begin{frame}{可持久化可并堆优化 K 短路问题}
\begin{itemize}
\item 给出一张 $n$ 个点,$m$ 条边的有向图,带正权边。
\item 求 $s$ 到 $t$ 的第 $k$ 短路径。
\end{itemize}
要求复杂度 $O(n \log n)$(假设 $n, k, m$ 同阶)。
\end{frame}
\begin{frame}{如何刻画这个问题}
\begin{itemize}[<+->]
\item 这背后实际上是一类贪心问题,即每次精确的在现有的解空间中找到最优解拓展并不重不漏。
\item 放在这个题就是每次找没被找过的路径中最短那个,不重不漏就好了。
\end{itemize}
\end{frame}
\begin{frame}{解法}
\begin{itemize}[<+->]
\item 建立到终点的最短路树,记 $dis_i$ 表示 $i$ 到终点的距离。
\item 用一个非树边集合来表示当前的决策。
\item 对于一组 $e_1, e_2, \dots, e_m$ 的意思是说,从起点开始,先到 $e_1$ 的起点,走过 $e_1$ 后沿着树边走到 $e_2$ 的起点,依次类推,最后走到终点。
\item 这个解的后继要么是修改 $e_m$ 的终点,要么是加入一条边 $e_{m+1}$,按这个意思解法自然是不重不漏的。
\item 定义 $e = (u, v, w)$ 的权值为 $\Delta e = dis_v + w - dis_u$。
\item 一个非树边集合的路径长度可以表示为 $dis_s + \sum \Delta e$。
\end{itemize}
\end{frame}
\begin{frame}{解法}
\begin{itemize}[<+->]
\item 看来我们只要能快速维护好一个非树边集合的后继就好了,而且非树边集合只需要记录最后一条边。
\item 对于修改终点,那么起点是固定的,直接维护后继。
\item 对于加入一条边,那么能加的边有什么要求呢?
\item 只能是起点是 $v_m$ 的祖先的边。
\item 那么我们要找到以一个点及其祖先为起点的边中的最小值,使用可持久化可并堆即可。
\end{itemize}
\end{frame}
\begin{frame}{可并堆的可持久化}
\begin{itemize}[<+->]
\item 和其他的可持久化数据结构一样,每次访问到的点新开一个拷贝即可。
\end{itemize}
\end{frame}
\section{bitset}
\begin{frame}{bitset}
我们直接看 \href{https://oi-wiki.org/lang/csl/bitset/}{OI-wiki}。
\end{frame}
\begin{frame}{几个注意点}
\begin{itemize}
\item 动态开 bitset 或者其他操作可以直接手写,本质上就是压位和位运算。
\item bitset 使用 cout 输出是从高位到低位。
\end{itemize}
\end{frame}
\section{K-D Tree}
\begin{frame}
\frametitle{K-D Tree}
\begin{itemize}[<+->]
\item K-D Tree 是一种可以高效处理 $k$ 维空间信息的数据结构。
\item 在算法竞赛的题目中,一般有 $k=2$,下文默认解决的是二维平面的问题。
\item KDT 的每个节点都维护了一个矩形,表示子树中的点都在这个矩形范围内,其中用作分割的点也要记录下来,下文均采用这种 Nodey 的表示方式。
\end{itemize}
\end{frame}
\begin{frame}{构建}
\begin{itemize}[<+->]
\item $\texttt{build(l, r)}$ 表示对 $[l, r]$ 内的点构建一个 KDT。
\item 每次划分平面相当于是选择一个点,注意几个优化:
\item[] 1. 轮流选择 $k$ 个维度(或选择方差最大的维度)。
\item[] 2. 每次选择按这个维度排序的中位数。
\item 实现利用 $\texttt{nth\_element}$ 即可做到复杂度 $\mathcal{O}(n\log n)$,同时也满足了树高为 $\mathcal{O}(\log n)$ 级别。
\end{itemize}
\end{frame}
\begin{frame}{插入和删除}
\begin{itemize}[<+->]
\item 类似二叉查找树地向下搜索,找到点为空即可插入。
\item 删除可以采用懒惰删除。
\item 此处如果暴力操作会导致复杂度退化,需要对 KDT 进行重构,常见方法有:
\item[] 1. 替罪羊式,即 $\mathrm{size}(u)\alpha < \max(\mathrm{size}(\text{lson}), \mathrm{size}(\text{rson}))$ 的时候暴力重构子树,这个 $\alpha$ 实际通常取 0.75 左右,\textbf{但是这种方法只能保证树高是} $O(\log n)$,在后面的复杂度分析需要严格 $\log n + O(1)$。
\item[] 2. 根号重构式,即每 $B$ 次操作进行一次重构,删除的话就是子树内删除点数大于 $B$ 的时候重构子树。
\item[] 3. 二进制分组,维护多个 KDT,每次新建一个大小为 $1$ 的 KDT,不断合并大小相同的树,查询则分别查询。
\end{itemize}
\end{frame}
\begin{frame}{矩形查询}
\begin{itemize}[<+->]
\item 若当前矩形和查询矩形无交或查询矩形包含当前矩形,则统计信息并返回。
\item 否则向左右递归查询。
\item 如果有矩形修改之类的也是一样,打上标记即可,可参考『2024.11.8 NOIP 模拟赛 T4』。
\item 时间复杂度是 $\mathcal{O}(\sqrt{n})$ 的,下面进行分析。
\end{itemize}
\end{frame}
\begin{frame}{矩形查询复杂度分析}
\begin{itemize}
\item 设查询矩形为 $R$,树中的矩形要么和 $R$ 无交,要么被 $R$ 完全包含,要么和 $R$ 有交。
\item 显然复杂度只和遍历到的第三类点的个数有关,分为两种情况:完全包含 $R$、互不包含。
\item 根据树高的性质,第一种情况只有 $O(\log n)$ 个点,下面主要分析第二种情况。
\item 此处放缩一下,考虑 $R$ 的每条边经过的矩形个数。
\item 一个点到其四个孙子,经过了横向和纵向的划分,也就是这条边至多经过两个孙子节点。
\item $T(n) = 2T\left (\dfrac{n}{4} \right ) + O(1)$,得到时间复杂度 $O(\sqrt{n})$。
\item 拓展到 $k$ 维的时间复杂度是 $O(n^{1-\frac{1}{k}})$。
\end{itemize}
\end{frame}
\begin{frame}{应用}
大概分成矩形查询和邻域查询,以及一些数据结构能干的事情。
\end{frame}
\begin{frame}{\href{https://www.luogu.com.cn/problem/P4848}{P4848 崂山白花蛇草水}}
\begin{itemize}
\item 每次插入点 $(x, y, w)$,查询矩形内 $w$ 的 $k$ 大值。
\item 要求复杂度 $O(n \sqrt{n}\log V)$。
\end{itemize}
\end{frame}
\begin{frame}{\href{https://www.luogu.com.cn/problem/P4848}{P4848 崂山白花蛇草水}}
\begin{itemize}[<+->]
\item 采用树套 KDT 的方式,外层套上你喜欢的以值域为下标的数据结构,这里以线段树为例。
\item 插入点,即在线段树上 $\log n$ 个节点所表示的 KDT 上插入一个点。
\item 查询就是线段树上二分,KDT 支持查询矩形内点的个数即可。
\item 时间复杂度:$\mathcal{O}(n\sqrt{n}\log V)$。
\end{itemize}
\end{frame}
\begin{frame}{\href{https://www.luogu.com.cn/problem/P4631}{P4631 [APIO2018] 选圆圈}}
\begin{itemize}
\item 给出 $n$ 个平面上的圆,每次从未删除的圆中选择半径最大的(如有同,选择编号最小的)圆 $i$,并删除和它相交或相切的圆 $j$,称圆 $j$ 被圆 $i$ 删除,求每个圆被哪个圆删除。
\item 数据范围:$n \le 3\times 10^5$。
\end{itemize}
\end{frame}
\begin{frame}{\href{https://www.luogu.com.cn/problem/P4631}{P4631 [APIO2018] 选圆圈}}
\begin{itemize}[<+->]
\item 两个圆相交的必要条件是其外接正方形相交。
\item 以此建立 KDT,每次查询时剪枝。
\item 注意,KDT 在解决此类问题是复杂度是错误的,可能退化到 $n^2$,但是实际情况往往比较优秀,不失为一种好方法。
\item 其余邻域查询问题也可类比,设计一个估价函数,来表示出尽可能精确的必要条件即可。
\end{itemize}
\end{frame}
\begin{frame}{\href{https://www.luogu.com.cn/problem/P5471}{P5471 [NOI2019] 弹跳}}
\begin{itemize}
\item 每个点有一个坐标 $(x_i, y_i)$,有若干次连边,每次形如一个点向一个矩形内的所有点连一条边权为 $w_i$ 的边。
\item 求从 $1$ 开始的单源最短路。
\item 数据范围:$1\le n\le 70000$,$1 \le m \le 150000$。
\end{itemize}
\end{frame}
\begin{frame}{\href{https://www.luogu.com.cn/problem/P5471}{P5471 [NOI2019] 弹跳}}
\begin{itemize}[<+->]
\item 显然的优化建图题,说明 KDT 其实也存在很多常见数据结构的衍生用法。
\item 一个点会向 $\mathcal{O}(\sqrt{n})$ 个 KDT 上的节点连边,KDT 上每个点连向其左右儿子和该点对应的点。
\item 不过你不能真的把边连出来,而是要用的时候去找一下就好了。
\end{itemize}
\end{frame}
\begin{frame}{\href{https://www.luogu.com.cn/problem/P9598}{P9598 [JOI Open 2018] 山体滑坡}}
\begin{itemize}
\item $n$ 个点,多次删边和加边,问断掉一端编号小于等于 $p_i$,另一端编号大于 $p_i$ 的边后形成的联通块数。
\item 注:本题做法繁多,如果是单纯做题,不建议思考 KDT 做法,但是在此处请先思考 KDT。
\item KDT 做法要求复杂度为 $O(n\sqrt{n}\log n)$。
\end{itemize}
\end{frame}
\begin{frame}{\href{https://www.luogu.com.cn/problem/P9598}{P9598 [JOI Open 2018] 山体滑坡}}
\begin{itemize}[<+->]
\item \sout{本题做法有操作分块,KDT 分治,LCT + 线段树分治等,不建议写 KDT。}
\item 下文会提到 LCT 做法,复杂度更优。
\item 发现 $[1, p_i]$ 和 $(p_i, n]$ 两个点集的答案是没影响的,分别求一下。
\item 那么现在是一个二维的问题,一个边贡献到的时间和 $p$ 都是一个区间,仿照线段树分治,写一个 KDT 分治即可。
\item 时间复杂度是 $O(n\sqrt{n}\log n)$ 的,居然跑得还可以。
\end{itemize}
\end{frame}
\section{Link Cut Tree}
\begin{frame}{Link Cut Tree}
\begin{itemize}[<+->]
\item Link Cut Tree 是一种数据结构,我们用它来解决动态树问题。
\item Link Cut Tree 简称 LCT,但它不叫动态树,动态树是指一类问题。
\item Splay Tree 是 LCT 的基础,但是 LCT 用的 Splay Tree 和普通的 Splay 在细节处不太一样。
\end{itemize}
\end{frame}
\begin{frame}{Link Cut Tree}
\begin{itemize}[<+->]
\item 常见的解决树链信息的方法是重链剖分,通过重链的性质来得到优秀的复杂度。
\item 但是遇到树的形态不固定的情况,常见为加边,删边,换根等,这种剖分方法就显得捉襟见肘了。
\item LCT 采用一种更为\textbf{自由}的剖分方式——虚实链剖分,对于每个点,选择至多一个实儿子,其余为虚儿子,相应的可以定义虚边,实边,实链等,并采用 Splay Tree 来维护这些实链。
\end{itemize}
\end{frame}
\begin{frame}{辅助树}
\begin{itemize}[<+->]
\item LCT 在对树的形态以及修改时,不会修改原树,而是对于一棵“辅助树”修改。
\item 你可以认为,一棵 Splay 对应一条实链,它们构成了一个辅助树,而若干辅助树则构成了一个辅助森林。
\item 这里的 Splay 用原树中的\textbf{深度}作为排序关键字。
\end{itemize}
\end{frame}
\begin{frame}{辅助树性质}
\begin{enumerate}
\item 辅助树由多棵 Splay 组成,每棵 Splay 维护原树中的一条路径,且中序遍历这棵 Splay 得到的点序列,从前到后对应原树「从上到下」的一条路径。
\item 原树每个节点与辅助树的 Splay 节点一一对应。
\item 每个 Splay 的根节点连向其原树上的父亲,但是不作为父亲的儿子出现,也即父不认子。
\end{enumerate}
由于辅助树的以上性质,我们维护任何操作都不需要维护原树,辅助树可以在任何情况下拿出一个唯一的原树,我们只需要维护辅助树即可,图示可见 \href{https://oi-wiki.org/ds/lct}{OI-wiki}。
\end{frame}
\begin{frame}{Splay 部分}
\begin{enumerate}
\item $\texttt{Get(x)}$ 获取 $x$ 是父亲的哪个儿子。
\item $\texttt{Splay(x)}$ 通过和 Rotate 操作联动实现把 $x$ 旋转到 当前 Splay 的根。
\item $\texttt{Rotate(x)}$ 将 $x$ 向上旋转一层的操作。
\end{enumerate}
\end{frame}
\begin{frame}{LCT 部分}
\begin{enumerate}
\item $\texttt{access(x)}$ 打通一条包含且仅包含根到 $x$ 的一条实链,为 LCT 的核心操作。
\item $\texttt{notroot(x)}$ 判断 $x$ 是否\textbf{不是}所在树的根。
\item $\texttt{makeroot(x)}$ 使 $x$ 点成为其所在树的根。
\item $\texttt{link(x, y)}$ 在 $x$, $y$ 两点间连一条边。
\item $\texttt{cut(x, y)}$ 把 $x$, $y$ 两点间边删掉。
\item $\texttt{findroot(x)}$ 找到 $x$ 所在树的根节点编号。
\item $\texttt{split(x, y)}$ 提取出 $x$, $y$ 间的路径,方便做区间操作。
\end{enumerate}
\end{frame}
\begin{frame}{access 操作}
\begin{itemize}[<+->]
\item 从 $x$ 开始,自下而上的生成所要的实链。
\item 将这个实链的 Splay 根节点记为 $y$。
\item 每次操作,先将 $x$ 转到所在 Splay 的根,然后将其右儿子更改为 $y$。
\item 我们按照深度作为关键字,那么深度小于等于 $x$ 的点显然就是 $x$ 不断跳父亲,大于 $x$ 的点我们已经得到了 $y$ 这个树,那么直接改掉就好了,此处并没有修改原来 $x$ 的右儿子,也就是符合了上面提到的父不认子,相当于把那条边变成了虚边。
\item 然后将 $y \leftarrow x$,$x \leftarrow \mathrm{father}(x)$,如此到根即可。
\end{itemize}
\end{frame}
\begin{frame}{notroot 操作}
要用父亲是否存在这个儿子来判断。
\end{frame}
\begin{frame}{makeroot 操作}
此处需要引入 Splay 的 reverse 操作,即像文艺平衡树一样下放标记。
\begin{itemize}[<+->]
\item 首先我们打通一条根到 $x$ 的路径,然后将其 Splay 到根。
\item 此时发现其一定没有右儿子(深度为关键字),而根一定没有左儿子,那么直接 reverse 这个 Splay 就好了。
\end{itemize}
\end{frame}
\begin{frame}{findroot 操作}
\begin{itemize}
\item 类似 makeroot,先 access(x) + splay(x)。
\item 根一定是深度最小的,不断跳左儿子即可。
\end{itemize}
\textbf{注意:返回之前要 splay(root),否则会导致复杂度错误。}
\end{frame}
\begin{frame}{link 操作}
\begin{itemize}[<+->]
\item 假设 $x,y$ 不连通,连接他们的边为虚边。
\item 直接 makeroot(x),然后将 father(x) = y 即可,父不认子。
\end{itemize}
\end{frame}
\begin{frame}{split 操作}
\begin{itemize}[<+->]
\item 用于取出 $(x,y)$ 这个路径,做法是 makeroot(x) + access(y) + splay(y)。
\item 此时我们得到的一个以 $y$ 为根的,大小为路径上点数的 Splay。
\item 在 cut 操作时 $y$ 的左儿子为 $x$。
\end{itemize}
\end{frame}
\begin{frame}{cut 操作}
\begin{itemize}[<+->]
\item 同样假设存在边 $(x, y)$。
\item 那么首先 split(x, y),取出这个边,然后直接修改父子信息即可,注意要同时修改父亲和自己,否则可能只是实边变成虚边而已。
\end{itemize}
\end{frame}
\begin{frame}{\href{https://www.luogu.com.cn/problem/P3690}{P3690 【模板】动态树(LCT)}}
给定 $n$ 个点以及每个点的权值 $a_i$,要你处理接下来的 $m$ 个操作。
操作有四种:
\begin{itemize}
\item 询问从 $x$ 到 $y$ 的路径上的点的权值的 $\text{xor}$ 和。保证 $x$ 到 $y$ 是联通的。
\item 连接 $x$ 到 $y$,若 $x$ 到 $y$ 已经联通则无需连接。
\item 删除边 $(x,y)$,不保证边 $(x,y)$ 存在。
\item 将点 $x$ 上的权值变成 $y$。
\end{itemize}
数据范围:$1 \leq n \leq 10^5$,$1 \leq m \leq 3 \times 10^5$,$1 \leq a_i \leq 10^9$。
\end{frame}
\begin{frame}{\href{https://www.luogu.com.cn/problem/P3690}{P3690 【模板】动态树(LCT)}}
此处应有一段代码。
\end{frame}
\begin{frame}{\href{https://codeforces.com/problemset/problem/1109/F}{CF1109F}}
\begin{itemize}
\item 给一个 $n\times m$ 的网格图,每个格子上有个权值 $f_{ij}$,保证 $f_{ij}$ 构成一个 $1\sim nm$ 的排列。
\item 问有多少区间 $l, r$ 满足 $1 \le l \le r \le nm$ 且权值在 $l, r$ 内的格子构成的连通块是一棵树。
\item 数据范围:$n, m \le 1000, nm \le 2\times 10^5$。
\end{itemize}
\end{frame}
\begin{frame}{\href{https://codeforces.com/problemset/problem/1109/F}{CF1109F}}
\begin{itemize}[<+->]
\item 首先,可以双指针求出一个 $l$ 对应的极大的 $r$,使得 $[l, i],(i \in [l, r])$ 不含环。
\item 这一步可以利用 LCT 求解。
\item 但是要判断森林就没有这么好的性质了,因为随着 $i$ 变化,图的形态可能在树和森林间变化。
\item 于是运用树的点数减去边数等于 $1$ 的性质,同时维护一个线段树来计算即可,查询 $[l, r]$ 内的 $1$ 的个数可以变成查询最小值的个数。
\end{itemize}
\end{frame}
\begin{frame}{利用 LCT 的均摊}
\begin{itemize}[<+->]
\item 有时候题目中的操作可能和 LCT 不谋而合,那么可以在 LCT 的基础上魔改一下。
\item 这样的话可以借助 LCT 的势能分析来做到比较优秀的复杂度。
\end{itemize}
\end{frame}
\begin{frame}{先来证明一下 LCT 的复杂度}
前面 Splay 部分省略了。
\begin{itemize}
\item LCT 的核心操作是 access,那么只需要证明 access 操作的复杂度。
\item 重虚边:从节点 v 到其父节点的虚边,其中 $size(v) > \frac{1}{2} size(parent(v))$。
\item 轻虚边:从节点 v 到其父节点的虚边,其中 $size(v) \leq \frac{1}{2} size(parent(v))$。
\item 定义势能是重虚边的条数。
\item 一次 access 至多走 $O(\log n)$ 条轻虚边,至多带来 $O(\log n)$ 条重虚边,而每次访问重虚边会以 $O(1)$ 的代价减少 $1$ 的势能,可以忽略。
\item 最终把初始势能加上势能变化量,得到复杂度为 $O(m\log n)$,$m$ 为操作次数。
\end{itemize}
\end{frame}
\begin{frame}{\href{https://www.luogu.com.cn/problem/P3703}{P3703 [SDOI2017] 树点涂色}}
\begin{itemize}
\item 给出一棵 $n$ 个点的有根树,其中 $1$ 号点是根节点,点 $i$ 的初始颜色为 $i$。
\item 定义一条路径的权值是:这条路径上的点(包括起点和终点)共有多少种不同的颜色。
\item 你需要维护如下几种操作,对于第 $i$ 次操作:
\end{itemize}
\begin{enumerate}
\item 把点 $x$ 到根节点的路径上所有的点染上颜色 $i + n$。
\item 求 $x$ 到 $y$ 的路径的权值。
\item 在以 $x$ 为根的子树中选择一个点,使得这个点到根节点的路径权值最大,求最大权值。
\end{enumerate}
数据范围:$1 \le n, m \le 10^5$。
\end{frame}
\begin{frame}{\href{https://www.luogu.com.cn/problem/P3703}{P3703 [SDOI2017] 树点涂色}}
\begin{itemize}
\item 题目给出的奇怪染色竟然就是 LCT 中的 access 操作!
\item 这样的话一个点的答案就是到根的路径上虚边的个数。
\item 对于路径查询,直接差分,对于子树查询,额外维护一个线段树即可。
\end{itemize}
时间复杂度是 $\mathcal{O}(n \log^2 n)$ 的。
\end{frame}
\begin{frame}{\href{http://codeforces.com/problemset/problem/1344/E}{CF1344E}}
\begin{itemize}
\item 给定 $n$ 个点的一棵树,以 $1$ 为根,边有边权。
\item 有 $m$ 辆从 $1$ 开始到 $s_i$ 的火车,第 $i$ 辆火车在 $t_i$ 时刻时从根出发,向着目标 $s_i$ 前进,当火车在 $x$ 时刻到达一个点 $u$ 时,假设下一个路径上的点是 $v$,两点间边权是 $d$,则在 $x+d$ 时刻到达 $v$,火车在到达目标点后停止。
\item 由于每个点可能可以到达多个点,每个点有且仅有一个当前时刻可以到的儿子,每秒钟只能切换某一个点可以到的儿子,切换比火车开动先进行,若一辆火车走向了非目标方向的点,则立刻自爆。每个点初始能到的儿子是确定的。
\item 求出能否不发生自爆,如果不能,第一次自爆最晚什么时候发生?在此基础上,至少切换多少次一个点可以到的儿子。
\end{itemize}
数据范围:$n, m \le 10^5$。
\end{frame}
\begin{frame}{\href{http://codeforces.com/problemset/problem/1344/E}{CF1344E}}
\begin{itemize}[<+->]
\item 按照时间操作,当遇到一个点需要修改的时候,设上一次经过该点的时间为 $x$,当前时间为 $t$,那么这个点就需要在 $(x, t]$ 内被操作一次。
\item 把这样的区间按照左端点排序,每次操作能操作中的右端点最小的区间,就是最优策略。
\item 那么这样的区间只会在 LCT access 虚实边切换的时候出现,只有 $O(m\log n)$ 个,直接来就好了。
\end{itemize}
\end{frame}
\begin{frame}{LCT 维护树 / 图结构}
LCT 可以支持动态的维护树(如最小生成树),和图结构。
在维护的时候,往往只能支持加边,不能删边,否则会变得不可名状。
\end{frame}
\begin{frame}{\href{https://www.luogu.com.cn/problem/P2387}{[NOI2014] 魔法森林}}
\begin{itemize}
\item 给出 $n$ 个点,$m$ 条边,每条边可以表示为一个四元组 $(u, v, a, b)$,其中 $u, v$ 是边,$a, b$ 是权值,要求一条从 $1$ 到 $n$ 的路径,使得经过的边 $\max a + \max b$ 最小。
\item 数据范围:$n \le 5\times 10^4$,$m \le 10^5$。
\end{itemize}
\end{frame}
\begin{frame}{\href{https://www.luogu.com.cn/problem/P2387}{[NOI2014] 魔法森林}}
\begin{itemize}[<+->]
\item LCT 维护最小生成树的板子,不过只能加边,不能删边。
\item 有两个权值,不妨按照 $a$ 从小到大排序,那么对 $a$ 扫描线,维护当前可用的边中 $1$ 到 $n$ 的边权最大值最小即可,那么这一定是最小生成树上的路径。
\item 这里注意 LCT 没有像重剖一样边化点的方法,需要给每个边开一个虚点,link 的时候若两者已经在同一个连通块内,那么必然有环形成,cut 掉权值最大的边就可以。
\item 这种东西可能会结合线段树分治使用。
\end{itemize}
\end{frame}
\begin{frame}{\href{https://www.luogu.com.cn/problem/P9598}{P9598 [JOI Open 2018] 山体滑坡}}
现在我们给出 $O(n\log^2 n)$ 的 LCT 解法。
\begin{itemize}[<+->]
\item 首先线段树分治,然后记边 $(u, v)$ 的边权为 $\max(u, v)$,用 LCT 维护最小生成树。
\item 对于一个询问 $[1,P_i]$ 查询 $\max(u, v) \le P_i$ 的边的个数,用一个树状数组辅助统计即可。
\item 对于 $(P_i, n]$ 的询问是对称的。
\end{itemize}
\end{frame}
\begin{frame}{\href{https://www.luogu.com.cn/problem/P5489}{P5489 EntropyIncreaser 与 动态图}}
\begin{itemize}
\item 有一个 $n$ 个点的图,初始没有边。
\item 有 $q$ 个操作,分为 $3$ 种,具体如下:
\item 1 u v 表示在 $u,v$ 之间连一条无向边。
\item 2 u v 表示求 $u,v$ 间的割边数量。
\item 3 u v 表示求 $u,v$ 间的割点数量。
\end{itemize}
强制在线,要求复杂度 $O(n\log n)$。
\end{frame}
\begin{frame}{LCT 维护割边}
\begin{itemize}[<+->]
\item 首先维护原图的一个生成树,对于一次 link(u, v) 来说,若 u, v 未连通,则直接相连(建立虚点)。
\item 否则将这一条链上的权值赋值为 $0$ 表示其再也不可能作为割边出现了,打上标记即可。
\end{itemize}
\end{frame}
\begin{frame}{LCT 维护割点}
\begin{itemize}[<+->]
\item 静态维护割点的方式是圆方树,不妨用 LCT 动态的维护圆方树。
\item 同样考虑 link(u, v) 时 u, v 已经连通的情况,那么直接暴力将环上的边断开,连向一个新的方点即可,此处的结构可能有点怪,会出现方点连向方点的情况,但是不影响答案和复杂度。
\item 复杂度证明考虑每次是以 $O(L \log n)$ 的代价将长度为 $L$ 的环删掉,那么就是 $O(n \log n)$ 的。
\end{itemize}
\end{frame}
\begin{frame}{LCT 维护子树信息}
LCT 通过对虚子树和子树信息的维护,可以实现一定程度上的子树信息维护。
下面通过一道例题来介绍。
\end{frame}
\begin{frame}{\href{https://www.luogu.com.cn/problem/SP16549}{Query on a tree VI}}
\begin{itemize}
\item 给出一个 $n$ 个点的树,每个点为黑色或白色,初始均为黑色,要求支持 $m$ 次操作。
\item 每次询问有多少节点到 $u$ 的路径上点颜色相同,或者翻转一个点的颜色。
\end{itemize}
数据范围:$1 \le n, m \le 10^5$。
\end{frame}
\begin{frame}{解法}
\begin{itemize}
\item 常见的想法是把同色的连通块合并起来,查询的是连通块大小,但是这样的复杂度是 $O(d \log n)$ 的,$d$ 是度数。
\item 这里用到一个技巧,以白色为例,每个白色点向父节点连边,黑色点则不连,那么最后会形成一个森林,真实的连通块是这些森林去掉每个树的根后得到的新森林。
\item 然后在每个点维护子树大小和虚子树大小,在 link 和 access 的时候修改即可。
\end{itemize}
加以扩展即可解决 \href{https://www.luogu.com.cn/problem/CF1172E}{CF1172E / P5526}。
\end{frame}
\begin{frame}{LCT 维护一些序列问题}
我做到的这些题基本上是在 P3203 弹飞绵羊 的基础上扩展。
所以也只能介绍这一点。
\end{frame}
\begin{frame}{\href{https://www.luogu.com.cn/problem/P3203}{P3203 [HNOI2010] 弹飞绵羊}}
\begin{itemize}
\item 游戏一开始,Lostmonkey 在地上沿着一条直线摆上 $n$ 个装置,每个装置设定初始弹力系数 $k_i$,当绵羊达到第 $i$ 个装置时,它会往后弹 $k_i$ 步,达到第 $i+k_i$ 个装置,若不存在第 $i+k_i$ 个装置,则绵羊被弹飞。
\item 绵羊想知道当它从第 $i$ 个装置起步时,被弹几次后会被弹飞。
\item 为了使得游戏更有趣,Lostmonkey 可以修改某个弹力装置的弹力系数,任何时候弹力系数均为正整数。
\end{itemize}
\end{frame}
\begin{frame}{\href{https://www.luogu.com.cn/problem/P3203}{P3203 [HNOI2010] 弹飞绵羊}}
\begin{itemize}[<+->]
\item 将 $i$ 和 $i+k_i$ 连边,每次加边删边,查询深度。
\item 这种根节点确定的情况,LCT 的 link 函数对应的可以写的简单一点,不必要 makeroot,实测常数变小很多。
\item 相似题可以尝试 CF1039E。
\end{itemize}
\end{frame}
\begin{frame}
\begin{center}
\huge $\texttt{Good Luck and Have Fun.}$
\end{center}
\end{frame}
\end{document}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】