基环树

基环树简单介绍

#1.0 啥是基环树?

首先,严格地讲,基环树不是树,它是一张\(n\) 个节点、\(n\) 条边的图

#1.1 无向图上的基环树

可以将这种有 \(n\) 个节点、\(n\) 条边的无向联通图看做在一棵树上加了一条边,形成了一张恰好包含一个环的图,如下图:

这便是一个无向图上的基环树。

当然,如果不保证联通,那么有 \(n\) 个节点、\(n\) 条边的无向图也有可能是一个基环树森林。

#1.2 有向图上的基环树

#1.2.1 内向树

每个节点以自己为起点的边只有一条。

#1.2.2 外向树

每个节点以自己为终点的边只有一条。

#2.0 基环树的一般处理思路

下面都以无向图为例子。

#2.1 大体方法

看起来基环树似乎比一棵树变得麻烦了许多。

但不要害怕,我们也是有一般方法的:

  1. 找到唯一的环;
  2. 对环之外的部分按照若干棵树处理;
  3. 考虑与环一起计算。

#2.2 找到环

#2.2.1 思路

找到环可以采用 \(\text{DFS}\) 来实现(我不会 \(\text{BFS}\) QwQ).

以下是本人的口胡做法:

  1. 从任意一点开始搜索;
  2. 每次拓展到的点涂为灰色,回溯的点涂为黑色;
  3. 重复第一、二步,直到通过一条未经过过的边拓展到一个灰色节点;
  4. 将拓展到的灰色节点涂为红色,函数返回 true
  5. 将经过的回溯时经过的节点涂为橙色,函数返回 true
  6. 重复第 5. 步,直到回溯到被涂为红色的节点,函数返回 false,算法结束。

经过以上几步,我们便得到了唯一的环——颜色最显眼的那些!其中红色节点是环的衔接处。

如下图:

注意到,上面的模拟过程中,以节点 \(10\) 为根的子树并没有被遍历,是因为 \(\text{DFS}\) 的实现本身便是一路到底,所以可能并不能遍历整颗基环树。

那么,当我们面对的是一个基环树森林时,便要注意了:如果以节点是否被访问过为标志,那么,标记一整棵基环树有以下几种方法:

  • 在环上节点递归前,用另一个 \(\text{DFS}\) 遍历该节点能到达的所有节点,并标记。
  • 在对去掉环之后的若干棵子树进行处理时进行另一种标记 tag2[],判断是否是一棵新的基环树的依据为 tag2[] 是否被标记。
  • 建图时使用并查集。

还有几个要注意的点:

  • 判断一个点是否可以被拓展的条件是拓展所经过的边是否已被经过,由于无向图建边是两个方向各建一条边,所以标记时需要成对变换,将这两条边同时标记。

#2.2.2 代码实现

//R[] 存储的是环上的节点

inline bool Get_ring(int x,int from){
if (v[x] == 1){
v[x] = 2;v2[x] = 1;
R[++ tot] = x;
return true;
}
v[x] = 1;int opt = 0;
for (int i = head[x];i != -1;i = e[i].nxt){
if (v_e[i]) continue;
v_e[i] = v_e[i ^ 1] = true;
if (Get_ring(e[i].v,x)){
pre_e[e[i].v] = i;
if (v[x] != 2){
R[++ tot] = x;
v2[x] = 1;
return true;
}
else return false;
}
}
return false;
}

#2.3 剩下的操作

剩下的两步多要视题目而定了,不过一般是一个树形DP(子树不考虑环)加一个线性DP+单调队列优化(在环上,断环为链,复制一遍)

基环树的定义为:一个有向或无向图中,只含有一个环的图,形式化的表达为:

关于这种形状关键点主要在于找环,那么我们可以一步一步的去寻找,当这个点走着走着走到了某个环里,我们可以直接遍历整个环,然后打个标记,这样环就找到了
具体的例题:
E - Reachability in Functional Graph
本题就是一个基环树森林,即不同的连通块,每个块内可能存在一个基环树,那么我们直接暴力的找出来每个环,并且算出环的节点个数,那么每个点的贡献就是,从这个点出发到环内某一节点经过的边数+环的大小,然后再直接进行记忆化搜索即可:

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+10,mod=1e9+7;
signed main()
{
    std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    int n; cin>>n;
    vector<int>a(n+1);
    for(int i=1;i<=n;i++) cin>>a[i];
    vector<int>vis(n+1),dp(n+1); // vis 数组是用来标记这个点是否被遍历过
                                // dp 环中每个点的贡献
    for(int i=1;i<=n;i++){
        if(vis[i]) continue; //如果被遍历到了,直接跳过
        int u=i;
        while(true){ //一直遍历下去
            if(vis[u]){ 
                if(vis[u]==i){ //如果是第一次被标记,也就是某个环的入口,我们就计算一下这个环
                    int x=a[u],cnt=1;
                    while(x!=u){ //走回来就停止
                        cnt++,x=a[x];
                    }
                    dp[u]=cnt,x=a[u];
                    while(x!=u){
                        dp[x]=cnt,x=a[x]; //每个节点的贡献都是这个环的大小
                    }
                    break;
                }
            }
            vis[u]=i,u=a[u]; //向下传点
        }
    }
    function<int(int)>dfs;
    dfs=[&](int u)->int{
        if(dp[u]) return dp[u];
        return dp[u]=dfs(a[u])+1;
    };
    int res=0;
    for(int i=1;i<=n;i++) res+=dfs(i);
    cout<<res;
    return 0;
}

E - Permute K times
稍微模拟一下会发现, 从任意一个点出发, 可以向它要到达的点连一条单向边, 从而可以转化成图上问题. 由于是 \(n\) 个点 \(n\) 条边, 且从任意一个点开始考虑, 发现在途中可能会走进一个环或者没有环, 很明显的可以转化为基环树, 而且各边不一定相连, 所以是个 基环树内向森林, 则问题转化为, 从该基环树为起点的第 \(k\) 级祖先问题.
可以通过倍增解决 \(k\) 级祖先问题:设 \(g(j, i)\) 表示从点 \(i\) 出发沿着出边走 \(2^j\) 次后到达的点,则有边界 \(g(0, i) = x_i\) 和转移 \(g(j + 1, i) = g(j, g(j, i))\)。要回答询问,只需将 \(k\) 进行二进制拆分,后对每个进制位 \(j\) 进行 \(p \gets g(j, p)\) 的跳跃即可。

时空复杂度为 \(\mathcal O(n \log k)\)

注:由于这里的 \(k\) 级祖先可以离线询问,存在线性时空的做法。具体而言,找到每个连通块的环后进行 DFS,记录 DFS 栈,然后定位栈上从当前点往前数 \(k\) 位的点,或栈长不足 \(k\) 时定位环上的特定点。由于代码实现比倍增的做法复杂,不展示具体代码。

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e6 + 10, mod = 1e9 + 7;
signed main()
{
    std::ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    int n, k; cin >> n >> k;
    vector<int> x[60];
    x[0].resize(n + 1);
    for(int i = 1; i <= n; i++) cin >> x[0][i];
    for(int j = 1; j <= 59; j++){
        x[j].resize(n + 1);
        for(int i = 1; i <= n; i++){
            x[j][i] = x[j - 1][x[j - 1][i]];
        }
    }
    vector<int> a(n + 1);
    for(int i = 1; i <= n; i++) cin >> a[i];
    for(int i = 1; i <= n; i++){
        int p = i;
        for(int j = 0; j <= 59; j++){ 
            if(k & (1LL << j)){
                p = x[j][p];
            }
        }
        cout << a[p] << " \n"[i == n];
    }
    return 0;
}
posted @ 2024-06-11 12:56  o-Sakurajimamai-o  阅读(16)  评论(0编辑  收藏  举报
-- --