图论笔记
图论基础
图的基本概念
- 图: 一个图由点集 V 和边集 E 组成, 如图 G = <V, E>
- 有无向图: 只有有/无向边组成的图
- 自环: 边连接的两个点事同一个点
- 重边: 无向图中两点间有多边,有向图在两个点中有多条同方向的边
- 孤点: 没有连接边的顶点
- 简单图: 无自环和重边的图
- 度数: 对于无向图,顶点 v 作为边端点的次数为 v 的度数, 记为 d(v), 对于有向图, 作为起点的个数为入度, 作为边终点的个数为出度
- 最大度: 所有顶点度数最大值, 反之有最小度
一个图 G 的所有点的度数和为边数的两倍, 有向图出度和等于入度和 - 完全图(无向图): G 为 n 个节点的无向图, 每个顶点与其他 n-1 个点都有边相连, 则 G 为 n 阶无向完全图, n 阶完全图, 记作 \(K_n\)
- 完全图(有向图): 相比无向完全图, 一条边有两个方向
- 竞赛图: 基于 n 阶无向完全图, 给每条边任意确定一个方向形成的图是 n 阶竞赛图
- 子图: G' 是 G 的子图, 当边集或点集为 G 的子集, 则 G 为 G' 的母图
- 生成子图: 点集相同的子图
- 真子图: 点集和边集都是母图的真子集
- 补图: 图和它的补图顶点集相同; 边集的交集为空, 并集是完全图的边集
- 同构: 对两个图 G 和 G' 存在双射 v -> v' ,使得对边 v -> v', 有 h(v) -> h(v') , 称两个图同构
- 通路: 图中的一条道路, 边的条数为长度, 有向图中边的方向一致, 点的数目是边的数目 + 1
- 回路: 通路的起点与终点相同, 中间的点都不同.
- 迹: 通路中所有边都不同, 若所有顶点也不相同则称为路径
- 距离: 图中连接两点的最短路径长度称为距离
- 无向图的连通
- 连通性: 图中 u 和 v 存在通路, 则 u, v 是连通的, 对于 v 和 v 子集也是连通的
- 连通图: 图中任意两点都是连通的
- 连通块: 对于图的一个连通子图 H, 不存在 H 的母图是连通图, 则 H 是 G 的一个连通块/连通分量, H 是一个极大连通子图.
- 有向图的连通
- 连通性: 图中存在 u -> v 的通路, 则 u 可达 v, 如果 u, v 互相可达, 则 u, v 连通
- 强连通: 如果有向图 G 中的顶点两两可达, 则 G 为 强连通图
- 强连通块: 类似于无向图中的连通块概念, 有强连通块/强连通分量
拓扑排序
- 时间复杂度: \(O(n+m)\)
- 拓扑排序是对有向无环图(DAG)的顶点进行一种线性排序, 序列中每个顶点只会出现一次, 对于所有有向边 u -> v,
排序完后 u 都在 v 前面 - 图中存在环, 就不能拓扑排序, 一个有向无环图可能存在多种拓扑排序结果
- 最小拓扑序列, 最大拓扑序 使用优先队列实现
拓扑排序判断环
int n, m;
const int N = 1e4 + 10;
int d[N];
vector<int> edge[N];
bool topo(){
queue<int> q;
for(int i = 1; i <= n; i++){
if(!d[i])
q.push(i);
}
int cnt = 0;
while(q.size()){
cnt++;
int t = q.front();
q.pop();
for(auto v: edge[t]){
d[v] --;
if(!d[v])
q.push(v);
}
}
return cnt == n;
}
int main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m;
while(m--){
int a, b;
cin >> a >> b;
edge[a].pb(b);
d[b] ++;
}
bool fl = topo();
!fl ? cout << "Yes\n" : cout << "No\n";
return 0;
}
欧拉路与欧拉回路
- 欧拉路: 图中所有边恰好经过一次的通路称为欧拉通路或者欧拉路
- 欧拉回路: 经过图中所有边恰好一次的回路称为欧拉回路
- 欧拉回路判断:
- 对于无向图 G, G 中所有度非 0 的点是连通的并且没有奇数度数的点
- 对于有向图 G, G 中所有度非 0 的店是强连通的并且入度和出度相同
- 欧拉路判断:
- 对于无向图 G, G 中所有度非 0 的点是连通的并且奇数度数的点只有 0 或者 2 个
- 对于有向图 G, G 中所有度非0的店是连通的(转为无向图), 且最多一个点出度=入度+1,入度=出度+1,其他点入度=出度
Code
有向图欧拉路
vector<int> edge[N];
int n, m, f[N], path[M], in[N], out[N], tot;
// f[i] 记录 i 点有多少条边
void dfs(int u){
int len = edge[u].size();
int x = 0;
while(f[u] < len){
int v = edge[u][f[u]];
++f[u];
dfs(v);
path[++tot] = v;
}
}
void Euler(){
int x = 0, y = 0, z = 0;
for(int i = 1; i <= n; i++){
if(out[i] == in[i] + 1)
y ++, x = i;
if(out[i] != in[i])
z++;
}
if(!(!z || (y == 1 && z == 2))){ // 不满足有向图欧拉路的条件
cout << "No\n";
return;
}
if(!x)
for(int i = 1; i <= n; i++)
if(in[i]){
x = i;
break;
}
dfs(x);
path[++tot] = x;
if(tot == m + 1) // 有向边变无向边是否连通可以用遍历过的边数是否为 m + 1 来检验
cout << "Yes\n";
else
cout << "No\n";
}
无向图欧拉路
int n, m;
int d[N], cnt = 1, f[N], tot, path[N << 1];
bool st[N << 1];
vector<PII> edge[N]; // 终点、边的编号
void dfs(int u){
int sz = edge[u].size();
while(f[u] < sz){
auto t = edge[u][f[u]];
if(!st[t.y]){
st[t.y] = st[t.y ^ 1] = true;
f[u] ++;
dfs(t.x);
path[++tot] = t.x;
}
else
f[u]++;
}
}
void Euler(){
int x = 0, y = 0;
for(int i = 1; i <= n; i++)
if(d[i] & 1)
x = i, y++;
if(y && y != 2){
cout << "No\n";
return;
}
if(!x)
for(int i = 1; i <= n; i++)
if(d[i])
x = i;
dfs(x);
path[++tot] = x;
tot == m + 1 ? cout << "Yes\n" : cout << "No\n";
}
二分图与最大匹配
拆点: 将 \(n\) 个点拆为 \(n\) 个进来的点和 \(n\) 出来的点.
int op[N * N];
bool find(int u){
st[u] = true;
for(auto v: edge[u]){
if(!op[v] || (!st[op[v]] && find(op[v]))){
op[v] = u;
return true;
}
}
return false;
}
int match(){
int res = 0;
for(int i = 1; i <= n1; i++){
memset(st, false, sizeof st);
if(find(i))
res++;
}
return res;
}