基环树
算法
基环树,指
基环树森林,指每棵树都是点数和边数相等。
容易发现,基环树和普通的树只有一条边的差别,普通的树是
对于此类题目,解决方法如下:
- 环和非环节点分开处理。
- 去掉环上一条边形成一棵树。
找环
考虑并查集。当边上的两点祖先相同,则说明这是环上的两个点。
根据这两个点,搜索之前的边,找到环上的所有点。
不难看出,有向图和无向图都适用。
namespace Fring {
int fa[N], S, T;
int find(int x) { return (fa[x] == x ? x : fa[x] = find(fa[x])); }
bool mksure(int x, int fa) {
if(x == T) {
G[sum].pb(x); vis[x] = 1;
return 1;
}
for(int i = 0;i < g[x].size(); ++i) {
int to = g[x][i].u;
if(to == fa) continue ;
if(mksure(to, x)) {
vis[x] = 1;
G[sum].pb(x), V[sum].pb(g[x][i].w);
return 1;
}
}
return 0;
}
void Two() {
for(int i = 1;i <= n; ++i) fa[i] = i;
for(int i = 1;i <= n; ++i) {
int x = find(X[i]), y = find(Y[i]);
if(x == y) {
S = X[i], T = Y[i];
++sum;
mksure(S, 0);
V[sum].pb(Z[i]);
}
else fa[x] = y;
}
}
}
也可以根据 tarjan 的性质搜索。考虑将点放入栈,搜到返租边则取出栈中元素得到所有环上的点。
void tarjan(int x, int f) {
dfn[x] = ++cnt;
st.push(x);
for(auto &to : G[x]) {
if(!dfn[to]) {
fa[to] = x;
tarjan(to, x);
}
else if(to != f) {
for(int i = x;i != to;i = fa[i]) ans[++num] = i;
ans[++num] = to;
}
}
}
例题
骑士
没有上司的舞会,不过从树变成了基环树。
首先,如果是一棵树,我们可以根据树形 DP 直接解决。大致的思路是定义
我们考虑转换成树。首先找到环,以及环上任意的两个相邻的点。将两点之间的这条边删掉,就变成了一棵树。以这两个环为根分别求树的直径取答案最大值。
每次到达环上点
最后,每个环只用两个相邻点就可以了,因为根据相邻不选的性质可以推出其它点的结果是一样的。于是时间复杂度就是树形 DP 的
Island
求基环树的最大直径。
首先找出环,先考虑直径不包含环点的情况,即以每个环点为根节点,向下求其子树的直径(不能走到环上)。容易发现,这种情况是可以独立的。可以直接树形 DP,记录最大值和次大值即可。
然后考虑环上的情况。从环上一点
设
然后把
假设我们枚举
设环长为
Rendezvous
找出
x 和y 使得a 向上走x 步和b 向上走y 步到同一个点。
首先考虑无解的情况,当且仅当
然后思考两点的位置关系,显然如果都不经过环,即都是环上一点的子树上的点,那么就是正常的树上 LCA。求出
还剩两点都在环上,以及一点在环的子树上,两点都在不同环点的子树上的问题。
首先,我们需要将这些非环点向上刚好走到环上(他的根节点),设分别走
然后因为是有向图,所以环也是有方向的。设
然后,根据题目给出的性质,判断是从
一个可能会影响思考的点是,点
旅行
你可以在环上遍历到一半中途回溯一次。最终使遍历到的点组成的序列的字典序最小。
首先我们要使
然后
思考是基环树怎么做。
观察数据范围,
但是能,此题有加强版告诉我们,有
Roads in the Kingdom
和 Island 相似。
“需拆除一条道路并同时要求所有城市仍然联通” 说明这条道路一定在环上。
设当前短的边为
- 路径
1\rightarrow i 。 - 路径
i+1\rightarrow m 。 - 路径
i\rightarrow 1 \rightarrow i-1 。
前两个可以用单调队列,但是我不想用。
于是递推,感觉情况比单调队列更复杂。
回顾 T2 中的这个式子
右边的情况同理,不过因为方向反了,所以式子变化成了
然后
直接给式子。
所以最后的答案为