秒懂算法 | 基环树

举报
TiAmoZhang 发表于 2023/05/04 11:07:50 2023/05/04
【摘要】 图论是一个“巨大”的专题,有大量的知识点,有众多广为人知的问题,有复杂的应用场景。 图论算法常常建立在复杂的数据结构之上。
简介: 图论是一个“巨大”的专题,有大量的知识点,有众多广为人知的问题,有复杂的应用场景。 图论算法常常建立在复杂的数据结构之上。

01、基环树

基环树是只有一个环的连通图,它有n个点和n条边。基环树不是一棵树,而是一棵“伪树”。它的特征是图中有且只有一个连通的环。下面分别讨论无向图和有向图两种情况下的基环树。

(1) 无向图上的基环树。在一棵基于无向图的无根树上加一条边,形成基环树。去掉环上任意一条边,基环树变成一棵真正的树。

(2) 有向图上的基环树。一个有向无环图(DAG),如果在图中加一条边能形成一个自连通的环,则形成一棵基环树。把这个环看作一个整体,根据它与环外点的关系,把基环树分成两种:内向树,环外的点只能进入环内;

外向树,环外的点无法进入环内。

图1(1)~图1(3)是基环树的3种形态,把环缩成一个“虚点”后得到图1(4)~图1(6)。请注意,缩成虚点后的无向图1(4)是一棵树,但是缩成虚点后的有向图1(5)和图1(6)不一定是真正的树,如图1(5)就不是一棵树。

640.png


■ 图1 基环树的三种形态和缩点图


由基环树的特征可知,与基环树有关的题目,首先是找到唯一的环,然后把这个环当作“虚点”。


由前面的无向图的连通性和有向图的连通性可知,基环树的找环问题是“图的连通性”的一个简化问题。

对于无向图,用拓扑排序的BFS可以找出环,操作结束后,度大于1的点就是环上的点。具体做法:①计算所有点的度;②把所有度为1的点入队;③从队列弹出度为1的点,把它所连的边去掉,并将边所连的邻居点的度减1,若这个邻居的度变为1,入队;④继续执行步骤③直到队列为空。操作结束后,统计所有点,度数大于1的点即为环上的点。注意,这种无向图找环的方法只适用于只有一个环的基环树。

如果只要找环上的一个点,用DFS可以方便地找到。如果有一个点v第2次被访问到,那么就存在环,且v是环上的一个点。这个方法可用于有向图和无向图。下面的例题用到了这个原理。

640.png


把x讨厌的人y设为x的父节点,形成从y指向x的有向边。本题每个点的入度为1,生成的图中包括很多独立的连通块,每个连通块肯定是一棵基环树,而且形状是一棵外向树。这些基环树形成了基环树森林。在每棵基环树上找到环,断开这个环后,这棵基环树变成了一棵真正的树,就可以套用洛谷P1352的做法了。


本题属于“基环树+树上DP”,下面给出代码。其中的DP代码和洛谷P1352一样,这里不再解释。基环树部分有以下两处。

(1) 找基环树环上一个点。用check_c()函数实现,它是一个DFS,如果发现某个点被第2次访问,它就是环上的一个点,用mark记录这个点。

(2) 分别断开mark和mark的父节点,形成两棵树,在这两棵树上分别做DP,取最大值。


#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e6 + 100;
vector<int> G[N];
int father[N],val[N],mark;
bool vis[N];
ll dp[N][2];
void addedge(int from,int to){
    G[from].push_back(to);          //用邻接表建树
    father[to] = from;              //父子关系
}
void dfs(int u){                    //和洛谷P1352 几乎一样
    dp[u][0] = 0;                   //赋初值:不参加
    dp[u][1] = val[u];              //赋初值:参加
    vis[u] = true;
    for(int v : G[u]){              //遍历u的邻居v
        if(v == mark)  continue;   
        dfs(v);
        dp[u][1] += dp[v][0];       //父结点选择,子结点不选
        dp[u][0] += max(dp[v][0],dp[v][1]);  //父结点不选,子结点可选可不选
    }
}
int check_c(ll u){                  //在基环树中找环上一个点
    vis[u] = true;
    int f = father[u];
    if(vis[f]) return f;            //第2次访问到,是环上一个点
    else       check_c(f);          //继续向父结点方向找
}
ll solve(int u){                    //检查一棵基环树
    ll res = 0;
    mark = check_c(u);              //mark是基环树的环上一个点 
    dfs(mark);                      //做一次dfs
    res = max(res,dp[mark][0]);     //mark不参加
    mark = father[mark];
    dfs(mark);                      //mark的父结点不参加,再做一次dfs
    res = max(res,dp[mark][0]);
    return res;
}
int main(){
    int n; scanf("%d",&n);
    for(int i = 1;i <= n;i++){
        int d;   scanf("%d%d",&val[i],&d);    addedge(d,i);
    }
    ll ans = 0;
    for(int i = 1;i <= n;i++)
        if(!vis[i]) ans += solve(i);  //逐个检查每棵基环树
    printf("%lld\n",ans);
    return 0;
}
【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。