圆方树学习笔记 —— 一种关于点双连通分量的思考方式

引言

本文原名为《圆方树学习笔记 & 最短路题解》,原始版本可见文末。

本文旨在系统梳理 圆方树(Block forest) 及其思想在图论问题中的应用,尤其是在信息学奥林匹克竞赛(OI)中的实际价值。

我们将从一种特殊的图结构------仙人掌图(Cactus Graph)出发,逐步扩展至一般无向图,分析如何通过构造圆方树,来将复杂图上问题转化为树上问题,并借助经典算法(如树形 DP、树链剖分、DDP、虚树等)解决。

本文对涉及的问题进行了系统归类,并配有例题与代码,构建出一套完整的知识体系,帮助读者深入理解核心思想并掌握其在实际题目中的变形与拓展。当然,并非所有问题都需要显式构造圆方树,部分情况下我们仅借助其结构思想进行分析和维护。

在此基础上,本文还创新性地提出了「圆树」这一辅助结构的概念,以优化特定类型问题的建模与求解过程。

希望本笔记能为正在学习相关内容的读者提供一套完整、实用的参考资料。

全文约二十万字符,除代码外有五万字,因此建议阅读的同时做题来巩固知识。

问题引入

在处理无向图上的复杂问题时,我们常借助图的结构性质进行化简,在简化后的图上使用算法解决问题。比如,使用 Tarjan 算法对图进行缩点:

  • 强连通分量 缩点后可构成一个有向无环图(DAG)
  • 边双连通分量 缩点后可转化为一棵
  • 那么,点双连通分量缩点之后,会变成什么结构?

答案是:圆方树(Block forest)

圆方树正是对图进行点双连通分量 缩点后的结构表示。它将原图中复杂的连通关系映射为树上的结构,从而使许多原本难以处理的问题得以简化为树上问题,显著降低分析与求解的难度。

圆方树的定义

在一张无向图中,对每个点双连通分量建立一个对应的超级节点,并将该分量中所有原图中的点与该超级节点相连,随后删除原图中的所有边。在构建完成后:

  • 将这些新增的超级节点称为 「方点」
  • 将原图中的普通节点称为 「圆点」

这样得到的一棵圆点与方点交错连接的树结构,即为该图的圆方树。

在圆方树上,一个方点的父亲一定是圆点,我们称这个圆点为它的 「父亲圆点」 ,类似定义 「孩子圆点」 ,对于一个圆点类似定义 「父亲方点」「孩子方点」

以下展示的是一张无向图及其对应的圆方树结构。例如,原图中点双 \(\{2,3,4,5\}\) 对应超级节点 \(15\),在圆方树中,\(15\) 和 \(2,3,4,5\) 都连有边。


本文中,若无特殊说明,认为「两点被一条边连接」这种图结构 是一个点双 ,如上图中 \(\{8,9\}\)。并为这种点双同样创建一个方点。即在圆方树上不会出现两个圆点直接相连的情况,即不出现圆圆边

圆方树的简单性质

我们首先需要对圆方树的基本特点有一定了解。

  1. 圆方树节点数小于 \(2n\),其中 \(n\) 为原图点数。

    一个点双引入一个方点,一张图的点双最多只有 \(n-1\) 个,这个上界在图退化为树的情况下达到。所以代码中不要忘记给相关数组开两倍大小。

  2. 圆方树上一种类型的点只会和另一种类型的点连边。

    方点只会和圆点连边,圆点只会和方点连边。由定义不难得到这一点。

  3. 圆方树上任意一条树链都是「圆方交错」的。

    结合上一条性质,不难发现这个性质。

圆方树构建方式

一种简洁而典型的圆方树建树方法是:在运行 Tarjan 算法求点双连通分量的同时构造圆方树 。并且可以直接把圆方树建成一棵外向树 ,便于后续树上遍历。我们直接让方点从 \(n+1\) 开始编号,圆点保持原图上的编号。

cpp 复制代码
void tarjan(int u, int fr) {
    dfn[u] = low[u] = ++timer;
    stack[++top] = u;
    for (int i = G.head[u]; i; i = G.edge[i].nxt) {
        if (i == (fr ^ 1)) continue;
        int v = G.edge[i].v;
        if (!dfn[v]) {
            tarjan(v, i);
            low[u] = min(low[u], low[v]);
            if (low[v] >= dfn[u]) {
                ++dcc_cnt;
                while (true) {
                    int x = stack[top--];
                    T.add(dcc_cnt + n, x); // 连边:方点 -> 圆点
                    if (x == v) break;
                }
                T.add(u, dcc_cnt + n);     // 连边:圆点 -> 方点
            }
        } else {
            low[u] = min(low[u], dfn[v]);
        }
    }
}

这是最基础的构建框架,在后文具体问题中,我们将在此代码的基础上进行拓展,比如,为圆方树赋上边权,维护当前点双的子图。

圆方树维护点对间所有简单路径信息

关于原图中两点 \(u,v\) 之间所有的简单路径,我们可以将其映射到圆方树上 \(u,v\) 之间的树链,并且借助圆方树的树形结构,来维护这些所有简单路径的信息和。

点对间所有简单路径信息

对于圆方树的映射方式,等到我们分析完信息维护的方式,就呼之欲出了。

我们先来看几种典型的信息:\(u,v\) 间最长简单路径长度、\(u,v\) 间简单路径条数、\(u,v\) 间所有简单路径长度之和、所有能被某一条 \(u,v\) 间简单路径经过的点权和。

我们可以大致分为如下两类。

类型一:满足分配率的边权、点权相关信息

让我们尝试用形式化的语言来形容,进行适当抽象。

设 \(p_i(u,v)\) 表示某一条 \(u\rightarrow v\) 的简单路径。对于这一条简单路径的信息,如果可以看做其中每一条边 \(e\in p_i(u,v)\) 的信息 \(\operatorname{info}(e)\) 按照一种特定的合并方式 \(\odot\) 合并后的结果,我们就称这种信息为 「边权相关信息」 ,即路径 \(p_i(u,v)\) 的信息为 \(\operatorname{info}(p_i(u,v)) = \bigodot\limits_{e\in p_i(u,v)}\operatorname{info}(e)\)。类似定义点权相关信息,处理这种信息,可以把所有点的点权按照一种方式变成边权,除了某一点需要特殊考虑,剩下维护的就是边权相关信息。因此为了简化讨论,仅考虑边权相关信息。例如:「路径长」是边权相关信息,对于一条边 \(e\),\(\operatorname{info}(e)\) 就是 \(e\) 的长度 \(\operatorname{len}(e)\),\(\odot\) 即为 \(+\),\(\operatorname{info}(p_i(u,v)) = \bigodot\limits_{e\in p_i(u,v)}\operatorname{info}(e) = \sum\limits_{e\in p_i(u,v)}\operatorname{len}(e)\)。

所求的是对于 \(u,v\) 间所有简单路径的信息,按照 \(\oplus\) 合并后的结果,即 \(\operatorname{info}(u,v)=\bigoplus_i\operatorname{info}(p_i(u,v))\)。\(\oplus\) 显然需要满足交换律,否则答案就不唯一了。例如,对于「最长简单路径长度」,就是「路径长」通过 \(\max\) 合并后的信息,\(\odot=+\),\(\oplus=\max\),\(\operatorname{info}(u,v)=\max_i\operatorname{info}(p_i(u,v))=\max_i\sum\limits_{e\in p_i(u,v)}\operatorname{len}(e)\)。

明确需要求解什么后,考虑如何维护。

考虑什么情况下可能可以合并 \(\operatorname{info}(u,v)\) 与 \(\operatorname{info}(v,w)\) 得到 \(\operatorname{info}(u,w)\)。发现当且仅当 \(v\) 是割点的时候,合并信息,才可能得到不重不漏的的结果。因为 \(v\) 作为割点,\(u\rightarrow w\) 的每一条简单路径总是可以拆分成 \(u\rightarrow v\) 和 \(v\rightarrow w\) 两条仅在 \(v\) 处相交的简单路径。这是类似于卷积的,即对于所有 \(i,j\),\(p_i(u,v)\sim p_j(v,w)\) 拼出了一条 \(p_k(u,w)\),并且这种 \(i,j\mapsto k\) 的映射是唯一的。那么 \(\operatorname{info}(u,w)=\bigoplus_k\operatorname{info}(p_k(u,w))=\bigoplus_{i,j}\operatorname{info}(p_i(u,v)\sim p_j(v,w))\)。

在树上,因为 \(u\rightarrow v\) 和 \(v\rightarrow w\) 的简单路径是唯一的,我们有 \(\operatorname{info}(u,w)=\operatorname{info}(u,v)\oplus\operatorname{info}(v,w)\),这是简单的。但是此时 \(u\rightarrow v\) 和 \(v\rightarrow w\) 的简单路径不是唯一的,就不能直接通过 \(\oplus\) 合并了。

有时候我们能够找到一种运算 \(\otimes\),使得我们可以打开这个 \(\bigoplus\),直接合并两个子问题的信息。它需要满足 \(\bigoplus_{i,j}\operatorname{info}(p_i(u,v)\sim p_j(v,w))=\Big(\bigoplus_{i}\operatorname{info}(p_i(u,v))\Big)\otimes\Big(\bigoplus_{j}\operatorname{info}(p_j(v,w))\Big)\),也就是通过 \(\otimes\) 类似树上直接合并两个子问题的信息。把 \(\operatorname{info}\) 用 \(\odot\) 表示,也就是 \(\bigoplus_{i,j}\bigodot_{e\in p_i(u,v)\sim p_j(v,w)}\operatorname{info}(e)=\Big(\bigoplus_{i}\bigodot_{e\in p_i(u,v)}\operatorname{info}(e)\Big)\otimes\Big(\bigoplus_{j}\bigodot_{e\in p_j(v,w)}\operatorname{info}(e)\Big)\)。为了方便观察,不妨用 \(S_i\) 表示 \(p_i(u,v)\),\(T_i\) 表示 \(p_i(v,w)\),等式变为:\(\bigoplus_{i,j}\bigodot_{e\in S_i\cup T_j}\operatorname{info}(e)=\Big(\bigoplus_{i}\bigodot_{e\in S_i}\operatorname{info}(e)\Big)\otimes\Big(\bigoplus_{j}\bigodot_{e\in T_j}\operatorname{info}(e)\Big)\)。用 \(f_i\) 表示 \(\bigodot_{e\in S_i}\operatorname{info}(e)\),\(g_j\) 表示 \(\bigoplus_{j}\bigodot_{e\in T_j}\operatorname{info}(e)\),上等式变成:\(\bigoplus_{i,j}\Big(f_i\odot g_j\Big)=\Big(\bigoplus_{i}f_i\Big)\otimes\Big(\bigoplus_{j}g_j\Big)\)。这个形式很像分配律,进一步,我们可以断言,当且仅当 \(\odot\) 对 \(\oplus\) 满足 分配律 时,\(\otimes\) 存在,且 \(\otimes\) 就是 \(\odot\) 本身,此时,\(\odot\) 和 \(\oplus\) 构成一个 半环

例如,当 \(\odot=+\),\(\oplus=\max\) 时,\(+\) 对 \(\max\) 满足分配律,此时 \(\otimes\) 存在,且 \(\otimes=\odot=+\)。实际图论意义也很好理解,\(u\rightarrow w\) 所有简单路径中的最长的长度,即为 \(u\rightarrow v\) 和 \(v\rightarrow w\) 分别找到两条最长的拼起来。当 \(\odot=\times\),\(\oplus=+\) 时,\(\times\) 对 \(+\) 满足分配律,此时 \(\otimes\) 存在,且 \(\otimes=\odot=\times\)。当 \(\odot=\oplus=+\) 时,\(+\) 对 \(+\) 不满足分配律,我们找不到 \(\otimes\) 这个运算吗?若仅记录「边权相关信息」,我们确实找不到这样一个运算。但是在后文,我们通过扩展信息的方式,找到了维护这种信息的方式。

有了合并信息的方式,我们要考虑 「原子信息」 ,即小到不能再小的信息,此时就是 \(u,w\) 处在同一个点双的时候,我们不能找到一条连接 \(u,w\) 的简单路径通过某一个割点 \(v\),于是不能通过合并信息的方式得到 \(u\rightarrow w\) 的信息。不妨对于处于同一点双中的 \(u,w\),用 \(\operatorname{info}(u,w)\) 表示 \(u,w\) 之间的原子信息。原子信息通常是能够方便处理出来的。

我们已经学会了如何维护 \(\odot\) 对 \(\oplus\) 满足分配律的「边权相关信息」、「点权相关信息」。

类型二:非权值相关信息或 \(\odot=\oplus\) 的权值相关信息

此时只存在 \(\otimes\) 这个合并运算。预处理出原子信息同样通常是简单的。例如对于「简单路径条数」,\(\otimes\) 就是 \(\times\)。

上文中遗留一个问题,当 \(\odot=\oplus=+\) 时,由于 \(+\) 对其自身不具有分配律时,不能直接通过「边权相关信息」的方式维护。考虑扩展信息,设 \(\operatorname{info}(u,v)=(s,c)\),其中 \(s\) 表示所求的「所有简单路径长度之和」,\(c\) 表示 \(u\rightarrow v\) 简单路径条数。我们惊喜地发现,有 \((s_1,c_1)\otimes(s_2,c_2)=(s_1c_2+s_2c_1,c_1\cdot c_2)\),这是可以维护的!式子的意义是,对于每一条 \(p_i(u,v)\),由于有 \(c_2\) 条 \(p_j(v,w)\),所以 \(s_1\) 会贡献 \(c_2\) 次。

推广到到一般情况,当 \(\odot=\oplus\) 且对自身不具有分配律时,由于 \(\oplus\) 具有交换律,那么 \(\odot\) 也具有交换律,我们总是可以多记录一个 \(c\),有 \((s_1,c_1)\otimes(s_2,c_2)=\Big((\odot_{c_2}(s_1))\odot(\odot_{c_1}(s_2)),c_1\cdot c_2\Big)\),其中 \(\odot_{k}(x)=\underbrace{x\odot x\odot\cdots\odot x}_{k\text{ 个 }x}\)。

还有另一种可能需要维护的信息,即求能被任意一条 \(u\rightarrow v\) 简单路径经过的所有点 / 边的权值和,这个问题其实非常好处理,只需令 \(\otimes=\oplus\) 即可,这样就能统计到所有点 / 边的权值和,这和树上十分类似。

总结

以上几种信息,在处理完原子信息后,最后都是需要在一个个割点处,将信息通过 \(\otimes\) 合并起来,这正是圆方树可以解决的。

支持查询信息

让我们回到圆方树上,看看它对我们维护信息有什么帮助。

对于 \(u\rightarrow v\) 路径上的信息,可以将路径拆分成不断从一个点双走到另一个点双的过程,即 \(w_1 \stackrel{d_1}{\longrightarrow} w_2\stackrel{d_2}{\longrightarrow} w_3\stackrel{\cdots}{\longrightarrow} w_{m-1}\stackrel{d_{m-1}}{\longrightarrow}w_m\),其中 \(w_1=u, w_m=v\),对于 \(i=1,\ldots,m-1\),\(w_{i},w_{i+1}\) 分别是编号为 \(d_i\) 的点双中两个点。根据上述讨论,我们要求的就是 \(\bigotimes\limits_{i=1}^{m-1}\operatorname{info}(w_i,w_{i+1})\),而每个 \(\operatorname{info}(w_i,w_{i+1})\) 均为原子信息。

\(w_i\stackrel{d_i}{\longrightarrow}w_{i+1}\) 是走进一个点双,再从中走出来,圆方树上,我们为每个点双建立了一个方点,这不就相当于从 \(w_i\) 走到 \(d_i\) 对应的方点,再走到 \(w_{i+1}\) 吗?进一步地,这不就构成了圆方树上一条以 \(u,v\) 为端点的树链了吗?我们于是自然得出了圆方树和原图之间的对应关系:

  1. 圆方树上一条「圆方圆」的路径,对应原图点双中两点之间所有简单路径。

    比如上图中 \(\{3,15,5\}\) 这条圆方圆路径,对应原图中 \(\{2,3,4,5\}\) 这个点双中以 \(3,5\) 为端点的所有简单路径,即 \(\{3,2,5\},\{3,4,5\}\) 两条简单路径。

    这就是进入一个点双,再从这个点双中走出来。

  2. 圆方树上任意一条树链 \((u,v)\) 对应原无向图中 \(u,v\) 之间所有简单路径。

    比如上图中 \((3,8)\) 这条树链,对应了原图中 \(\{3,2,5,9,8\},\{3,2,5,6,9,8\},\{3,4,5,9,8\},\{3,4,5,6,9,8\}\) 这些简单路径。

    这种把每一条「圆方圆」的路径合并起来的过程,对应不断从一个点双走到另一个点双的过程。

圆方树很好地刻画了通过 \(\otimes\) 合并信息的过程!让我们继续分析。

对于一次查询 \((u,v)\),设 \(p=\operatorname{lca}(u,v)\),将树链拆分成 \(u\rightarrow p\rightarrow v\)。考虑 \(u\rightarrow p\) 一侧,另一侧类似。

考虑如下圆方树一部分:

当我们经过某一个圆点(蓝点)\(u\),跳向它爷爷(绿点)\(v\) 时,需要求的是 \(u\) 和 \(v\) 的原子信息 \(\operatorname{info}(u,v)\)。不考虑修改的情况下,每一次从 \(u\) 跳到 \(v\),原子信息都是相同的,因为它的爷爷 \(v\) 是唯一的。我们于是这可以预处理出这些原子信息,而这类原子信息的个数和非根圆点个数相同,是 \(\mathcal{O}(n)\) 的。我们将 \(u\) 和 \(v\) 的信息,放在 \(u\) 到它父亲方点(红点)的边权上,每个方点和它父亲圆点之间的边权设为单位元。那么我们考虑一对具有祖孙关系的节点,他们之间的信息,便是圆方树上他们之间边权按序合并的结果。

对于 \(u\rightarrow p\) 和 \(p\rightarrow v\) 两段,根据我们赋的边权,和我们之前学过的树上信息维护方式,这两段路径的信息已经可以求出。对于不满足交换律的信息,可能需要维护向上跳的信息和向下跳的信息。接下来,好像直接把这两个信息合并起来就是对的,其实不然。对于 \(p\) 是圆点的情况,这么做是正确的;但是对于 \(p\) 是方点的情况,设 \(u'\) 为 \(p\) 的孩子且是 \(u\) 的祖先,\(v'\) 同理,那么考虑直接合并信息的实际意义,是从 \(u\) 走到 \(u'\),进入了 \(p\) 对应的点双,走到了 \(\operatorname{fa}(p)\),再走进 \(p\) 对应的点双,走到 \(v'\),最终到 \(v\)。这显然是错误的,我们要求 \(u'\rightarrow v'\) 的原子信息 \(\operatorname{info}(u',v')\),而非 \(\operatorname{info}(u',\operatorname{fa}(p))\otimes\operatorname{info}(\operatorname{fa}(p),v')\)。解决方式很简单,只需要特殊查询一次 \(u'\rightarrow v'\) 的原子信息,和之前两个信息合并。

在实现上,我们需要支持询问点双内两点之间的原子信息,那么这是否意味着需要为每个点记录其所在所有点双内的信息?如果真的需要,我们可以为每一个元素开 \(m+1\) 个 vector,其中 \(m\) 个 vector<> info 用来记录信息,\(1\) 个 vector<int> id 用来存所有 \(u\) 点双的编号,意为 \(u\) 在编号为 id[u][i] 的点双内对应的信息为 info[u][i],对于查询 \(u\) 在编号为 \(x\) 的点双中的信息,先在 id[u] 中二分出 \(x\) 的下标,再拿这个下标去访问 info[u]。但是这是不必要的,这是因为我们的查询并不真的「任意」,而是只会查询一个圆点,在其父亲方点对应点双中的信息。而每个孩子圆点的父亲是唯一的,如果他被查询到,所用到的信息也是唯一的,那就不需要使用 vector 了。vector 的写法请参考下文仙人掌部分例五代码

至此,我们借助圆方树的特殊结构,结合树上信息维护方式,通过 \(\otimes\) 的合并方式,可以查询给定点对间所有简单路径的信息。

支持修改信息

如果有了修改呢?树上我们使用树剖来支持修改查询树链信息,那么接下来要做的就是把圆方树剖开,尝试在上面维护信息。

先来考虑一种特殊的信息,求 \(u\rightarrow v\) 能到达的所有点的点权的信息和。这时候同一个点双内 \(u\rightarrow v\) 的信息为点双中除去 \(v\) 的点权信息和。发现圆方树上的边权很有规律,对于一个方点,它连向所有孩子圆点的边权总是相同的,都是该点双中除去父亲圆点的点权信息和。我们把边权上放,给方点一个点权(注意和原点权加以区分),为原先我们给圆方树赋的边权,再把圆点的点权设置为单位元。我们的查询似乎就变成了圆方树上路径的点权信息之和。

考虑在 LCA 处的特殊情况。若 LCA 为方点,同样需要得到 \(u'\rightarrow v'\) 的信息。如果严格遵循上文对原子信息的定义,我们需要先扣掉 \(v'\) 的点权,但是这样到最后会漏掉 \(v'\) 的点权(点权转换后的特殊情况),所以我们完全不必扣掉 \(v'\) 的点权,而是查出 LCA 的点权后,再加上 LCA 父亲圆点的点权。对于 LCA 为圆点的情况,我们统计漏了这个 LCA 圆点的信息,这把它的点权贡献到答案里就好了。如此就完成了查询。

修改某个点的权值的时候,除了需要维护仙人掌上每个点的点权,还要修改其在圆方树上父亲方点的点权。要想快速得到新的点权,要对每一个方点维护一个支持插入删除的数据结构,每次先删除原先的贡献,再加入修改后点权的贡献即可。

我们现在只需要在一棵树上,支持修改点权,询问 \(\operatorname{lca}\),询问路径点权和。这个可以树剖做到 \(\log^2\),或者可以做到单 \(\log\)。另外,理论上来说,我们还可以继续上放点权,也就是在圆树操作。但是这会造成很多边界情况,不优雅,故不展开讨论。

考虑更为一般的情况。我们发现边权上放的本质是为了修改的时候,能够将父亲方点的所有孩子圆点往上跳的边权都修改。但是真的一定需要更新每个孩子圆点吗?并不是,我们只需要修改重孩子圆点。每次跳重链时,只需要考虑链顶这个轻儿子的特殊情况。于是完成了修改和维护信息。

「圆树」:不设置方点的可行性

上文中,方点的作用仅是用来判断 \(u'\) 和 \(v'\) 是否在同一点双内,在一些简单的问题中,我们确实可以不建出方点,只建出「圆树」,并在每个点记录它在圆方树上父亲方点是谁,在查询的时候,只需要要找出 \(u',v'\)(如果存在的话),然后判断他们的父亲圆点是否相同,就能知道 LCA 是方点还是圆点。

优点

  1. 节点总数为 \(n\):

    不引入新的方点,整棵树的节点数量保持为原图大小的 \(n\),在最坏情况下仅为传统圆方树节点数的一半。这样可以节省时间空间,而且不会因为手残数组开小,或者预处理只处理到 \(n\) 而红温。

  2. 实现简单:

    若采用倍增算法来维护树上信息和求 LCA,当一个节点是另一个节点的祖先时,不会出现 LCA 是方点的特殊情况。剩下来的情况,我们倍增本来就是先求出了 \(u',v'\),然后再跳最后一步到 LCA 的,那我们在跳之前判断一下就好了,不必等到跳了才发现是同一个方点。

缺点

  1. 过度依赖倍增:
    若题目采用树上前缀和、差分等方式来维护信息,通常使用基于 DFS 序的 \(\mathcal{O}(1)\) LCA 算法,而寻找 \(u', v'\) 却是 \(\mathcal{O}(\log n)\) 的,导致效率不匹配。除非你愿意使用如长链剖分实现 \(\mathcal{O}(1)\) 的 kth-father 查询:先求出 LCA,再通过深度差计算 \(u'\) 或 \(v'\),这会显著增加代码复杂度。

    发明了基于 DFS 序 \(\mathcal{O}(1)\) 求 \(u',v'\) 的算法,可见:博客

  2. 在特定情况下有过多边界问题:

    例如维护需要支持修改的信息,使用圆树将需要很多分类讨论,万一没有讨论所有情况,就会出错,而且代码将变得难以理解。

  3. 过于小众:
    求调的时候别人说看不懂。


总而言之,这是仅是一种小优化,在掌握了圆方树的基本构造与思想后,读者可根据题目类型与实现习惯,自行选择是否采用此优化版本。

圆树和圆方树两种写法,在例题五例题十三均分别给出,供参考对比。

圆方树在「仙人掌」上的应用

我们先从 仙人掌 这种特殊的无向图开始研究圆方树,这是因为仙人掌具有十分良好的性质。

初识仙人掌

仙人掌的定义

仙人掌指任意一条边最多出现在一个简单环中的无向连通图。

仙人掌的性质

  1. 仙人掌边数上界为 \(\mathcal{O}(n)\)。

    准确来说,为 \(2n-2\) 或 \(\lfloor\frac{3}{2}(n-1)\rfloor\)。

    当允许重边时,把一棵树的所有 \(n-1\) 条树边,复制一遍,得到一棵 \(2n-2\) 条边的仙人掌。注意不可能出现三条重边连接两个点,否则就不满足仙人掌的定义了。

    当不允许重边时,可以发现如下「类菊花」的结构使得边数达到最多,为 \(\lfloor\frac{3}{2}(n-1)\rfloor\)。

  2. 仙人掌具有类似树的结构。

    我们发现,由于仙人掌的环不交(准确的说为边不交),这使得其保留了树的一些形态。

    把每一个环看做一个巨大的节点,仙人掌就成了一棵树了,只不过有些节点间不依靠边相连,而是在公共点处相切。

    类似树上「子树」,我们可以定义「子仙人掌」。
    「子仙人掌」

    指定一个根,定义 \(u\) 的「子仙人掌」为,断开根和 \(u\) 的所有简单路径的边后,\(u\) 所在的连通块。

    不难发现,\(u\) 的「子仙人掌」对应圆方树上 \(u\) 的子树。画个图就十分清晰了:

    不难做进一步推广,类似在一般无向图上定义「子无向图」,容易发现,这同样对应圆方树的子树。

    类似树上「树链」,在 只存在奇环 的仙人掌上,由于最长 / 短路唯一,我们可以定义「长链」、「短链」的概念。
    「长链」、「短链」

    \(u,v\) 之间经过边数最多的的简单路径称为 \(u,v\) 间的「长链」;\(u,v\) 之间经过边数最少的简单路径称为 \(u,v\) 间的「短链」。

    显然,在仙人掌中每经过一个环时,若都选择在环上走最长(或最短)路径 ,则整条走出的路径即为 \(u, v\) 间的长链(或短链)。

需要注意仙人掌上的点双并 不一定是环,通过一条不在环上的边连接的两个点,构成了两个点的点双,而这个点双不是一个环。有时候我们需要特判这种情况。

仙人掌上构造圆方树

我们可能需要在构造圆方树的时候,维护当前环的形态,即得到当前环对应的子图。

方法一:树上前缀和

一种做法是,对每个节点维护在 tarjan 的 dfs 树上的树上前缀和(即到根的信息),对于在 dfs 树上 \(u\) 是 \(v\) 的祖先,用 \(v\) 的前缀和减去 \(u\) 的前缀和,就能得到 \(u\) 到 \(v\) 之间的信息。那我们选用父亲圆点 \(u\) 作为环首,这样环上每一个点都处于 dfs 树中 \(u\) 的子树中,于是可以得到每个点到环首 \(u\) 的信息。至于总环长,发现我们上述过程考虑的是一条以 \(u\) 为环首的一条链,对于环尾 \(v\),他有且仅有一条返祖边,且这条返祖边恰连向 \(u\),从而构成一个环。一个点不可能有两条或以上返祖边,否则不满足仙人掌的定义。而这个返祖边我们可以轻松维护。如下图,蓝色的边为 dfs 树树边,红色的边为返祖边,细黑边为圆方树上的树边。当前的环即为若干条蓝边和一条红边构成的环。绿色箭头表示环首走向环尾的方向,蓝紫色箭头指向形成闭环的那条环尾指向环首的边。

方法二:弹边栈

这种做法对于不可差分信息不太好处理,并且依赖于环这种形态,不好向一般点双拓展。考虑类似点的栈,维护一个边的栈,存返祖边和树边,在 tarjan 的过程中,每次找到一个点双(\(\operatorname{low}(v) \geq \operatorname{dfn}(u)\)),就不断弹栈,直到弹出连接 \(u,v\) 的树边 (注意必须是树边)。这个过程中弹出的边,便构成了这个点双的子图。对于复杂一些的点双,我们需要建出这个子图,然后在这个子图上跑一些算法,但是于仙人掌而言,我们把一个环建出来再处理,未免小题大做了。我们发现按照弹栈的顺序,弹出的边依次为环中的返祖边,深度最深的树边,深度次深的树边,直到 \(u,v\) 之间的树边。更加优雅的做法是,我们按照弹栈的顺序,假设当前弹出的边为 \(u'\rightarrow v'\),边权为 \(w\),并且不是连接 \(u,v\) 的树边,设 \(u\) 在环上的信息前缀和为 \(p_u\),那么就让 \(p_{u'}\gets p_{v'}+w\)。这种方法下,环首为 \(u\),环尾是 \(v\),\(u,v\) 之间的树边成了环尾连向环首的边。如下图。事实上,使用了这种方式,我们不需要原先的点栈,每次弹出的边的起点,相当于原先点栈每次弹出的点。

类型一:单次询问整体信息

计数类问题

在处理计数类问题时,往往需要借助圆方树进行类似树形 DP,或其他类似树上的统计方法。

在普通树上,这类问题我们已非常熟悉;放到基环树上,常见的技巧是将环上 DP 与树形 DP 分开处理,其本质原因是,把环上每个结点看做对应树根,这棵树是一个子问题,所以剩下的部分是一个环上问题。

在仙人掌上,由于我们前文提到的仙人掌具有的类似树的性质,所以在 DP 转移的时候,可以分环上 DP 和树形 DP 进行转移,或者按照环上和树上进行统计答案。这在圆方树上体现为,在方点、圆点上分别处理。

实际上,对于这类问题,我们甚至无需显式构建整棵圆方树。只需在分析与转移过程中设想当前所处的是圆点还是方点,并据此决定转移逻辑,即可完成建模与求解。这也是圆方树思想灵活而强大的体现。
例一、静态仙人掌最大独立集(小 C 的独立集,黑暗爆炸洛谷Hydro) Problem Statement

给你一个有 \(n\) 个点和 \(m\) 条边的仙人掌,求它的最大独立集大小。

\(n\leq 5\times 10^4\),\(m\leq 6\times 10^4\)。
Problem Analysis

树上最大独立集是经典的(猜你想找:没有上司的舞会),我们设 \(f_{u,0/1}\) 表示以 \(u\) 为根的子树这个子问题,\(u\) 选 / 不选,最大独立集为多少。转移为 \(f_{u,0}=\sum\limits_{v\in\operatorname{son}(u)}\max\Big\{f_{v,0},f_{v,1}\Big\}\),\(f_{u,1}=\operatorname{val}(u)+\sum\limits_{v\in\operatorname{son}(u)}f_{v,0}\)。

基环树上最大独立集是经典的(猜你想找:MAFIJAZJOI2008 骑士),我们先把树形 DP 跑了,得到环上每个点 \(u\) 的 \(f_{u,0/1}\),然后再做一遍环上 DP。如果这个环没有首尾之间的独立集限制,这其实就是一条链,按照树的方式转移即可。但是首尾之间有限制,我们就先钦定首必不能选,然后做一遍 DP,把尾的 \(f_{0/1}\) 算到答案的贡献里,然后钦定首必选(亦可钦定其可选可不选,如果需要求独立集方案,为了不冲不漏,此处要钦定必选),再做一遍 DP,把尾的 \(f_0\) 贡献进去,这里就不贡献 \(f_1\) 了,不然会不满足首尾之间的限制条件。
另外一种基环树 DP 形式

有人说,我学的基环树 DP 不太一样,是:把环上一条边拎出来,剩下一棵树,然后类似上面的钦定,做两遍树形 DP。这两种方法其实是本质相同的,只不过这种方法把断边后的环当做树上一条链,整体做一遍 DP,相当于把环上 DP 放在树形 DP 里做了。对于仙人掌来说,我们还是喜欢单独对环做 DP。

那么仙人掌上也很好做了。\(f\) 的定义便是关于 \(u\) 的子仙人掌上的信息。圆点上我们赋初值 \(f_{u,0}\gets 0, f_{u,1}\gets 1\)。在方点上,我们做环上 DP,然后把答案统计到方点的父亲圆点 \(u\) 上。先使得 \(u\) 在环尾,然后类似基环树上的环做两遍 DP,得到 \(f_{u,0/1}\)。在两遍环上的 DP 的时候,不要把答案直接赋给 \(f_u\),而是应该使用临时变量存放,做完两遍 DP 后再存到 \(f_u\) 里面,这样是为了避免第一次 DP 对第二次 DP 造成的影响。

于是,我们做到了时空线性 \(\mathcal{O}(n+m)\) 解决了本题。


做完这题可以去尝试一下 SDOI2010 城市规划,树套环上求至少间隔两个位置的最大独立集,可以参考我的题解
Solution

cpp 复制代码
#include <cstdio>
#include <iostream>
#include <vector>
using namespace std;

const int N = 5e4 + 10;
const int M = 6e4 + 10;

int n, m;

struct node {
    int v, nxt;
} edge[M << 1];
int head[N], tot = 1;
void add(int u, int v) {
    edge[++tot] = { v, head[u] };
    head[u] = tot;
}

int dfn[N], low[N], timer;
int stack[N], top;

int f[N][2], g[N][2];

void tarjan(int u, int fr) {
    f[u][1] = 1, f[u][0] = 0;  // 圆点直接赋初值
    dfn[u] = low[u] = ++timer;
    stack[++top] = u;
    for (int i = head[u]; i; i = edge[i].nxt) {
        if (i == (fr ^ 1)) continue;
        int v = edge[i].v;
        if (!dfn[v]) {
            tarjan(v, i);
            low[u] = min(low[u], low[v]);
            if (low[v] >= dfn[u]) {
                // 这里应该会新建一个方点
                // 但我们并不需要真的把圆方树建出来
                
                vector<int> scc;
                while (true) {
                    int x = stack[top--];
                    scc.emplace_back(x);
                    if (x == v) break;
                }
                scc.emplace_back(u);   // u 在环尾
                
                int m = scc.size();
                int f0 = 0, f1 = 0;
                
                // 强制首不选
                g[0][0] = f[scc[0]][0], g[0][1] = -0x3f3f3f3f;
                for (int i = 1; i < m; ++i) {
                    g[i][0] = max(g[i - 1][0], g[i - 1][1]) + f[scc[i]][0];
                    g[i][1] = g[i - 1][0] + f[scc[i]][1];
                }
                f0 = max(f0, g[m - 1][0]);
                f1 = max(f1, g[m - 1][1]);
                
                // 首可选可不选
                g[0][0] = f[scc[0]][0], g[0][1] = f[scc[0]][1];
                for (int i = 1; i < m; ++i) {
                    g[i][0] = max(g[i - 1][0], g[i - 1][1]) + f[scc[i]][0];
                    g[i][1] = g[i - 1][0] + f[scc[i]][1];
                }
                f0 = max(f0, g[m - 1][0]);
                
                f[u][0] = f0, f[u][1] = f1;
            }
        } else {
            low[u] = min(low[u], dfn[v]);
        }
    }
}

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1, u, v; i <= m; ++i) {
        scanf("%d%d", &u, &v);
        add(u, v), add(v, u);
    }
    tarjan(1, 0);
    printf("%d", max(f[1][0], f[1][1]));
    return 0;
}

从最简单的树开始思考,然后想想基环树怎么处理,最后思考仙人掌上的问题,是我们处理此类问题的一般模式。这是由 \(0\) 个环,到 \(1\) 个环,再到若干个环,不断加强问题的过程。
例二、静态仙人掌直径(SHOI2008 仙人掌图,黑暗爆炸洛谷Hydro) Problem Statement

给你一个有 \(n\) 个点和 \(m\) 条边的仙人掌,求它的直径。直径被定义为两点间最短距离的最大值。

\(n\leq 5\times 10^4\),\(m\leq 10^7\)。
Problem Analysis

树的直径是经典的(猜你想找:Longest path in a tree)。我们有两种解法,一种是两遍 DFS,但是它碰到负权边就挂了,而且不太具有可拓展性;另一种解法是树形 DP,记 \(f_u\) 表示从 \(u\) 开始往叶子的方向走,最长距离是多少,枚举 \(v\in \operatorname{son}(u)\),先贡献答案 \(D\Leftarrow f_u+f_v+1\),再做 DP \(f_u\Leftarrow f_v+1\)。(其中 \(a\Leftarrow b\) 表示 \(a\gets\max\{a,b\}\)。)

基环树求直径是经典的(猜你想找:IOI 2008 IslandNOI2013 快餐店,但是需要说明的是,这两题形式和本题相同,但是对直径的定义都略有不同,以下按照本题的定义分析)。先跑树形 DP,该贡献答案的贡献到答案里去,然后得到环上每一个点的 \(f\),考虑一条经过环边的路径,设 \(\{c_m\}\) 表示这个环,那么这条路径形如:\(u \stackrel{\displaystyle f_{c_i}}{\longrightarrow} c_i \stackrel{w(i,j)}{\longrightarrow} c_j \stackrel{\displaystyle f_{c_j}}{\longrightarrow} v\),其中 \(w(i,j)\) 表示环上第 \(i\) 个点到第 \(j\) 个点之间的最短距离,即为 \(\min\Big\{j-i,m-(j-i)\Big\}\)。考虑怎么求出 \(f_{c_i}+w(i,j)+f_{c_j}\) 的最大值。我们先拆换成链,再复制一份接在后面。枚举 \(i\) 和小于它的 \(j\),强制让 \(w(j,i)\) 就等于 \(i-j\),此时 \(j\) 需要满足的条件为 \(i-j\leq m-(i-j)\) 即 \(j\geq i-\lfloor m/2\rfloor\),如此,我们统计的信息就是 \(\max\limits_{j=i-\lfloor m/2\rfloor}^{i-1}\Big\{f_{c_j}-j\Big\}+i+f_{c_i}\),滑动窗口最值,可以用单调队列维护。注意到,这样我们并不会漏掉某一种情况。

那么仙人掌求直径也是简单的。我们在方点统计完答案的贡献后,还要处理出方点的父亲圆点 \(u\) 的 \(f\) 值,这个直接枚举环上另一个点 \(v\) 就可以了。

时空复杂度 \(\mathcal{O}(n+m)\)。
Solution

cpp 复制代码
#include <cstdio>
#include <iostream>
#include <vector>
using namespace std;

const int N = 5e4 + 10;
const int M = 1e7 + 10;

int n, m, ans;

struct node {
    int v, nxt;
} edge[M << 1];
int head[N], tot = 1;
void add(int u, int v) {
    edge[++tot] = { v, head[u] };
    head[u] = tot;
}

int dfn[N], low[N], timer;
int stack[N], top;

int f[N], g[N << 1], Q[N << 1];

void tarjan(int u, int fr) {
    f[u] = 0;
    dfn[u] = low[u] = ++timer;
    stack[++top] = u;
    for (int i = head[u]; i; i = edge[i].nxt) {
        if (i == (fr ^ 1)) continue;
        int v = edge[i].v;
        if (!dfn[v]) {
            tarjan(v, i);
            low[u] = min(low[u], low[v]);
            if (low[v] >= dfn[u]) {
                vector<int> scc;
                while (true) {
                    int x = stack[top--];
                    scc.emplace_back(x);
                    if (x == v) break;
                }
                scc.emplace_back(u);
                int m = scc.size();
                
                for (int j = 0; j < m; ++j)
                    g[j] = g[j + m] = f[scc[j]];
                
                int head = 0, tail = -1;
                for (int j = 0; j < m << 1; ++j) {
                    while (head <= tail && Q[head] < j - m/2) ++head;
                    if (head <= tail) ans = max(ans, g[Q[head]] - Q[head] + j + g[j]);
                    while (head <= tail && g[Q[tail]] - Q[tail] <= g[j] - j) --tail;
                    Q[++tail] = j;
                }
                
                for (int j = 0; j < m - 1; ++j)
                    f[u] = max(f[u], f[scc[j]] + min(j + 1, m - j - 1));
            }
        } else {
            low[u] = min(low[u], dfn[v]);
        }
    }
}

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1, k; i <= m; ++i) {
        scanf("%d", &k);
        for (int u = 0, v; k--; u = v) {
            scanf("%d", &v);
            if (u) add(u, v), add(v, u);
        }
    }
    tarjan(1, 0);
    printf("%d", ans);
    return 0;
}

通过这两道板子题,相信你已经对静态仙人掌上全局问题的求解有了清晰的认识。

以上,我们了解了静态仙人掌上统计全局信息的方法。尽管我们并没有把圆方树建出来,我们还是可以想象在一棵虚拟的圆方树上求解问题:在一个方点上,处理这环上的信息,把孩子圆点的信息合并到父亲圆点中去;在一个圆点上,我们赋 DP 初值。这也是标题中「一种关于点双连通分量的思考方式」的体现,我们解题可能不需要圆方树,但是思考过程会借助圆方树方便思考。

所有路径类问题

如果你需要统计所有路径的信息,树上我们可能需要用到点分治。那么在仙人掌上,我们要对圆方树进行点分治,类似于树上,对于一个点统计经过这个点的信息。理论上来说点分树也可以被应用在仙人掌上。

由于作者还没有学习到 FFT,例题暂时不能完成,这部分暂且不展开。

所有「子仙人掌」类问题

树上这类问题我们有 树上启发式合并(DSU on tree) 的做法,也可以使用线段树合并优化。

根据结论,子仙人掌对应圆方树的子树,问题就被放到圆方树上,真的成了一棵树上的问题了。
例三、persistent DSU on cactus(洛谷) Problem Statement

\(n\) 个点 \(m\) 条边的仙人掌,每个结点有一个颜色 \(c_u\)。

\(q\) 次询问 \((u,k)\),求 \(u\) 的子仙人掌中出现次数不小于 \(k\) 的颜色的颜色编号和。

\(u\) 的子仙人掌被定义为:断开在 \(1,u\) 所有简单路径上出现过的边后,\(u\) 所在的连通块。

不知道为什么出题的时候出成了强制在线。只要空间复杂度别太离谱就行。

\(2\leq n\leq10^5\)。\(1\leq q\leq 10^5\)。\(k\leq n\)。\(c_u\in1,n\)。
Problem Analysis

离线 DSU on tree,时间复杂度两只 \(\log\),如果强制在线,其实就是需要树状数组的在每一个结点上的版本。换成一个可以支持:继承、单点修改、区间查询的数据结构,就是主席树。
Solution Code

cpp 复制代码
#include <cstdio>
#include <iostream>
using namespace std;

const int N = 1e5 + 10;
const int M = N * 2;
const int NN = N * 2;

int n, m, q, c[N], O;

template <size_t _N, size_t _M>
class Graph {
public:
    Graph(): tot(1) {}
    struct node {
        int v, nxt;
    } edge[_M];
    int head[_N], tot;
    void add(int u, int v) {
        edge[++tot] = { v, head[u] };
        head[u] = tot;
    }
};

Graph<N, M << 1> G;
Graph<NN, NN> T;

namespace $build {

int dfn[N], low[N], timer;
int stack[N], top;
int dcc_cnt;

void tarjan(int u, int fr) {
    dfn[u] = low[u] = ++timer;
    stack[++top] = u;
    for (int i = G.head[u]; i; i = G.edge[i].nxt) {
        if (i == (fr ^ 1)) continue;
        int v = G.edge[i].v;
        if (!dfn[v]) {
            tarjan(v, i);
            low[u] = min(low[u], low[v]);
            if (low[v] >= dfn[u]) {
                ++dcc_cnt;
                T.add(u, dcc_cnt + n);
                while (true) {
                    int x = stack[top--];
                    T.add(dcc_cnt + n, x);
                    if (x == v) break;
                }
            }
        } else {
            low[u] = min(low[u], dfn[v]);
        }
    }
}

}

namespace $yzh {

const int M = N * 230;

struct node {
    int ls, rs;
    long long sum;
} tr[M];

int tot;
inline int copy(int u) {
    return tr[++tot] = tr[u], tot;
}

void modify(int& u, int l, int r, int p, int v) {
    tr[u = copy(u)].sum += v;
    if (l == r) return;
    int mid = (l + r) >> 1;
    if (p <= mid) modify(tr[u].ls, l, mid, p, v);
    else modify(tr[u].rs, mid + 1, r, p, v);
}

long long query(int u, int trl, int trr, int l, int r) {
    if (!u) return 0;
    if (l <= trl && trr <= r) return tr[u].sum;
    int mid = (trl + trr) >> 1;
    long long res = 0;
    if (l <= mid) res += query(tr[u].ls, trl, mid, l, r);
    if (r >  mid) res += query(tr[u].rs, mid + 1, trr, l, r);
    return res;
}

int combine(int u, int v) {
    if (!u || !v) return u | v;
    int o = copy(u);
    tr[o].sum += tr[v].sum;
    tr[o].ls = combine(tr[o].ls, tr[v].ls);
    tr[o].rs = combine(tr[o].rs, tr[v].rs);
    return o;
}

}

int dfn[NN], L[NN], R[NN], timer;
int siz[NN], son[NN];

void dfs(int u) {
    dfn[L[u] = ++timer] = u;
    siz[u] = 1;
    for (int i = T.head[u]; i; i = T.edge[i].nxt) {
        int v = T.edge[i].v;
        dfs(v);
        siz[u] += siz[v];
        if (siz[v] > siz[son[u]])
            son[u] = v;
    }
    R[u] = timer;
}

int cnt[N], rt[NN];

void sub(int l, int r) {
    for (int i = l; i <= r; ++i) {
        int u = dfn[i];
        if (u > n) continue;
        cnt[c[u]] -= 1;
    }
}

void add(int l, int r, int& rt) {
    for (int i = l; i <= r; ++i) {
        int u = dfn[i];
        if (u > n) continue;
        if (cnt[c[u]])
            $yzh::modify(rt, 1, n, cnt[c[u]], -c[u]);
        $yzh::modify(rt, 1, n, ++cnt[c[u]], c[u]);
    }
}

void redfs(int u) {
    for (int i = T.head[u]; i; i = T.edge[i].nxt) {
        int v = T.edge[i].v;
        if (v == son[u]) continue;
        redfs(v);
        sub(L[v], R[v]);
    }
    if (son[u]) redfs(son[u]), rt[u] = rt[son[u]];
    for (int i = T.head[u]; i; i = T.edge[i].nxt) {
        int v = T.edge[i].v;
        if (v == son[u]) continue;
        add(L[v], R[v], rt[u]);
    }
    add(L[u], L[u], rt[u]);
}

int main() {
    scanf("%d%d%d", &O, &n, &m);
    for (int i = 1; i <= n; ++i)
        scanf("%d", &c[i]);
    for (int i = 1, u, v; i <= m; ++i) {
        scanf("%d%d", &u, &v);
        G.add(u, v), G.add(v, u);
    }
    $build::tarjan(1, 0);
    dfs(1), redfs(1);
    scanf("%d", &q);
    for (long long u, k, lst = 0; q--; ) {
        scanf("%lld%lld", &u, &k);
        if (O) u ^= lst, k ^= lst;
        printf("%lld\n", lst = $yzh::query(rt[u], 1, n, k, n));
    }
    return 0;
}

Gen

cpp 复制代码
#include <cstdio>
#include <iostream>
#include <vector>
#include <random>
#include <chrono>
#include <map>
#include <sstream>
#include <cstdio>
#include <iostream>
#include <vector>
using namespace std;

const int SEED = chrono::system_clock::now().time_since_epoch().count();

mt19937 rnd(SEED);

inline int rand(int l, int r) {
    if (l > r) throw "invalid range";
    return l + rnd() % (r - l + 1);
}

bool multiedge = true;
bool onlyOddCircle = false;

int n = rand(99000, 100000);

vector<pair<int, int>> edges;
vector<vector<int>> son(n, vector<int>());
vector<int> dpt(n);

int dfs(int u) {
    int res = u;
    int cnt = 0;
    for (size_t i = 0; i < son[u].size(); ++i) {
        int v = son[u][i];
        dpt[v] = dpt[u] + 1;
        int t = dfs(v);
        if (rand(0, son[u].size()) == 0)
            res = t;
        else if ((t != v || (multiedge && cnt < 2)) && ((dpt[t] - dpt[u] + 1) % 2 == 1 || !onlyOddCircle))
            edges.emplace_back(u, t), cnt += t == v;
    }
    return res;
}

std::ostringstream odata;

const int QSIZE = 1e5;

namespace $yzh {
void build(istringstream&);
vector<pair<pair<int, int>, long long>> qrys;
}

int main() {
    for (int i = 1; i < n; ++i) {
        int fa = rand(0, i - 1);
        edges.emplace_back(fa, i);
        son[fa].emplace_back(i);
    }
    dfs(0);
    for (size_t i = 1; i < edges.size(); ++i) {
        swap(edges[i], edges[rand(0, i)]);
    }
    
    int QJ = 1;
    
    odata << QJ << endl;
    
    int m = edges.size();
    
    odata << n << ' ' << m << endl;
    
    for (int i = 1; i <= n; ++i) odata << rand(1, 3) << " \n"[i == n];
    
    for (size_t i = 0; i < edges.size(); ++i) {
        int u = edges[i].first;
        int v = edges[i].second;
        if (rand(0, 1)) swap(u, v);
        odata << u + 1 << ' ' << v + 1 << endl;
    }
    
    auto _ = istringstream(odata.str());
    $yzh::build(_);
    
    auto& Q = $yzh::qrys;
    int q = Q.size();
    for (int i = 1; i < q; ++i)
        swap(Q[i], Q[rand(0, i)]);
    
    odata << q << endl;
    
    std::ostringstream oans;
    
    long long lstans = 0;
    for (auto [_, ans] : Q) {
        long long u = _.first, k = _.second;
        if (QJ) u ^= lstans, k ^= lstans;
        odata << u << ' ' << k << endl;
        oans << ans << endl;
        lstans = ans;
    }
    
    FILE* f = fopen("testdata\\10.in", "w");
    fprintf(f, "%s", odata.str().c_str());
    fclose(f);
    
    f = fopen("testdata\\10.ans", "w");
    fprintf(f, "%s", oans.str().c_str());
    fclose(f);
    
    
    fprintf(stderr, "SEED = %d\n", SEED);
    fprintf(stderr, "circle = %d\n", (int)edges.size() - (n - 1));
    fprintf(stderr, "n = %d, m = %d, q = %d\n", n, m, q);
    
    return 0;
}

namespace $yzh {

const int N = 1e5 + 10;
const int M = N * 2;
const int NN = N * 2;

int n, m, q, c[N];

template <size_t _N, size_t _M>
class Graph {
public:
    Graph(): tot(1) {}
    struct node {
        int v, nxt;
    } edge[_M];
    int head[_N], tot;
    void add(int u, int v) {
        edge[++tot] = { v, head[u] };
        head[u] = tot;
    }
};

Graph<N, M << 1> G;
Graph<NN, NN> T;

namespace $build {

int dfn[N], low[N], timer;
int stack[N], top;
int dcc_cnt;

void tarjan(int u, int fr) {
    dfn[u] = low[u] = ++timer;
    stack[++top] = u;
    for (int i = G.head[u]; i; i = G.edge[i].nxt) {
        if (i == (fr ^ 1)) continue;
        int v = G.edge[i].v;
        if (!dfn[v]) {
            tarjan(v, i);
            low[u] = min(low[u], low[v]);
            if (low[v] >= dfn[u]) {
                ++dcc_cnt;
                T.add(u, dcc_cnt + n);
                while (true) {
                    int x = stack[top--];
                    T.add(dcc_cnt + n, x);
                    if (x == v) break;
                }
            }
        } else {
            low[u] = min(low[u], dfn[v]);
        }
    }
}

}

long long ans[N];

int dfn[NN], L[NN], R[NN], timer;
int siz[NN], son[NN];

void dfs(int u) {
    dfn[L[u] = ++timer] = u;
    siz[u] = 1;
    for (int i = T.head[u]; i; i = T.edge[i].nxt) {
        int v = T.edge[i].v;
        dfs(v);
        siz[u] += siz[v];
        if (siz[v] > siz[son[u]])
            son[u] = v;
    }
    R[u] = timer;
}

namespace $yzh {

vector<pair<int, int>> qrys;

long long tr[N];
void modify(int p, int v) {
    for (; p; p &= p - 1)
        tr[p] += v;
}
long long query(int p) {
    long long res = 0;
    for (; p <= n; p += p & -p)
        res += tr[p];
    return res;
}

}

int cnt[N];

void add(int l, int r, int f) {
    for (int i = l; i <= r; ++i) {
        int u = dfn[i];
        if (u > n) continue;
        $yzh::modify(cnt[c[u]], -c[u]);
        $yzh::modify(cnt[c[u]] += f, c[u]);
    }
}

void redfs(int u) {
    for (int i = T.head[u]; i; i = T.edge[i].nxt) {
        int v = T.edge[i].v;
        if (v == son[u]) continue;
        redfs(v);
        add(L[v], R[v], -1);
    }
    if (son[u]) redfs(son[u]);
    for (int i = T.head[u]; i; i = T.edge[i].nxt) {
        int v = T.edge[i].v;
        if (v == son[u]) continue;
        add(L[v], R[v], 1);
    }
    add(L[u], L[u], 1);
    if (qrys.size() == QSIZE || u > n) return;
    int x = rand(0, 2);
    while (x) {
        int p = rand(1, 10);
        if ($yzh::query(p) == 0) continue;
        qrys.push_back({{ u, p }, $yzh::query(p) });
        --x;
        if (qrys.size() == QSIZE) return;
    }
}

void build(istringstream& is) {
    int o;
    is >> o;
    is >> n >> m;
    for (int i = 1; i <= n; ++i)
        is >> c[i];
    for (int i = 1, u, v; i <= m; ++i) {
        is >> u >> v;
        G.add(u, v), G.add(v, u);
    }
    $build::tarjan(1, 0);
    dfs(1);
    redfs(1);
}

}

分别以每个点为根求解

树上这类问题我们有 换根 DP 的做法,仙人掌上也可以类似做换根 DP。
例四、GAME on cactus(洛谷) Problem Statement

小 X 和小 Y 来到一棵 \(n\) 个点 \(m\) 条边的仙人掌上玩游戏,点编号为 \(1\sim n\)。

一开始某个节点上有个棋子,小 X 和小 Y 轮流移动这个棋子,已经走过的边不能再走,谁不能移动谁就输了。

小 Y 先手,她向你求助,对于每一个节点,若其作为初始放置棋子的节点,她是否有必胜策略?

\(2\leq n\leq2\times10^5\)。
Problem Analysis

我的题解
Solution Code

我的题解
Gen

cpp 复制代码
#include <cstdio>
#include <iostream>
#include <vector>
#include <random>
#include <chrono>
using namespace std;

const int SEED = chrono::system_clock::now().time_since_epoch().count();

void _err(const char* msg, int lineNum) {
    fprintf(stderr, "Error at line #%d: %s\n", lineNum, msg);
    exit(1);
}
#define err(msg) _err(msg, __LINE__)

inline int rand(int l, int r) {
    static mt19937 rnd(SEED);
    if (l > r) err("invalid range");
    return l + rnd() % (r - l + 1);
}

vector<vector<int>> pts;
vector<pair<int, int>> edges;

int main() {
	int rem = rand(230990, 231000), o = 0;  // data may be wrong!
	int m = 0;
	pts.push_back({});
	while (rem > 0) {
		int x = rand(3, max(3, min(10, rem)));
		rem -= x;
		if (rem == 1) --rem, ++x;
		m++;
		vector<int> vec;
		if (m == 1) {
			for (int i = 1; i <= x; ++i)
				vec.emplace_back(++o);
		} else {
			int fa = rand(1, max(1, m - 20));
			auto& y = pts[fa];
			int t = rand(0, (int)y.size() - 1);
			vec.emplace_back(y[t]);
			for (int i = 2; i <= x; ++i)
			 	vec.emplace_back(++o);
		}
		for (int i = 2; i <= x; ++i)
			edges.emplace_back(vec[i - 2], vec[i - 1]);
		edges.emplace_back(vec[x - 1], vec[0]);
		pts.emplace_back(vec);
	}
	
	int n = o;
	
    vector<int> id(n);
    for (int i = 0; i < n; ++i) id[i] = i;
    for (int i = 1; i < n; ++i)
        swap(id[i], id[rand(0, i)]);
	
	for (size_t i = 1; i < edges.size(); ++i)
        swap(edges[i], edges[rand(0, i)]);
    
    printf("%d %d\n", o, (int)edges.size());
    for (size_t i = 0; i < edges.size(); ++i) {
        int u = edges[i].first;
        int v = edges[i].second;
        if (rand(0, 1)) swap(u, v);
        u = id[u - 1], v = id[v - 1];
        printf("%d %d\n", u + 1, v + 1);
    }
	
	return 0;
}
cpp 复制代码
#include <cstdio>
#include <iostream>
#include <vector>
#include <random>
#include <chrono>
using namespace std;

/* =========== Parameter =========== */

const int SEED = chrono::system_clock::now().time_since_epoch().count();

bool multiedge = true;
bool onlyOddCircle = false;

int MULTI_P = 50;

/* =========== Parameter =========== */

void _err(const char* msg, int lineNum) {
    fprintf(stderr, "Error at line #%d: %s\n", lineNum, msg);
    exit(1);
}
#define err(msg) _err(msg, __LINE__)

inline int rand(int l, int r) {
    static mt19937 rnd(SEED);
    if (l > r) err("invalid range");
    return l + rnd() % (r - l + 1);
}

int n = rand(199990, 200000);
int LEN = rand(1314, 1324);

int m_limit = -1;  // -1 for not limit

vector<pair<int, int>> edges;
vector<vector<int>> son(n, vector<int>());
vector<int> dpt(n);

int maxcir = LEN;
int chong = 0;

void dfs(int u) {
    for (size_t i = 0; i < son[u].size(); ++i) {
        int v = son[u][i];
        dpt[v] = dpt[u] + 1;
        dfs(v);
    }
}

int redfs(int u, int ban, vector<int>& vec) {
    vec.emplace_back(u);
    int res = u;
    for (size_t i = 0; i < son[u].size(); ++i) {
        int v = son[u][i];
        if (v == ban) continue;
        int t = redfs(v, -1, vec);
        int len = dpt[t] - dpt[u] + 1;
        if ((t != v || (multiedge && rand(0, MULTI_P) == 0))
                && (len % 2 == 1 || !onlyOddCircle)
                && (m_limit == -1 || (int)edges.size() < m_limit))
            edges.emplace_back(u, t), maxcir = max(maxcir, len), chong += t == v;
        else if (/* res == u ||  */rand(0, son[u].size()) == 0 || dpt[t] > dpt[res])
            res = t;
    }
    return res;
}

int main() {
    // freopen(".\\testdata\\4.in", "w", stdout);
    
    // freopen("yzh", "w", stdout);
    // freopen("in", "w", stdout);
    
    if (n < 1) err("n shouldn't be less than 1");
    if (m_limit != -1 && m_limit < n - 1)
        err("m_limit shouldn't less than n-1");
    if (LEN > n)
        err("LEN too long");
    
    vector<int> id(n);
    for (int i = 0; i < n; ++i) id[i] = i;
    for (int i = 1; i < n; ++i)
        swap(id[i], id[rand(0, i)]);
    
    for (int i = 1; i < LEN; ++i) {
        int fa = i - 1;
        edges.emplace_back(fa, i);
        son[fa].emplace_back(i);
    }
    for (int i = LEN; i < n; ++i) {
        // int fa = rand(0, i - 1);
        int fa = rand(max(0, i - 20), i - 1);
        edges.emplace_back(fa, i);
        son[fa].emplace_back(i);
    }
    if (LEN > 1 && m_limit != n - 1) {
        edges.emplace_back(0, LEN - 1);
    }
    dfs(0);
    vector<int> son[n];
    for (int i = 1; i < LEN; ++i)
        redfs(i - 1, i, son[i - 1]);
    redfs(LEN - 1, -1, son[LEN - 1]);
    
    for (size_t i = 1; i < edges.size(); ++i)
        swap(edges[i], edges[rand(0, i)]);
    
    printf("%d %d\n", n, (int)edges.size());
    for (size_t i = 0; i < edges.size(); ++i) {
        int u = edges[i].first;
        int v = edges[i].second;
        if (rand(0, 1)) swap(u, v);
        u = id[u], v = id[v];
        printf("%d %d\n", u + 1, v + 1);
    }
    
    fprintf(stderr, "Success!\n");
    fprintf(stderr, "n = %d, m = %d\n", n, (int)edges.size());
    fprintf(stderr, "circle = %d\n", (int)edges.size() - (n - 1));
    fprintf(stderr, "maxcir = %d\n", maxcir);
    fprintf(stderr, "chong = %d\n", chong);
    return 0;
}

类型二:静态仙人掌,维护点对间所有简单路径信息

这类问题需要我们支持:修改点权或边权,询问点对之间路径上信息。
例五、仙人掌点对间最短路长度(【模板】静态仙人掌,洛谷) Problem Statement

给你一个有 \(n\) 个点和 \(m\) 条边的仙人掌,和 \(q\) 组询问,每次询问两点 \(u,v\) 间最短路长度。

\(n\leq 10^4\),\(m\leq 2\times 10^4\),\(q\leq 10^4\)。
Problem Analysis

属于「边权相关信息」,\(\odot=+\),\(\oplus=\min\),\(+\) 对 \(\min\) 有分配律,可以求解。

对于原子信息的求解,我们需要对每个点双维护总环长,对于每个圆点维护,他在他父亲方点代表的环中,到环首的距离。

对于前文提到过的不是环的点双,特殊处理成一个环即可。

于是,我们可以在 \(\mathcal{O}\Big(m+(n+q)\log n\Big)\) 或者长链剖分优化求 kth-father \(\mathcal{O}(n\log n + q+m)\) 的时间复杂度内求解问题。
Solution

均采用倍增维护树上信息。
正常圆方树,采用树上前缀和预处理信息

cpp 复制代码
#include <cstdio>
#include <iostream>
#include <vector>
using namespace std;

const int N = 1e4 + 10;
const int M = 2e4 + 10;
const int lg2N = __lg(N << 1) + 1;

int n, m, q;

template <size_t _N, size_t _M>
class Graph {
public:
    Graph(): tot(1) {}
    struct node {
        int v, w, nxt;
    } edge[_M];
    int head[_N], tot;
    void add(int u, int v, int w) {
        edge[++tot] = { v, w, head[u] };
        head[u] = tot;
    }
};

Graph<N, M << 1> G;
Graph<N << 1, N << 1> T;  // 圆方树

int dfn[N], low[N], timer;
int stack[N], top;

int pre[N];  // dfs 树的树上前缀和
int rev[N];  // 返祖边边权
int dis[N];  // 在父亲方点代表的点双中,到环首的距离

int dcc_cnt;
int cirsum[N];  // 环的总环长

inline int query(int u, int v, int x) {
    // u, v 在编号为 x 的点双中的距离
    int d = abs(dis[u] - dis[v]);
    return min(d, cirsum[x] - d);
}

void tarjan(int u, int fr) {
    dfn[u] = low[u] = ++timer;
    stack[++top] = u;
    for (int i = G.head[u]; i; i = G.edge[i].nxt) {
        if (i == (fr ^ 1)) continue;
        int v = G.edge[i].v, w = G.edge[i].w;
        if (!dfn[v]) {
            pre[v] = pre[u] + w;
            tarjan(v, i);
            low[u] = min(low[u], low[v]);
            if (low[v] >= dfn[u]) {
                ++dcc_cnt;
                cirsum[dcc_cnt] = pre[stack[top]] - pre[u] + rev[stack[top]];
                if (stack[top] == v)
                    cirsum[dcc_cnt] <<= 1;
                T.add(u, dcc_cnt + n, 0);
                while (true) {
                    int x = stack[top--];
                    dis[x] = pre[x] - pre[u];
                    T.add(dcc_cnt + n, x, min(dis[x], cirsum[dcc_cnt] - dis[x]));
                    if (x == v) break;
                }
            }
        } else {
            low[u] = min(low[u], dfn[v]);
            if (dfn[v] < dfn[u])
                rev[u] = w;
        }
    }
}

int dpt[N << 1];
int fa[lg2N][N << 1];
int sum[lg2N][N << 1];

void dfs(int u) {
    for (int i = T.head[u]; i; i = T.edge[i].nxt) {
        int v = T.edge[i].v, w = T.edge[i].w;
        fa[0][v] = u;
        sum[0][v] = w;
        dpt[v] = dpt[u] + 1;
        dfs(v);
    }
}

int query(int u, int v) {
    int ans = 0;
    if (dpt[u] < dpt[v]) swap(u, v);
    for (int i = lg2N - 1; ~i; --i)
        if (fa[i][u] && dpt[fa[i][u]] >= dpt[v]) {
            ans += sum[i][u];
            u = fa[i][u];
        }
    if (u == v) return ans;
    for (int i = lg2N - 1; ~i; --i)
        if (fa[i][u] != fa[i][v]) {
            ans += sum[i][u] + sum[i][v];
            u = fa[i][u], v = fa[i][v];
        }
    // 此时 u,v 即为 u',v'
    int p = fa[0][u];  // lca
    if (p <= n)  // lca 为圆点
        ans += sum[0][u] + sum[0][v];  // 这一行其实没有作用,因为 sum[0][u]=sum[0][v]=0
    else
        ans += query(u, v, p - n);
    return ans;
}

int main() {
    scanf("%d%d%d", &n, &m, &q);
    for (int i = 1, u, v, w; i <= m; ++i) {
        scanf("%d%d%d", &u, &v, &w);
        G.add(u, v, w), G.add(v, u, w);
    }
    tarjan(1, 0);
    dfs(1);
    for (int k = 1; k < lg2N; ++k)
        for (int i = 1; i <= n + dcc_cnt; ++i) {
            fa[k][i] = fa[k - 1][fa[k - 1][i]];
            sum[k][i] = sum[k - 1][i] + sum[k - 1][fa[k - 1][i]];
        }
    for (int u, v; q--; ) {
        scanf("%d%d", &u, &v);
        printf("%d\n", query(u, v));
    }
    return 0;
}

正常圆方树,采用弹边栈预处理信息

cpp 复制代码
#include <cstdio>
#include <iostream>
#include <vector>
using namespace std;

const int N = 1e4 + 10;
const int M = 2e4 + 10;
const int lg2N = __lg(N << 1) + 1;

int n, m, q;

template <size_t _N, size_t _M>
class Graph {
public:
    Graph(): tot(1) {}
    struct node {
        int v, w, nxt;
    } edge[_M];
    int head[_N], tot;
    void add(int u, int v, int w) {
        edge[++tot] = { v, w, head[u] };
        head[u] = tot;
    }
};

Graph<N, M << 1> G;
Graph<N << 1, N << 1> T;  // 圆方树

int dfn[N], low[N], timer;
int stack[M], top;  // 注意这里的边栈的数组大小

int dis[N];  // 在父亲方点代表的点双中,到环首的距离

int dcc_cnt;
int cirsum[N];  // 环的总环长

inline int query(int u, int v, int x) {
    // u, v 在编号为 x 的点双中的距离
    int d = abs(dis[u] - dis[v]);
    return min(d, cirsum[x] - d);
}

void tarjan(int u, int fr) {
    dfn[u] = low[u] = ++timer;
    for (int i = G.head[u]; i; i = G.edge[i].nxt) {
        if (i == (fr ^ 1)) continue;
        int v = G.edge[i].v;
        if (!dfn[v]) {
            stack[++top] = i;
            tarjan(v, i);
            low[u] = min(low[u], low[v]);
            if (low[v] >= dfn[u]) {
                static int tmp[N];
                int tot = 0;
                ++dcc_cnt;
                while (true) {
                    int j = stack[top--];
                    int uu = G.edge[j ^ 1].v;
                    int vv = G.edge[j].v;
                    int w = G.edge[j].w;
                    cirsum[dcc_cnt] += w;
                    if (j == i) break;
                    dis[uu] = dis[vv] + w;
                    tmp[++tot] = uu;
                }
                if (tot == 0) {
                    tmp[++tot] = v;
                    dis[v] = cirsum[dcc_cnt];
                    cirsum[dcc_cnt] <<= 1;
                }
                T.add(u, dcc_cnt + n, 0);
                for (int j = 1; j <= tot; ++j) {
                    int x = tmp[j];
                    T.add(dcc_cnt + n, x, min(dis[x], cirsum[dcc_cnt] - dis[x]));
                }
            }
        } else {
            low[u] = min(low[u], dfn[v]);
            if (dfn[v] < dfn[u])
                stack[++top] = i;
        }
    }
}

int dpt[N << 1];
int fa[lg2N][N << 1];
int sum[lg2N][N << 1];

void dfs(int u) {
    for (int i = T.head[u]; i; i = T.edge[i].nxt) {
        int v = T.edge[i].v, w = T.edge[i].w;
        fa[0][v] = u;
        sum[0][v] = w;
        dpt[v] = dpt[u] + 1;
        dfs(v);
    }
}

int query(int u, int v) {
    int ans = 0;
    if (dpt[u] < dpt[v]) swap(u, v);
    for (int i = lg2N - 1; ~i; --i)
        if (fa[i][u] && dpt[fa[i][u]] >= dpt[v]) {
            ans += sum[i][u];
            u = fa[i][u];
        }
    if (u == v) return ans;
    for (int i = lg2N - 1; ~i; --i)
        if (fa[i][u] != fa[i][v]) {
            ans += sum[i][u] + sum[i][v];
            u = fa[i][u], v = fa[i][v];
        }
    // 此时 u,v 即为 u',v'
    int p = fa[0][u];  // lca
    if (p <= n)  // lca 为圆点
        ans += sum[0][u] + sum[0][v];  // 这一行其实没有作用,因为 sum[0][u]=sum[0][v]=0
    else
        ans += query(u, v, p - n);
    return ans;
}

int main() {
    scanf("%d%d%d", &n, &m, &q);
    for (int i = 1, u, v, w; i <= m; ++i) {
        scanf("%d%d%d", &u, &v, &w);
        G.add(u, v, w), G.add(v, u, w);
    }
    tarjan(1, 0);
    dfs(1);
    for (int k = 1; k < lg2N; ++k)
        for (int i = 1; i <= n + dcc_cnt; ++i) {
            fa[k][i] = fa[k - 1][fa[k - 1][i]];
            sum[k][i] = sum[k - 1][i] + sum[k - 1][fa[k - 1][i]];
        }
    for (int u, v; q--; ) {
        scanf("%d%d", &u, &v);
        printf("%d\n", query(u, v));
    }
    return 0;
}

不设置方点的圆树

cpp 复制代码
#include <cstdio>
#include <iostream>
#include <vector>
using namespace std;

const int N = 1e4 + 10;
const int M = 2e4 + 10;
const int lgN = __lg(N) + 1;

int n, m, q;

template <size_t _N, size_t _M>
class Graph {
public:
    Graph(): tot(1) {}
    struct node {
        int v, w, nxt;
    } edge[_M];
    int head[_N], tot;
    void add(int u, int v, int w) {
        edge[++tot] = { v, w, head[u] };
        head[u] = tot;
    }
};

Graph<N, M << 1> G;
Graph<N, N> T;  // 不设置方点的圆树

int dfn[N], low[N], timer;
int stack[N], top;

int pre[N];  // dfs 树的树上前缀和
int rev[N];  // 返祖边边权
int dis[N];  // 在父亲方点代表的点双中,到环首的距离

int dcc_cnt;
int cirsum[N];  // 环的总环长

int tfa[N];  // 父亲方点对应的点双编号

inline int query(int u, int v, int x) {
    // u, v 在编号为 x 的点双中的距离
    int d = abs(dis[u] - dis[v]);
    return min(d, cirsum[x] - d);
}

void tarjan(int u, int fr) {
    dfn[u] = low[u] = ++timer;
    stack[++top] = u;
    for (int i = G.head[u]; i; i = G.edge[i].nxt) {
        if (i == (fr ^ 1)) continue;
        int v = G.edge[i].v, w = G.edge[i].w;
        if (!dfn[v]) {
            pre[v] = pre[u] + w;
            tarjan(v, i);
            low[u] = min(low[u], low[v]);
            if (low[v] >= dfn[u]) {
                ++dcc_cnt;
                cirsum[dcc_cnt] = pre[stack[top]] - pre[u] + rev[stack[top]];
                if (stack[top] == v)
                    cirsum[dcc_cnt] <<= 1;
                while (true) {
                    int x = stack[top--];
                    dis[x] = pre[x] - pre[u];
                    tfa[x] = dcc_cnt;
                    T.add(u, x, min(dis[x], cirsum[dcc_cnt] - dis[x]));
                    if (x == v) break;
                }
            }
        } else {
            low[u] = min(low[u], dfn[v]);
            if (dfn[v] < dfn[u])
                rev[u] = w;
        }
    }
}

int dpt[N];
int fa[lgN][N];
int sum[lgN][N];

void dfs(int u) {
    for (int i = T.head[u]; i; i = T.edge[i].nxt) {
        int v = T.edge[i].v, w = T.edge[i].w;
        fa[0][v] = u;
        sum[0][v] = w;
        dpt[v] = dpt[u] + 1;
        dfs(v);
    }
}

int query(int u, int v) {
    int ans = 0;
    if (dpt[u] < dpt[v]) swap(u, v);
    for (int i = lgN - 1; ~i; --i)
        if (fa[i][u] && dpt[fa[i][u]] >= dpt[v]) {
            ans += sum[i][u];
            u = fa[i][u];
        }
    if (u == v) return ans;
    for (int i = lgN - 1; ~i; --i)
        if (fa[i][u] != fa[i][v]) {
            ans += sum[i][u] + sum[i][v];
            u = fa[i][u], v = fa[i][v];
        }
    // 此时 u,v 即为 u',v'
    if (tfa[u] != tfa[v])
        ans += sum[0][u] + sum[0][v];  // 这一行现在就必要了
    else
        ans += query(u, v, tfa[u]);
    return ans;
}

int main() {
    scanf("%d%d%d", &n, &m, &q);
    for (int i = 1, u, v, w; i <= m; ++i) {
        scanf("%d%d%d", &u, &v, &w);
        G.add(u, v, w), G.add(v, u, w);
    }
    tarjan(1, 0);
    dfs(1);
    for (int k = 1; k < lgN; ++k)
        for (int i = 1; i <= n; ++i) {
            fa[k][i] = fa[k - 1][fa[k - 1][i]];
            sum[k][i] = sum[k - 1][i] + sum[k - 1][fa[k - 1][i]];
        }
    for (int u, v; q--; ) {
        scanf("%d%d", &u, &v);
        printf("%d\n", query(u, v));
    }
    return 0;
}

正常圆方树,使用 vector 处理信息

cpp 复制代码
#include <cstdio>
#include <iostream>
#include <vector>
#include <cassert>
#include <algorithm>
using namespace std;

const int N = 1e4 + 10;
const int M = 2e4 + 10;
const int lg2N = __lg(N << 1) + 1;

int n, m, q;

template <size_t _N, size_t _M>
class Graph {
public:
    Graph(): tot(1) {}
    struct node {
        int v, w, nxt;
    } edge[_M];
    int head[_N], tot;
    void add(int u, int v, int w) {
        edge[++tot] = { v, w, head[u] };
        head[u] = tot;
    }
};

Graph<N, M << 1> G;
Graph<N << 1, N << 1> T;  // 圆方树

int dfn[N], low[N], timer;
int stack[N], top;

int pre[N];  // dfs 树的树上前缀和
int rev[N];  // 返祖边边权

vector<int> idx[N];
vector<int> dis[N];

int dcc_cnt;
int cirsum[N];  // 环的总环长

inline int query(int u, int v, int x) {
    // u, v 在编号为 x 的点双中的距离
    int ku = lower_bound(idx[u].begin(), idx[u].end(), x) - idx[u].begin();
    int kv = lower_bound(idx[v].begin(), idx[v].end(), x) - idx[v].begin();
    assert(idx[u][ku] == x && idx[v][kv] == x);
    int d = abs(dis[u][ku] - dis[v][kv]);
    return min(d, cirsum[x] - d);
}

void tarjan(int u, int fr) {
    dfn[u] = low[u] = ++timer;
    stack[++top] = u;
    for (int i = G.head[u]; i; i = G.edge[i].nxt) {
        if (i == (fr ^ 1)) continue;
        int v = G.edge[i].v, w = G.edge[i].w;
        if (!dfn[v]) {
            pre[v] = pre[u] + w;
            tarjan(v, i);
            low[u] = min(low[u], low[v]);
            if (low[v] >= dfn[u]) {
                ++dcc_cnt;
                cirsum[dcc_cnt] = pre[stack[top]] - pre[u] + rev[stack[top]];
                if (stack[top] == v)
                    cirsum[dcc_cnt] <<= 1;
                idx[u].push_back(dcc_cnt);
                dis[u].push_back(0);
                T.add(u, dcc_cnt + n, 0);
                while (true) {
                    int x = stack[top--];
                    idx[x].push_back(dcc_cnt);
                    dis[x].push_back(pre[x] - pre[u]);
                    T.add(dcc_cnt + n, x, query(u, x, dcc_cnt));
                    if (x == v) break;
                }
            }
        } else {
            low[u] = min(low[u], dfn[v]);
            if (dfn[v] < dfn[u])
                rev[u] = w;
        }
    }
}

int dpt[N << 1];
int fa[lg2N][N << 1];
int sum[lg2N][N << 1];

void dfs(int u) {
    for (int i = T.head[u]; i; i = T.edge[i].nxt) {
        int v = T.edge[i].v, w = T.edge[i].w;
        fa[0][v] = u;
        sum[0][v] = w;
        dpt[v] = dpt[u] + 1;
        dfs(v);
    }
}

int query(int u, int v) {
    int ans = 0;
    if (dpt[u] < dpt[v]) swap(u, v);
    for (int i = lg2N - 1; ~i; --i)
        if (fa[i][u] && dpt[fa[i][u]] >= dpt[v]) {
            ans += sum[i][u];
            u = fa[i][u];
        }
    if (u == v) return ans;
    for (int i = lg2N - 1; ~i; --i)
        if (fa[i][u] != fa[i][v]) {
            ans += sum[i][u] + sum[i][v];
            u = fa[i][u], v = fa[i][v];
        }
    // 此时 u,v 即为 u',v'
    int p = fa[0][u];  // lca
    if (p <= n)  // lca 为圆点
        ans += sum[0][u] + sum[0][v];  // 这一行其实没有作用,因为 sum[0][u]=sum[0][v]=0
    else
        ans += query(u, v, p - n);
    return ans;
}

int main() {
    scanf("%d%d%d", &n, &m, &q);
    for (int i = 1, u, v, w; i <= m; ++i) {
        scanf("%d%d%d", &u, &v, &w);
        G.add(u, v, w), G.add(v, u, w);
    }
    tarjan(1, 0);
    dfs(1);
    for (int k = 1; k < lg2N; ++k)
        for (int i = 1; i <= n + dcc_cnt; ++i) {
            fa[k][i] = fa[k - 1][fa[k - 1][i]];
            sum[k][i] = sum[k - 1][i] + sum[k - 1][fa[k - 1][i]];
        }
    for (int u, v; q--; ) {
        scanf("%d%d", &u, &v);
        printf("%d\n", query(u, v));
    }
    return 0;
}

接下来看有修改的问题。

对于前文分析的特殊点权相关信息,由于其对于仙人掌的形态没有要求,所以将在无向图部分例题十四给出一种实现方式。
例六、BUY on cactus(洛谷) Problem Statement

小 Y 来到一棵 \(n\) 个点 \(m\) 条边的仙人掌上旅游,并希望通过买卖赚差价,点编号为 \(1\sim n\)。

在一次旅游时,她告诉你旅游的起点和终点 \(u,v\)。她会选择一条以 \(u,v\) 为端点的简单路径 ,并在路上选择一个结点 \(u'\),花费 \(c_{u'}\) 的代价购入某个物品,再选择一个结点 \(v'\),以 \(w_{v'}\) 的价格卖出,获得 \(w_{v'}-c_{u'}\) 的利润。显然她必须先购入才能卖出,但 \(u'\) 可以等于 \(v'\)。她不能啥也不做。小 Y 问你,在最优计划下,她能够获得的利润最大值,这个值可能为负。

由于这棵仙人掌很不稳定,每一个点的 \(c_u,w_u\) 都可能发生变化。你需要支持这种修改的同时,能够回答小 Y 的问题。具体来说,总共有 \(q\) 次修改或询问。

\(2\leq n\leq2\times10^5\),\(1\leq q\leq 10^5\)。
Problem Analysis

购买类问题是十分经典的,我们就做过静态树上的问题:POJ-3728 The merchant

需要维护的信息是什么?千万不要矩阵学傻了,说需要维护矩阵。我们可以维护三元组 \((\mathrm{mic},\mathrm{mxw},\mathrm{ans})\),分别表示 minimum cost, maximum value and answer(最小花费、最大收益和路上的答案)。树上我们只需要考虑拼接两条简单路径的情况,有:

\\\begin{matrix} (\\mathrm{mic}_1,\\mathrm{mxw}_1,\\mathrm{ans}_1)+(\\mathrm{mic}_2,\\mathrm{mxw}_2,\\mathrm{ans}_2)\\\\ \\Downarrow\\\\ \\Big(\\min\\{\\mathrm{mic}_1,\\mathrm{mic}_2\\},\\max\\{\\mathrm{mxw}_1,\\mathrm{mxw}_2\\},\\max\\{\\mathrm{ans}_1,\\mathrm{ans}_2,\\mathrm{mxw}_2-\\mathrm{mic}_1\\}\\Big) \\end{matrix} \\

一步一步来看看这个模型怎么放到仙人掌上。

首先对于每一个环,可以拉下来变成一个序列,容易得到每一个点,通过左链和右链到环顶的两个信息,注意潜在的信息合并顺序问题。我们需要将这两个信息或起来,而不是上文的 \(+\) 运算:

\\\begin{matrix} (\\mathrm{mic}_1,\\mathrm{mxw}_1,\\mathrm{ans}_1)\\cup(\\mathrm{mic}_2,\\mathrm{mxw}_2,\\mathrm{ans}_2)\\\\ \\Downarrow\\\\ \\Big(\\min\\{\\mathrm{mic}_1,\\mathrm{mic}_2\\},\\max\\{\\mathrm{mxw}_1,\\mathrm{mxw}_2\\},\\max\\{\\mathrm{ans}_1,\\mathrm{ans}_2\\}\\Big) \\end{matrix} \\

我们于是可以得到它与方点父亲之间的边权,注意潜在的方向问题。

其实已经解决了整个问题。修改的时候修改重儿子,查询的时候跳重链,链顶轻儿子特殊查询。可以参考示例代码。

时间复杂度:\(\mathcal{O}(m+n\log n+q\log^2n)\)。单次修改 \(\mathcal{O}(\log n)\),单次查询 \(\mathcal{O}(\log^2n)\)。
Solution

cpp 复制代码
#include <cstdio>
#include <iostream>
#include <cstring>
#include <vector>
#include <cassert>
using namespace std;

const int N = 5e5 + 10;
const int M = 1e6 + 10;
const int N2 = N << 1;

const int inf = 0x3f3f3f3f;

int n, m, q;
int c[N], w[N];

struct node {
    int mic, mxw, a1, a2;
    void init(int c, int w) {
        mic = c, mxw = w;
        a1 = a2 = w - c;
    }
    void unit() {
        mic = inf, mxw = -inf;
        a1 = a2 = -inf;
    }
    friend inline node operator + (const node& a, const node& b) {
        node c;
        c.mic = min(a.mic, b.mic);
        c.mxw = max(a.mxw, b.mxw);
        c.a1 = max(a.a1, b.a1);
        c.a2 = max(a.a2, b.a2);
        c.a1 = max(c.a1, b.mxw - a.mic);
        c.a2 = max(c.a2, a.mxw - b.mic);
        return c;
    }
    friend inline node operator | (const node& a, const node& b) {
        node c;
        c.mic = min(a.mic, b.mic);
        c.mxw = max(a.mxw, b.mxw);
        c.a1 = max(a.a1, b.a1);
        c.a2 = max(a.a2, b.a2);
        return c;
    }
    inline node rev() const {
        return { mic, mxw, a2, a1 };
    }
};
inline node gen(int c, int w) {
    node r;
    r.init(c, w);
    return r;
}

template <size_t _N, size_t _M>
class Graph {
public:
    Graph(): tot(1) {}
    struct node {
        int v, nxt;
    } edge[_M];
    int head[_N], tot;
    void add(int u, int v) {
        edge[++tot] = { v, head[u] };
        head[u] = tot;
    }
};

Graph<N, M << 1> G;
Graph<N2, N2> T;

int dcc_cnt;

namespace $build {

int dfn[N], low[N], timer;
int stack[N], top;

void tarjan(int u, int fr) {
    dfn[u] = low[u] = ++timer;
    stack[++top] = u;
    for (int i = G.head[u]; i; i = G.edge[i].nxt) {
        if (i == (fr ^ 1)) continue;
        int v = G.edge[i].v;
        if (!dfn[v]) {
            tarjan(v, i);
            low[u] = min(low[u], low[v]);
            if (low[v] >= dfn[u]) {
                ++dcc_cnt;
                T.add(u, dcc_cnt + n);
                while (true) {
                    int x = stack[top--];
                    T.add(dcc_cnt + n, x);
                    if (x == v) break;
                }
            }
        } else {
            low[u] = min(low[u], dfn[v]);
        }
    }
}

}

class Segment_Tree {
private:
    struct _node {
        int ls, rs;
        node v;
    };
    vector<_node> buf;
    int n, rt[2], tot;
#define ls buf[u].ls
#define rs buf[u].rs
    inline void pushup(int u, bool f) {
        if (f)
            buf[u].v = buf[rs].v + buf[ls].v;
        else
            buf[u].v = buf[ls].v + buf[rs].v;
    }
    void _build(int& u, int l, int r, node val[], bool f) {
        u = ++tot;
        if (l == r) {
            if (f)
                buf[u].v = val[l].rev();
            else
                buf[u].v = val[l];
            return;
        }
        int mid = (l + r) >> 1;
        _build(ls, l, mid, val, f);
        _build(rs, mid + 1, r, val, f);
        pushup(u, f);
    }
    void _upd(int u, int l, int r, int p, node v, bool f) {
        if (l == r) {
            if (f)
                buf[u].v = v.rev();
            else
                buf[u].v = v;
            return;
        }
        int mid = (l + r) >> 1;
        if (p <= mid) _upd(ls, l, mid, p, v, f);
        else _upd(rs, mid + 1, r, p, v, f);
        pushup(u, f);
    }
    node query(int u, int trl, int trr, int l, int r) {
        if (l <= trl && trr <= r) return buf[u].v;
        int mid = (trl + trr) >> 1;
        if (r <= mid)
            return query(ls, trl, mid, l, r);
        if (l > mid)
            return query(rs, mid + 1, trr, l, r);
        return query(ls, trl, mid, l, r) + query(rs, mid + 1, trr, l, r);
    }
    node query_rev(int u, int trl, int trr, int l, int r) {
        if (l <= trl && trr <= r) return buf[u].v;
        int mid = (trl + trr) >> 1;
        if (r <= mid)
            return query_rev(ls, trl, mid, l, r);
        if (l > mid)
            return query_rev(rs, mid + 1, trr, l, r);
        return query_rev(rs, mid + 1, trr, l, r) + query_rev(ls, trl, mid, l, r);
    }
#undef ls
#undef rs
public:
    void build(int _n, node val[]) {
        n = _n, rt[0] = rt[1] = 0, tot = 0;
        buf.resize(n * 4 + 10);
        _build(rt[0], 1, n, val, 0);
        _build(rt[1], 1, n, val, 1);
    }
    void upd(int p, node v) {
        assert(1 <= p && p <= n);
        _upd(rt[0], 1, n, p, v, 0);
        _upd(rt[1], 1, n, p, v, 1);
    }
    node query(int l, int r) {
        assert(1 <= l && l <= r && r <= n);
        return query(rt[0], 1, n, l, r);
    }
    node query_rev(int l, int r) {
        assert(1 <= l && l <= r && r <= n);
        return query_rev(rt[1], 1, n, l, r);
    }
};

using tree_t = Segment_Tree;

int siz[N2], son[N2];
int fa[N2], top[N2], dpt[N2];
int idx[N2], dfn[N2], timer;
int bl[N], cir[N];

tree_t tr[N], yzh;

node getfa(int u) {
    int v = fa[u];
    assert(v != 0 && v > n);
    int m = cir[v - n];
    node L = tr[v - n].query(1, bl[u]);
    node R = tr[v - n].query_rev(bl[u], m);
    return L | R;
}

node getfa_rev(int u) {
    return getfa(u).rev();
}

node __val[N2];
void dfs(int u) {
    siz[u] = 1;
    for (int i = T.head[u]; i; i = T.edge[i].nxt) {
        int v = T.edge[i].v;
        fa[v] = u;
        dpt[v] = dpt[u] + 1;
        dfs(v), siz[u] += siz[v];
        if (siz[v] > siz[son[u]])
            son[u] = v;
    }
    if (u > n) {
        static node tmp[N];
        int m = 0;
        for (int i = T.head[u]; i; i = T.edge[i].nxt) {
            int v = T.edge[i].v;
            tmp[++m].init(c[v], w[v]);
            bl[v] = m;
        }
        cir[u - n] = m;
        tr[u - n].build(m, tmp);
    }
}

void redfs(int u, int tp) {
    top[u] = tp;
    dfn[idx[u] = ++timer] = u;
    if (u == tp || u > n)
        __val[idx[u]].unit();
    else
        __val[idx[u]] = getfa(u);
    if (son[u]) redfs(son[u], tp);
    for (int i = T.head[u]; i; i = T.edge[i].nxt) {
        int v = T.edge[i].v;
        if (v == son[u]) continue;
        redfs(v, v);
    }
}

node qcir(int u, int v) {
    assert(fa[u] == fa[v]);
    int p = fa[u];
    assert(p != 0 && p > n);
    assert(u != v);
    int bu = bl[u], bv = bl[v];
    int m = cir[p - n];
    if (bu <= bv) {
        node A = tr[p - n].query(bu, bv);
        node B = tr[p - n].query_rev(1, bu) + gen(c[fa[p]], w[fa[p]]) + tr[p - n].query_rev(bv, m);
        return A | B;
    } else {
        node A = tr[p - n].query_rev(bv, bu);
        node B = tr[p - n].query(bu, m) + gen(c[fa[p]], w[fa[p]]) + tr[p - n].query(1, bv);
        return A | B;
    }
}

int query(int u, int v) {
    int lu = 0, lv = 0;
    node L, R;
    L.unit(), R.unit();
    while (top[u] != top[v]) {
        if (dpt[top[u]] < dpt[top[v]]) {
            if (lv && lv <= n) R = getfa(lv) + R;
            R = yzh.query(idx[top[v]], idx[v]) + R;
            lv = top[v];
            v = fa[top[v]];
        } else {
            if (lu && lu <= n) L = L + getfa_rev(lu);
            L = L + yzh.query_rev(idx[top[u]], idx[u]);
            lu = top[u];
            u = fa[top[u]];
        }
    }
    if (u == v) {
        if (u <= n) {
            if (lu && lu <= n) L = L + getfa_rev(lu);
            if (lv && lv <= n) R = getfa(lv) + R;
            L = L + gen(c[u], w[u]);
        } else {
            assert(lu != 0 && lv != 0);
            L = L + qcir(lu, lv);
        }
    } else if (dpt[u] > dpt[v]) {
        if (v <= n) {
            if (lu && lu <= n) L = L + getfa_rev(lu);
            if (lv && lv <= n) R = getfa(lv) + R;
            L = L + yzh.query_rev(idx[v] + 1, idx[u]);
            L = L + gen(c[v], w[v]);
        } else {
            if (lu && lu <= n) L = L + getfa_rev(lu);
            assert(lv != 0);
            if (idx[v] + 2 <= idx[u])
                L = L + yzh.query_rev(idx[v] + 2, idx[u]);
            L = L + qcir(dfn[idx[v] + 1], lv);
        }
    } else {
        if (u <= n) {
            if (lu && lu <= n) L = L + getfa_rev(lu);
            if (lv && lv <= n) R = getfa(lv) + R;
            R = yzh.query(idx[u] + 1, idx[v]) + R;
            R = gen(c[u], w[u]) + R;
        } else {
            if (lv && lv <= n) R = getfa(lv) + R;
            assert(lu != 0);
            if (idx[u] + 2 <= idx[v])
                R = yzh.query(idx[u] + 2, idx[v]) + R;
            R = qcir(lu, dfn[idx[u] + 1]) + R;
        }
    }
    node ans = L + R;
    return ans.a1;
}

void modify(int u, int v, int t) {
    if (t == 0) c[u] = v;
    else w[u] = v;
    if (u != 1) {
        int p = fa[u];
        tr[p - n].upd(bl[u], gen(c[u], w[u]));
        int x = son[p];
        yzh.upd(idx[x], getfa(x));
    }
}

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1, u, v; i <= m; ++i) {
        scanf("%d%d", &u, &v);
        G.add(u, v), G.add(v, u);
    }
    for (int i = 1; i <= n; ++i)
        scanf("%d", &c[i]);
    for (int i = 1; i <= n; ++i)
        scanf("%d", &w[i]);
    $build::tarjan(1, 0);
    dfs(1);
    redfs(1, 1);
    yzh.build(n + dcc_cnt, __val);
    scanf("%d", &q);
    for (int op, u, v, t; q--; ) {
        scanf("%d%d%d", &op, &u, &v);
        if (op == 1) {
            printf("%d\n", query(u, v));
        } else {
            scanf("%d", &t);
            modify(u, v, t);
        }
    }
    return 0;
}

Gen

cpp 复制代码
#include <cstdio>
#include <iostream>
#include <vector>
#include <random>
#include <chrono>
using namespace std;

/* =========== Parameter =========== */

const int SEED = chrono::system_clock::now().time_since_epoch().count();

bool multiedge = true;
bool onlyOddCircle = false;

int MULTI_P = 50;

/* =========== Parameter =========== */

void _err(const char* msg, int lineNum) {
    fprintf(stderr, "Error at line #%d: %s\n", lineNum, msg);
    exit(1);
}
#define err(msg) _err(msg, __LINE__)

inline int rand(int l, int r) {
    static mt19937 rnd(SEED);
    if (l > r) err("invalid range");
    return l + rnd() % (r - l + 1);
}

int n = rand(199990, 200000);
int q = rand(99990, 100000);
int LEN = rand(13140, 23240);

int m_limit = -1;  // -1 for not limit

vector<pair<int, int>> edges;
vector<vector<int>> son(n, vector<int>());
vector<int> dpt(n);

int maxcir = LEN;
int chong = 0;

void dfs(int u) {
    for (size_t i = 0; i < son[u].size(); ++i) {
        int v = son[u][i];
        dpt[v] = dpt[u] + 1;
        dfs(v);
    }
}

int redfs(int u, int ban, vector<int>& vec) {
    vec.emplace_back(u);
    int res = u;
    for (size_t i = 0; i < son[u].size(); ++i) {
        int v = son[u][i];
        if (v == ban) continue;
        int t = redfs(v, -1, vec);
        int len = dpt[t] - dpt[u] + 1;
        if ((t != v || (multiedge && rand(0, MULTI_P) == 0))
                && (len % 2 == 1 || !onlyOddCircle)
                && (m_limit == -1 || (int)edges.size() < m_limit))
            edges.emplace_back(u, t), maxcir = max(maxcir, len), chong += t == v;
        else if (rand(0, son[u].size()) == 0 || dpt[t] > dpt[res])
            res = t;
    }
    return res;
}

int main() {
    freopen(".\\testdata\\10.in", "w", stdout);
    
    // freopen("yzh", "w", stdout);
    // freopen("in", "w", stdout);
    
    if (n < 1) err("n shouldn't be less than 1");
    if (m_limit != -1 && m_limit < n - 1)
        err("m_limit shouldn't less than n-1");
    if (LEN > n)
        err("LEN too long");
    
    vector<int> id(n);
    for (int i = 0; i < n; ++i) id[i] = i;
    for (int i = 1; i < n; ++i)
        swap(id[i], id[rand(0, i)]);
    
    for (int i = 1; i < LEN; ++i) {
        int fa = i - 1;
        edges.emplace_back(fa, i);
        son[fa].emplace_back(i);
    }
    for (int i = LEN; i < n; ++i) {
        // int fa = rand(0, i - 1);
        int fa = rand(max(0, i - 20), i - 1);
        edges.emplace_back(fa, i);
        son[fa].emplace_back(i);
    }
    if (LEN > 1 && m_limit != n - 1) {
        edges.emplace_back(0, LEN - 1);
    }
    dfs(0);
    vector<int> son[n];
    for (int i = 1; i < LEN; ++i)
        redfs(i - 1, i, son[i - 1]);
    redfs(LEN - 1, -1, son[LEN - 1]);
    
    for (size_t i = 1; i < edges.size(); ++i)
        swap(edges[i], edges[rand(0, i)]);
    
    printf("%d %d\n", n, (int)edges.size());
    for (size_t i = 0; i < edges.size(); ++i) {
        int u = edges[i].first;
        int v = edges[i].second;
        if (rand(0, 1)) swap(u, v);
        u = id[u], v = id[v];
        printf("%d %d\n", u + 1, v + 1);
    }
    
    const int V = 5e8;
    
    for (int i = 1; i <= n; ++i)
        printf("%d%c", rand(-V, V), " \n"[i == n]);
    for (int i = 1; i <= n; ++i)
        printf("%d%c", rand(-V, V), " \n"[i == n]);
    
    printf("%d\n", q);
    for (int i = 1; i <= q; ++i) {
        int op = rand(1, 2);
        if (op == 1) {
            if (rand(0, 100) == 0) {
                int u = rand(1, n);
                int v = rand(1, n);
                printf("1 %d %d\n", u, v);
            } else {
                int uu = rand(0, LEN - 1 - 1);
                int vv = rand(uu + 1, LEN - 1);
                int u = son[uu][rand(0, (int)son[uu].size() - 1)];
                int v = son[vv][rand(0, (int)son[vv].size() - 1)];
                u = id[u] + 1;
                v = id[v] + 1;
                printf("1 %d %d\n", u, v);
            }
        } else {
            int u = rand(1, n);
            int v = rand(-V, V);
            int t = rand(0, 1);
            printf("2 %d %d %d\n", u, v, t);
        }
    }
    
    fprintf(stderr, "Success!\n");
    fprintf(stderr, "n = %d, m = %d\n", n, (int)edges.size());
    fprintf(stderr, "circle = %d\n", (int)edges.size() - (n - 1));
    fprintf(stderr, "maxcir = %d\n", maxcir);
    fprintf(stderr, "chong = %d\n", chong);
    return 0;
}

类型三:静态仙人掌,支持维护链和子仙人掌

回想我们树上树链剖分可以支持什么操作?树链、子树修改,树链、子树查询。那么由于仙人掌具有一定树的形态,我们也可以将仙人掌剖分,实现长链(短链)修改、子仙人掌查询。接下来考虑的信息均是点权。

回顾我们是如何定义仙人掌上的链的,以及不要忘记,在仙人掌中每经过一个奇环时,若始终选择在环上走最长(或最短)的路径,则整条走出的路径即为 \(u, v\) 间的长链(或短链)。

我们不妨先只考虑如何修改 \(u,v\) 间的长链。拆解问题,如果 \(p=\operatorname{lca}(u,v)\) 是个圆点,相当于修改 \(u\rightarrow p\) 和 \(p\rightarrow v\) 两条长链;否则 \(p\) 为方点,除了 \(u\rightarrow u'\) 和 \(v'\rightarrow v\) 这两个和 \(p\) 为圆点时形式相同的子问题,还有一个环上 \(u'\rightarrow v'\) 最长路的问题。

先考虑如何修改一对具有祖先关系的 \(u,v\) 之间的长链。树链剖分后,在圆方树上跳一段重链时,DFS 序上对应的连续一段区间为圆方树上这条重链。树上我们需要的正是一段连续的区间,因为这样我们就可以用数据结构来维护了,但在仙人掌上,这是不对的,我们要修改的不是这样一条圆方交错的圆方树树链,而是需要修改仙人掌上长链的每一个点。

我们不妨修改一下这个 DFS 序,使得对于这样连续一段区间,完整包含了我们需要修改的点。并且显然我们不需要方点出现在 DFS 序上,因为它身上没有我们需要维护的信息。

对于方点,我们先把其轻儿子放到 DFS 序后面,再放其重儿子,接着处理重儿子的子仙人掌,最后处理其他轻儿子的子仙人掌。从 DFS 序的角度上考虑,相当于把一个方点替换成了它的所有轻孩子,那么这个方点对应的环就完整地出现在了我们的区间上。

举个例子,比如 \(1\sim 9\) 这条重链,原先 DFS 序上对应区间为 \(1,7=\{1,16,2,15,5,14,9\}\)。而在修改后,方点 \(15\) 变为了其所有轻儿子 \(\{11,4,3\}\),\(14\) 同理变成了 \(6\),修改后的 DFS 序对应区间为 \(1,8=\{1,2,11,4,3,5,6,9\}\),涵盖了 \(1\sim 9\) 长链上的所有点 \(\{1,2,3,4,5,6,9\}\)。

完整包含了还不够,我们还需要能够精准的修改想要修改的点。例如,例子中 \(11\) 点出现在了区间上,而它不在 \(1\sim 9\) 的长链上,这等价于它不在 \(2\sim 5\) 环上最长路径上。由于重儿子的唯一性,这启示我们将一个方点的所有轻儿子分类,分为「在重儿子到父亲圆点(原图上)的最长路上」和「在重儿子到父亲圆点(原图上)的最短路上」两种类型,这是方便处理出来的。例如,\(11\) 属于后者,而 \(3,4\) 属于前者。现在我们区间上有三种类型的圆点,因为不要忘了重儿子也是一种圆点。每次我们只需要将对应类型的轻孩子,和所有重儿子修改就行了。这个是可以用线段树维护的。

至此,我们通过修改 DFS 序,和使用线段树的区间分类修改操作,完成了「修改一段重链对应原仙人掌上的长 / 短链」。

仔细思考一下,在将其推广到一对具有祖孙关系的节点的链修改时,还有细节没有处理。重链拆分后,对于 \(v\rightarrow u\) 这条重链(\(v\) 是链顶),当 \(u\) 为圆点且 \(v\) 为方点时,根据上面讨论,对应区间为 \(v\) 第一个轻儿子在 DFS 序的位置,到 \(u\) 在 DFS 序中的位置。当 \(v\) 也为圆点时,我们不能将区间左端点设为 \(v\) 在 DFS 序中的位置,如下图:

它的 DFS 序如下:

\x_3,\\colorbox{#409eff}{$ (x_5) $},\\colorbox{#409eff}{$ x_4,v,x_1,x_2,\\colorbox{#e6a23c}{$ (x_2) $},\\colorbox{#e6a23c}{$ (x_4) $},\\colorbox{#e6a23c}{$ ...,u $},\\colorbox{#e6a23c}{$ (x_1) $} $} \\

如果我们取出的区间为:

\v,x_1,x_2,\\colorbox{#e6a23c}{$ (x_2) $},\\colorbox{#e6a23c}{$ (x_4) $},\\colorbox{#e6a23c}{$ ...,u $} \\

就会出错,因为包含了 \((x_2),(x_4)\) 这两个无关的子仙人掌对应的区间,以及 \(x_1,x_2\) 不一定就是我们需要修改的点,如果我们需要修改长链,就需要修改 \(x_4\),而它却没有出现在我们取出的区间上。

我们可以特殊处理。对于 \(...,u\),这是链顶为方点的情况,正常处理即可。剩下就是在 \(w\) 对应的环上,修改 \(v\sim x_3\) 的长链 / 短链,这和 LCA 处的问题形式相同。接下来,我们就可以在 \(x_3\) 继续往上跳重链。

如果 \(u\) 为方点呢?注意到,我们上述过程实际保证了 \(u\) 时刻为圆点,就不需要考虑 \(u\) 为方点的情况了。

我们已经可以「修改一对具有祖孙关系 \(v,u\) 间的长 / 短链」。在修改过的 DFS 序上,考虑能不能支持 LCA 处的操作,即修改环上一段连续的链。

以上图为例,我们假设这个点双对应的方点为 \(u\),\(u\) 的父亲圆点为 \(1\),重儿子为 \(5\)。按照我们上面的定义,\(2,3,4\) 在 \(5\) 到 \(1\) 的最长路上,是一类轻儿子,\(6,7\) 则是另一类轻儿子。这个环对应 DFS 序可以为 \(1,{\color{red}7},{\color{yellow}2},{\color{yellow}4},{\color{red}6},{\color{yellow}3},5\),也可以为 \(1,{\color{red}7},{\color{red}6},{\color{yellow}2},{\color{yellow}3},{\color{yellow}4},5\) 或者 \(1,{\color{yellow}4},{\color{yellow}3},{\color{yellow}2},{\color{red}6},{\color{red}7},5\),上述三种 DFS 序都是符合目前我们要求的,但是为了解决 LCA 处的问题,我们需要环上连续一段链在 DFS 序上被拆分为 \(\mathcal{O}(1)\) 个区间,也就希望 DFS 序为后两者,或类似后两者的形态。很容易在类似后两种 DFS 序的形态中,拆出我们想要的区间。例如考虑 DFS 为 \(1,{\color{red}7},{\color{red}6},{\color{yellow}2},{\color{yellow}3},{\color{yellow}4},5\),那么 \(3\rightarrow7\) 的长链为 \(3,4,5,6,7\),对应 DFS 序区间是 \(5,6\cup7,7\cup2,3\)。

剩下只有子仙人掌操作没有解决了,但是我们惊讶地发现,在修改后的 DFS 序上,在一定程度上很好地保留了 DFS 序一段区间对应子树这个性质,而子仙人掌就是圆方树的子树。具体来说,我们发现 \(u\) 的子仙人掌被拆分成了两部分,一部分是 \(u\) 本身这个点,另一部分为它的不包括本身的子仙人掌。那么只需要分别查询这两段区间的信息就行了。

对于代码实现上的细节,请参考例题代码。时间复杂度瓶颈在于线段树。
例七、【清华集训2015】静态仙人掌(UOJ) Problem Statement

一棵 \(n\) 个点、\(m\) 条边的仙人掌,指定 \(1\) 为根。每个结点有一个颜色(黑或白),初始时均为黑色。有 \(q\) 次操作:

  1. 将 \(u\) 到根的最短路径 / 最长路径上所有点的颜色取反。
  2. 询问点 \(u\) 子仙人掌中的黑点数目。

\(n,q\leq 5\times10^4\)。
Problem Analysis

问题严格弱于前文的分析的模型,可以用线段树方便维护。可以参考示例代码的具体实现。

时间复杂度:\(\mathcal{O}((n+q)\log n)\)。
Solution

cpp 复制代码
#include <cstdio>
#include <iostream>
#include <vector>
using namespace std;

const int N = 5e4 + 10;
const int M = 1e5 + 10;
const int N2 = N << 1;

int n, m, q;

template <size_t _N, size_t _M>
class Graph {
public:
    Graph(): tot(1) {}
    struct node {
        int v, nxt;
    } edge[_M];
    int head[_N], tot;
    void add(int u, int v) {
        edge[++tot] = { v, head[u] };
        head[u] = tot;
    }
};

Graph<N, M << 1> G;
Graph<N2, N2> T;

int dcc_cnt;

namespace $build {

int dfn[N], low[N], timer;
int stack[N], top;

void tarjan(int u, int fr) {
    dfn[u] = low[u] = ++timer;
    stack[++top] = u;
    for (int i = G.head[u]; i; i = G.edge[i].nxt) {
        if (i == (fr ^ 1)) continue;
        int v = G.edge[i].v;
        if (!dfn[v]) {
            tarjan(v, i);
            low[u] = min(low[u], low[v]);
            if (low[v] >= dfn[u]) {
                ++dcc_cnt;
                T.add(u, dcc_cnt + n);
                while (true) {
                    int x = stack[top--];
                    T.add(dcc_cnt + n, x);
                    if (x == v) break;
                }
            }
        } else {
            low[u] = min(low[u], dfn[v]);
        }
    }
}

}

int siz[N2], son[N2];
int fa[N2], top[N2];
int dfn[N], idx[N], L[N2], R[N2], timer;
int dis[N2], typ[N];
/*
dfn:修改后的 DFS 序
对于圆点:
    idx[u]:在 DFS 序上位置
    L[u]~R[u]:子树(不包括本身)在 DFS 序上的一段区间,或 L[u]=R[u]+1 为空
    dis[u]:环上到环首距离
    typ[u]:类型,0 为重儿子,1 为
对于方点:
    L[u]~R[u]:孩子(轻儿子和重儿子)在 DFS 序上的一段区间
    dis[u]:总环长
*/

void dfs(int u) {
    siz[u] = 1;
    for (int i = T.head[u]; i; i = T.edge[i].nxt) {
        int v = T.edge[i].v;
        fa[v] = u;
        dfs(v), siz[u] += siz[v];
        if (siz[v] > siz[son[u]])
            son[u] = v;
    }
}

void redfs(int u, int tp) {
    top[u] = tp;
    if (u <= n) {
        L[u] = timer + 1;
        if (son[u]) redfs(son[u], tp);
        for (int i = T.head[u]; i; i = T.edge[i].nxt) {
            int v = T.edge[i].v;
            if (v == son[u]) continue;
            redfs(v, v);
        }
        R[u] = timer;
    } else {
        for (int i = T.head[u]; i; i = T.edge[i].nxt) {
            int v = T.edge[i].v;
            dis[v] = ++dis[u];
        }
        ++dis[u];
        L[u] = timer + 1;
        int t = dis[son[u]] <= dis[u] / 2 ? 1 : 2;
        for (int i = T.head[u]; i; i = T.edge[i].nxt) {
            int v = T.edge[i].v;
            if (v == son[u]) t ^= 3;
            else {
                dfn[idx[v] = ++timer] = v;
                typ[v] = t;
            }
        }
        R[u] = timer;
        dfn[idx[son[u]] = ++timer] = son[u];
        typ[son[u]] = 0;
        redfs(son[u], tp);
        for (int i = T.head[u]; i; i = T.edge[i].nxt) {
            int v = T.edge[i].v;
            if (v == son[u]) continue;
            redfs(v, v);
        }
    }
}

namespace $tree {

#define lson (idx << 1)
#define rson (idx << 1 | 1)

struct node {
    int tot[3], val[3];
    bool lazy[3];
} tr[N2 << 2];

void build(int idx, int l, int r) {
    if (l == r) {
        int u = dfn[l];
        if (u <= n) {
            int t = typ[u];
            tr[idx].tot[t] = tr[idx].val[t] = 1;
        }
        return;
    }
    int mid = (l + r) >> 1;
    build(lson, l, mid);
    build(rson, mid + 1, r);
    for (int i = 0; i < 3; ++i) {
        tr[idx].tot[i] = tr[lson].tot[i] + tr[rson].tot[i];
        tr[idx].val[i] = tr[lson].val[i] + tr[rson].val[i];
    }
}

inline void pushtag(int idx, int o) {
    tr[idx].lazy[o] ^= 1;
    tr[idx].val[o] = tr[idx].tot[o] - tr[idx].val[o];
}

inline void pushdown(int idx) {
    for (int o = 0; o < 3; ++o) {
        if (!tr[idx].lazy[o]) continue;
        pushtag(lson, o);
        pushtag(rson, o);
        tr[idx].lazy[o] = 0;
    }
}

void modify(int idx, int trl, int trr, int l, int r, int o) {
    if (l > r) throw;
    if (l <= trl && trr <= r) {
        if (o == 0) {
            for (int i = 0; i < 3; ++i) pushtag(idx, i);
        } else
            pushtag(idx, o), pushtag(idx, 0);
        return;
    }
    pushdown(idx);
    int mid = (trl + trr) >> 1;
    if (l <= mid) modify(lson, trl, mid, l, r, o);
    if (r >  mid) modify(rson, mid + 1, trr, l, r, o);
    for (int i = 0; i < 3; ++i)
        tr[idx].val[i] = tr[lson].val[i] + tr[rson].val[i];
}

int query(int idx, int trl, int trr, int l, int r) {
    if (l > r) throw;
    if (l <= trl && trr <= r)
        return tr[idx].val[0] + tr[idx].val[1] + tr[idx].val[2];
    pushdown(idx);
    int mid = (trl + trr) >> 1;
    if (r <= mid) return query(lson, trl, mid, l, r);
    if (l >  mid) return query(rson, mid + 1, trr, l, r);
    return query(lson, trl, mid, l, r) + query(rson, mid + 1, trr, l, r);
}

#undef lson
#undef rson

}

void modify(int u, int op) {
    // 在跳重链的时候,需要时刻保持 u 为圆点,这是为了简化讨论
    while (top[u] != 1) {
        int v = top[u];
        if (v > n) {
            $tree::modify(1, 1, n, L[v], idx[u], op);
            u = fa[v];
        } else {
            if (u != v)
                $tree::modify(1, 1, n, L[son[v]], idx[u], op);
            u = fa[v];
            int o = dis[v] <= dis[u] / 2 ? 1 : 2;
            if (o == op) {
                $tree::modify(1, 1, n, L[u], idx[v], 0);
                if (dis[son[u]] <= dis[v])
                    $tree::modify(1, 1, n, idx[son[u]], idx[son[u]], 0);
            } else {
                $tree::modify(1, 1, n, idx[v], R[u], 0);
                if (dis[son[u]] >= dis[v])
                    $tree::modify(1, 1, n, idx[son[u]], idx[son[u]], 0);
            }
            u = fa[u];
        }
    }
    $tree::modify(1, 1, n, idx[1], idx[u], op);
}

int query(int u) {
    int res = 0;
    res += $tree::query(1, 1, n, idx[u], idx[u]);
    if (L[u] <= R[u])
        res += $tree::query(1, 1, n, L[u], R[u]);
    return res;
}

int main() {
    scanf("%d%d%d", &n, &m, &q);
    for (int i = 1, u, v; i <= m; ++i) {
        scanf("%d%d", &u, &v);
        G.add(u, v), G.add(v, u);
    }
    $build::tarjan(1, 0);
    dfs(1);
    dfn[idx[1] = ++timer] = 1;
    redfs(1, 1);
    $tree::build(1, 1, n);
    for (int op, u; q--; ) {
        scanf("%d%d", &op, &u);
        if (op <= 2) modify(u, op);
        else printf("%d\n", query(u));
    }
    return 0;
}

类型四:静态仙人掌,求给定点集的整体信息

这类问题每次询问会给出一个点集,求这个点集的整体信息,比如统计一些东西,做一些 DP 什么的,类似于类型一,只不过只关于点集中的点了。

对于树上给定点集的问题,我们可以建出这些点的虚树,在虚树上统计答案,做树形 DP。由于点集 \(S\) 对应虚树结点个数是 \(\mathcal{O}(|S|)\) 的,时间复杂度得到了保证。

唯一需要注意的是,对于方点 \(u\),其虚树上一个孩子 \(v\),那么不能想当然地认为 \(v\) 是 \(u\) 的孩子圆点,事实上,\(v\) 并不一定是其孩子圆点,甚至连个圆点都可能不是。所以,我们需要通过一些方法求出 \(v'\),这个才是 \(u\) 的孩子圆点。
例八、mx 的仙人掌(UOJ) Problem Statement

一棵 \(n\) 个点、\(m\) 条边的仙人掌。

\(q\) 此询问,每次给出一个点集 \(S\)。求出从 \(S\) 中选出两个点 \(u,v\),他们之间最短路的最大值是多少,\(u,v\) 可以相同。

\(n,\sum|S|\leq3\times10^5\)。
Problem Analysis

每次可以 \(\mathcal{O}(n)\) 类似类型一求直径,做一遍圆方树上的树形 DP。

那么先把虚树建出来。然后考虑在 LCA 处统计答案,即枚举 LCA \(p\),考虑它两个孩子的子树中的点 \(u,v\) 对答案的贡献。分 \(p\) 为方点、圆点讨论。

  1. \(p\) 为圆点:
    此时 \(u,v\) 之间的最短路即为 \(s_u+s_v-2s_p\),其中 \(s_u\) 表示圆方树边权的树上前缀和。我们只需要求出 \(s_u+s_v\) 的最大值。这个很容易,对于每个点 \(u\) 维护 \(f_u\) 表示子树中 \(s_u\) 的最大值,然后就可以统计了。
  2. \(p\) 为方点:
    设 \(u',v'\) 表示 \(p\) 在圆方树上两个孩子,对应为 \(u,v\) 的祖先。\(u',v'\) 可以 kth-kather 求出。此时 \(u,v\) 之间的最短路为 \(s_u-s_{u'}+s_v-s_{v'} + \min\{|d_{v'}-d_{u'}|,\operatorname{sum}(p)-|d_{v'}-d_{u'}|\}\),其中 \(\operatorname{sum}(p)\) 表示方点 \(p\) 对应的环的总长,\(d_u\) 表示 \(u\) 到环首的距离。相当于 \(p\) 的每个孩子 \(u\) 有一个点权 \(w_u=f_u-s_u\),要求 \(\max\limits_{u,v\in\operatorname{son}(p)}\Bigg\{w_u+w_v + \min\Big\{|d_v-d_u|,\operatorname{sum}(p)-|d_v-d_u|\Big\}\Bigg\}\),这个可以拆换成链,再复制一份接在后面,然后就能单调队列做了。

时间复杂度为:\(\mathcal{O}(m+n\log n+\sum|S|(\log|S|+\log n))\)。
Solution

cpp 复制代码
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;

using ll = long long;

const int N = 3e5 + 10;
const int M = N * 3 + 10;
const int NN = N << 1;
const int lg2N = __lg(NN) + 1;

int n, m, q;

template <size_t _N, size_t _M, typename T>
class Grpah {
public:
    Grpah(): tot(1) {}
    struct node {
        int v, nxt;
        T w;
    } edge[_M];
    int head[_N], tot;
    void add(int u, int v, const T& w) {
        edge[++tot] = { v, head[u], w };
        head[u] = tot;
    }
};
Grpah<N, M << 1, int> G;  // original grpah
Grpah<N << 1, N << 1, ll> T;  // rebuild tree

template <size_t _N, size_t _M>
class Graph_ {
public:
    Graph_(): tot(1) {}
    struct node {
        int v, nxt;
    } edge[_M];
    int head[_N], tot;
    void add(int u, int v) {
        edge[++tot] = { v, head[u] };
        head[u] = tot;
    }
};
Graph_<N << 1, N << 1> VT;  // virtual tree

int dcc_cnt;
ll cirsum[N], dis[N];

namespace $build {

int dfn[N], low[N], timer;
int stack[M], top;  // 注意这里的边栈的数组大小

void tarjan(int u, int fr) {
    dfn[u] = low[u] = ++timer;
    for (int i = G.head[u]; i; i = G.edge[i].nxt) {
        if (i == (fr ^ 1)) continue;
        int v = G.edge[i].v;
        if (!dfn[v]) {
            stack[++top] = i;
            tarjan(v, i);
            low[u] = min(low[u], low[v]);
            if (low[v] >= dfn[u]) {
                static int tmp[N];
                int tot = 0;
                ++dcc_cnt;
                while (true) {
                    int j = stack[top--];
                    int uu = G.edge[j ^ 1].v;
                    int vv = G.edge[j].v;
                    int w = G.edge[j].w;
                    cirsum[dcc_cnt] += w;
                    if (j == i) break;
                    dis[uu] = dis[vv] + w;
                    tmp[++tot] = uu;
                }
                if (tot == 0) {
                    tmp[++tot] = v;
                    dis[v] = cirsum[dcc_cnt];
                    cirsum[dcc_cnt] <<= 1;
                }
                T.add(u, dcc_cnt + n, 0);
                for (int j = 1; j <= tot; ++j) {
                    int x = tmp[j];
                    T.add(dcc_cnt + n, x, min(dis[x], cirsum[dcc_cnt] - dis[x]));
                }
            }
        } else {
            low[u] = min(low[u], dfn[v]);
            if (dfn[v] < dfn[u])
                stack[++top] = i;
        }
    }
}

}

int dpt[NN], fa[NN], timer, kthfa[lg2N][NN];
int idx[NN], st[lg2N][NN];
ll sum[NN];  // tree prefix sum

void dfs(int u) {
    st[0][idx[u] = ++timer] = u;
    for (int i = T.head[u]; i; i = T.edge[i].nxt) {
        int v = T.edge[i].v;
        ll w = T.edge[i].w;
        dpt[v] = dpt[u] + 1;
        fa[v] = kthfa[0][v] = u;
        sum[v] = sum[u] + w;
        dfs(v);
    }
}
inline int Min(int a, int b) {
    return dpt[a] < dpt[b] ? a : b;
}
inline int lca(int u, int v) {
    if (u == v) return u;
    if ((u = idx[u]) > (v = idx[v])) swap(u, v);
    int k = __lg(v - u++);
    return fa[Min(st[k][u], st[k][v - (1 << k) + 1])];
}
inline int jump(int u, int k) {
    for (int i = lg2N - 1; ~i; --i)
        if (k & 1 << i) u = kthfa[i][u];
    return u;
}

ll ans, f[NN];
int bl[NN];  // 设 v 为 u 在虚树上的父亲,bl[u] 为 v 的一个在圆方树上的孩子,且为 u 的祖先

void redfs(int u) {
    f[u] = sum[u];
    if (u <= n) {
        for (int i = VT.head[u]; i; i = VT.edge[i].nxt) {
            int v = VT.edge[i].v;
            redfs(v);
            ans = max(ans, f[v] + f[u] - 2 * sum[u]);
            f[u] = max(f[u], f[v]);
        }
    } else {
        for (int i = VT.head[u]; i; i = VT.edge[i].nxt) {
            int v = VT.edge[i].v;
            redfs(v);
            f[u] = max(f[u], f[v]);
        }
        // do algorithm after dfs!
        static int son[N];
        static ll d[NN], w[NN];
        static int Q[NN];
        int cnt = 0;
        for (int i = VT.head[u]; i; i = VT.edge[i].nxt) {
            int v = VT.edge[i].v;
            son[++cnt] = v;
        }
        sort(son + 1, son + cnt + 1, [] (int a, int b) {
            return dis[bl[a]] < dis[bl[b]];
        });
        ll tot = cirsum[u - n];
        for (int i = 1; i <= cnt; ++i) {
            int v = son[i];
            int tv = bl[v];
            d[i] = dis[tv];
            w[i] = f[v] - sum[tv];
            d[cnt + i] = d[i] + tot;
            w[cnt + i] = w[i];
        }
        int head = 0, tail = -1;
        for (int i = 1; i <= cnt << 1; ++i) {
            while (head <= tail && d[Q[head]] < d[i] - tot/2) ++head;
            if (head <= tail) ans = max(ans, w[Q[head]] + w[i] + d[i] - d[Q[head]]);
            while (head <= tail && w[Q[tail]] - d[Q[tail]] <= w[i] - d[i]) --tail;
            Q[++tail] = i;
        }
        
        // for (int i = 1; i <= cnt; ++i)
        //     for (int j = i + 1; j <= cnt; ++j) {
        //         ll D = abs(d[i] - d[j]);
        //         ans = max(ans, w[i] + w[j] + min(D, tot - D));
        //     }
    }
}

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1, u, v, w; i <= m; ++i) {
        scanf("%d%d%d", &u, &v, &w);
        G.add(u, v, w);
        G.add(v, u, w);
    }
    $build::tarjan(1, 0);
    dfs(1);
    for (int k = 1; k < lg2N; ++k) {
        for (int i = 1; i + (1 << k) - 1 <= n + dcc_cnt; ++i)
            st[k][i] = Min(st[k - 1][i], st[k - 1][i + (1 << (k - 1))]);
        for (int i = 1; i <= n + dcc_cnt; ++i)
            kthfa[k][i] = kthfa[k - 1][kthfa[k - 1][i]];
    }
    scanf("%d", &q);
    for (int k; q--; ) {
        static int st[NN];
        scanf("%d", &k);
        for (int i = 1; i <= k; ++i)
            scanf("%d", &st[i]);
        sort(st + 1, st + k + 1, [] (int a, int b) {
            return idx[a] < idx[b];
        });
        for (int i = 1; i < k; ++i)
            st[k + i] = lca(st[i], st[i + 1]);
        sort(st + 1, st + k * 2, [] (int a, int b) {
            return idx[a] < idx[b];
        });
        int m = unique(st + 1, st + k * 2) - st - 1;
        for (int i = 1; i < m; ++i) {
            int u = st[i + 1], v = lca(st[i], st[i + 1]);
            VT.add(v, u);
            if (v > n) {
                int _u = jump(u, dpt[u] - dpt[v] - 1);
                bl[u] = _u;
            }
        }
        ans = 0;
        redfs(st[1]);
        printf("%lld\n", ans);
        VT.tot = 1;
        for (int i = 1; i <= m; ++i) {
            int u = st[i];
            VT.head[u] = 0;
        }
    }
    return 0;
}

使用 $\\mathcal{O}(1)$ 查找 $u',v'$ 算法(https://www.cnblogs.com/XuYueming/p/18919143)

cpp 复制代码
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <tuple>
using namespace std;

using ll = long long;

const int N = 3e5 + 10;
const int M = N * 3 + 10;
const int NN = N << 1;
const int lg2N = __lg(NN) + 1;

int n, m, q;

template <size_t _N, size_t _M, typename T>
class Grpah {
public:
    Grpah(): tot(1) {}
    struct node {
        int v, nxt;
        T w;
    } edge[_M];
    int head[_N], tot;
    void add(int u, int v, const T& w) {
        edge[++tot] = { v, head[u], w };
        head[u] = tot;
    }
};
Grpah<N, M << 1, int> G;  // original grpah
Grpah<N << 1, N << 1, ll> T;  // rebuild tree

template <size_t _N, size_t _M>
class Graph_ {
public:
    Graph_(): tot(1) {}
    struct node {
        int v, nxt;
    } edge[_M];
    int head[_N], tot;
    void add(int u, int v) {
        edge[++tot] = { v, head[u] };
        head[u] = tot;
    }
};
Graph_<N << 1, N << 1> VT;  // virtual tree

int dcc_cnt;
ll cirsum[N], dis[N];

namespace $build {

int dfn[N], low[N], timer;
int stack[M], top;  // 注意这里的边栈的数组大小

void tarjan(int u, int fr) {
    dfn[u] = low[u] = ++timer;
    for (int i = G.head[u]; i; i = G.edge[i].nxt) {
        if (i == (fr ^ 1)) continue;
        int v = G.edge[i].v;
        if (!dfn[v]) {
            stack[++top] = i;
            tarjan(v, i);
            low[u] = min(low[u], low[v]);
            if (low[v] >= dfn[u]) {
                static int tmp[N];
                int tot = 0;
                ++dcc_cnt;
                while (true) {
                    int j = stack[top--];
                    int uu = G.edge[j ^ 1].v;
                    int vv = G.edge[j].v;
                    int w = G.edge[j].w;
                    cirsum[dcc_cnt] += w;
                    if (j == i) break;
                    dis[uu] = dis[vv] + w;
                    tmp[++tot] = uu;
                }
                if (tot == 0) {
                    tmp[++tot] = v;
                    dis[v] = cirsum[dcc_cnt];
                    cirsum[dcc_cnt] <<= 1;
                }
                T.add(u, dcc_cnt + n, 0);
                for (int j = 1; j <= tot; ++j) {
                    int x = tmp[j];
                    T.add(dcc_cnt + n, x, min(dis[x], cirsum[dcc_cnt] - dis[x]));
                }
            }
        } else {
            low[u] = min(low[u], dfn[v]);
            if (dfn[v] < dfn[u])
                stack[++top] = i;
        }
    }
}

}

int dpt[NN], fa[NN], timer;
int idx[NN], st[lg2N][NN];
ll sum[NN];  // tree prefix sum

void dfs(int u) {
    st[0][idx[u] = ++timer] = u;
    for (int i = T.head[u]; i; i = T.edge[i].nxt) {
        int v = T.edge[i].v;
        ll w = T.edge[i].w;
        dpt[v] = dpt[u] + 1;
        fa[v] = u;
        sum[v] = sum[u] + w;
        dfs(v);
    }
}
inline int Min(int a, int b) {
    return dpt[a] < dpt[b] ? a : b;
}
inline tuple<int, int, int> lca(int u, int v) {
    if (u == v) return { 0, 0, u };
    int U, V, p, iu = idx[u], iv = idx[v];
    bool F = false;
    if (iu > iv) swap(iu, iv), swap(u, v), F = true;
    int k = __lg(iv - iu);
    V = Min(st[k][iu + 1], st[k][iv - (1 << k) + 1]);
    p = fa[V];
    if (p == u) U = 0;
    else {
        int ip = idx[p];
        k = __lg(iu - ip++);
        U = Min(st[k][ip], st[k][iu - (1 << k) + 1]);
    }
    if (F) swap(U, V);
    return { U, V, p };
}

ll ans, f[NN];
int bl[NN];  // 设 v 为 u 在虚树上的父亲,bl[u] 为 v 的一个在圆方树上的孩子,且为 u 的祖先

void redfs(int u) {
    f[u] = sum[u];
    if (u <= n) {
        for (int i = VT.head[u]; i; i = VT.edge[i].nxt) {
            int v = VT.edge[i].v;
            redfs(v);
            ans = max(ans, f[v] + f[u] - 2 * sum[u]);
            f[u] = max(f[u], f[v]);
        }
    } else {
        for (int i = VT.head[u]; i; i = VT.edge[i].nxt) {
            int v = VT.edge[i].v;
            redfs(v);
            f[u] = max(f[u], f[v]);
        }
        // do algorithm after dfs!
        static int son[N];
        static ll d[NN], w[NN];
        static int Q[NN];
        int cnt = 0;
        for (int i = VT.head[u]; i; i = VT.edge[i].nxt) {
            int v = VT.edge[i].v;
            son[++cnt] = v;
        }
        sort(son + 1, son + cnt + 1, [] (int a, int b) {
            return dis[bl[a]] < dis[bl[b]];
        });
        ll tot = cirsum[u - n];
        for (int i = 1; i <= cnt; ++i) {
            int v = son[i];
            int tv = bl[v];
            d[i] = dis[tv];
            w[i] = f[v] - sum[tv];
            d[cnt + i] = d[i] + tot;
            w[cnt + i] = w[i];
        }
        int head = 0, tail = -1;
        for (int i = 1; i <= cnt << 1; ++i) {
            while (head <= tail && d[Q[head]] < d[i] - tot/2) ++head;
            if (head <= tail) ans = max(ans, w[Q[head]] + w[i] + d[i] - d[Q[head]]);
            while (head <= tail && w[Q[tail]] - d[Q[tail]] <= w[i] - d[i]) --tail;
            Q[++tail] = i;
        }
        
        // for (int i = 1; i <= cnt; ++i)
        //     for (int j = i + 1; j <= cnt; ++j) {
        //         ll D = abs(d[i] - d[j]);
        //         ans = max(ans, w[i] + w[j] + min(D, tot - D));
        //     }
    }
}

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1, u, v, w; i <= m; ++i) {
        scanf("%d%d%d", &u, &v, &w);
        G.add(u, v, w);
        G.add(v, u, w);
    }
    $build::tarjan(1, 0);
    dfs(1);
    for (int k = 1; k < lg2N; ++k) {
        for (int i = 1; i + (1 << k) - 1 <= n + dcc_cnt; ++i)
            st[k][i] = Min(st[k - 1][i], st[k - 1][i + (1 << (k - 1))]);
    }
    scanf("%d", &q);
    for (int k; q--; ) {
        static int st[NN];
        scanf("%d", &k);
        for (int i = 1; i <= k; ++i)
            scanf("%d", &st[i]);
        sort(st + 1, st + k + 1, [] (int a, int b) {
            return idx[a] < idx[b];
        });
        for (int i = 1; i < k; ++i)
            st[k + i] = get<2>(lca(st[i], st[i + 1]));
        sort(st + 1, st + k * 2, [] (int a, int b) {
            return idx[a] < idx[b];
        });
        int m = unique(st + 1, st + k * 2) - st - 1;
        for (int i = 1; i < m; ++i) {
            int u = st[i + 1];
            auto [_, U, v] = lca(st[i], st[i + 1]);
            VT.add(v, u);
            if (v > n) bl[u] = U;
        }
        ans = 0;
        redfs(st[1]);
        printf("%lld\n", ans);
        VT.tot = 1;
        for (int i = 1; i <= m; ++i) {
            int u = st[i];
            VT.head[u] = 0;
        }
    }
    return 0;
}

例九、【集训队互测2016】火车司机出秦川(UOJ) Problem Statement

一棵 \(n\) 个点、\(m\) 条边的仙人掌,有边权,和 \(2q\) 次操作。

询问,第 \(i\) 次询问有 \(k_i\) 条简单路径,称作「路径」。一条「路径」用三个整数 \((u,v,0/1)\) 表示,其中 \(u,v\) 表示它的端点,\(0\) 表示「路径」为 \(u,v\) 之间所有简单路径中经过最少结点的那条,\(1\) 表示经过最多结点的那条。保证「路径」端点不重复,保证所有环为奇环。求这些「路径」的并的边权和。

每次询问后可能跟着一条边权的修改。

\(n,\sum|k_i|\leq3\times10^5\)。
Problem Analysis

先把这些「路径」的端点在圆方树上的虚树建出来。下称经过最少城市的路径称作最短路,与边权最短路区别,同理最长路。

考虑哪些东西会对答案产生贡献,也就是考虑一条「路径」可能会包含哪些边。LCA \(p\) 若是圆点,就包含了 \(p\rightarrow u\) 和 \(p\rightarrow v\) 的最长 / 最短路。\(p\) 若是方点,就包含了 \(u'\rightarrow u\),\(v'\rightarrow v\) 的最长 / 最短路,这个和前面的问题形式是相同的,以及 \(p\) 对应环上 \(u'\rightarrow v'\) 的最长 / 最短路。

先不考虑修改,如果只有第一种问题怎么求。一条虚树边对应圆方树上一条方圆交错的路径,在原仙人掌上体现为,若干个简单环被连接成一个链状的结构。差分之后做树上前缀和,就能知道一条虚树边是否需要统计最短 / 最长路。而知道了是否需要统计最短 / 最长路,我们也很方便求出这一条链的贡献了。记 \(s_{0/1}(u)\) 表示 \(u\) 到根的最短 / 最长路路径长度,好像这样可以计算了,对于只需要统计最短 / 最长路,就是 \(s_{0/1}\) 相减,对于两个都需要统计,就是 \(s_0(u)+s_1(u)\) 相减......吗?对于环来说,最短最长路加在一起正好凑成了一个整个环,但是对于一条非环边来说就被重复统计了。所以此时对于「两点被一边相连」这种点双不能再当做一个环考虑了。还需要维护 \(s_2(u)\) 表示 \(u\) 到根的非环边路径长度。两种路径都要统计时,就是 \(s_0(u)+s_1(u)-s_2(u)\) 相减。

考虑如果只有第二种问题怎么求。不妨令在环的顺序下 \(u'\) 在 \(v'\) 之前,那么最短 / 最长路,就是 \(u'\sim v', v'\sim tail\sim head\sim u'\) 这两条路径中较短 / 长的那条。可以用差分完成区间覆盖,然后也能统计了。

如果两种问题同时存在,需要注意,第一种问题的路径可能会经过第二种路径的环,这个在第二个问题里面特殊处理一下,看看需不需要额外添加一些覆盖区间。

如果有了修改呢?如果这是一条环边,会影响到:总环长,这个很好维护;增加环上后缀所有点的到环首距离,所以需要对每个环开一个树状数组;对连续一段子树的 \(s_{0/1}\) 造成影响,这个用树状数组维护 DFS 序,就是区间加。如果这是一条非环边,会对子树的 \(s_{0/1/2}\) 产生增量,这个也用树状数组维护。

时间复杂度:\(\mathcal{O}(m+(n+q+\sum|S|)\log n)\)。
Solution

给出了没有修改的部分分代码,方便理解。
没有修改

cpp 复制代码
#include <cstdio>
#include <iostream>
#include <set>
#include <algorithm>
#include <vector>
using namespace std;

using ll = long long;

const int N = 3e5 + 10;
const int M = N * 2 + 10;
const int NN = N << 1;
const int lg2N = __lg(NN) + 1;

int n, m, q;

template <size_t _N, size_t _M, typename T>
class Grpah {
public:
    Grpah(): tot(1) {}
    struct node {
        int v, nxt;
        T w;
    } edge[_M];
    int head[_N], tot;
    void add(int u, int v, const T& w) {
        edge[++tot] = { v, head[u], w };
        head[u] = tot;
    }
};
Grpah<N, M << 1, int> G;  // original grpah
Grpah<N << 1, N << 1, ll> T;  // rebuild tree

template <size_t _N, size_t _M>
class Graph_ {
public:
    Graph_(): tot(1) {}
    struct node {
        int v, nxt;
    } edge[_M];
    int head[_N], tot;
    void add(int u, int v) {
        edge[++tot] = { v, head[u] };
        head[u] = tot;
    }
};
Graph_<N << 1, N << 1> VT;  // virtual tree

int dcc_cnt;
ll cirSum[N], dis[N];
int cirTot[N], cirDpt[N];

bool notInCir[M];
ll s[3][NN];  // tree prefix sum

namespace $build {

int dfn[N], low[N], timer;
int stack[M], top;

void tarjan(int u, int fr) {
    dfn[u] = low[u] = ++timer;
    for (int i = G.head[u]; i; i = G.edge[i].nxt) {
        if (i == (fr ^ 1)) continue;
        int v = G.edge[i].v;
        if (!dfn[v]) {
            stack[++top] = i;
            tarjan(v, i);
            low[u] = min(low[u], low[v]);
            if (low[v] >= dfn[u]) {
                static int tmp[N];
                int tot = 0;
                ++dcc_cnt;
                while (true) {
                    int j = stack[top--];
                    int uu = G.edge[j ^ 1].v;
                    int vv = G.edge[j].v;
                    int w = G.edge[j].w;
                    cirSum[dcc_cnt] += w;
                    if (j == i) break;
                    dis[uu] = dis[vv] + w;
                    cirDpt[uu] = cirDpt[vv] + 1;
                    tmp[++tot] = uu;
                }
                cirTot[dcc_cnt] = tot + 1;
                if (tot == 0) {
                    T.add(u, dcc_cnt + n, 0);
                    T.add(dcc_cnt + n, v, -cirSum[dcc_cnt]);  // 负数表示非环边
                    notInCir[i >> 1] = true;
                } else {
                    T.add(u, dcc_cnt + n, 0);
                    for (int j = 1; j <= tot; ++j) {
                        int x = tmp[j];
                        ll len;
                        if (cirDpt[x] < cirTot[dcc_cnt] - cirDpt[x])
                            len = dis[x];
                        else
                            len = cirSum[dcc_cnt] - dis[x];
                        T.add(dcc_cnt + n, x, len);
                    }
                }
            }
        } else {
            low[u] = min(low[u], dfn[v]);
            if (dfn[v] < dfn[u])
                stack[++top] = i;
        }
    }
}

}

int dpt[NN], fa[NN], timer, kthfa[lg2N][NN];
int idx[NN], st[lg2N][NN];

void dfs(int u) {
    st[0][idx[u] = ++timer] = u;
    for (int i = T.head[u]; i; i = T.edge[i].nxt) {
        int v = T.edge[i].v;
        ll w = T.edge[i].w;
        dpt[v] = dpt[u] + 1;
        fa[v] = kthfa[0][v] = u;
        
        if (u <= n) {
            s[0][v] = s[0][u];
            s[1][v] = s[1][u];
            s[2][v] = s[2][u];
        } else if (w > 0) {
            s[2][v] = s[2][u];
            s[0][v] = s[0][u] + w;
            s[1][v] = s[1][u] + (cirSum[u - n] - w);
        } else {
            w = -w;
            s[2][v] = s[2][u] + w;
            s[0][v] = s[0][u] + w;
            s[1][v] = s[1][u] + w;
        }
        
        dfs(v);
    }
}
inline int Min(int a, int b) {
    return dpt[a] < dpt[b] ? a : b;
}
inline int lca(int u, int v) {
    if (u == v) return u;
    if ((u = idx[u]) > (v = idx[v])) swap(u, v);
    int k = __lg(v - u++);
    return fa[Min(st[k][u], st[k][v - (1 << k) + 1])];
}
inline int jump(int u, int k) {
    for (int i = lg2N - 1; ~i; --i)
        if (k & 1 << i) u = kthfa[i][u];
    return u;
}

struct {
    int u, v, tp;
    int _u, _v;
} rd[N];  // Road

int bl[NN];  // 设 v 为 u 在虚树上的父亲,bl[u] 为 v 的一个在圆方树上的孩子,且为 u 的祖先
int cha[2][NN], _cha[2][NN];
vector<int> vec[NN];  // 方点 LCA 处的特殊处理
long long qans;

void redfs(int u) {
    if (u <= n) {
        for (int i = VT.head[u]; i; i = VT.edge[i].nxt) {
            int v = VT.edge[i].v;
            redfs(v);
            int w0 = cha[0][v], w1 = cha[1][v];
            cha[0][u] += w0;
            cha[1][u] += w1;
            long long x0 = s[0][v] - s[0][u];
            long long x1 = s[1][v] - s[1][u];
            long long x2 = s[2][v] - s[2][u];
            if (w0 && w1) {
                qans += x0 + x1 - x2;
            } else if (w0) {
                qans += x0;
            } else if (w1) {
                qans += x1;
            }
        }
    } else {
        for (int i = VT.head[u]; i; i = VT.edge[i].nxt) {
            int v = VT.edge[i].v;
            redfs(v);
            int w0 = cha[0][v], w1 = cha[1][v];
            long long x0 = s[0][v] - s[0][bl[v]];
            long long x1 = s[1][v] - s[1][bl[v]];
            long long x2 = s[2][v] - s[2][bl[v]];
            if (w0 && w1) {
                qans += x0 + x1 - x2;
            } else if (w0) {
                qans += x0;
            } else if (w1) {
                qans += x1;
            }
        }
        vector<pair<ll, int>> tmp;
        for (int id : vec[u]) {
            int uu = rd[id]._u;
            int vv = rd[id]._v;
            int tp = rd[id].tp;
            if (cirDpt[uu] > cirDpt[vv])
                swap(uu, vv);
            int A = cirDpt[vv] - cirDpt[uu];  // uu -> vv
            int B = cirTot[u - n] - A;  // uu -> head -> tail -> vv
            if (tp == (A > B)) {
                // A
                tmp.emplace_back(dis[uu], 1);
                tmp.emplace_back(dis[vv], -1);
            } else {
                // B
                tmp.emplace_back(0, 1);
                tmp.emplace_back(dis[uu], -1);
                tmp.emplace_back(dis[vv], 1);
            }
        }
        for (int i = VT.head[u]; i; i = VT.edge[i].nxt) {
            int v = VT.edge[i].v;
            int vv = bl[v];
            int w0 = cha[0][v], w1 = cha[1][v];
            w0 += _cha[0][vv], w1 += _cha[1][vv];
            cha[0][u] += w0;
            cha[1][u] += w1;
            int L = cirDpt[vv];
            int R = cirTot[u - n] - L;
            if (w0 && w1) {
                tmp.emplace_back(0, 1);
            } else if (w0 || w1) {
                if (bool(w0) == (L < R)) {
                    tmp.emplace_back(0, 1);
                    tmp.emplace_back(dis[vv], -1);
                } else {
                    tmp.emplace_back(dis[vv], 1);
                }
            }
        }
        sort(tmp.begin(), tmp.end());
        int m = tmp.size();
        int sum = 0;
        for (int i = 0, j = -1; i < m; i = j + 1) {
            while (j + 1 < m && tmp[j + 1].first == tmp[i].first)
                sum += tmp[++j].second;
            ll len;
            if (j == m - 1)
                len = cirSum[u - n] - tmp[i].first;
            else
                len = tmp[j + 1].first - tmp[i].first;
            if (sum) {
                qans += len;
            }
        }
    }
}

void query() {
    static int st[NN], _st[NN];
    int k;
    scanf("%d", &k);
    if (!k) {
        puts("0");
        return;
    }
    int m = 0;
    for (int i = 1; i <= k; ++i) {
        scanf("%d%d%d", &rd[i].u, &rd[i].v, &rd[i].tp);
        st[++m] = rd[i].u;
        st[++m] = rd[i].v;
    }
    sort(st + 1, st + m + 1, [] (int a, int b) {
        return idx[a] < idx[b];
    });
    for (int i = 1; i < m; ++i)
        st[m + i] = lca(st[i], st[i + 1]);
    sort(st + 1, st + m * 2, [] (int a, int b) {
        return idx[a] < idx[b];
    });
    m = unique(st + 1, st + m * 2) - st - 1;
    int _m = 0;
    for (int i = 1; i < m; ++i) {
        int u = st[i + 1], v = lca(st[i], st[i + 1]);
        VT.add(v, u);
        if (v > n) {
            int _u = jump(u, dpt[u] - dpt[v] - 1);
            _st[++_m] = _u;
            bl[u] = _u;
        }
    }
    for (int i = 1; i <= k; ++i) {
        int u = rd[i].u, v = rd[i].v, w = rd[i].tp;
        int p = lca(u, v);
        if (p > n) {
            int _u = jump(u, dpt[u] - dpt[p] - 1);
            int _v = jump(v, dpt[v] - dpt[p] - 1);
            ++cha[w][u], --_cha[w][_u];
            ++cha[w][v], --_cha[w][_v];
            rd[i]._u = _u;
            rd[i]._v = _v;
            vec[p].emplace_back(i);
        } else {
            ++cha[w][u], ++cha[w][v];
            cha[w][p] -= 2;
        }
    }
    qans = 0;
    redfs(st[1]);
    printf("%lld\n", qans);
    VT.tot = 1;
    for (int i = 1; i <= m; ++i) {
        int u = st[i];
        VT.head[u] = 0;
        vec[u].clear();
        cha[0][u] = cha[1][u] = 0;
    }
    for (int i = 1; i <= _m; ++i) {
        int u = _st[i];
        _cha[0][u] = _cha[1][u] = 0;
    }
}

void modify() {
    int w, x;
    scanf("%d%d", &w, &x);
    if (!w) return;
    throw;
}

int main() {
    scanf("%d%d%d", &n, &m, &q);
    for (int i = 1, u, v, w; i <= m; ++i) {
        scanf("%d%d%d", &u, &v, &w);
        G.add(u, v, w);
        G.add(v, u, w);
    }
    $build::tarjan(1, 0);
    dfs(1);
    for (int k = 1; k < lg2N; ++k) {
        for (int i = 1; i + (1 << k) - 1 <= n + dcc_cnt; ++i)
            st[k][i] = Min(st[k - 1][i], st[k - 1][i + (1 << (k - 1))]);
        for (int i = 1; i <= n + dcc_cnt; ++i)
            kthfa[k][i] = kthfa[k - 1][kthfa[k - 1][i]];
    }
    while (q--) query(), modify();
    return 0;
}

满分做法

cpp 复制代码
#include <cstdio>
#include <iostream>
#include <set>
#include <algorithm>
#include <vector>
#include <cassert>
using namespace std;

using ll = long long;

const int N = 3e5 + 10;
const int M = N * 2 + 10;
const int NN = N << 1;
const int lg2N = __lg(NN) + 1;

int n, m, q;

template <size_t _N, size_t _M, typename T>
class Grpah {
public:
    Grpah(): tot(1) {}
    struct node {
        int v, nxt;
        T w;
    } edge[_M];
    int head[_N], tot;
    void add(int u, int v, const T& w) {
        edge[++tot] = { v, head[u], w };
        head[u] = tot;
    }
};
Grpah<N, M << 1, int> G;  // original grpah
Grpah<N << 1, N << 1, ll> T;  // rebuild tree

template <size_t _N, size_t _M>
class Graph_ {
public:
    Graph_(): tot(1) {}
    struct node {
        int v, nxt;
    } edge[_M];
    int head[_N], tot;
    void add(int u, int v) {
        edge[++tot] = { v, head[u] };
        head[u] = tot;
    }
};
Graph_<N << 1, N << 1> VT;  // virtual tree

template <typename T>
class Bit_Tree {
private:
    vector<T> tree;
    int size() const { return tree.size(); }
    void _modify(int p, T v) {
        for (; p <= size(); p += p & -p)
            tree[p - 1] += v;
    }
    T query(int p) const {
        T res = 0;
        for (; p; p &= p - 1)
            res += tree[p - 1];
        return res;
    }
public:
    void init(int n) {
        tree = vector<T>(n, 0);
    }
    void modify(int l, int r, T v) {
        assert(1 <= l && l <= r && r <= size());
        _modify(l, v);
        _modify(r + 1, -v);
    }
    void modify(int p, T v) {
        assert(1 <= p && p <= size());
        modify(p, p, v);
    }
    T operator [] (int p) const {
        assert(1 <= p && p <= size());
        return query(p);
    }
};

int dcc_cnt;
ll cirSum[N];
Bit_Tree<ll> dis[N];
vector<int> circle[N];
int cirTot[N], cirDpt[N];

bool notInCir[M];

namespace $build {

int dfn[N], low[N], timer;
int stack[M], top;

void tarjan(int u, int fr) {
    dfn[u] = low[u] = ++timer;
    for (int i = G.head[u]; i; i = G.edge[i].nxt) {
        if (i == (fr ^ 1)) continue;
        int v = G.edge[i].v;
        if (!dfn[v]) {
            stack[++top] = i;
            tarjan(v, i);
            low[u] = min(low[u], low[v]);
            if (low[v] >= dfn[u]) {
                static int tmp[N];
                int tot = 0;
                ++dcc_cnt;
                for (int ttop = top; ; ) {
                    int j = stack[ttop--];
                    int uu = G.edge[j ^ 1].v;
                    if (j == i) break;
                    tmp[++tot] = uu;
                    circle[dcc_cnt].emplace_back(uu);
                }
                dis[dcc_cnt].init(tot);
                while (true) {
                    int j = stack[top--];
                    int uu = G.edge[j ^ 1].v;
                    int vv = G.edge[j].v;
                    int w = G.edge[j].w;
                    cirSum[dcc_cnt] += w;
                    if (j == i) break;
                    cirDpt[uu] = cirDpt[vv] + 1;
                    dis[dcc_cnt].modify(cirDpt[uu], tot, w);
                }
                cirTot[dcc_cnt] = tot + 1;
                if (tot == 0) {
                    T.add(u, dcc_cnt + n, 0);
                    T.add(dcc_cnt + n, v, -cirSum[dcc_cnt]);  // 负数表示非环边
                    notInCir[i >> 1] = true;
                } else {
                    T.add(u, dcc_cnt + n, 0);
                    for (int j = tot; j; --j) {  // attention, reversed
                        int x = tmp[j];
                        ll len;
                        if (cirDpt[x] < cirTot[dcc_cnt] - cirDpt[x])
                            len = dis[dcc_cnt][cirDpt[x]];
                        else
                            len = cirSum[dcc_cnt] - dis[dcc_cnt][cirDpt[x]];
                        T.add(dcc_cnt + n, x, len);
                    }
                }
            }
        } else {
            low[u] = min(low[u], dfn[v]);
            if (dfn[v] < dfn[u])
                stack[++top] = i;
        }
    }
}

}

Bit_Tree<ll> s[3];  // tree prefix sum
int dpt[NN], fa[NN], timer, kthfa[lg2N][NN];
int idx[NN], st[lg2N][NN];
int L[NN], R[NN];

void dfs(int u) {
    st[0][idx[u] = ++timer] = u;
    L[u] = idx[u];
    for (int i = T.head[u]; i; i = T.edge[i].nxt) {
        int v = T.edge[i].v;
        ll w = T.edge[i].w;
        dpt[v] = dpt[u] + 1;
        fa[v] = kthfa[0][v] = u;
        
        dfs(v);
        
        if (u <= n) {
            
        } else if (w > 0) {
            s[0].modify(L[v], R[v], w);
            s[1].modify(L[v], R[v], cirSum[u - n] - w);
        } else {
            w = -w;
            s[0].modify(L[v], R[v], w);
            s[1].modify(L[v], R[v], w);
            s[2].modify(L[v], R[v], w);
        }
    }
    R[u] = timer;
}
inline int Min(int a, int b) {
    return dpt[a] < dpt[b] ? a : b;
}
inline int lca(int u, int v) {
    if (u == v) return u;
    if ((u = idx[u]) > (v = idx[v])) swap(u, v);
    int k = __lg(v - u++);
    return fa[Min(st[k][u], st[k][v - (1 << k) + 1])];
}
inline int jump(int u, int k) {
    for (int i = lg2N - 1; ~i; --i)
        if (k & 1 << i) u = kthfa[i][u];
    return u;
}

struct {
    int u, v, tp;
    int _u, _v;
} rd[N];  // Road

int bl[NN];  // 设 v 为 u 在虚树上的父亲,bl[u] 为 v 的一个在圆方树上的孩子,且为 u 的祖先
int cha[2][NN], _cha[2][NN];
vector<int> vec[NN];  // 方点 LCA 处的特殊处理
long long qans;

void redfs(int u) {
    if (u <= n) {
        for (int i = VT.head[u]; i; i = VT.edge[i].nxt) {
            int v = VT.edge[i].v;
            redfs(v);
            int w0 = cha[0][v], w1 = cha[1][v];
            cha[0][u] += w0;
            cha[1][u] += w1;
            long long x0 = s[0][idx[v]] - s[0][idx[u]];
            long long x1 = s[1][idx[v]] - s[1][idx[u]];
            long long x2 = s[2][idx[v]] - s[2][idx[u]];
            if (w0 && w1) {
                qans += x0 + x1 - x2;
            } else if (w0) {
                qans += x0;
            } else if (w1) {
                qans += x1;
            }
        }
    } else {
        for (int i = VT.head[u]; i; i = VT.edge[i].nxt) {
            int v = VT.edge[i].v;
            redfs(v);
            int w0 = cha[0][v], w1 = cha[1][v];
            long long x0 = s[0][idx[v]] - s[0][idx[bl[v]]];
            long long x1 = s[1][idx[v]] - s[1][idx[bl[v]]];
            long long x2 = s[2][idx[v]] - s[2][idx[bl[v]]];
            if (w0 && w1) {
                qans += x0 + x1 - x2;
            } else if (w0) {
                qans += x0;
            } else if (w1) {
                qans += x1;
            }
        }
        vector<pair<ll, int>> tmp;
        for (int id : vec[u]) {
            int uu = rd[id]._u;
            int vv = rd[id]._v;
            int tp = rd[id].tp;
            if (cirDpt[uu] > cirDpt[vv])
                swap(uu, vv);
            int A = cirDpt[vv] - cirDpt[uu];  // uu -> vv
            int B = cirTot[u - n] - A;  // uu -> head -> tail -> vv
            if (tp == (A > B)) {
                // A
                tmp.emplace_back(dis[u - n][cirDpt[uu]], 1);
                tmp.emplace_back(dis[u - n][cirDpt[vv]], -1);
            } else {
                // B
                tmp.emplace_back(0, 1);
                tmp.emplace_back(dis[u - n][cirDpt[uu]], -1);
                tmp.emplace_back(dis[u - n][cirDpt[vv]], 1);
            }
        }
        for (int i = VT.head[u]; i; i = VT.edge[i].nxt) {
            int v = VT.edge[i].v;
            int vv = bl[v];
            int w0 = cha[0][v], w1 = cha[1][v];
            w0 += _cha[0][vv], w1 += _cha[1][vv];
            cha[0][u] += w0;
            cha[1][u] += w1;
            int L = cirDpt[vv];
            int R = cirTot[u - n] - L;
            if (w0 && w1) {
                tmp.emplace_back(0, 1);
            } else if (w0 || w1) {
                if (bool(w0) == (L < R)) {
                    tmp.emplace_back(0, 1);
                    tmp.emplace_back(dis[u - n][cirDpt[vv]], -1);
                } else {
                    tmp.emplace_back(dis[u - n][cirDpt[vv]], 1);
                }
            }
        }
        sort(tmp.begin(), tmp.end());
        int m = tmp.size();
        int sum = 0;
        for (int i = 0, j = -1; i < m; i = j + 1) {
            while (j + 1 < m && tmp[j + 1].first == tmp[i].first)
                sum += tmp[++j].second;
            ll len;
            if (j == m - 1)
                len = cirSum[u - n] - tmp[i].first;
            else
                len = tmp[j + 1].first - tmp[i].first;
            if (sum) {
                qans += len;
            }
        }
    }
}

void query() {
    static int st[NN], _st[NN];
    int k;
    scanf("%d", &k);
    if (!k) {
        puts("0");
        return;
    }
    int m = 0;
    for (int i = 1; i <= k; ++i) {
        scanf("%d%d%d", &rd[i].u, &rd[i].v, &rd[i].tp);
        st[++m] = rd[i].u;
        st[++m] = rd[i].v;
    }
    sort(st + 1, st + m + 1, [] (int a, int b) {
        return idx[a] < idx[b];
    });
    for (int i = 1; i < m; ++i)
        st[m + i] = lca(st[i], st[i + 1]);
    sort(st + 1, st + m * 2, [] (int a, int b) {
        return idx[a] < idx[b];
    });
    m = unique(st + 1, st + m * 2) - st - 1;
    int _m = 0;
    for (int i = 1; i < m; ++i) {
        int u = st[i + 1], v = lca(st[i], st[i + 1]);
        VT.add(v, u);
        if (v > n) {
            int _u = jump(u, dpt[u] - dpt[v] - 1);
            _st[++_m] = _u;
            bl[u] = _u;
        }
    }
    for (int i = 1; i <= k; ++i) {
        int u = rd[i].u, v = rd[i].v, w = rd[i].tp;
        int p = lca(u, v);
        if (p > n) {
            int _u = jump(u, dpt[u] - dpt[p] - 1);
            int _v = jump(v, dpt[v] - dpt[p] - 1);
            ++cha[w][u], --_cha[w][_u];
            ++cha[w][v], --_cha[w][_v];
            rd[i]._u = _u;
            rd[i]._v = _v;
            vec[p].emplace_back(i);
        } else {
            ++cha[w][u], ++cha[w][v];
            cha[w][p] -= 2;
        }
    }
    qans = 0;
    redfs(st[1]);
    printf("%lld\n", qans);
    VT.tot = 1;
    for (int i = 1; i <= m; ++i) {
        int u = st[i];
        VT.head[u] = 0;
        vec[u].clear();
        cha[0][u] = cha[1][u] = 0;
    }
    for (int i = 1; i <= _m; ++i) {
        int u = _st[i];
        _cha[0][u] = _cha[1][u] = 0;
    }
}

void modify() {
    int w, x;
    scanf("%d%d", &w, &x);
    if (!w) return;
    int u = G.edge[w << 1].v;
    int v = G.edge[w << 1 | 1].v;
    int tw = G.edge[w << 1].w;
    if (dpt[u] > dpt[v]) swap(u, v);
    if (notInCir[w]) {
        s[0].modify(L[v], R[v], x - tw);
        s[1].modify(L[v], R[v], x - tw);
        s[2].modify(L[v], R[v], x - tw);
    } else {
        int o = fa[v] - n;
        int tot = cirTot[o];
        const auto& circle = ::circle[o];
        int lp = circle[tot / 2 - 1];
        int rp = circle[tot / 2];
        int hd = circle[0];
        int tl = circle[tot - 2];
        int whr;  // -1: left, 0: middle, 1: right
        if (u == fa[fa[v]]) {
            if (cirDpt[v] == 1)
                whr = -1;
            else
                whr = 1;
        } else {
            if (cirDpt[u] > cirDpt[v])
                swap(u, v);
            if (u == lp && v == rp) {
                whr = 0;
            } else {
                if (cirDpt[v] <= tot / 2)
                    whr = -1;
                else {
                    whr = 1;
                    swap(u, v);
                }
            }
        }
        if (whr == 0) {
            dis[o].modify(cirDpt[v], tot - 1, x - tw);
            s[1].modify(L[hd], R[lp], x - tw);
            s[1].modify(L[rp], R[tl], x - tw);
        } else if (whr == -1) {
            dis[o].modify(cirDpt[v], tot - 1, x - tw);
            s[0].modify(L[v], R[lp], x - tw);
            if (v != hd)
                s[1].modify(L[hd], R[u], x - tw);
            s[1].modify(L[rp], R[tl], x - tw);
        } else {
            s[0].modify(L[rp], R[v], x - tw);
            if (v != tl) {
                dis[o].modify(cirDpt[u], tot - 1, x - tw);
                s[1].modify(L[u], R[tl], x - tw);
            }
            s[1].modify(L[hd], R[lp], x - tw);
        }
        cirSum[o] += x - tw;
    }
    G.edge[w << 1].w = x;
    G.edge[w << 1 | 1].w = x;
}

int main() {
    scanf("%d%d%d", &n, &m, &q);
    for (int i = 1, u, v, w; i <= m; ++i) {
        scanf("%d%d%d", &u, &v, &w);
        G.add(u, v, w);
        G.add(v, u, w);
    }
    $build::tarjan(1, 0);
    for (int i = 0; i < 3; ++i)
        s[i].init(n + dcc_cnt);
    dfs(1);
    for (int k = 1; k < lg2N; ++k) {
        for (int i = 1; i + (1 << k) - 1 <= n + dcc_cnt; ++i)
            st[k][i] = Min(st[k - 1][i], st[k - 1][i + (1 << (k - 1))]);
        for (int i = 1; i <= n + dcc_cnt; ++i)
            kthfa[k][i] = kthfa[k - 1][kthfa[k - 1][i]];
    }
    while (q--) query(), modify();
    return 0;
}

类型五:静态仙人掌,动态维护全局信息

其实就是仙人掌上的 DDP 问题。

我们不妨从一个具体的问题开始思考:支持修改点权,动态维护仙人掌的最大独立集权值。

在树上,我们做过模板题,没有修改时,我们在前文例题一探讨过单次 \(\mathcal{O}(n)\) 求仙人掌最大独立集的算法,我们要做的就是把这两个算法结合起来。

回顾在一个序列上我们如何动态维护独立集的?设 \(f_0,f_1\) 表示选或不选最后一个元素的最大独立集,那么根据定义有 \(f_1'=f_0+a_i,f_0'=\max\{f_0,f_1\}\),容易写出转移方程的 \((+,\max)\) 矩阵乘法形式:

\\\begin{bmatrix} f_0 \& f_1 \\end{bmatrix} \\times \\begin{bmatrix} 0 \& a_i \\\\ 0 \& -\\infty \\\\ \\end{bmatrix} = \\begin{bmatrix} f_0' \& f_1' \\end{bmatrix} \\

回顾这个东西是如何上树的。我们轻重链剖分后,重链其实就是普通的一条链,但是对于不选择 / 选择 \(u\) 的收益不再是 \(0/a_u\),而是 \(t_0/t_1+a_u\),其中 \(t_{0/1}\) 表示轻儿子对它的贡献,\(t_0\) 为所有轻儿子的 \(\max\{f_0,f_1\}\) 之和,\(t_1\) 为所有轻儿子的 \(f_0\) 之和。转移矩阵为:\(\begin{bmatrix} t_0 & t_1+a_u \\ t_0 & -\infty \\ \end{bmatrix}\)。如此,我们实际完成了,通过求一条以 \(u\) 为链顶的重链的矩阵乘法,得到 \(u\) 子树的答案。

这个东西怎么上仙人掌呢?无非就是考虑如何处理环。

目前状态肯定是不够了,想到把扩展状态成 \(f_0,f_1,g_0,g_1\),分别对应环尾必不选和可能选的 DP 值。但是这是不好做的。如上图,假设这个点双对应的方点为 \(u\),\(u\) 的父亲圆点为 \(1\),重儿子为 \(5\)。为了转移顺序,我们也许会想到把 DFS 序设置为:

\7,6,\\colorbox{#409eff}{$ 5,... $},4,3,2 \\

但是 \(5,...\) 得到的是 \(5\) 的子仙人掌的 DP 值,我们并不能将它再乘上某一个矩阵,得到对应在 \(f_0,f_1,g_0,g_1\) 上的转移矩阵。所以这个想法是错误的。

以上想法错误之处,根本在于重儿子不能出现在转移方程的中间,否则无法通过 DP 值得到对应的转移矩阵。那么 \(5\) 作为 DP 的初始位置,应该如何设置转移顺序和状态呢?

我们想到一种「兵分两路」的转移方式,将环拆成两条链:\(5,6,7,1\) 和 \(5,4,3,2\),并要求 \(5\) 要么同时选,要么同时不选,最后统计答案的时候 \(1,2\) 不能同时选。设 \(f_{a,b}\),其中 \(a,b\) 分别表示左右两条链,目前考虑到的链顶选或不选的答案。初始矩阵为:

\\\begin{bmatrix} f_{0,0}\&f_{0,1}\&f_{1,0}\&f_{1,1} \\end{bmatrix} = \\begin{bmatrix} t_0\&-\\infty\&-\\infty\&t_1 \\end{bmatrix} \\

其中 \(t_{0/1}\) 表示重儿子 \(5\) 传统的 DP 值。这同时解决了 \(5\) 只能最多选一次的问题。

对于左边这条链上的点 \(u\),会独立转移 \(f_{0/1,*}\),即对于 \(p=0,1\),\(f_{0,p}'=\max\{f_{0,p},f_{1,p}\}+t_0,f_{1,p}'=f_{0,p}+t_1\),其中 \(t_{0/1}\) 表示 \(u\) 考虑其子仙人掌的传统 DP 值,下文在不引起歧义的前提下,省去了对 \(t_{0/1}\) 的解释。写成转移矩阵的形式就是:

\f(t_0,t_1,1)=\\begin{bmatrix} t_0 \& \& t_1 \& \\\\ \& t_0 \& \& t_1 \\\\ t_0 \& \& \& \\\\ \& t_0 \& \& \\\\ \\end{bmatrix} \\

同理,对于右边那条链上的 \(u\) 的转移为:

\f(t_0,t_1,2)=\\begin{bmatrix} t_0 \& t_1 \& \& \\\\ t_0 \& \& \& \\\\ \& \& t_0 \& t_1 \\\\ \& \& t_0 \& \\\\ \\end{bmatrix} \\

在 \(1\) 点,我们就能求出想要的值,不妨令 \(1\) 在左侧链上,那么 \(t_0=\max\{f_{0,0},f_{0,1}\},t_1=f_{1,0}\),我们可以写出一个矩阵,通过 \(\begin{bmatrix} f_{0,0}&f_{0,1}&f_{1,0}&f_{1,1} \end{bmatrix}\) 得到 \(\begin{bmatrix} t_0&-\infty&-\infty&t_1 \end{bmatrix}\),这是正是环上 DP 的初始值,从而形成逻辑上的闭环。

\M=\\begin{bmatrix} 0 \& \& \& \\\\ 0 \& \& \& \\\\ \& \& \& 0 \\\\ \& \& \& \\end{bmatrix} \\

当然,以上所有矩阵都是转移矩阵,我们初始矩阵为:

\M_0=\\begin{bmatrix} 0 \& \& \& 0 \\end{bmatrix} \\

我们已经初步确认,这个问题是可做的,并且粗浅地想出了一种转移的方式。接下来,需要解决遗留下来尚未处理的细节。

  1. 如何设计 DFS 序?

    类似类型三的 DFS 序设计,将方点替换为其所有轻孩子,接着是重儿子和它的子树。同时,我们需要左链右链上的节点的相对顺序满足转移顺序。

    例如,用不同颜色区分左右链,上图的一个不合法 的 DFS 序为 \({\color{red}1},{\color{red}7},{\color{yellow}2},{\color{yellow}4},{\color{red}6},{\color{yellow}3},5\),因为右链转移顺序并不是 \(3,4,2\),而应该是 \(4,3,2\)。一种合法的 DFS 序为 \({\color{red}1},{\color{red}7},{\color{yellow}2},{\color{yellow}3},{\color{red}6},{\color{yellow}4},5\)。当然,我们显然喜欢更简单的 DFS 序 \({\color{red}1},{\color{red}7},{\color{red}6},{\color{yellow}2},{\color{yellow}3},{\color{yellow}4},5\),它先按照顺序转移完了右链上的 \(4,3,2\),再转移完左链上 \(6,7,1\)。

  2. 如何分配 DFS 序上对应转移矩阵?

    先来明确一下各种 DFS 序区间和其对应矩阵的关系。

    对于一个方点,它对应 DFS 序上的区间求出来的矩阵,是一个环上 DP 未完成的矩阵,因为缺少了链顶的转移。例如,\({\color{red}1},{\color{red}7},{\color{red}6},{\color{yellow}2},{\color{yellow}3},{\color{yellow}4},5\) 中,实际方点对应的 DFS 序为 \({\color{red}7},{\color{red}6},{\color{yellow}2},{\color{yellow}3},{\color{yellow}4},5\),这缺少了 \({\color{red}1}\) 的转移。

    对于一个重儿子圆点,它对应 DFS 序上的区间求出来的矩阵,已经完成了环上的转移,左乘 \(M_0\) 后形如 \(\begin{bmatrix}t_0&-\infty&-\infty&t_1\end{bmatrix}\)。

    对于一个轻儿子圆点,它没有对应的 DFS 序区间。

    类似树上,维护 \(u\) 所有轻儿子的答案和,记为 \(g_0,g_1\),以及不妨令 \(g_1\) 中包含 \(u\) 的点权。

    1. \(u\) 为重儿子。

      它在 DFS 序上位置形如:\(...,u,(a),(b_1),(b_2),...\),其中 \((a)\) 为 \(u\) 的重方儿子对应子树,\((b_i)\) 对应 \(u\) 的轻方儿子的子树,两个 \(...\) 分别对应 \(u\) 的轻儿子兄弟,和这些点的方儿子子树。

      \((a)\) 求出来一个环上 DP 未完成的矩阵,所以要先乘上 \(f(g_0,g_1,1)\) 完成环上 DP,再乘上 \(M\),得到形如 \(\begin{bmatrix}t_0&-\infty&-\infty&t_1\end{bmatrix}\) 的矩阵。这也就是 \(u,(a)\) 求出的结果。

    2. \(u\) 为轻儿子。

      先将 \(g_0,g_1\) 加上其重儿子的贡献,然后它在 DFS 序上的矩阵即为 \(f(g_0,g_1,\operatorname{type}(u))\)。

  3. 如何求答案?

    \(1\) 为链顶的这条重链,对应的区间的矩阵之积,就是转移矩阵,左乘 初始矩阵 \(M_0\) 得到答案矩阵 \(\begin{bmatrix}t_0&-\infty&-\infty&t_1\end{bmatrix}\),答案即为 \(\max\{t_0,t_1\}\)。

  4. 如何修改?

    树上修改的时候,设当前点为 \(u\),其重链顶为 \(v\),\(v\) 的父亲为 \(w\),那么要先在 \(w\) 的矩阵中减去这条重链的贡献,再加上修改后的贡献,接着令 \(u\gets w\),继续操作,直到 \(v=1\) 停止。上述过程中要求我们维护一个 \(M_{\mathrm{last}}\) 表示以 \(v\) 为链顶的这条重链修改前的矩阵之积。在修改 \(w\) 前要先求出 \(w\) 所在重链的矩阵之积,以赋值给 \(M_{\mathrm{last}}\)。

    仙人掌上,对于 \(v\) 的类型分类讨论。

    1. \(v\) 为圆点。

      我们需要处理 \(v\) 的转移矩阵。\(v\) 的 \(g_0,g_1\) 可以方便维护,那么只需要和初始化的时候一样,求出重儿子的贡献,在线段树上修改即可。发现竟然不需要 \(M_{\mathrm{last}}\) 之类的东西。

    2. \(v\) 为方点。

      我们需要处理 \(w\) 的转移矩阵。\(v\) 作为 \(w\) 的轻儿子,就需要在 \(w\) 的 \(g_0,g_1\) 中,减去原先的贡献,再加上修改后的贡献。于是类似树上 DDP,需要维护 \(M_{\mathrm{last}}\),接着,按照 \(w\) 为重儿子还是轻儿子,修改其转移矩阵即可。

    如何维护 \(M_{\mathrm{last}}\)?似乎只需要和树上 DDP 类似,当 \(w\) 链顶 \(p\) 为方点时,先查询 \(p\) 对应的转移矩阵,赋值给 \(M_{\mathrm(last)}\) 即可。但这在某种情况下是错误的。当 \(u=v\) 为圆点时,我们查询 \(p\) 的转移矩阵时,会包含 \(u\) 的转移矩阵,我们能够保证这个矩阵是没有更新过的吗?不能,因为可能上一次操作为情况 2「\(v\) 为方点」,而这会更新到 \(u\) 的转移矩阵,所以我们需要在上一次操作,提前判断这种特殊情况,提前赋值 \(M_{\mathrm{last}}\),在这一次操作就不对 \(M_{\mathrm{last}}\) 赋值。具体可以参考例题代码。

我们至此成功解决了这个问题,时间复杂度 \(\mathcal{O}(nw^3\log n+qw^3\log^2n)\),其中 \(w=4\) 为矩阵的大小。或许使用全局平衡二叉树可以优化到一只 \(\log\),但是没有实现。

我们在上文中探讨了一种经典的 DDP 问题在仙人掌上的求解方式,对于其他 DDP 问题,或许可以参考这种方式解决问题。
例十、DDP on cactus(洛谷) Problem Statement

\(n\) 个点 \(m\) 条边的仙人掌,点编号为 \(1\sim n\),每个结点有一个权值 \(w_u\)。

\(q\) 次点权修改操作,你需要在第一次修改前以及每次修改后,输出这棵仙人掌的最大独立集。

\(2\leq n\leq10^5\)。\(1\leq q\leq 10^5\)。任意时刻 \(0\leq w_i\leq 10^9\)。
Problem Analysis

前文分析的仙人掌上最大独立集问题。给出了一种参考实现。
Solution

cpp 复制代码
#include <cstdio>
#include <iostream>
#include <cstring>
#include <cassert>
using namespace std;

using ll = long long;

const int N = 1e5 + 10;
const int M = 2e5 + 10;
const int N2 = N << 1;
const ll oinf = 0xc1c1c1c1c1c1c1c1;

int n, m, q;
int val[N];

template <size_t _N, size_t _M>
class Graph {
public:
    Graph(): tot(1) {}
    struct node {
        int v, nxt;
    } edge[_M];
    int head[_N], tot;
    void add(int u, int v) {
        edge[++tot] = { v, head[u] };
        head[u] = tot;
    }
};

Graph<N, M << 1> G;
Graph<N2, N2> T;

int dcc_cnt;

namespace $build {

int dfn[N], low[N], timer;
int stack[N], top;

void tarjan(int u, int fr) {
    dfn[u] = low[u] = ++timer;
    stack[++top] = u;
    for (int i = G.head[u]; i; i = G.edge[i].nxt) {
        if (i == (fr ^ 1)) continue;
        int v = G.edge[i].v;
        if (!dfn[v]) {
            tarjan(v, i);
            low[u] = min(low[u], low[v]);
            if (low[v] >= dfn[u]) {
                ++dcc_cnt;
                T.add(u, dcc_cnt + n);
                while (true) {
                    int x = stack[top--];
                    T.add(dcc_cnt + n, x);
                    if (x == v) break;
                }
            }
        } else {
            low[u] = min(low[u], dfn[v]);
        }
    }
}

}

inline ll max(ll a, ll b) {
    return a > b ? a : b;
}
struct Matrix {
    ll v[4][4];
    void init() { memset(v, 0xc1, sizeof(v)); }
    void unit() { v[0][0] = v[1][1] = v[2][2] = v[3][3] = 0; }
    ll* operator [] (int x) { return v[x]; }
    ll const* operator [] (int x) const { return v[x]; }
    friend inline Matrix operator * (const Matrix& a, const Matrix& b) {
        Matrix c;
        c[0][0] = max(oinf, max(max(a[0][0] + b[0][0], a[0][1] + b[1][0]), max(a[0][2] + b[2][0], a[0][3] + b[3][0])));
        c[0][1] = max(oinf, max(max(a[0][0] + b[0][1], a[0][1] + b[1][1]), max(a[0][2] + b[2][1], a[0][3] + b[3][1])));
        c[0][2] = max(oinf, max(max(a[0][0] + b[0][2], a[0][1] + b[1][2]), max(a[0][2] + b[2][2], a[0][3] + b[3][2])));
        c[0][3] = max(oinf, max(max(a[0][0] + b[0][3], a[0][1] + b[1][3]), max(a[0][2] + b[2][3], a[0][3] + b[3][3])));
        c[1][0] = max(oinf, max(max(a[1][0] + b[0][0], a[1][1] + b[1][0]), max(a[1][2] + b[2][0], a[1][3] + b[3][0])));
        c[1][1] = max(oinf, max(max(a[1][0] + b[0][1], a[1][1] + b[1][1]), max(a[1][2] + b[2][1], a[1][3] + b[3][1])));
        c[1][2] = max(oinf, max(max(a[1][0] + b[0][2], a[1][1] + b[1][2]), max(a[1][2] + b[2][2], a[1][3] + b[3][2])));
        c[1][3] = max(oinf, max(max(a[1][0] + b[0][3], a[1][1] + b[1][3]), max(a[1][2] + b[2][3], a[1][3] + b[3][3])));
        c[2][0] = max(oinf, max(max(a[2][0] + b[0][0], a[2][1] + b[1][0]), max(a[2][2] + b[2][0], a[2][3] + b[3][0])));
        c[2][1] = max(oinf, max(max(a[2][0] + b[0][1], a[2][1] + b[1][1]), max(a[2][2] + b[2][1], a[2][3] + b[3][1])));
        c[2][2] = max(oinf, max(max(a[2][0] + b[0][2], a[2][1] + b[1][2]), max(a[2][2] + b[2][2], a[2][3] + b[3][2])));
        c[2][3] = max(oinf, max(max(a[2][0] + b[0][3], a[2][1] + b[1][3]), max(a[2][2] + b[2][3], a[2][3] + b[3][3])));
        c[3][0] = max(oinf, max(max(a[3][0] + b[0][0], a[3][1] + b[1][0]), max(a[3][2] + b[2][0], a[3][3] + b[3][0])));
        c[3][1] = max(oinf, max(max(a[3][0] + b[0][1], a[3][1] + b[1][1]), max(a[3][2] + b[2][1], a[3][3] + b[3][1])));
        c[3][2] = max(oinf, max(max(a[3][0] + b[0][2], a[3][1] + b[1][2]), max(a[3][2] + b[2][2], a[3][3] + b[3][2])));
        c[3][3] = max(oinf, max(max(a[3][0] + b[0][3], a[3][1] + b[1][3]), max(a[3][2] + b[2][3], a[3][3] + b[3][3])));
        return c;
    }
    void output() const {
        for (int i = 0; i < 4; ++i) {
            for (int j = 0; j < 4; ++j) {
                if (v[i][j] <= -10000000)
                    printf("-inf ");
                else
                    printf("%lld ", v[i][j]);
            }
            puts("");
        }
    }
};
using mat_t = Matrix;

int siz[N2], son[N2];
int fa[N2], top[N2], tail[N2];

int dfn[N], idx[N], L[N2], R[N2], timer;
int typ[N];
/**
 * u is yuan:
 *      [L[u], R[u]] = subtree(u)
 *      typ[u] = { 0: son, 1: F, 2: G }
 * u is fang:
 *      [L[u], R[u]] = son(u)
*/

void dfs(int u) {
    siz[u] = 1;
    for (int i = T.head[u]; i; i = T.edge[i].nxt) {
        int v = T.edge[i].v;
        fa[v] = u;
        dfs(v), siz[u] += siz[v];
        if (siz[v] > siz[son[u]])
            son[u] = v;
    }
}

const mat_t ans2init = {{
    { 0   , oinf, oinf, oinf },
    { 0   , oinf, oinf, oinf },
    { oinf, oinf, oinf, 0    },
    { oinf, oinf, oinf, oinf },
}};

const mat_t beginst = {{
    { 0   , oinf, oinf, 0    },
    { oinf, oinf, oinf, oinf },
    { oinf, oinf, oinf, oinf },
    { oinf, oinf, oinf, oinf },
}};

inline mat_t getmat(ll t0, ll t1, int c) {
    mat_t m;
    m.init();
    if (c == 1) {
        m[0][0] = m[1][1] = m[2][0] = m[3][1] = t0;
        m[0][2] = m[1][3] = t1;
    } else {
        m[0][0] = m[1][0] = m[2][2] = m[3][2] = t0;
        m[0][1] = m[2][3] = t1;
    }
    return m;
}

namespace $yzh {

#define ls (u << 1)
#define rs (u << 1 | 1)

mat_t tr[N << 2];

void _upd(int u, int l, int r, int p, const mat_t& mat) {
    if (l == r) {
        tr[u] = mat;
        return;
    }
    int mid = (l + r) >> 1;
    if (p <= mid) _upd(ls, l, mid, p, mat);
    else _upd(rs, mid + 1, r, p, mat);
    tr[u] = tr[rs] * tr[ls];
}

mat_t _qry(int u, int trl, int trr, int l, int r) {
    if (l <= trl && trr <= r) return tr[u];
    int mid = (trl + trr) >> 1;
    if (r <= mid) return _qry(ls, trl, mid, l, r);
    if (l >  mid) return _qry(rs, mid + 1, trr, l, r);
    return _qry(rs, mid + 1, trr, l, r) * _qry(ls, trl, mid, l, r);
}

void upd(int p, const mat_t& mat) { _upd(1, 1, n, p, mat); }
mat_t query(int l, int r) { return _qry(1, 1, n, l, r); }

#undef ls
#undef rs

}

ll f[2][N];

void redfs(int u, int tp) {
    top[u] = tp;
    tail[tp] = u;
    if (u <= n) {
        L[u] = timer + 1;
        ll f0 = 0, f1 = val[u];
        if (son[u]) redfs(son[u], tp);
        for (int i = T.head[u]; i; i = T.edge[i].nxt) {
            int v = T.edge[i].v;
            if (v == son[u]) continue;
            redfs(v, v);
            mat_t res = beginst * $yzh::query(L[v], idx[tail[v]]);
            res = res * getmat(0, 0, 1) * ans2init;
            f0 += res[0][0];
            f1 += res[0][3];
        }
        f[0][u] = f0;
        f[1][u] = f1;
        if (typ[u] != 0) {
            if (son[u]) {
                mat_t res = beginst * $yzh::query(L[son[u]], idx[tail[u]]);
                res = res * getmat(0, 0, 1) * ans2init;
                f0 += res[0][0], f1 += res[0][3];
            }
            /*
            also:
            mat_t res = beginst;
            if (son[u])
                res = res * $yzh::query(L[son[u]], idx[tail[u]]);
            res = res * getmat(f0, f1, 1) * ans2init;
            f0 = res[0][0], f1 = res[0][3];
            */
            
            $yzh::upd(idx[u], getmat(f0, f1, typ[u]));
        } else {
            $yzh::upd(idx[u], getmat(f0, f1, 1) * ans2init);
        }
        R[u] = timer;
    } else {
        L[u] = timer + 1;
        static int stk[N];
        int top = 0, i;
        for (i = T.head[u]; i; i = T.edge[i].nxt) {
            int v = T.edge[i].v;
            if (v == son[u]) break;
            dfn[idx[v] = ++timer] = v;
            typ[v] = 2;
        }
        for (i = T.edge[i].nxt; i; i = T.edge[i].nxt) {
            int v = T.edge[i].v;
            stk[++top] = v;
        }
        while (top) {
            int v = stk[top--];
            dfn[idx[v] = ++timer] = v;
            typ[v] = 1;
        }
        dfn[idx[son[u]] = ++timer] = son[u];
        typ[son[u]] = 0;
        R[u] = timer;
        redfs(son[u], tp);
        for (int i = T.head[u]; i; i = T.edge[i].nxt) {
            int v = T.edge[i].v;
            if (v == son[u]) continue;
            redfs(v, v);
        }
    }
}

void solve() {
    mat_t res = beginst * $yzh::query(idx[1], idx[tail[1]]);
    ll t0 = res[0][0], t1 = res[0][3];
    ll ans = max(t0, t1);
#ifdef XuYueming
    printf(">>>>>>>>>>>>>> ");
#endif
    printf("%lld\n", ans);
}

void modify(int u, int w) {
    mat_t lst;
    bool flag = false;
    {
        f[1][u] += w - val[u], val[u] = w;
        int v = top[u];
        if (v > n)
            lst = $yzh::query(L[v], idx[tail[v]]);
        else if (u == v) {
            int p = top[fa[v]];
            if (p > n)
                lst = $yzh::query(L[p], idx[tail[p]]), flag = true;
        }
        ll f0 = f[0][u], f1 = f[1][u];
        if (typ[u] != 0) {
            mat_t res = beginst;
            if (son[u])
                res = res * $yzh::query(L[son[u]], idx[tail[u]]);
            res = res * getmat(f0, f1, 1) * ans2init;
            $yzh::upd(idx[u], getmat(res[0][0], res[0][3], typ[u]));
        } else {
            $yzh::upd(idx[u], getmat(f0, f1, 1) * ans2init);
        }
    }
    while (top[u] != 1) {
        int v = top[u];
        int w = fa[v];
        
        if (v > n) {
            assert(flag == false);
            ll &f0 = f[0][w], &f1 = f[1][w];
            {
                mat_t res = beginst * lst;
                res = res * getmat(0, 0, 1) * ans2init;
                f0 -= res[0][0];
                f1 -= res[0][3];
            }
            {
                mat_t res = beginst * $yzh::query(L[v], idx[tail[v]]);
                res = res * getmat(0, 0, 1) * ans2init;
                f0 += res[0][0];
                f1 += res[0][3];
            }
            int p = top[w];
            if (p > n)
                lst = $yzh::query(L[p], idx[tail[p]]);
            else if (w == p) {
                p = top[fa[p]];
                if (p > n) {
                    lst = $yzh::query(L[p], idx[tail[p]]), flag = true;
                }
            }
            if (typ[w] != 0) {
                mat_t res = beginst;
                if (son[w])
                    res = res * $yzh::query(L[son[w]], idx[tail[w]]);
                res = res * getmat(f0, f1, 1) * ans2init;
                $yzh::upd(idx[w], getmat(res[0][0], res[0][3], typ[w]));
            } else {
                $yzh::upd(idx[w], getmat(f0, f1, 1) * ans2init);
            }
        } else {
            ll f0 = f[0][v], f1 = f[1][v];
            assert(typ[v] != 0);
            int p = top[w];
            if (p > n) {
                assert(flag == (u == v));
                if (flag) flag = false;
                else lst = $yzh::query(L[p], idx[tail[p]]);
            }
            mat_t res = beginst;
            if (son[v])
                res = res * $yzh::query(L[son[v]], idx[tail[v]]);
            res = res * getmat(f0, f1, 1) * ans2init;
            f0 = res[0][0], f1 = res[0][3];
            $yzh::upd(idx[v], getmat(f0, f1, typ[v]));
        }
        u = w;
    }
}

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1, u, v; i <= m; ++i) {
        scanf("%d%d", &u, &v);
        G.add(u, v), G.add(v, u);
    }
    for (int i = 1; i <= n; ++i) {
        scanf("%d", &val[i]);
    }
    $build::tarjan(1, 0);
    dfs(1);
    dfn[idx[1] = ++timer] = 1;
    typ[1] = 0;
    redfs(1, 1);
    
    scanf("%d", &q);
    solve();
    for (int u, w; q--; ) {
        scanf("%d%d", &u, &w);
        modify(u, w);
        solve();
    }
    return 0;
}

/*
15:25        static tree
day2 10:11   static cactus
day2 20:55   dynamic cactus
*/

Gen

cpp 复制代码
#include <cstdio>
#include <iostream>
#include <vector>
#include <random>
#include <chrono>
using namespace std;

/* =========== Parameter =========== */

const int SEED = chrono::system_clock::now().time_since_epoch().count();

bool multiedge = true;
bool onlyOddCircle = false;

int MULTI_P = 50;

// int LEN = 1e4;
// int LEN = 4;

/* =========== Parameter =========== */

void _err(const char* msg, int lineNum) {
    fprintf(stderr, "Error at line #%d: %s\n", lineNum, msg);
    exit(1);
}
#define err(msg) _err(msg, __LINE__)

inline int rand(int l, int r) {
    static mt19937 rnd(SEED);
    if (l > r) err("invalid range");
    return l + rnd() % (r - l + 1);
}

int n = rand(99990, 100000);
int q = 100000;
int ROUND = 5;
int LEN = rand(1000, 1314);

// int n = rand(9500, 10000);
// // int n = 20;
// int q = 100000;
// int ROUND = 5;

// int LEN = rand(3, 1314);

int m_limit = -1;  // -1 for not limit

vector<pair<int, int>> edges;
vector<vector<int>> son(n, vector<int>());
vector<int> dpt(n);

int maxcir = LEN;
int chong = 0;

void dfs(int u) {
    for (size_t i = 0; i < son[u].size(); ++i) {
        int v = son[u][i];
        dpt[v] = dpt[u] + 1;
        dfs(v);
    }
}

int redfs(int u, int ban) {
    int res = u;
    int cnt = 0;
    for (size_t i = 0; i < son[u].size(); ++i) {
        int v = son[u][i];
        dpt[v] = dpt[u] + 1;
        if (v == ban) continue;
        int t = redfs(v, -1);
        int len = dpt[t] - dpt[u] + 1;
        if ((t != v || (multiedge && cnt < 2 && rand(0, MULTI_P) == 0))
                && (len % 2 == 1 || !onlyOddCircle)
                && (m_limit == -1 || (int)edges.size() < m_limit))
            edges.emplace_back(u, t), cnt += t == v, maxcir = max(maxcir, len), chong += t == v;
        else if (rand(0, son[u].size()) == 0 || dpt[t] > dpt[res])
            res = t;
    }
    return res;
}

int main() {
    // freopen(".\\testdata\\4.in", "w", stdout);
    
    // freopen("yzh", "w", stdout);
    // freopen("in", "w", stdout);
    
    if (n < 1) err("n shouldn't be less than 1");
    if (m_limit != -1 && m_limit < n - 1)
        err("m_limit shouldn't less than n-1");
    if (LEN > n)
        err("LEN too long");
    
    vector<int> id(n);
    for (int i = 0; i < n; ++i) id[i] = i;
    for (int i = 1; i < n; ++i)
        swap(id[i], id[rand(0, i)]);
    
    for (int i = 1; i < LEN; ++i) {
        int fa = i - 1;
        edges.emplace_back(fa, i);
        son[fa].emplace_back(i);
    }
    for (int i = LEN; i < n; ++i) {
        int fa = rand(0, i - 1);
        edges.emplace_back(fa, i);
        son[fa].emplace_back(i);
    }
    if (LEN > 1 && m_limit != n - 1) {
        edges.emplace_back(0, LEN - 1);
    }
    dfs(0);
    for (int i = 1; i < LEN; ++i)
        redfs(i - 1, i);
    redfs(LEN - 1, -1);
    
    for (size_t i = 1; i < edges.size(); ++i)
        swap(edges[i], edges[rand(0, i)]);
    
    printf("%d %d\n", n, (int)edges.size());
    for (size_t i = 0; i < edges.size(); ++i) {
        int u = edges[i].first;
        int v = edges[i].second;
        if (rand(0, 1)) swap(u, v);
        u = id[u], v = id[v];
        printf("%d %d\n", u + 1, v + 1);
    }
    
    const int maxV = 1e9;
    const int minV = 100;
    
    for (int i = 1; i <= n; ++i)
        printf("%d%c", rand(0, minV), " \n"[i == n]);
    
    printf("%d\n", q);
    if (ROUND) {
        if (q % (ROUND * 2)) throw;
        int t = q / (ROUND * 2);
        vector<int> vec;
        while (t--) {
            for (int i = 1; i <= ROUND; ++i) {
                int u = rand(1, n);
                printf("%d %d\n", u, rand(0, maxV));
                vec.emplace_back(u);
            }
            for (int i = 1; i <= ROUND; ++i) {
                if (rand(0, 1000) && !vec.empty()) {
                    int u = vec.back();
                    vec.pop_back();
                    printf("%d %d\n", u, rand(0, minV));
                } else {
                    int u = rand(1, n);
                    printf("%d %d\n", u, rand(0, maxV));
                    vec.emplace_back(u);
                }
            }
        }
    } else {
        for (int i = 1; i <= q; ++i) {
            printf("%d %d\n", rand(1, n), rand(0, maxV));
        }
    }
    
    fprintf(stderr, "Success!\n");
    fprintf(stderr, "n = %d, m = %d\n", n, (int)edges.size());
    fprintf(stderr, "circle = %d\n", (int)edges.size() - (n - 1));
    fprintf(stderr, "maxcir = %d\n", maxcir);
    fprintf(stderr, "chong = %d\n", chong);
    return 0;
}
cpp 复制代码
#include <cstdio>
#include <iostream>
#include <vector>
#include <algorithm>
#include <cassert>
#include <random>
#include <chrono>
using namespace std;

const int SEED = chrono::system_clock::now().time_since_epoch().count();

inline int rand(int l, int r) {
    static mt19937 rnd(SEED);
    if (l > r) throw "invalid range";
    return l + rnd() % (r - l + 1);
}

int q = 1e5;
int ROUND = 5;

int n = 1e5, pts;
vector<pair<int, int>> edges;

int solve(int tot) {
    if (tot == 1)
        return ++pts;
    --tot;
    int du;
    if (rand(0, 4)) du = 2;
    else if (rand(0, 3)) du = 3;
    else du = 4;
    if (du > tot) du = tot;
    int one = tot / du;
    int u = ++pts;
    vector<int> son;
    for (int i = 1; i <= du - 1; ++i) {
        int v = solve(one);
        son.emplace_back(v);
    }
    {
        int v = solve(tot - (du - 1) * one);
        son.emplace_back(v);
    }
    edges.emplace_back(u, son.front());
    edges.emplace_back(son.back(), u);
    for (int i = 1; i < (int)son.size(); ++i)
        edges.emplace_back(son[i - 1], son[i]);
    return u;
}

int main() {
    solve(n);
    if (pts != n) throw;
    
    int m = edges.size();
    assert(n - 1 <= m && m <= 2 * (n - 1));
    
    for (size_t i = 1; i < edges.size(); ++i) {
        swap(edges[i], edges[rand(0, i)]);
    }
    
    vector<int> id(n);
    for (int i = 0; i < n; ++i) id[i] = i;
    for (int i = 1; i < n; ++i) {
        swap(id[i], id[rand(0, i)]);
    }
    
    printf("%d %d\n", n, (int)edges.size());
    for (size_t i = 0; i < edges.size(); ++i) {
        int u = edges[i].first;
        int v = edges[i].second;
        if (rand(0, 1)) swap(u, v);
        u = id[u - 1], v = id[v - 1];
        printf("%d %d\n", u + 1, v + 1);
    }
    
    const int maxV = 1e9;
    const int minV = 100;
    
    for (int i = 1; i <= n; ++i)
        printf("%d%c", rand(0, minV), " \n"[i == n]);
    
    printf("%d\n", q);
    if (q % (ROUND * 2)) throw;
    int t = q / (ROUND * 2);
    vector<int> vec;
    while (t--) {
        for (int i = 1; i <= ROUND; ++i) {
            int u = rand(1, n);
            printf("%d %d\n", u, rand(0, maxV));
            vec.emplace_back(u);
        }
        for (int i = 1; i <= ROUND; ++i) {
            if (rand(0, 1000) && !vec.empty()) {
                int u = vec.back();
                vec.pop_back();
                printf("%d %d\n", u, rand(0, minV));
            } else {
                int u = rand(1, n);
                printf("%d %d\n", u, rand(0, maxV));
                vec.emplace_back(u);
            }
        }
    }
    
    fprintf(stderr, "Success!\n");
    fprintf(stderr, "n = %d, m = %d\n", n, (int)edges.size());
    fprintf(stderr, "circle = %d\n", (int)edges.size() - (n - 1));
    return 0;
}

类型六:动态仙人掌

删边连边,保证操作前后都是森林,这类动态树问题,我们使用 LCT,SATT 等方式维护原树。

删边连边,保证操作前后都是沙漠,这类动态仙人掌问题,我们使用 LCT,SATT 等方式维护圆方树。

笔者能力有限,以后有时间再来补这一部分。

拓展:多层仙人掌

既然仙人掌的形态很像把树上节点替换成环,我们可以尝试把树上每个节点换成一棵仙人掌,如此进行下去,得到多层仙人掌的结构。

熟悉了仙人掌,其实这种结构无非是仙人掌的小拓展罢了,可以使用类似的算法解决。

圆方树在「一般无向图」上的应用

相信你已经掌握了如何使用圆方树解决仙人掌的问题,接下来我们将更进一步,学习如何在一般无向图上应用圆方树。

相比仙人掌而言,一般无向图并没有许多良好的性质,问题并不难于仙人掌上对应类型的问题。

类型一:单次询问整体信息

例十一、APIO2018 铁人两项(洛谷) Problem Statement

给你一张有 \(n\) 个点和 \(m\) 条边的无向图,你需要统计有序三元组 \((s,c,f)\) 的个数,且需要满足存在一条简单路径依次经过 \(s,c,f\)。简单路径指不重复经过点的路径。

\(n\leq 10^5\),\(m\leq 2\times 10^5\)。
Problem Analysis

考虑在树上统计三元组的个数。不妨将 \(s,f\) 视为地位相等的「端点」,最后答案乘以二即可。设 \(g_u\) 表示 \(u\) 子树中选出一个端点的方案数,其实就是 \(u\) 为根的子树大小。再设 \(f_u\) 表示 \(u\) 子树中选出一个端点 \(v\),再在 \(v\sim u\) 这条路径上选出中间点 \(c\) 的方案数。\(g\) 的转移显然。\(f\) 的转移也很简单,分为选不选择 \(u\) 为 \(c\) 两种情况讨论。对于一个 \(v\in \operatorname{son}(u)\),若选择,\(f_u\Leftarrow g_v\);若不选择,\(f_u\Leftarrow f_v\)。其中 \(a\Leftarrow b\) 表示 \(a\gets a+b\)。那么如何统计答案呢?我们考虑不断合并子树的过程,\(f_u,g_u\) 的含义为 \(u\) 和已经考虑过的子树,构成的一棵以 \(u\) 为根子树的信息。注意这个子树和传统意义上以 \(u\) 为根的子树之间的区别。考虑这个子树和一个没被合并进来的,以 \(v\) 为根的子树,它们之间对答案产生的贡献。显然就是 \(f_u\cdot g_v+f_v\cdot g_u\),加号前表示 \(c\) 落在以 \(u\) 为根的子树中,加号后表示 \(c\) 落在以 \(v\) 为根的子树中,然后运用加法原理即可,这是不重不漏的。

放到无向图上呢?我们考虑无非就是选取 \(c\) 的这一步变得有些复杂------在一个点双中选点而不是在一个点上选点。分为如何转移和如何统计答案两部分思考。记这个点双大小为 \(m\)。转移很不难想到,设方点的父亲圆点为 \(u\),某一个孩子圆点为 \(v\),\(g_u\Leftarrow g_v\),\(f_u\Leftarrow f_v + (m-1)g_v\),这里 \(m-1\) 的系数表示,\(c\) 可以选择除了 \(v\) 以外任意一个点双中的点,对于 \(c=v\) 的情况已经统计在 \(f_v\) 内了。我们先考虑暴力统计答案,枚举 \(m\) 个圆点中两个不同的圆点 \(u,v\),然后一样按照 \(c\) 放在哪里分类讨论。\(c\) 在 \(u\) 那一边,\(ans\Leftarrow g_u\cdot f_v\);\(c\) 在 \(v\) 那一边,\(ans\Leftarrow g_v\cdot f_u\);\(c\) 在点双中除了 \(u,v\) 其他的点上,\(ans\Leftarrow g_u\cdot g_v \cdot(m-2)\)。优化到线性也很简单,用类似前缀和的方法维护 \(\sum f_v\) 和 \(\sum g_v\) 即可。

时空复杂度 \(\mathcal{O}(n+m)\)。

注意此题坑点:图可能不连通,对每一个连通块做一遍即可。
Solution

cpp 复制代码
#include <cstdio>
#include <iostream>
#include <vector>
using namespace std;

const int N = 1e5 + 10;
const int M = 2e5 + 10;

int n, m;

struct node {
    int v, nxt;
} edge[M << 1];
int head[N], tot = 1;
void add(int u, int v) {
    edge[++tot] = { v, head[u] };
    head[u] = tot;
}

int dfn[N], low[N], timer;
int stack[N], top;

long long f[N], g[N], ans;

void tarjan(int u, int fr) {
    g[u] = 1;
    dfn[u] = low[u] = ++timer;
    stack[++top] = u;
    for (int i = head[u]; i; i = edge[i].nxt) {
        if (i == (fr ^ 1)) continue;
        int v = edge[i].v;
        if (!dfn[v]) {
            tarjan(v, i);
            low[u] = min(low[u], low[v]);
            if (low[v] >= dfn[u]) {
                vector<int> scc;
                while (true) {
                    int x = stack[top--];
                    scc.emplace_back(x);
                    if (x == v) break;
                }
                scc.emplace_back(u);
                int m = scc.size();
                
                long long sf = 0, sg = 0;
                for (int j = 0; j < m; ++j) {
                    int u = scc[j];
                    ans += g[u] * sf + sg * f[u];
                    ans += (m - 2) * g[u] * sg;
                    sf += f[u], sg += g[u];
                }
                // for (int a = 0; a < m; ++a)
                //     for (int b = a + 1; b < m; ++b) {
                //         int u = scc[a], v = scc[b];
                //         ans += g[u] * f[v] + g[v] * f[u];
                //         ans += (m - 2) * g[u] * g[v];
                //     }
                
                for (int j = 0; j < m - 1; ++j) {
                    int v = scc[j];
                    g[u] += g[v];
                    f[u] += f[v] + (m - 1) * g[v];
                }
            }
        } else {
            low[u] = min(low[u], dfn[v]);
        }
    }
}

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1, u, v; i <= m; ++i) {
        scanf("%d%d", &u, &v);
        add(u, v), add(v, u);
    }
    for (int u = 1; u <= n; ++u)
        if (!dfn[u])
            tarjan(u, 0);
    printf("%lld", ans << 1);
    return 0;
}

类型二:维护点对间所有简单路径信息

先来看一道经典的问题。
例十二、必经点个数(洛谷类似问题 UVA类似问题 hydro-bzoj-3331 压力) Problem Statement

给你一张有 \(n\) 个点和 \(m\) 条边的无向图,有 \(q\) 次询问,给出点对 \((u,v)\),求从 \(u\) 经过一条简单路径到 \(v\),必须经过的节点数。

\(n,q\leq 5\times10^5\),\(m\leq10^6\)。
Problem Analysis

放到圆方树上,均被包含的点,就是圆方树上路径上的圆点。这是因为对于非端点的一个圆点,它作为割点是必定会被经过的。于是可以简单求解。

时间复杂度 \(\mathcal{O}(m+n+q\log n)\),尽管可以优化到完全线性。
Solution

cpp 复制代码
#include <cstdio>
#include <iostream>
using namespace std;

const int N = 5e5 + 10;
const int M = 1e6 + 10;
const int lg2N = __lg(N << 1) + 1;

int n, m, q;

template <size_t _N, size_t _M>
class Graph {
public:
    Graph(): tot(1) {}
    struct node {
        int v, nxt;
    } edge[_M];
    int head[_N], tot;
    void add(int u, int v) {
        edge[++tot] = { v, head[u] };
        head[u] = tot;
    }
};

Graph<N, M << 1> G;
Graph<N << 1, N << 1> T;  // 圆方树

int dfn[N], low[N], timer;
int stack[N], top;

int dcc_cnt;
int pre[N << 1];

void tarjan(int u, int fr) {
    dfn[u] = low[u] = ++timer;
    stack[++top] = u;
    for (int i = G.head[u]; i; i = G.edge[i].nxt) {
        if (i == (fr ^ 1)) continue;
        int v = G.edge[i].v;
        if (!dfn[v]) {
            tarjan(v, i);
            low[u] = min(low[u], low[v]);
            if (low[v] >= dfn[u]) {
                ++dcc_cnt;
                T.add(u, dcc_cnt + n);
                while (true) {
                    int x = stack[top--];
                    T.add(dcc_cnt + n, x);
                    if (x == v) break;
                }
            }
        } else {
            low[u] = min(low[u], dfn[v]);
        }
    }
}

int fa[N << 1], dpt[N << 1];
int idx[N << 1], st[lg2N][N << 1];
void dfs(int u) {
    pre[u] = pre[fa[u]] + (u <= n);
    st[0][idx[u] = ++timer] = u;
    for (int i = T.head[u]; i; i = T.edge[i].nxt) {
        int v = T.edge[i].v;
        fa[v] = u;
        dpt[v] = dpt[u] + 1;
        dfs(v);
    }
}
inline int Min(int u, int v) {
    return dpt[u] < dpt[v] ? u : v;
}
inline int lca(int u, int v) {
    if (u == v) return u;
    if ((u = idx[u]) > (v = idx[v]))
        swap(u, v);
    int k = __lg(v - u++);
    return fa[Min(st[k][u], st[k][v - (1 << k) + 1])];
}
// O(n log n) ~ O(1) LCA

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1, u, v; i <= m; ++i) {
        scanf("%d%d", &u, &v);
        G.add(u, v), G.add(v, u);
    }
    tarjan(1, 0);
    timer = 0, dfs(1);
    for (int k = 1; k < lg2N; ++k)
        for (int i = 1; i + (1 << k) - 1 <= timer; ++i)
            st[k][i] = Min(st[k - 1][i], st[k - 1][i + (1 << (k - 1))]);
    scanf("%d", &q);
    for (int u, v; q--; ) {
        scanf("%d%d", &u, &v);
        int p = lca(u, v);
        int ans = pre[u] + pre[v] - pre[p] - pre[fa[p]];
        printf("%d\n", ans);
    }
    return 0;
}

接下来看一道典型的圆方树维护点对间所有简单路径信息的问题。
例十三、ZJOI2022 简单题(洛谷UOJ) Problem Statement

一张 \(n\) 个点、\(m\) 条边的无重边无自环连通无向图,带有正整数边权。

一开始图上任意两个简单环的边权和相等。后来其中一部分边的权值改成了新的权值,因此,修改后的图可能没有这个性质。

现在给出修改后的图,同时给出多组询问,每次询问两点 \(S, T\) 间所有简单路径权值和对 \(998244353\) 取模的结果。

简单环和简单路径指不包含重复节点。

\(1 \le n, q \le 5 \times {10}^5\),\(n - 1 \le m \le 6.4 \times {10}^5\)。
Problem Analysis

一道好题,关键在于分析出图的性质,否则无从下手。
这张图一开始任意两个简单环的边权和相等,这对这张图的形态做了限制,也就是说存在一种赋边权的方式,满足这个条件。

对于每一个点双考虑,因为简单环显然不会横跨两个点双,所以只会对点双内部的结构产生约束。接下来由简单到复杂,确定一个点双可能的形态。

  1. 这个点双没有简单环。

    也即两个点被一条边连接的情况,显然合法。

  2. 这个点双是一个简单环。

    显然合法。

  3. 这个点双在一个简单环的基础上多了一条简单链。

    我们假设多出的一条链的两端是 \(S, T\)。那么他们之间就被三条简单路径连接。为了方便起见,我们把这三条链看做是连接 \(S, T\) 的三条边,边权对应简单链的边权和。

    我们只需要说明,存在一种给 \(w_1, w_2, w_3\) 赋权的方式,使得任意两个简单环的边权和相等。

    图上有三个简单环,设边权和分别为 \(s_1, s_2, s_3\),有:

    \\\begin{cases} s_1=w_1+w_2 \\\\ s_2=w_1+w_3 \\\\ s_3=w_2+w_3 \\end{cases} \\

    只需令 \(w_1=w_2=w_3=k\),其中 \(k\) 为任意正整数,就能使 \(s_1=s_2=s_3\)。

  4. 这个点双在一个简单环的基础上,多了若干条以 \(S,T\) 为端点的简单链。

    类比 case 3 的推导,只需令 \(w_1=w_2=\cdots=w_t=k\),其中 \(k\) 为任意正整数,就能使 \(s_1=s_2=\cdots=s_t\)。

  5. 这个点双在一个简单环的基础上,多了两条不共端点的平行简单链。

    形如:

    类似地,这里存在六个环:

    \\\begin{cases} s_1=w_1+w_2 \\\\ s_2=w_3+w_4 \\\\ s_3=w_2+w_3+w_5+w_6 \\\\ s_4=w_2+w_3+w_5+w_6 \\\\ s_5=w_1+w_3+w_5+w_6 \\\\ s_6=w_2+w_4+w_5+w_6 \\\\ \\end{cases} \\

    令 \(w_1=w_2=w_3=w_4=w_5=w_6\),看看能不能导出什么矛盾。手玩一会后发现,由 \(s_1+s_2=s_5+s_6\),得到 \(w_5+w_6=0\),而我们的边权都是正整数,所以这是不合法的。

    也就是说,一个点双的结构不可能是这样。有趣的是,如果我们令 \(w_5=w_6=0\),那么这张图相当于退化成了 case 4 中 \(4\) 条链的情况,这似乎在暗示我们 case 4 或许就是我们要找的性质。

  6. 这个点双在一个简单环的基础上,多了若干条不共端点的平行简单链。

    一定能得到类似 case 5 中的若干条等式,从而导出不可能。

  7. 这个点双在一个简单环的基础上,多了两条不共端点的相交简单链。

    形如:

    这里同样存在六个环:

    \\\begin{cases} s_1=w_1+w_2+w_5 \\\\ s_2=w_1+w_3+w_6 \\\\ s_3=w_3+w_4+w_5 \\\\ s_4=w_2+w_4+w_6 \\\\ s_5=w_1+w_4+w_5+w_6 \\\\ s_6=w_2+w_3+w_5+w_6 \\\\ \\end{cases} \\

    类似根据 \(s_1+s_2=s_5+s_6\) 得到 \(w_6=0\),根据对称性同样有 \(w_5=0\),这也是不合法的,而且退化后的图也是 case 4 中 \(4\) 条链的情况。

  8. 这个点双在一个简单环的基础上,多了若干条不共端点的相交简单链。

    一定能得到类似 case 7 中的若干条等式,从而导出不可能。

  9. 这个点双在一个简单环的基础上,多了若干条不共端点的平行简单链,或相交简单链。

    一定能得到类似 case 5 或 case 7 中的若干条等式,从而导出不可能。

从而,我们证明了,只有 case 1、case 2、case 3、case 4 这几种点双的形态是合法的,进一步全部都可以归纳到 case 4 中:存在两个点 \(S,T\),他们之间连了若干条简单链。\(1,2,3\) 条链分别退化到 case 1、case 2、case 3。

每个点双的形态就是确定的了:

这种图似乎被称作「杏仁」,无论如何,这是一个关键的性质。

肯定需要上圆方树了。

根据经验,不难想到把 \((c,s)\) 这样的二元组作为所求信息,其中 \(c\) 表示简单路径条数,\(s\) 表示这些简单路径的边权总和。两个信息 \((c_1,s_1),(c_2,s_2)\) 合并后的结果即为 \((c_1c_2,c_1s_2+c_2s_1)\)。单位元即为 \((1,0)\)。

考虑 \(u\rightarrow v\) 的原子信息怎么求。记这个点双中 \(S,T\) 间有 \(c\) 条简单路径,边权和为 \(s\),对于不为 \(S,T\) 的点 \(u\),定义 \(\operatorname{from}(u)\) 表示 \(u\) 在 \(c\) 条简单路径中的哪一条上,\(\operatorname{disS}(u)\) 表示 \(S\) 通过 \(\operatorname{from}(u)\) 到 \(u\) 的距离,\(\operatorname{depthS}(u)\) 类似表示 \(S\) 通过 \(\operatorname{from}(u)\) 到 \(u\) 的边的条数,类似定义 \(\operatorname{disT}(u),\operatorname{depthT}(u)\)。接下来分类讨论。

  1. \(u=S,v=T\):

    信息即为 \((c,s)\)。

  2. \(u=S,v\neq T\):

    信息为 \(\Big(c,s+\operatorname{disT}(v)\cdot(c-1-1)\Big)\),\(c-1-1\) 表示 \(v\rightarrow T\) 这条边被 \(c-1\) 条路径经过,但是已经在 \(s\) 中贡献了一次,所以是 \(c-1-1\)。

  3. \(u,v\not\in\{S,T\},\operatorname{from}(u)=\operatorname{from}(v),\operatorname{depthS}(u)\leq\operatorname{depthS}(v)\):

    信息为 \(\Big(c,s+(\operatorname{disT}(v)+\operatorname{disS}(u))\cdot(c-1-1)\Big)\)。

  4. \(u,v\not\in\{S,T\},\operatorname{from}(u)\neq\operatorname{from}(v)\):

    信息为 \(\Big(2c-2, 2s + (\operatorname{disS}(u)+\operatorname{disT}(u))\cdot(c-1-2) + (\operatorname{disS}(v)+\operatorname{disT}(v))\cdot(c-1-2)\Big)\)。

剩下的情况都是对称的,此略去。

至于如何求出这些信息,只需要把点双的子图建出来,这就是前文仙人掌部分提到的弹边栈的方式,在弹出一条边的时候,在另一张图上加边。这另一张图就是这个点双的子图,根据度数找出 \(S,T\) 后,就可以方便处理出其他信息了。

于是我们可以套用圆方树多次询问点对信息的模式方便求解。

这个信息显然满足交换律,所以不需要考虑合并信息的顺序。

在了解圆方树后,这份代码实现起来并不困难。

时间复杂度 \(\mathcal{O}(m+n\log n+q\log n)\)。
Solution 圆方树

cpp 复制代码
#include <cstdio>
#include <iostream>
using namespace std;

const int mod = 998244353;
const int N = 5e5 + 10;
const int M = 6.4e5 + 10;
const int N2 = N << 1;
const int lg2N = __lg(N2) + 1;

int n, m, q;

inline int add(int a, int b) {
    return a += b, a >= mod ? a - mod : a;
}
inline int mul(int a, int b) {
    return 1ll * a * b % mod;
}

struct node {
    int cnt, sum;
    friend node operator * (const node& a, const node& b) {
        return {
            mul(a.cnt, b.cnt),
            add(mul(b.cnt, a.sum), mul(a.cnt, b.sum))
        };
    }
};

template <size_t _N, size_t _M, typename T = int>
class Grpah {
public:
    Grpah(): tot(1) {}
    struct node {
        int v, nxt;
        T w;
    } edge[_M];
    int head[_N], tot;
    void add(int u, int v, const T& w) {
        edge[++tot] = { v, head[u], w };
        head[u] = tot;
    }
};
Grpah<N, M << 1> G;  // original grpah
Grpah<N << 1, N << 1, node> T;  // rebuild tree
Grpah<N, M << 1> SG;  // sub graph

int dfn[N], low[N], timer;
int est[N], etp;
int sccno[N], scc_cnt, scc_siz[N];
int _du[N], _scc[N], _siz;

int A[N], B[N];
int esum[N];
int pathCnt[N];
int from[N], dptS[N];
int disT[N], disS[N];

void getFromDisS(int x, int T, int fr) {
    for (int i = SG.head[x]; i; i = SG.edge[i].nxt) {
        int v = SG.edge[i].v;
        int w = SG.edge[i].w;
        if (v == fr || v == T) continue;
        disS[v] = add(disS[x], w);
        dptS[v] = dptS[x] + 1;
        from[v] = from[x];
        getFromDisS(v, T, x);
    }
}

void getDisT(int x, int S, int fr) {
    for (int i = SG.head[x]; i; i = SG.edge[i].nxt) {
        int v = SG.edge[i].v;
        int w = SG.edge[i].w;
        if (v == fr || v == S) continue;
        disT[v] = add(disT[x], w);
        getDisT(v, S, x);
    }
}

node getdis(int u, int v, int s) {
    if (u == v) return { 1, 0 };
    int frU = from[u], frV = from[v];
    int disSU = disS[u], disSV = disS[v];
    int disTU = disT[u], disTV = disT[v];
    int du = dptS[u], dv = dptS[v];
    int S = A[s], T = B[s];
    int sum = esum[s];
    int pcnt = pathCnt[s];
    if (u == S && v == T)
        return { pcnt, sum };
    if (v == S && u == T)
        return { pcnt, sum };
    if (u == S)
        return { pcnt, add(sum, mul(disTV, pcnt - 1 - 1)) };
    if (u == T)
        return { pcnt, add(sum, mul(disSV, pcnt - 1 - 1)) };
    if (v == S)
        return { pcnt, add(sum, mul(disTU, pcnt - 1 - 1)) };
    if (v == T)
        return { pcnt, add(sum, mul(disSU, pcnt - 1 - 1)) };
    if (frU == frV) {
        if (du < dv)
            return { pcnt, add(sum, mul(add(disSU, disTV), pcnt - 1 - 1)) };
        else
            return { pcnt, add(sum, mul(add(disSV, disTU), pcnt - 1 - 1)) };
    }
    return { (pcnt - 1) * 2, add(mul(sum, 2), add(mul(add(disSU, disTU), pcnt - 1 - 2), mul(add(disSV, disTV), pcnt - 1 - 2))) };
}

void deal(int p) {
    A[p] = B[p] = -1;
    for (int i = 0; i < _siz; ++i) {
        int u = _scc[i];
        if (_du[u] <= 2) continue;
        if (A[p] == -1)
            A[p] = u;
        else {
            B[p] = u;
            break;
        }
    }
    if (A[p] == -1)
        A[p] = _scc[0], B[p] = _scc[1];
    int S = A[p], T = B[p];
    disS[S] = 0;
    dptS[S] = 0;
    for (int i = SG.head[S]; i; i = SG.edge[i].nxt) {
        ++pathCnt[p];
        int v = SG.edge[i].v;
        int w = SG.edge[i].w;
        if (v == T) continue;
        dptS[v] = 1;
        disS[v] = w;
        from[v] = v;
        getFromDisS(v, T, S);
    }
    disT[T] = 0;
    getDisT(T, S, 0);
}

int _vis[N], _vis_tim;
void insert(int u) {
    if (_vis[u] == _vis_tim) return;
    _vis[u] = _vis_tim;
    _scc[_siz++] = u;
}

void tarjan(int u, int fr) {
    dfn[u] = low[u] = ++timer;
    for (int i = G.head[u]; i; i = G.edge[i].nxt) {
        if (i == (fr ^ 1)) continue;
        int v = G.edge[i].v;
        if (!dfn[v]) {
            est[++etp] = i;
            tarjan(v, i);
            low[u] = min(low[u], low[v]);
            if (low[v] >= dfn[u]) {
                ++scc_cnt, ++_vis_tim, _siz = 0;
                SG.tot = 1;
                while (true) {
                    int x = est[etp--];
                    int u = G.edge[x    ].v;
                    int v = G.edge[x ^ 1].v;
                    int w = G.edge[x    ].w;
                    insert(u), insert(v);
                    ++_du[u], ++_du[v];
                    esum[scc_cnt] = add(esum[scc_cnt], w);
                    SG.add(u, v, w);
                    SG.add(v, u, w);
                    if (x == i) break;
                }
                deal(scc_cnt);
                for (int i = 0; i < _siz; ++i) {
                    int x = _scc[i];
                    if (x == u) continue;
                    T.add(scc_cnt + n, x, getdis(u, x, scc_cnt));
                }
                T.add(u, scc_cnt + n, { 1, 0 });
                
                _du[u] = SG.head[u] = 0;
            }
        } else {
            low[u] = min(low[u], dfn[v]);
            if (dfn[v] < dfn[u])
                est[++etp] = i;
        }
    }
}

int fa[lg2N][N << 1], dpt[N << 1];
node val[lg2N][N << 1];

void dfs(int u) {
    for (int i = T.head[u]; i; i = T.edge[i].nxt) {
        int v = T.edge[i].v;
        fa[0][v] = u;
        val[0][v] = T.edge[i].w;
        dpt[v] = dpt[u] + 1;
        dfs(v);
    }
}

int query(int u, int v) {
    if (dpt[u] < dpt[v]) swap(u, v);
    node res = { 1, 0 };
    for (int i = lg2N - 1; ~i; --i)
        if (fa[i][u] && dpt[fa[i][u]] >= dpt[v]) {
            res = res * val[i][u];
            u = fa[i][u];
        }
    if (u == v) return res.sum;
    for (int i = lg2N - 1; ~i; --i)
        if (fa[i][u] != fa[i][v]) {
            res = res * val[i][u] * val[i][v];
            u = fa[i][u], v = fa[i][v];
        }
    if (fa[0][u] > n)
        res = res * getdis(u, v, fa[0][u] - n);
    else
        res = res * val[0][u] * val[0][v];  // this is not necessary
    return res.sum;
}

int main() {
#ifndef XuYueming
    // freopen("simple.in", "r", stdin);
    // freopen("simple.out", "w", stdout);
#endif
    scanf("%d%d%d", &n, &m, &q);
    for (int i = 1, u, v, w; i <= m; ++i) {
        scanf("%d%d%d", &u, &v, &w);
        G.add(u, v, w);
        G.add(v, u, w);
    }
    tarjan(1, 0);
    dfs(1);
    for (int k = 1; k < lg2N; ++k)
        for (int u = 1; u <= n + scc_cnt; ++u) {
            fa[k][u] = fa[k - 1][fa[k - 1][u]];
            if (fa[k][u])
                val[k][u] = val[k - 1][u] * val[k - 1][fa[k - 1][u]];
        }
    for (int u, v; q--; ) {
        scanf("%d%d", &u, &v);
        printf("%d\n", query(u, v));
    }
    return 0;
}

圆树

cpp 复制代码
#include <cstdio>
#include <iostream>
using namespace std;

const int mod = 998244353;
const int N = 5e5 + 10;
const int M = 6.4e5 + 10;
const int lgN = __lg(N) + 1;

int n, m, q;

inline int add(int a, int b) {
    return a += b, a >= mod ? a - mod : a;
}
inline int mul(int a, int b) {
    return 1ll * a * b % mod;
}

struct node {
    int cnt, sum;
    friend node operator * (const node& a, const node& b) {
        return {
            mul(a.cnt, b.cnt),
            add(mul(b.cnt, a.sum), mul(a.cnt, b.sum))
        };
    }
};

template <size_t _N, size_t _M, typename T = int>
class Grpah {
public:
    Grpah(): tot(1) {}
    struct node {
        int v, nxt;
        T w;
    } edge[_M];
    int head[_N], tot;
    void add(int u, int v, const T& w) {
        edge[++tot] = { v, head[u], w };
        head[u] = tot;
    }
};
Grpah<N, M << 1> G;  // original grpah
Grpah<N, N, node> T;  // rebuild tree
Grpah<N, M << 1> SG;  // sub graph

int dfn[N], low[N], timer;
int est[N], etp;
int sccno[N], scc_cnt, scc_siz[N];
int _du[N], _scc[N], _siz;

int A[N], B[N];
int esum[N];
int pathCnt[N];
int from[N], dptS[N];
int disT[N], disS[N];

void getFromDisS(int x, int T, int fr) {
    for (int i = SG.head[x]; i; i = SG.edge[i].nxt) {
        int v = SG.edge[i].v;
        int w = SG.edge[i].w;
        if (v == fr || v == T) continue;
        disS[v] = add(disS[x], w);
        dptS[v] = dptS[x] + 1;
        from[v] = from[x];
        getFromDisS(v, T, x);
    }
}

void getDisT(int x, int S, int fr) {
    for (int i = SG.head[x]; i; i = SG.edge[i].nxt) {
        int v = SG.edge[i].v;
        int w = SG.edge[i].w;
        if (v == fr || v == S) continue;
        disT[v] = add(disT[x], w);
        getDisT(v, S, x);
    }
}

node getdis(int u, int v, int s) {
    if (u == v) return { 1, 0 };
    int frU = from[u], frV = from[v];
    int disSU = disS[u], disSV = disS[v];
    int disTU = disT[u], disTV = disT[v];
    int du = dptS[u], dv = dptS[v];
    int S = A[s], T = B[s];
    int sum = esum[s];
    int pcnt = pathCnt[s];
    if (u == S && v == T)
        return { pcnt, sum };
    if (v == S && u == T)
        return { pcnt, sum };
    if (u == S)
        return { pcnt, add(sum, mul(disTV, pcnt - 1 - 1)) };
    if (u == T)
        return { pcnt, add(sum, mul(disSV, pcnt - 1 - 1)) };
    if (v == S)
        return { pcnt, add(sum, mul(disTU, pcnt - 1 - 1)) };
    if (v == T)
        return { pcnt, add(sum, mul(disSU, pcnt - 1 - 1)) };
    if (frU == frV) {
        if (du < dv)
            return { pcnt, add(sum, mul(add(disSU, disTV), pcnt - 1 - 1)) };
        else
            return { pcnt, add(sum, mul(add(disSV, disTU), pcnt - 1 - 1)) };
    }
    return { (pcnt - 1) * 2, add(mul(sum, 2), add(mul(add(disSU, disTU), pcnt - 1 - 2), mul(add(disSV, disTV), pcnt - 1 - 2))) };
}

void deal(int p) {
    A[p] = B[p] = -1;
    for (int i = 0; i < _siz; ++i) {
        int u = _scc[i];
        if (_du[u] <= 2) continue;
        if (A[p] == -1)
            A[p] = u;
        else {
            B[p] = u;
            break;
        }
    }
    if (A[p] == -1)
        A[p] = _scc[0], B[p] = _scc[1];
    int S = A[p], T = B[p];
    disS[S] = 0;
    dptS[S] = 0;
    for (int i = SG.head[S]; i; i = SG.edge[i].nxt) {
        ++pathCnt[p];
        int v = SG.edge[i].v;
        int w = SG.edge[i].w;
        if (v == T) continue;
        dptS[v] = 1;
        disS[v] = w;
        from[v] = v;
        getFromDisS(v, T, S);
    }
    disT[T] = 0;
    getDisT(T, S, 0);
}

int _vis[N], _vis_tim;
void insert(int u) {
    if (_vis[u] == _vis_tim) return;
    _vis[u] = _vis_tim;
    _scc[_siz++] = u;
    sccno[u] = scc_cnt;
}

void tarjan(int u, int fr) {
    dfn[u] = low[u] = ++timer;
    for (int i = G.head[u]; i; i = G.edge[i].nxt) {
        if (i == (fr ^ 1)) continue;
        int v = G.edge[i].v;
        if (!dfn[v]) {
            est[++etp] = i;
            tarjan(v, i);
            low[u] = min(low[u], low[v]);
            if (low[v] >= dfn[u]) {
                ++scc_cnt, ++_vis_tim, _siz = 0;
                SG.tot = 1;
                while (true) {
                    int x = est[etp--];
                    int u = G.edge[x    ].v;
                    int v = G.edge[x ^ 1].v;
                    int w = G.edge[x    ].w;
                    insert(u), insert(v);
                    ++_du[u], ++_du[v];
                    esum[scc_cnt] = add(esum[scc_cnt], w);
                    SG.add(u, v, w);
                    SG.add(v, u, w);
                    if (x == i) break;
                }
                deal(scc_cnt);
                for (int i = 0; i < _siz; ++i) {
                    int x = _scc[i];
                    if (x == u) continue;
                    T.add(u, x, getdis(u, x, scc_cnt));
                }
                _du[u] = SG.head[u] = 0;
            }
        } else {
            low[u] = min(low[u], dfn[v]);
            if (dfn[v] < dfn[u])
                est[++etp] = i;
        }
    }
}

int fa[lgN][N], dpt[N];
node val[lgN][N];

void dfs(int u) {
    for (int i = T.head[u]; i; i = T.edge[i].nxt) {
        int v = T.edge[i].v;
        fa[0][v] = u;
        val[0][v] = T.edge[i].w;
        dpt[v] = dpt[u] + 1;
        dfs(v);
    }
}

int query(int u, int v) {
    if (dpt[u] < dpt[v]) swap(u, v);
    node res = { 1, 0 };
    for (int i = lgN - 1; ~i; --i)
        if (fa[i][u] && dpt[fa[i][u]] >= dpt[v]) {
            res = res * val[i][u];
            u = fa[i][u];
        }
    if (u == v) return res.sum;
    for (int i = lgN - 1; ~i; --i)
        if (fa[i][u] != fa[i][v]) {
            res = res * val[i][u] * val[i][v];
            u = fa[i][u], v = fa[i][v];
        }
    if (sccno[u] == sccno[v])
        res = res * getdis(u, v, sccno[u]);
    else
        res = res * val[0][u] * val[0][v];  // this is now necessary
    return res.sum;
}

int main() {
#ifndef XuYueming
    // freopen("simple.in", "r", stdin);
    // freopen("simple.out", "w", stdout);
#endif
    scanf("%d%d%d", &n, &m, &q);
    for (int i = 1, u, v, w; i <= m; ++i) {
        scanf("%d%d%d", &u, &v, &w);
        G.add(u, v, w);
        G.add(v, u, w);
    }
    tarjan(1, 0);
    dfs(1);
    for (int k = 1; k < lgN; ++k)
        for (int u = 1; u <= n; ++u) {
            fa[k][u] = fa[k - 1][fa[k - 1][u]];
            if (fa[k][u])
                val[k][u] = val[k - 1][u] * val[k - 1][fa[k - 1][u]];
        }
    for (int u, v; q--; ) {
        scanf("%d%d", &u, &v);
        printf("%d\n", query(u, v));
    }
    return 0;
}

接下来我们来看一道特殊的带修改的问题。
例十四、Tourists(Codeforces) Problem Statement

一张 \(n\) 个点、\(m\) 条边的连通无向图,带有点权。你需要支持:

  1. 修改点权。
  2. 询问 \(u,v\) 为端点的所有简单路径中,点权最小值的最小值。

\(1\leq n,m,q\leq 10^5\)。
Problem Analysis

方点在圆方树上的点权为,它的所有孩子圆点在原图上的点权的最小值。圆点的在圆方树上的点权为最小值的单位元,即 \(\infty\)。

给出树链剖分维护圆方树的实现方式。
Solution

cpp 复制代码
#include <cstdio>
#include <iostream>
#include <set>
using namespace std;

const int N = 1e5 + 10;
const int M = 1e5 + 10;
const int NN = N << 1;
const int lg2N = __lg(NN) + 1;
const int inf = 0x3f3f3f3f;

int n, m, q, w[N];

template <size_t _N, size_t _M>
class Grpah {
public:
    Grpah(): tot(1) {}
    struct node {
        int v, nxt;
    } edge[_M];
    int head[_N], tot;
    void add(int u, int v) {
        edge[++tot] = { v, head[u] };
        head[u] = tot;
    }
};
Grpah<N, M << 1> G;  // original grpah
Grpah<N << 1, N << 1> T;  // rebuild tree

multiset<int> sss[N];  // 每个方点的数据结构
int scc_cnt;

namespace $build {

int dfn[N], low[N], timer;
int stack[N], top;

void tarjan(int u, int fr) {
    dfn[u] = low[u] = ++timer;
    stack[++top] = u;
    for (int i = G.head[u]; i; i = G.edge[i].nxt) {
        if (i == (fr ^ 1)) continue;
        int v = G.edge[i].v;
        if (!dfn[v]) {
            tarjan(v, i);
            low[u] = min(low[u], low[v]);
            if (low[v] >= dfn[u]) {
                ++scc_cnt;
                while (true) {
                    int x = stack[top--];
                    T.add(scc_cnt + n, x);
                    sss[scc_cnt].insert(w[x]);
                    if (x == v) break;
                }
                T.add(u, scc_cnt + n);
            }
        } else {
            low[u] = min(low[u], dfn[v]);
        }
    }
}

}

namespace TREE {

#define lson (idx << 1)
#define rson (idx << 1 | 1)

int mi[NN << 2];

void build(int idx, int l, int r) {
    mi[idx] = inf;
    if (l == r) return;
    int mid = (l + r) >> 1;
    build(lson, l, mid);
    build(rson, mid + 1, r);
}

void modify(int idx, int trl, int trr, int p, int v) {
    if (trl == trr) return mi[idx] = v, void();
    int mid = (trl + trr) >> 1;
    if (p <= mid) modify(lson, trl, mid, p, v);
    else modify(rson, mid + 1, trr, p, v);
    mi[idx] = min(mi[lson], mi[rson]);
}

int query(int idx, int trl, int trr, int l, int r) {
    if (l <= trl && trr <= r) return mi[idx];
    int mid = (trl + trr) >> 1;
    if (r <= mid) return query(lson, trl, mid, l, r);
    if (l >  mid) return query(rson, mid + 1, trr, l, r);
    return min(query(lson, trl, mid, l, r), query(rson, mid + 1, trr, l, r));
}

#undef lson
#undef rson

}

int siz[NN], son[NN], top[NN];
int fa[NN], dpt[NN];
int idx[NN], timer;

void dfs(int u) {
    siz[u] = 1;
    for (int i = T.head[u]; i; i = T.edge[i].nxt) {
        int v = T.edge[i].v;
        fa[v] = u;
        dpt[v] = dpt[u] + 1;
        dfs(v);
        siz[u] += siz[v];
        if (siz[v] > siz[son[u]])
            son[u] = v;
    }
}

void redfs(int u, int tp) {
    top[u] = tp;
    idx[u] = ++timer;
    if (u <= n)
        TREE::modify(1, 1, n + scc_cnt, idx[u], inf);  // 圆点设置为单位元
    else
        TREE::modify(1, 1, n + scc_cnt, idx[u], *sss[u - n].begin());
    if (son[u]) redfs(son[u], tp);
    for (int i = T.head[u]; i; i = T.edge[i].nxt) {
        int v = T.edge[i].v;
        if (v == son[u]) continue;
        redfs(v, v);
    }
}

int query(int u, int v) {
    int res = inf;
    while (top[u] != top[v]) {
        if (dpt[top[u]] < dpt[top[v]])
            swap(u, v);
        res = min(res, TREE::query(1, 1, n + scc_cnt, idx[top[u]], idx[u]));
        u = fa[top[u]];
    }
    if (dpt[u] > dpt[v])
        swap(u, v);
    res = min(res, TREE::query(1, 1, n + scc_cnt, idx[u], idx[v]));
    if (u <= n) {
        res = min(res, w[u]);
    } else {
        if (fa[u])
            res = min(res, w[fa[u]]);
    }
    return res;
}

void modify(int u, int ww) {
    if (fa[u]) {
        int x = fa[u] - n;
        sss[x].erase(sss[x].find(w[u]));
        sss[x].insert(ww);
        TREE::modify(1, 1, n + scc_cnt, idx[x + n], *sss[x].begin());
    }
    w[u] = ww;
}

int main() {
    scanf("%d%d%d", &n, &m, &q);
    for (int i = 1; i <= n; ++i)
        scanf("%d", &w[i]);
    for (int i = 1, u, v; i <= m; ++i) {
        scanf("%d%d", &u, &v);
        G.add(u, v);
        G.add(v, u);
    }
    $build::tarjan(1, 0);
    TREE::build(1, 1, n + scc_cnt);
    dfs(1), redfs(1, 1);
    for (int u, v; q--; ) {
        char op[2];
        scanf("%s%d%d", op, &u, &v);
        if (*op == 'C')
            modify(u, v);
        else
            printf("%d\n", query(u, v));
    }
    return 0;
}

类型三:求给定点集的整体信息

来看一道没有显式建出虚树,而是利用虚树结构解决的问题。
例十五、SDOI2018 战略游戏(洛谷) Problem Statement

一张 \(n\) 个点、\(m\) 条边的连通无向图。

\(q\) 此询问,每次给出一个点集 \(S\)。你可以选择一个不在 \(S\) 中的点,如果存在 \(S\) 中两个点,在不经过你选择的这个点的前提下,不能够到达对方,那么这个方案就是合法的。求你有多少种选点的方案。

\(2\leq n\leq10^5\),\(n-1\leq m\leq2\times10^5\),\(1\leq q\leq10^5\),\(\sum|S|\leq2\times10^5\)。
Problem Analysis

圆方树上,如果存在一个圆点,删去它之后的若干个连通块中,如果至少有两个连通块中都有 \(S\) 中的点,那么不在同一个连通块里的点就不能够互相到达,这个圆点就是合法的。

显然需要建出圆方树的虚树。观察下图,为 \(\{2,7,10\}\) 对应的虚树:

其中紫色圈出来的点表示贡献到答案里的点。我们不妨把 \(S\) 中的点先看做合法的,最后再减去他们。这样,我们就可以确定哪些圆点需要贡献到答案中去了。对于一条虚树上的树边 \((u,v)\),圆方树上 \(u\rightarrow v\) 这条简单路径上的所有圆点都是合法的。正确性也十分显然。计数的话只需要做一遍树上前缀和就能单次 \(\mathcal{O}(1)\) 求出路径上有多少圆点。端点可能会重复统计?只需要在统计时,不统计父亲即可,因为父亲会恰好作为一次孩子被统计到,当然,除了根,那么最后看一下根是不是一个圆点即可。

时间复杂度 \(\mathcal{O}(m+n\log n+\sum|S|\log|S|)\)。

或者可以离线下来,配合 \(\mathcal{O}(n)\sim\mathcal{O}(1)\) LCA,做到 \(\mathcal{O}(|S|)\) 建立虚树,总的时间复杂度是线性 \(\mathcal{O}(m+n+\sum|S|)\)。
Solution

cpp 复制代码
#include <cstdio>
#include <iostream>
#include <set>
#include <algorithm>
using namespace std;

const int N = 1e5 + 10;
const int M = 2e5 + 10;
const int NN = N << 1;
const int lg2N = __lg(NN) + 1;

int n, m, q;

template <size_t _N, size_t _M>
class Grpah {
public:
    Grpah(): tot(1) {}
    struct node {
        int v, nxt;
    } edge[_M];
    int head[_N], tot;
    void add(int u, int v) {
        edge[++tot] = { v, head[u] };
        head[u] = tot;
    }
};
Grpah<N, M << 1> G;  // original grpah
Grpah<N << 1, N << 1> T;  // rebuild tree

int scc_cnt;

namespace $build {

int dfn[N], low[N], timer;
int stack[N], top;

void tarjan(int u, int fr) {
    dfn[u] = low[u] = ++timer;
    stack[++top] = u;
    for (int i = G.head[u]; i; i = G.edge[i].nxt) {
        if (i == (fr ^ 1)) continue;
        int v = G.edge[i].v;
        if (!dfn[v]) {
            tarjan(v, i);
            low[u] = min(low[u], low[v]);
            if (low[v] >= dfn[u]) {
                ++scc_cnt;
                while (true) {
                    int x = stack[top--];
                    T.add(scc_cnt + n, x);
                    if (x == v) break;
                }
                T.add(u, scc_cnt + n);
            }
        } else {
            low[u] = min(low[u], dfn[v]);
        }
    }
}

}

int dpt[NN], fa[NN], timer;
int idx[NN], st[lg2N][NN];
int sum[NN];  // tree prefix sum, how many circle point

void dfs(int u) {
    st[0][idx[u] = ++timer] = u;
    for (int i = T.head[u]; i; i = T.edge[i].nxt) {
        int v = T.edge[i].v;
        dpt[v] = dpt[u] + 1;
        fa[v] = u;
        sum[v] = sum[u] + (v <= n);
        dfs(v);
    }
}
inline int Min(int a, int b) {
    return dpt[a] < dpt[b] ? a : b;
}
inline int lca(int u, int v) {
    if (u == v) return u;
    if ((u = idx[u]) > (v = idx[v])) swap(u, v);
    int k = __lg(v - u++);
    return fa[Min(st[k][u], st[k][v - (1 << k) + 1])];
}

void solve() {
    scanf("%d%d", &n, &m);
    for (int i = 1, u, v; i <= m; ++i) {
        scanf("%d%d", &u, &v);
        G.add(u, v);
        G.add(v, u);
    }
    $build::tarjan(1, 0);
    dfs(1);
    for (int k = 1; k < lg2N; ++k)
        for (int i = 1; i + (1 << k) - 1 <= n + scc_cnt; ++i)
            st[k][i] = Min(st[k - 1][i], st[k - 1][i + (1 << (k - 1))]);
    scanf("%d", &q);
    for (int i = 1, k; i <= q; ++i) {
        static int st[N << 1];
        scanf("%d", &k);
        for (int i = 1; i <= k; ++i)
            scanf("%d", &st[i]);
        sort(st + 1, st + k + 1, [] (int a, int b) {
            return idx[a] < idx[b];
        });
        for (int i = 1; i < k; ++i)
            st[k + i] = lca(st[i], st[i + 1]);
        sort(st + 1, st + k * 2, [] (int a, int b) {
            return idx[a] < idx[b];
        });
        int m = unique(st + 1, st + k * 2) - st - 1;
        int ans = 0;
        for (int i = 1; i < m; ++i) {
            int u = lca(st[i], st[i + 1]);
            int v = st[i + 1];
            ans += sum[v] - sum[u];
        }
        ans += st[1] <= n;
        ans -= k;
        printf("%d\n", ans);
    }
    for (int i = 1; i <= n; ++i) G.head[i] = 0, $build::dfn[i] = 0;
    for (int i = 1; i <= n + scc_cnt; ++i) T.head[i] = 0;
    G.tot = T.tot = 1;
    $build::top = $build::timer = 0;
    timer = scc_cnt = 0;
}

int main() {
    int t;
    scanf("%d", &t);
    while (t--) solve();
    return 0;
}

其他想讲的题目

例十五、同一点双? Problem Statement

给你一个有 \(n\) 个点和 \(m\) 条边的无向图,和 \(q\) 组询问,每次询问两点 \(u,v\) 是否在同一点双内。

\(n\leq 10^5\),\(m\leq 10^6\),\(q\leq 10^6\)。
Problem Analysis

在经历了种种难题的洗礼,这一题就显得十分简单了。

两个点在同一个点双中,当且仅当存在一个连接他们的方点。

分类讨论可知,情况只有两种:其中一个是另一个的爷爷、其中一个是另一个的兄弟。前一种情况对应于一个是方点的父亲,一个是方点的孩子;后一种情况对应于两个都是方点的孩子。

于是每次可以 \(\mathcal{O}(1)\) 判断。


之所以有这一道水题,是因为《POI1999 Store-keeper 题解》中,本人采用「用来弹栈的割点」来理解此问题。事实上,在学习圆方树后,可以更清晰地理解这个问题:最终我们标记出来每个点所属的点双,和其在圆方树上的父亲是等价的。「用来弹栈的割点」便是方点的父亲圆点。

总结

通过本文的学习,我们深入探讨了圆方树这一结构在图论问题中的建模价值,特别是在仙人掌图及一般无向图中的实践应用。我们探究了树形 DP、点分治、树上启发式合并(DSU on tree)、树链剖分、虚树、动态 DP(DDP)、动态树等多种算法技巧,在这些图结构上的适用性和实现方法。同时,我们也厘清了一个核心问题:如何在这些图中高效维护点对之间所有简单路径的信息,从而为我们提供一种解决问题的全新的视角。

习题

这里总结了一些习题,大家可以练练手。

  1. 例一、静态仙人掌最大独立集(小 C 的独立集,黑暗爆炸洛谷Hydro
  2. 例二、静态仙人掌直径(SHOI2008 仙人掌图,黑暗爆炸洛谷Hydro
  3. 例三、persistent DSU on cactus(洛谷
  4. 例四、GAME on cactus(洛谷
  5. 例五、仙人掌点对间最短路长度(【模板】静态仙人掌,洛谷
  6. 例六、BUY on cactus(洛谷
  7. 例七、【清华集训2015】静态仙人掌(UOJ
  8. 例八、mx 的仙人掌(UOJ
  9. 例九、【集训队互测2016】火车司机出秦川(UOJ
  10. 例十、DDP on cactus(洛谷
  11. 例十一、APIO2018 铁人两项(洛谷
  12. 例十二、必经点个数(洛谷类似问题 UVA类似问题 hydro-bzoj-3331 压力
  13. 例十三、ZJOI2022 简单题(洛谷UOJ
  14. 例十四、Tourists(Codeforces
  15. 例十五、SDOI2018 战略游戏(洛谷

欢迎补充。

编辑历史

  • 2024-7-21:初版发布。
  • 2025-3-19:开始重构。
  • 2025-5-4:完成初稿。
  • 2025-6-7:进一步整理重写,完成二稿。
  • 2025-6-13:完成语言润色。
  • 2025-7-19:学考后继续完善。新增仙人掌上换根 DP。
  • 2025-7-19:检查审稿,发布终稿,投稿博客园首页。

原版博客

原版博客

前言

圆方树学习笔记,从一道例题讲起。

题目链接:Hydro & bzoj

题意简述

仙人掌上求两点距离。

题目分析

为了把仙人掌的性质发挥出来,考虑将其变成一棵树。圆方树就是这样转换的工具。

先讲讲圆方树的概念:原图上的点为圆点,每个点双对应一个方点,树边都是方点连向点双内的圆点。

具体代码实现也十分简单,就是在 tarjan 求点双的时候,弹栈的时候,新建结点,并和点双内的点连边即可。或者在处理最后连边即可。

注意,对于孤立点的处理,是保持原样不动,还是变为一对方点和圆点,视情况讨论。以下代码按照上述定义,即对于孤立点,在圆方树上体现为一对方点和白点。

cpp 复制代码
void tarjan(int now, int fa){
    dfn[now] = low[now] = ++timer, stack[++top] = now;
    int son = 0;
    for (int i = head[now]; i; i = edge[i].nxt){
        int to = edge[i].to;
        if (!dfn[to]){
            tarjan(to, now), low[now] = min(low[now], low[to]), ++son;
            if (low[to] >= dfn[now]){
                scc[++scc_cnt].push_back(now);
                do scc[scc_cnt].push_back(stack[top--]); while (stack[top + 1] != to);
            }
        } else if (to != fa) low[now] = min(low[now], dfn[to]);
    }
    if (!son && !fa) scc[++scc_cnt].push_back(now);
}
for (int i = 1; i <= scc_cnt; ++i)
    for (const auto& u: scc[i]) {
        yzh.add(i + n, u);
        yzh.add(u, i + n);
    }
cpp 复制代码
void tarjan(int now, int fa){
    dfn[now] = low[now] = ++timer, stack[++top] = now;
    int son = 0;
    for (int i = head[now]; i; i = edge[i].nxt){
        int to = edge[i].to;
        if (!dfn[to]){
            tarjan(to, now), low[now] = min(low[now], low[to]), ++son;
            if (low[to] >= dfn[now]){
                ++scc_cnt;
                yzh.add(now, scc_cnt + n);
                yzh.add(scc_cnt + n, now);
                do {
                    int u = stack[top--];
                    yzh.add(u, scc_cnt + n);
                    yzh.add(scc_cnt + n, u);
                } while (stack[top + 1] != to);
            }
        } else if (to != fa) low[now] = min(low[now], dfn[to]);
    }
    if (!son && !fa) {
        ++scc_cnt;
        yzh.add(now, scc_cnt + n);
        yzh.add(scc_cnt + n, now);
    }
}

注意到,有时候,我们需要根据题意记录树边的边权。就比如例题。

思考我们建出树的意义是什么------原仙人掌上,不好求两点间的距离,而在树上,我们可以轻松利用 LCA 等方法求得两点间的距离。所以树的边权应该和距离有关。

我们发现,仙人掌中,原来的环都变成了菊花。原图上的距离是环上的最短距离,而在树上是从花瓣到花心,再从花心到花瓣。这样似乎没看出些什么。我们不妨把比较特殊的 LCA 处放在最后讨论,接下来只考虑往上跳 LCA 的过程。

发现,只会存在花瓣到花心,再到花顶端的那个花瓣。我们不妨设花心到顶端花瓣的距离为 \(0\),而其余花瓣到花心的距离设置成原图环上到顶点的距离。这样就可以很好转化边权了。

至于 LCA 处,也很简单。如果 LCA 是圆点,那么不用处理;反之是方点,就要算其对应的原图上环的两点间的距离,这两点是来自询问两点不超过 LCA 的最浅祖先。这很好理解。

其实分析到这里,代码已经可以打出来了,但是在建树的时候注意如何优雅地处理边权,并精简代码。

那么再来模一模样例加深印象。

这里加粗的是原图的圆点,其他是方点。左图是圆仙人掌,右图是建出来的圆方树。

\(\operatorname{dist}(5, 7)\):由于 \(\operatorname{LCA}(5, 7) = 1\) 是圆点,所以直接路径和相加,即 \(\operatorname{dist}(5, 7) = 3 + 0 + 1 + 0 + 2 + 0 = 6\)。

\(\operatorname{dist}(4, 8)\):由于 \(\operatorname{LCA}(4, 8) = 14\) 是方点,所以要先跳到差一步到 \(14\) 的地方,即 \(4\) 和 \(3\),环上距离为 \(\min \lbrace 1, 4 - 1 \rbrace = 1\),\(\operatorname{dist}(4, 8) = 2 + 0 + 1 + 0 + 1 = 4\)。

代码

略去了快读快写,使用树剖求树上距离、LCA、和跳 father。

时间复杂度:\(\Theta(n + m + q \log n)\),瓶颈在树剖查询的 \(\log\)。

cpp 复制代码
#include <iostream>
#include <cstdio>
#include <vector>
using namespace std;

struct Graph{
    struct node{
        int to, nxt, len;
    } edge[1000010 << 1];
    int eid = 1, head[20010];
    inline void add(int u, int v, int w){
        edge[++eid] = {v, head[u], w};
        head[u] = eid;
    }
    inline node & operator [] (const int x){
        return edge[x];
    }
} xym, yzh;

int n, m, q;

int dfn[20010], low[20010], timer, cnt;
int dis[20010], re[20010];
int sum[20010], stack[20010], stop;
bool onleft[20010];

void tarjan(int now, int fr) {
    dfn[now] = low[now] = ++timer, stack[++stop] = now;
    for (int i = xym.head[now], to; to = xym[i].to, i; i = xym[i].nxt) if (i ^ fr ^ 1) {
        if (!dfn[to]) {
            dis[to] = dis[now] + xym[i].len;
            re[to] = xym[i].len;  // 如果这是一条非环边,那么把它看做 now -> to -> now 的环,所以这里要设置初值
            tarjan(to, i), low[now] = min(low[now], low[to]);
            if (low[to] >= dfn[now]) {
                ++cnt, sum[cnt] = re[stack[stop]] + dis[stack[stop]] - dis[now];  // 记录环的总长
                yzh.add(cnt, now, 0), yzh.add(now, cnt, 0);
                do {
                    int u = stack[stop--];
                    int len = min(dis[u] - dis[now], sum[cnt] - dis[u] + dis[now]);
                    onleft[u] = len == dis[u] - dis[now];
                    yzh.add(u, cnt, len);
                    yzh.add(cnt, u, len);
                } while (stack[stop + 1] != to);
            }
        } else if (dfn[to] < dfn[now]) {
            re[now] = xym[i].len;  // 环的闭环那条边的长度
            low[now] = min(low[now], dfn[to]);
        }
    }
}

int siz[20010], top[20010], son[20010];
int line[20010], tfa[20010], dpt[20010];

void dfs(int now, int fa) {
    tfa[now] = fa, siz[now] = 1, dpt[now] = dpt[fa] + 1;
    for (int i = yzh.head[now], to; to = yzh[i].to, i; i = yzh[i].nxt) {
        if (to == fa) continue;
        dis[to] = dis[now] + yzh[i].len;
        dfs(to, now), siz[now] += siz[to];
        if (siz[to] > siz[son[now]]) son[now] = to;
    }
}

void redfs(int now, int tp) {
    line[dfn[now] = ++timer] = now;
    top[now] = tp;
    if (son[now]) redfs(son[now], tp);
    for (int i = yzh.head[now], to; to = yzh[i].to, i; i = yzh[i].nxt) {
        if (to == tfa[now]) continue;
        if (to == son[now]) continue;
        redfs(to, to);
    }
}

inline int lca(int u, int v) {
    while (top[u] != top[v]) {
        if (dpt[top[u]] < dpt[top[v]]) swap(u, v);
        u = tfa[top[u]];
    }
    if (dpt[u] < dpt[v]) swap(u, v);
    return v;
}

inline int jump(int now, int ed) {
    int res = 0;
    while (top[now] != top[ed])
        res = top[now], now = tfa[top[now]];
    return now == ed ? res : line[dfn[ed] + 1];
}

inline int query(int u, int v) {
    int p = lca(u, v);
    if (p <= n)
        return dis[u] + dis[v] - 2 * dis[p];
    int fu = jump(u, p), fv = jump(v, p);
    int d1 = dis[fu] - dis[p], d2 = dis[fv] - dis[p];
    if (!onleft[fu]) d1 = sum[p] - d1;
    if (!onleft[fv]) d2 = sum[p] - d2;
    return dis[u] - dis[fu] + dis[v] - dis[fv] + min(abs(d1 - d2), sum[p] - abs(d1 - d2));
}

signed main() {
    fread(buf, 1, MAX, stdin);
    read(n), read(m), read(q);
    for (int i = 1, u, v, w; i <= m; ++i) {
        read(u), read(v), read(w);
        xym.add(u, v, w);
        xym.add(v, u, w);
    }
    cnt = n;
    for (int i = 1; i <= n; ++i) if (!dfn[i]) tarjan(i, 0);
    timer = dis[1] = 0, dfs(1, 0), redfs(1, 1);
    for (int i = 1, u, v; i <= q; ++i) {
        read(u), read(v);
        write(query(u, v)), putchar('\n');
    }
    fwrite(obuf, 1, o - obuf, stdout);
    return 0;
}

后记

不用圆方树亦可解此题,但是要多一些讨论、不如圆方树的直观。据说圆方树能在一般无向图上使用?我太蒻了啊。

js 复制代码
// run
sidebarToggle();
window.cancelDetailsAnimation = true;
相关推荐
Tisfy2 天前
LeetCode 3689.最大子数组总值 I:What The Medium
算法·leetcode·题解·贪心·模拟·脑筋急转弯
cpp_25013 天前
P2947 [USACO09MAR] Look Up S
数据结构·c++·算法·题解·单调栈·洛谷
8Qi86 天前
LeetCode 235. 二叉搜索树的最近公共祖先(LCA)
算法·leetcode·二叉树·递归·二叉搜索树·lca·迭代
cpp_25017 天前
P11375 [GESP202412 六级] 树上游走
数据结构·c++·算法·题解·洛谷·树形结构·gesp六级
cpp_25018 天前
P10722 [GESP202406 六级] 二叉树
数据结构·c++·算法·题解·洛谷·树形结构·gesp六级
8Qi89 天前
LeetCode 236. 二叉树的最近公共祖先(LCA)
算法·leetcode·二叉树·递归·lca·后序遍历
cpp_25019 天前
P10109 [GESP202312 六级] 工作沟通
数据结构·c++·算法·题解·洛谷·gesp六级
cpp_25019 天前
P10377 [GESP202403 六级] 好斗的牛
数据结构·c++·算法·题解·洛谷·gesp六级
朔北之忘 Clancy11 天前
2026 年 3 月青少年软编等考 C 语言二级真题解析
c语言·开发语言·c++·学习·青少年编程·题解·考级
朔北之忘 Clancy12 天前
2026 年 3 月青少年软编等考 C/C++ 一级真题解析
c语言·开发语言·c++·青少年编程·题解·考级