图论基础汇总
图论基础汇总
一、定义
基本定义
图:一张图由任意多的点和边构成,设点的集合点集为 $ V $,边集为 $ E $,一张图就可以被记作 $ G = (V, E) $
阶:图里的点的数量,即 $ |V| $,叫做阶,记作 $ |G| $
无向图:边没有方向的图叫做无向图,无向图的边记作 $ e = (u, v) $
有向图:边有方向的图叫做有向图,边记作 $ e = u \to v $,这里无向边可以视作两条有向边
重边:两条边,起点和终点都相同的边叫做重边
自环:自己连接自己的边叫做自环
相邻
相邻:在无向图中,当有边连接 $ u $ 和 $ v $,称 $ u, v $ 相邻
邻域:和 $ u $ 相邻的所有点组成的点集称为邻域
邻边:与 $ u $ 相连的边叫做 $ u $ 的邻边
入边、出边:有向图中,到达 $ u $ 的边叫做入边,从 $ u $ 出发的边叫做出边
度数:这里度数指的是出入边的总数,记作 $ d(u) $,自环产生 $ 2 $ 的贡献
入度、出度:即出入边的数量,分别计算,入度记作 $ d^-(u) $,出度记作 $ d^+(u) $
路径
途径:连接一串节点的序列叫做途径,记作 $ v_0 \to v_1 \to \dots \to v_k $
迹:不经过重复边的途径称为迹
回路:起点和终点相同的迹叫做回路
路径:不经过重复点的迹叫做路径,也叫简单路径
环:除了起点终点相同以外,其他点互不相同的途径称为环
连通性
连通:如果两个点存在一条途径连接两点,就称两点连通
弱连通:只有把的有向边改成无向边才连通的两个点称为弱连通
连通图:任意两点连通的无向图叫做连通图
弱连通图:任意两点弱连通的有向图称为弱连通图
可达:如果有向图中存在途径 $ v_0 = u $ 并且 $ v_k = v $,则 $ u $ 可达 $ v $,记作 $ u \rightsquigarrow v $
特殊图
简单图:不存在重边和自环的图叫做简单图
有向无环图:不存在环的有向图叫做有向无环图,简称 $ DAG $(Directed Acyclic Graph)
完全图:任意不同两点间恰好有一条边的图叫做完全图
树:不含环的无向连通图叫做树,满足 $ |V| = |E| + 1 $
森林:若干树组成的连通块叫做森林
稀疏图、稠密图:$ |E| $ 远小于 $ |V|^2 $ 的图叫稀疏图,$ |E| $ 接近 $ |V|^2 $ 的图叫稠密图,主要用于讨论算法复杂度
子图
子图:$ V' \subseteq V $ 并且 $ E' \subseteq E $ 的图 $ G' = (V', E') $
导出子图:选择若干个点以及两端都在该点集的所有边构成的子图称为该图的导出子图,记作 $ G[V'] $
生成子图:$ |V'|=|V| $ 的子图称为生成子图
极大子图(分量):在子图满足某性质的前提下,称子图 $ G' $ 是极大的,当且仅当不存在同样满足该性质的子图 $ G' $ 且 $ G' \subsetneq G'' \subseteq G $ 称 $ G' $ 为满足该性质的分量,如连通分量,点双连通分量,极大子图不能再扩张,例如,极大的连通的子图称为原图的连通分量,也就是我们熟知的连通块
二、一些基本知识
邻接矩阵存图
就是使用一个二维数组 G[][]
来存储,第一维表示起点,第二维表示终点,存储边权
如果没有边权可以写 $ 1 $
int G[1005][1005];
int n, m;
cin >> n >> m;
for (int i = 1; i <= m; ++ i) {
int u, v, w;
cin >> u >> v >> w;
G[u][v] = G[v][u] = w; // 无向图
G[u][v] = w; // 有向图
}
邻接表存图
这种就是对邻接矩阵的优化,使用 vector < int >
去优化空间,可以存数据范围更大的图
具体就是建一个一维数组,每一项是一个 vector < int >
然后第一维表示起点,vector
存的是相邻的点
vector < int > Vec[1000005];
inline void add_edge (int u, int v, int w, bool d) {
Vec[u].push_back (v);
if (d) Vec[v].push_back (v);
}
int n, m;
cin >> n >> m;
for (int i = 1; i <= m; ++ i) {
int u, v, w;
cin >> u >> v >> w;
add_edge (u, v, w, 1); // 无向图
add_edge (u, v, w, 0); // 有向图
}
链式前向星存图
可以使用链表的形式把空间和时间进一步优化,存在一维数组里,还是更优的
具体用 $ cnt $ 记录边数,用 $ to_i $ 记录边的终点,用 $ val_i $ 记录边权,用 $ head_i $ 记录点的第一条边,也就是链表头,$ nxt_i $ 表示一条边的下一条边
int N = 2000005;
int head[N], to[N], val[N], nxt[N], cnt;
inline void add_edge (int u, int v, int w, bool d) {
to[++ cnt] = v, val[cnt] = w, nxt[cnt] = head[u], head[u] = cnt;
if (d) to[++ cnt] = u, val[cnt] = w, nxt[cnt] = head[v], head[v] = cnt;
}
int n, m;
cin >> n >> m;
for (int i = 1; i <= m; ++ i) {
int u, v, w;
cin >> u >> v >> w;
add_edge (u, v, w, 1); // 无向图
add_edge (u, v, w, 0); // 有向图
}
DFS 遍历
用 DFS 的深度优先搜索顺序遍历一张图,即每次找到一个点就一直递归,直到不行就回溯
int vis[2000005];
inline void dfs (int u, int fa) {
vis[u] = 1;
for (int i = head[u]; i; i = nxt[i]) {
int v = to[i];
if (vis[v]) continue;
vis[v] = 1;
dfs (v, u);
}
}
BFS 遍历
用队列存储,每次用广搜进行找点就可以了,找到点就加进队列里就可以了
int vis[2000005];
queue < int > Q;
inline void BFS (int st) {
while (! Q.empty ()) Q.pop ();
Q.push (st);
vis[st] = 1;
while (! Q.empty ()) {
int u = Q.front (); Q.pop ();
for (int i = head[u]; i; i = nxt[i]) {
int v = to[i];
if (vis[v]) continue;
Q.push (v);
vis[v] = 1;
}
}
}
拓扑排序
对一张图中所有的点进行排序,保证每条边的起点比终点更靠前
因为原图无环,所以在拓扑排序没有结束时,必然存在入度为 $ 0 $ 的点否则,从任意一点出发走每个点的第一条出边得到无限长的结点序列,因为结点数有限,所以序列一定经过了重复的点,与原图无环矛盾
先取入度是 $ 0 $ 的点,然后加入队列,每次取出,把连接的点放进队列,按照从队列中取出的顺序做答案就可以了
这里数据范围小,使用 vector
存储
下面给出模板题的代码
# include <bits/stdc++.h>
using namespace std;
int n, deg[1005];
vector < int > Vec[N];
signed main () {
cin >> n;
for (int i = 1; i <= n; ++ i) {
int x;
cin >> x;
while (x) { Vec[i].push_back (x); ++ deg[x]; cin >> x; }
}
queue < int > Q;
for (int i = 1; i <= n; ++ i) if (deg[i] == 0) Q.push (i);
while (! Q.empty ()) {
int u = Q.front ();
Q.pop ();
cout << u << " ";
for (int i = 0; i < Vec[u].size (); ++ i) {
-- deg[Vec[u][i]];
if (! deg[Vec[u][i]]) Q.push (Vec[u][i]);
}
}
return 0;
}