基环树学习指南
前置芝士
一个图中包含n个点n条边,且图中只存在一个环,这样的图被称为基环树(环套树)。
基环树比树多了一条边,从而形成了一个环。基环树可以看做以坏点为根的一颗颗子树构成。
三大分类
基环无向图
基环内向图(每个点有且只有一条出边)
基环外向图(每一个点只有一条入边)
[解题步骤]
深搜找环。
断环成树,对树深搜计算。
保证这 (n) 个点 (n) 条边构成的是一个连通图时有唯一环。
如果图不连通但是每个联通块点数都等于边数时,这个图就是一个基环树森林。
基环树找环
dfs+并查集
发现环
[problem description]
小明的实验室有 \(N\) 台电脑,编号 \(1 \sim N\)。原本这 \(N\) 台电脑之间有 \(N-1\) 条数据链接相连,恰好构成一个树形网络。在树形网络上,任意两台电脑之间有唯一的路径相连。
不过在最近一次维护网络时,管理员误操作使得某两台电脑之间增加了一条数据链接,于是网络中出现了环路。环路上的电脑由于两两之间不再是只有一条路径,使得这些电脑上的数据传输出现了 BUG。
为了恢复正常传输。小明需要找到所有在环路上的电脑,你能帮助他吗?
[input]
第一行包含一个整数 \(N\)。
以下 \(N\) 行每行两个整数 \(a\) 和 \(b\),表示 \(a\) 和 \(b\) 之间有一条数据链接相连。
输入保证合法。
[output]
按从小到大的顺序输出在环路上的电脑的编号,中间由一个空格分隔。
[a.in]
5
1 2
3 1
2 4
2 5
5 3
[a.out]
1 2 3 5
[datas]
\(1 \le N \le 10^5\),\(1 \le a,b \le N\)
[solved]
[dfs+并查集]
const int N = 100010;
vector<int> e[N];
int fa[N];
int n;
set<int> ans;
bool vis[N];
int pa[N];
void init(int n) {
for (int i = 1; i <= n; i++) fa[i] = i;
}
int find(int x) {
while (x != fa[x]) x = fa[x] = fa[fa[x]];
return x;
}
void dfs(int u, int father) {
for (auto v : e[u]) {
if (!vis[v]) {
pa[v] = u;
vis[v] = 1;
dfs(v, u);
} else {
if (v != father) {
int p = u;
while (p != v) {
ans.insert(p);
p = pa[p];
}
ans.insert(v);
}
}
}
}
void solve() {
//freopen("a.in","r",stdin);
//freopen("a.out","w",stdout);
cin >> n;
init(n);
int s, fas;
for (int i = 1, x, y; i <= n; i++) {
cin >> x >> y;
int fax = find(x);
int fay = find(y);
if (fax == fay) {
s = x, fas = y;
} else {
fa[fax] = fay;
}
e[x].push_back(y);
e[y].push_back(x);
}
vis[s] = 1;
// vis[fas] = 1;
pa[s] = fas;
dfs(s, fas);
for (auto x : ans) {
cout << x << " ";
}
cout << endl;
}
基环内向树访问计数
[problem description]
现有一个有向图,其中包含 n
个节点,节点编号从 0
到 n - 1
。此外,该图还包含了 n
条有向边。
给你一个下标从 0 开始的数组 edges
,其中 edges[i]
表示存在一条从节点 i
到节点 edges[i]
的边。
- 你从节点
x
开始,通过边访问其他节点,直到你在此过程中再次访问到之前已经访问过的节点。 - 返回数组
answer
作为答案,其中answer[i]
表示如果从节点i
开始执行该过程,你可以访问到的不同节点数。 2<=n<= 10^5
[solved]
(1)反向建边,更有利于将树枝删去。
(2)拓扑排序,剪掉 图上的所有树枝,deg 值为 1 的点必定在基环上,为 0 的点必定在树枝上。
(3)dfs,需要考虑节点不在一个连通块里。
时间复杂度:O(n)
[python]
def countVisitedNodes(self, edges: List[int]) -> List[int]:
n=len(edges)
rg=[[]for _ in range(n)] #反图
deg=[0]*n #统计出度
for x,y in enumerate(edges):
rg[y].append(x)
deg[y]+=1
# 拓扑排序,剪掉 g 上的所有树枝
# 拓扑排序后,deg 值为 1 的点必定在基环上,为 0 的点必定在树枝上
q=deque()
for i in range(n):
if deg[i]==0:
q.append(i)
while q:
u=q.popleft()
v=edges[u]
deg[v]-=1
if deg[v]==0: # 树枝上的点在拓扑排序后,入度均为 0
q.append(v)
ans=[0]*n
# 在反图上遍历树枝
def dfs(x:int,depth:int)->None:
ans[x]=depth
for v in rg[x]:
if deg[v]==0:
dfs(v,depth+1)
for i,x in enumerate(deg):
if x<=0:
continue
ring=[]
v=i
while True:
deg[v]=-1 # 将基环上的点的入度标记为 -1,避免重复访问
ring.append(v) # 收集在基环上的点
v=edges[v]
if v==i:
break
for i in ring:
dfs(i,len(ring)) # 为方便计算,以 len(ring) 作为初始深度
return ans
[c++]
vector<int> countVisitedNodes(vector<int> &g) {
int n = g.size();
vector<vector<int>> rg(n); // 反图
vector<int> deg(n);
for (int x = 0; x < n; x++) {
int y = g[x];
rg[y].push_back(x);
deg[y]++;
}
// 拓扑排序,剪掉 g 上的所有树枝
// 拓扑排序后,deg 值为 1 的点必定在基环上,为 0 的点必定在树枝上
queue<int> q;
for (int i = 0; i < n; i++) {
if (deg[i] == 0) {
q.push(i);
}
}
while (!q.empty()) {
int x = q.front();
q.pop();
int y = g[x];
if (--deg[y] == 0) {
q.push(y);
}
}
vector<int> ans(n, 0);
// 在反图上遍历树枝
function<void(int, int)> rdfs = [&](int x, int depth) {
ans[x] = depth;
for (int y: rg[x]) {
if (deg[y] == 0) { // 树枝上的点在拓扑排序后,入度均为 0
rdfs(y, depth + 1);
}
}
};
for (int i = 0; i < n; i++) {
if (deg[i] <= 0) {
continue;
}
vector<int> ring;
for (int x = i;; x = g[x]) {
deg[x] = -1; // 将基环上的点的入度标记为 -1,避免重复访问
ring.push_back(x); // 收集在基环上的点
if (g[x] == i) {
break;
}
}
for (int x: ring) {
rdfs(x, ring.size()); // 为方便计算,以 ring.size() 作为初始深度
}
}
return ans;
}
基环无向图简单路径计数
[problem description]
You are given an undirected graph consisting of \(n\) vertices and \(n\) edges. It is guaranteed that the given graph is connected (i. e. it is possible to reach any vertex from any other vertex) and there are no self-loops and multiple edges in the graph.
Your task is to calculate the number of simple paths of length at least \(1\) in the given graph. Note that paths that differ only by their direction are considered the same (i. e. you have to calculate the number of undirected paths). For example, paths \([1, 2, 3]\) and \([3, 2, 1]\) are considered the same.
You have to answer \(t\) independent test cases.
Recall that a path in the graph is a sequence of vertices \(v_1, v_2, \ldots, v_k\) such that each pair of adjacent (consecutive) vertices in this sequence is connected by an edge. The length of the path is the number of edges in it. A simple path is such a path that all vertices in it are distinct.
[input]
The first line of the test case contains one integer \(n\) (\(3 \le n \le 2 \cdot 10^5\)) — the number of vertices (and the number of edges) in the graph.
The next \(n\) lines of the test case describe edges: edge \(i\) is given as a pair of vertices \(u_i\), \(v_i\) (\(1 \le u_i, v_i \le n\), \(u_i \ne v_i\)), where \(u_i\) and \(v_i\) are vertices the \(i\)-th edge connects. For each pair of vertices \((u, v)\), there is at most one edge between \(u\) and \(v\). There are no edges from the vertex to itself. So, there are no self-loops and multiple edges in the graph. The graph is undirected, i. e. all its edges are bidirectional. The graph is connected, i. e. it is possible to reach any vertex from any other vertex by moving along the edges of the graph.
[output]
print one integer: the number of simple paths of length at least \(1\) in the given graph. Note that paths that differ only by their direction are considered the same (i. e. you have to calculate the number of undirected paths).
[solved]
(1)考虑一个简单环,任意两点间的简单路径数为2,其总简单路径数为n*(n-1);
(2)基环树中的简单路径数就是n*(n-1)-多算的路径数.
(3)整个图可以看成是一个环,环上的每个点挂着一棵树,由于环上任意两点间的简单路径数为2,不同树上任意两点间简单路径数为2,因此多算的部分只是所有树上的简单路径数,大小为siz(i)的树简单路径数为siz(i)*(siz(i)-1)/2。
(4)在每个以环上的点为根的树上的任意两个点只有\(C^{2}_{n}\)的取法,其他的任意两个点间的取法都是有两种路径共计\(C^{2}_{n}*2\)。我们先假设任意两个点间都有两种到达方法,那么\(res=C^{2}_{n}*2\),然后我们再减去只有一种方法的路径,即为所求。
[BFS,并查集,拓扑排序]
const int N = 200010;
int n;
vector<int> e[N];
int deg[N];
int BFS[N], cnt = 0;//数组模拟BFS,cnt:当前节点个数
int fa[N], num[N];//num:统计一个x为根节点的子树大小
int find(int x) {
while (x != fa[x]) x = fa[x] = fa[fa[x]];
return x;
}
//因为不止一个测试数据,需要初始化数据
void init(int n) {
for (int i = 1; i <= n; i++) {
deg[i] = 0;
num[i] = 1;
fa[i] = i;
}
cnt = 0;
}
void merge(int x, int y) {
int fx = find(x);
int fy = find(y);
if (fx == fy) return;
if (num[fx] < num[fy]) {
num[fy] += num[fx];
fa[fx] = fy;
} else {
num[fx] += num[fy];
fa[fy] = fx;
}
}
bool is_root(int x) {
return fa[x] == x;
}
void solve() {
cin >> n;
//初始化工作
init(n);
int u, v;
for (int i = 0; i < n; i++) {
cin >> u >> v;
deg[u]++, deg[v]++;
e[u].push_back(v);
e[v].push_back(u);
}
//从deg=1的叶子节点开始向上删去,向上合并,最后得到以环内节点为根节点的一颗颗树
for (int i = 1; i <= n; i++) {
if (deg[i] == 1) {
BFS[++cnt] = i;
}
}
for (int i = 1; i <= cnt; i++) {
int u = BFS[i];
for (int v : e[u]) {
merge(u, v);
deg[v] -= 1;
if (deg[v] == 1) BFS[++cnt] = v;
}
}
ll res = (ll)n * (n - 1);
for (int i = 1; i <= n; i++) {
if (is_root(i)) res -= (ll)num[i] * ((num[i]) - 1) / 2;
}
cout << res << endl;
for (int i = 1; i <= n; i++) {
e[i].clear();
}
}