基环树和笛卡尔树总结
一个图论,一个半数据结构。咱也不知道为啥这两个毫无关联的东西会放在一块。
基环树(环套树)
一些定义
- 基环树:一张有
个点和 条边的图,如果不保证连通的话,那么整张图是一张基环树森林。并且如果将环上的任意一条边去除,那么整棵基环树会成为一棵普通的树。 - 内向树:一棵所有节点出度为
的有向基环树,如图: - 外向树:一棵所有节点入度为
的有向基环树,如图:
处理方法
基环树一般用于给普通的树上问题增加难度。
树形 DP
找到环,断开环上一边,使之成为普通的树,然后进行树形 DP。又因为断开了一条边,于是这条边连接的两个端点无法处理,所以通常需要进行两次树形 DP。
典型例题:P2607,P5022,创世纪。
环形 DP
找到环,由于环上每一点都相当于根节点引导一棵子树,于是将子树信息合并到环上点,于是基环树就变成了一个环,然后跑环形 DP。
典型例题:P4381,P3533。
找环
基环树就如上两种处理方法,但不论哪种,第一步都是找到环,下面看如何找环。
对于无向图
拓扑排序:可以很方便地找出环上所有节点。
void toposort() {
queue<int> q;
for (int i = 1; i <= n; i++) if (in[i] == 1) q.push(i);
while (!q.empty()) {
int u = q.front();
q.pop();
for (auto vv : g[u]) {
int v = vv.first, w = vv.second;
if (in[v] > 1) {
in[v]--;
s[tr[v]] = max(s[tr[v]], dis[v] + dis[u] + w);
dis[v] = max(dis[v], dis[u] + w);
if (in[v] == 1) q.push(v);
}
}
}
}
DFS:也可找出环上所有节点,在无向图上不如拓扑排序简洁。
void isring(int u, int fno) {
if (ins[u]) {
ring[u] = 1;
for (int i = top; stk[i] != u; i--) ring[stk[i]] = 1;
return;
}
if (vis[u]) return;
vis[u] = 1;
stk[++top] = u;
ins[u] = 1;
for (auto v : g[u]) if (v != fno) isring(v);
--top;
ins[u] = 0;
}
对于有向图
DFS:和无向图的 DFS 类似,不需像无向图一样判断父节点。
void isring(int u) {
if (ins[u]) {
ring[u] = 1;
for (int i = top; stk[i] != u; i--) ring[stk[i]] = 1;
return;
}
if (vis[u]) return;
vis[u] = 1;
stk[++top] = u;
ins[u] = 1;
for (auto v : g[u]) isring(v);
--top;
ins[u] = 0;
}
DFS 找出环上一点
有向图和无向图皆可用。如果有一个点
int check(int u) {
vis[u] = 1;
if (vis[fa[u]]) return fa[u];
return check(fa[u]);
}
例 1 [ZJOI2008] 骑士
题意
骑士团有很多骑士,一些骑士之间相互有矛盾,每个骑士有且仅有一个他讨厌的骑士,且绝对不和自己讨厌的骑士一起出征。为了组成一个战斗力最大的军团,要求这个团内所有人都没有矛盾且战斗力最大。骑士按
解析
本题和 P1352 没有上司的舞会 相似,但本题给出了一个基环树。考虑把
例 2 [NOIP2018 提高组] 旅行
看到数据范围中的 当年的 NOIP 这么良心,只考虑树的情况就能有 60pts。
此时整张图为一棵树,因为题目要求路径序列字典序最小,所以对邻接表进行从小到大排序,从节点
此时整张图为一棵基环树。看到数据范围:
当然有更高级的
例 3 [IOI2008] Island
题意
给定一个基环树森林,求每个基环树的最长链长度。
解析
08 年的 IOI 啊。
题意如此直接。
从此题开始难度骤升,让你认识到基环树的魅力。
回归题目。所谓 “最长链”,让你想到了什么?没错,树的直径。
但是树的直径只是针对树,而本题给的是基环树。
那么基环树无非多了什么?一个环。所以有两种情况。
- 最长链两端点全部位于环上同一点的子树内。换句话说,不经过环。那么此时的最长链直接就是该子树的直径。
- 最长链两端点位于不同子树。那么此时的最长链 = 两个端点在各自子树中到根节点的距离 + 环上两点的最短距离。
思路就以上这些。打到代码上,我们需要维护:
,表示节点 位于哪棵基环树,用于遍历森林; ,表示当前子树中从节点 出发向下的最长距离,用于查询环上节点到最长链端点的距离(相当于将子树信息合并到环上节点); ,表示第 棵基环树在情况一下的最长链,其实就是每棵子树的直径取最大值,用于分类讨论; ,表示从环上任意一点到环上节点 的距离,其实是前缀和数组:用 表示 两点在环上的距离。- 在实际实现中,最终我们将基环树合并为一个环,所以我们用一个
同样表示 的维护内容,只不过这里的 仅限于环上节点,即将环上节点重新编号为 , 为环长。
环形 DP 的时候需要单调队列维护,具体不讲了。
然后,梳理一遍思路:
- 读入。
- 一个 DFS 跑出所有基环树,对不同基环树上的点作标记。
- 拓扑排序求出每棵基环树的环,顺便可以求出
和 。 - 对于每个基环树跑一遍环形 DP,其中用到单调队列解决。
例 4 [POI2012] RAN-Rendezvous
题解在此。
例 5 创世纪
题意
上帝手中有着
上帝希望知道他最多可以投放多少种世界元素。
题解
这里解子挺多的,思路基本就是这个思路,不再多说了。
笛卡尔树
什么是笛卡尔树?
我们先回顾 Treap 树,它的每个节点有两个属性:键值和优先级。笛卡尔树是一种特殊、简化的 Treap,它的每个节点的键值预先给定,但是优先级或预先给定、或随机生成。
笛卡尔树主要用于处理一个确定的序列,序列中的数有两个属性:在序列中的位置和数值。把位置看作键值,数值看作优先级,构造出来的笛卡尔树符合 Treap 的特征,即:
- 以每个数的位置(记作
)为键值,它是一棵 BST。 - 把数值(记作
)看作优先级,它是一个堆。
性质
除了以上的性质,笛卡尔树还有其他的一些有用的性质。
- 对于一棵笛卡尔树,它的中序遍历就是原序列。
由 Treap 的性质不难证明。 - 在原序列中的区间
的最小值(或最大值),就是这个序列的笛卡尔树上 和 的 LCA。
因为 和 的 LCA 一定在区间 中,且在整个区间中深度最小。因为笛卡尔树关于 是堆,如果是小根堆则 的最小值是 LCA,如果是大根堆则最大值是 LCA。 - 对于一个序列,如果其
均互不重复,则其所构建的笛卡尔树是唯一的。
因为 互不重复,所以能构建出唯一无根 BST。又因为笛卡尔树关于 是堆,所以 最小/最大的节点为根。因为 互不重复,所以根确定,所以笛卡尔树确定。
建树
如果用 Treap 的建树方法,逐一加入节点,再通过 FHQ 维护,时间复杂度为
笛卡尔树是简化的 Treap,它所有节点均预先给定,所以有
由于所有节点均预先给定,每次插入新的节点,横向的位置是固定的,新的点总是插入最右边,而且需要调整的是纵向位置,所以只需考虑 “最右链”,可以用单调栈维护。在最右链上,从上一个插入的点开始,若栈顶节点的
void insert(int k, int w) {
while (!st.empty() && w < a[st.top()].second) {
ls[k] = st.top();
st.pop();
}
if (!st.empty()) rs[st.top()] = k;
st.push(k);
}
维护笛卡尔树时,只需维护一个
例 1 [TJOI2011] 树的序
题目相当于给了
例 2 RMQ Similar Sequence
题意
定义
两个序列
给定一个序列
题解 from @Dyc780511
因为序列
例 3 [COCI2008-2009#4] PERIODNI
题解在此。
例 4 Moles
一看就知道是笛卡尔树。题目中给的顺序相当于
部分人会认为此题卡 Hash,实际上是不卡的,完全是你自己打炸了,请自行检查。比如,请检查你的欧拉序字符数组是否开了二倍。
例 5 Kcats
题解 from @Dyc780511
考虑到
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】