基环树完全入门
基环树
基环树的概念
众所周知,N个点的树有N-1条边.若在树上任意添加一条边,则会形成一个环,除了环之外,其余部分由若干棵子树构成。
我们把这种N个点N条边的连通无向图,即在树上加一条边构成的恰好包含一个环的图,称为“基环树”。如果不保证连通,那么N个点N条边的无向图也可能是若干棵基环树组成的森林,简称为“基环树森林“。
在有向图中,我们也有类似的概念。N个点、N条边、每个节点有且仅有一条入边的有向图就好像以“基环”为中心,有向外扩展的趋势,故称为“外向树”。N个点、N条边、每个节点有且仅有一条出边的有向连通图就好像以“基环”为中心,有向内收缩的趋势,故称为“内向树”。外向树和内向树也经常统称为“基环树”。如果不保证连通,那么N个点、N条边、每个节点有且仅有一条出(入)边的有向图也可能是“内(外)向树森林”的形态。
基环树(N个点N条边的连通无向图)
外向树(每个点的出度都为1,进了环就出不去)
内向树(每个点的入度都为1,出了环就回不来)
基环树相关问题
1)基本思路
基环树的结构仍然很简单,但比树要复杂些,因此常作为一些经典模型的扩展,以适当增加题目的难度,例如基环树的ﻪ直径、基环树两点之间的距离、基环树动态规划等。无论哪种模型,求解基环树相关问题的方法一般都是先找出图中唯一的环,把环作为基环树的广义“根节点”,把除了环之外的部分按照若干棵树处理,再考虑与环一起计算。1
2)基环树的直径
3)基环树DP
经典例题
1)NOIP2018 旅行(拆边+贪心)
对于前 的数据,因为是一棵树,所以你一旦访问了某个子节点,那么你就必须把这颗子树访问完才能访问其他子节点,否则剩余的节点将无法访问到。
要求字典序最小,那么我们肯定先从
出发,然后对于每一个节点,我们肯定是按照字典序的大小依次访问,看一眼良心的数据范围,我就暴力地把所有子节点存下来,排序一遍依次访问(数组尽量不要开在递归里,爆栈我不负责)。
然后记录一下输出就好了。
后 的数据是基环树,对于一个环,假设有 条边,那么无论如何旅行,有且仅有 条边能被访问,也就是说,旅行的路径仍旧是一棵树。也就是每次暴力的割断环上的每一条边,然后跑一遍,找一个字典序最小的就好了。找环可以用 。也可以把所有的边都存下来,挨个枚举,暴力拆边。2
//代码框架来自luogu_chen_zhe
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>
#include <vector>
#include <queue>
using namespace std;
inline long long read()
inline void write(long long x) ///篇幅有限,读入/输出优化函数省略
const int kmax=5010,INF=0x3f3f3f3f;
vector<int> graph[kmax];
int n,m,ans[kmax],edge[kmax][2];
namespace solve1 ///树上求解
{
int cnt=0; ///ans数组尾指针
bool vis[kmax]; ///dfs标记
inline void dfs(int u) ///大法师不解释
{
ans[++cnt]=u;
vis[u]=true;
int l=graph[u].size();
for(int i=0;i<l;i++)
{
int v=graph[u][i];
if(!vis[v]) dfs(v);
}
}
void main()
{
cnt=0; ///给每条边所连的点排序,保证贪心最优性
for(int i=1;i<=n;i++) sort(graph[i].begin(),graph[i].end());
dfs(1);
}
}
namespace solve2 ///基环树上求解
{
int cnt=0; ///计数器
int res[kmax]; ///存储结果
int del_u,del_v; ///要删除的边的起点与终点
bool vis[kmax]; ///dfs标记
inline bool comp() ///字典序比较
{
for(int i=1;i<=n;i++)
{
if(ans[i]!=res[i]) return ans[i]>res[i];
}
return false;
}
inline bool check(int u,int v) ///如果是所删边,dfs不搜
{
if((u==del_u&&v==del_v)||(u==del_v&&v==del_u)) return false;
return true;
}
inline void dfs(int u) ///大法师
{
res[++cnt]=u;
vis[u]=true;
int l=graph[u].size();
for(int i=0;i<l;i++)
{
int v=graph[u][i];
if(!vis[v]&&check(u,v)) dfs(v);
}
}
void main()
{
ans[1]=INF; ///将ans字典序置为最大,保证其能被res更新
for(int i=1;i<=n;i++) sort(graph[i].begin(),graph[i].end());
for(int i=1;i<=m;i++)
{
cnt=0;
memset(res,0,sizeof(res));
memset(vis,0,sizeof(vis));
del_u=edge[i][0]; ///枚举要删除边的起点和终点
del_v=edge[i][1];
dfs(1);
if(comp()&&cnt==n) memcpy(ans,res,sizeof(res)); ///res更新ans
}
}
}
int main()
{
n=read(),m=read();
for(int i=1;i<=m;i++)
{
int u=read(),v=read();
graph[u].push_back(v); ///graph存图
graph[v].push_back(u);
edge[i][0]=u; ///edge存边
edge[i][1]=v;
}
if(m==n-1) solve1::main(); ///判定为树
else solve2::main(); ///判定为基环树
for(int i=1;i<=n;i++)
{
write(ans[i]);
putchar(' ');
}
putchar('\n');
return 0;
}
2)ZJOI2008 骑士(拆边+树形DP)
3)IOI2008 / BZOJ1791 岛屿(基环树直径)
【未完待续】
浅谈仙人掌
修正了一个错误,并用 优化了样例可视化处理