树与图的储存与遍历
树与图的储存与遍历
树是无环连通的有向图,因此我们先从图的储存入手
图的储存
邻接矩阵法
我们使用一个矩阵 \(Graph\) 来存储一张图,\(Graph[a][b]\) 代表了点\(a\)与点\(b\)之间的关系
如果一个图是无权的无向图,那么 \(Graph[a][b]\) 与 \(Graph[b][a]\) 存储同样的信息,即\(a\)与\(b\)之间是否存在一条边;如果是有权的无向图,那么我们可以在\(Graph[a][b]\)与\(Graph[b][a]\)中储存边权,并使用一个特殊值来代表\(a\)与\(b\)之间没有边;如果图是有向的,如果有\(a\)向\(b\)的边,那么仅在\(Graph[a][b]\)中储存信息即可
邻接矩阵法需要\(O(n^2)\)的空间,因此,对于储存边数多的稠密图是良好的。但对于边数少的稀疏图而言则会浪费许多的空间
邻接表法
对于边数较少的稀疏图,使用邻接表法储存更好,并且大多数情况下处理图论相关问题都是使用邻接表法更优。
如果存在点\(a\)到点\(b\)的边,那么我们将节点\(b\)纳入以\(a\)为起始端的链表中。维护所有这样的链表的数组叫做邻接表。
//h[i]是以i为起始端的链表的头结点的next位置
int h[N];
//静态链表
const int M = 2*N;//对于无向图,b在以a为起始的链表中,a也会在以b为起始的链表中,因此总节点数为点数的两倍
int e[M],ne[M],idx=0;
void init(void){
memset(h, -1, sizeof h);
}
//添加边a<-->b
void add(int a, int b){
//在a链表中头插入b
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
//在b链表中插入a
e[idx] = a;
ne[idx] = h[b];
h[b] = idx++;
}
树的储存
树是无环连通有向图,我们按照储存有向图的方式储存树即可
int tr[N];
int e[N],ne[N],idx=0;
void init(){
memset(tr,-1,sizeof tr);
}
void add(int father, int son){
e[idx] = son;
ne[idx] = tr[father];
tr[father] = idx++;
}
注意,按照这种方式储存的树无法显式地求得树根
我们把树的概念在扩大到若干个树的集合中:
对于一个连通的无向图,如果图中不存在环,那么指定一个节点\(r\)为起始节点进行深度优先遍历。如果由\(a\)遍历到了节点\(b\),那么\(b\)的父节点就是\(a\)。最终,当所有节点都遍历过了以后,图就变为了以\(r\)为树根的树。这棵树是在连通无环无向图中指定根节点后“生长”出来的。
对于一个给定的连通无环无向图\(G\),\(T\)是在\(G\)中指派某个节点为根“生长”出的树的集合。这些树在某些方面具有相同的性质,这里暂时不做讨论。另外,除了深度优先的生长方式外,广度优先的生长方式生长出的树集和前者是完全相同的。
图的遍历
大多数情况下,图的遍历都是不重复遍历,即同一个点不会遍历两次。
深度优先遍历
深度优先搜索就是递归地处理每个点和其没有被搜索过的下一个节点
bool st[N];//记录已被搜索过的节点
//递归处理u节点和其子节点
void dfs(int u){
st[u] = true;
for(int i=h[u];i!=-1;i = ne[i]){
if(!st[e[i]]) dfs(e[i]);
}
}
根据节点和子节点的处理顺序,我们可以分为前序遍历和后序遍历
树集的重心
树集的重心是这样定义的:给定无向无环连通图\(G\) ,指定某个根点\({root}_i\),将\({root}_i\)和与其相连的边从图中删除后,形成若干个无向无环连通子图 \(G_j\),记其中包含节点数最大的图的子节点数为 \(N(root,G)\),使得\(N(root,G)\)最小的\(root\)称为树集的重心,求最小的\(N(root,G)\)
我们枚举图中的每一个点,求得剔除该节点后子连通块上的最小值即可。
我们知道无向无环连通图任意两个节点都是连通的,从任意节点出发都可以生长出一颗树,我们从树的角度来考虑这个算法。
根点\(v\) 剖分出的子图在树的角度下,将分为两类:
- 以\(v\)的子节点为根节点的子树
- 原树除去以\(v\)为根节点的子树后剩余的树
对于第一类,我们可以枚举所有子节点的子树,求其中的节点数,维护其中的最小值;第二类则可以用所有子节点的子树节点个数加上\(v\)后与树节点总数相减求得。
显然,求子树的节点数使用深度优先最佳。
#include<iostream>
using namespace std;
const int N = 100010;
const int M = 2*N;
int h[N],e[M],en[M],idx=0;
bool st[N];
// res 记录答案
int n,res=1000000;
int dfs(int u){
st[u] = true;
// son_size 维护子节点子树节点数的最大值
int sum = 1,son_size=0;
for(int i = h[u];i!=-1;i = en[i]){
int j = e[i];
if(!st[j]){
int ssz = dfs(j);
son_size = max(ssz,son_size);
sum += ssz;
}
}
size = max(n-sum,son_size);
res = min(res,son_size);
//返回以u为根节点的子树的节点数
return sum;
}
#include<cstring>
int main(){
memset(h,-1,sizeof h);
//输入无环无向连通图的节点总数
cin>>n;
for(int i=1;i<=n-1;i++){
//储存图
int a,b;
cin>>a>>b;
e[idx] = b;en[idx] = h[a];h[a] = idx++;
e[idx] = a;en[idx] = h[b];h[b] = idx++;
}
//1到n的任意整数都可以
dfs(n);
cout<<res<<endl;
return 0;
}
广度优先遍历
拓扑排序
给定一个有向图 \(G\),图可能存在重边和环,求出该图的拓扑排序序列
其中,拓扑排列的定义是:点\(a\)排在\(b\)的前方断言不存在由\(b\)到\(a\)的有向边
容易发现,拓扑序排在第一位的节点的入度一定为\(0\) 。当我们把入度为\(0\)的点从序列前端删除,并从图中删除该点和与该点相邻的边后,序列中的新的第一位的节点入度仍然是\(0\)。
在环上的节点是不可能被纳入拓扑序列中的,因为如果想要使环上的 \(a\) 点入度变为\(0\),除非移除\(b \to a\) 的\(b\)点,而移除\(b\)点的条件是\(b\)的入度变为了\(0\),最终,条件变为:如果想要使环上的 \(a\) 点入度变为\(0\),除非\(a\)的入度为\(0\)
因此,存在环的有向图无法将所有点进行拓扑排序。
下面给出拓扑排序的代码,如果图无法将所有点拓扑排序,它将输出\(-1\):
#include<iostream>
using namespace std;
int n,m;
const int N = 100010,M = 200020;
int h[N],e[M],ne[M],idx=0;
void add(int a, int b){
e[idx] = b;ne[idx] = h[a];h[a] = idx++;
}
//储存拓扑序列
int stk[N],tp=0;
//in 记录入度 q 广搜队列
int in[N],q[N],ff=0,tt=-1;
void bfs(){
while(ff<=tt){
int j = q[ff++];
for(int i = h[j];i!=-1;i = ne[i]){
//移除边后将终点入度减一
--in[e[i]];
//入度为0,入队和拓扑序列
if(in[e[i]]==0){
q[++tt] = e[i];
stk[tp++] = e[i];
}
}
}
}
#include<cstring>
int main(){
memset(h,-1,sizeof h);
cin>>n>>m;
while(m--){
int a, b;
cin>>a>>b;
add(a,b);
in[b]++;
}
//将所有入度为0的点入队和拓扑序列
for(int i=1;i<=n;i++){
if(in[i]==0)q[++tt]=i,stk[tp++]=i;
}
bfs();
//当拓扑序列长度不等于点数时,说明图存在环,无法全部拓扑排序
if(tp==n)for(int i=0;i<tp;i++){
cout<<stk[i]<<' ';
}
else cout<<-1;
cout<<endl;
return 0;
}
本文来自博客园,作者:Sarfish,转载请注明原文链接:https://www.cnblogs.com/sarfish/articles/15942370.html

浙公网安备 33010602011771号