本文章为参照这位博主图论总结进行学习的笔记 引用了部分该博主的内容
欧拉通路与欧拉回路问题
基本概念
欧拉通路:通过图中所有边一次且仅一次行遍所有顶点的通路
欧拉回路:通过图中所有边一次且仅一次行遍所有顶点的回路
欧拉图:具有欧拉回路的图
半欧拉图:具有欧拉通路而无欧拉回路的图
欧拉通路/回路的判定
无向图通路
除了起点和终点外都是偶度点
无向图回路
全都是偶度点
有向图通路
除了两个顶点(一个入度=出度+1,一个出度=入度+1)外其他点的入度等于出度
有向图回路
所有点的入度等于出度
算法实现
并查集判断是否有回路
首先记录连通分量,若连通分量大于1,则不存在回路。然后再统计是否存在奇度点,如果存在就不存在回路。
#include <iostream>
#include <cstring>
#define maxn 100
using namespace std;
int f[maxn], degree[maxn];
int Find(int x)
{
if (f[x] == x) return x;
return f[x] = Find(f[x]);
}
void merge(int x, int y)
{
int f1 = Find(x), f2 = Find(y);
if (f1 != f2) f[f1] = f2;
}
int main()
{
for (int i = 0; i < maxn; i++)
f[i] = i;
memset(degree, 0, sizeof(degree));
int n, x, y, group = 0, cnt = 0;
cin >> n; //输入点的个数
for (int i = 0; i < n; i++)
{
scanf("%d%d", &x, &y);
degree[x]++;
degree[y]++;
merge(x, y);
}
for (int i = 0; i < n; i++)
{
if (f[i] == i) group++; //集合数统计
if (degree[i] & 1) cnt++; //奇度点统计
}
//如果集合数大于1,或者存在奇度点,则不存在欧拉回路
if (group > 1 || cnt) cout << "no Eurler way" << endl;
return 0;
}
Fleury 算法求无向图回路
在已经确定存在欧拉回路的情况下任选一个点开始遍历,经过一个点就将其入栈,并删去这条边,当某个点没有边的时候出栈,记录路径,然后从栈的下一个点开始遍历,不断重复以上过程直至所有的边都被删去。
//假设输入的数据保证是有回路的
#include <cstring>
#include <iostream>
#include <stack>
#define maxn 1000
using namespace std;
int g[maxn][maxn], path[maxn], n, m, cnt = 0;
stack<int> S;
void dfs(int x)
{
S.push(x);
for (int i = 1; i <= n; i++)
{
if (g[x][i])
{
g[x][i] = 0;
g[i][x] = 0;
dfs(i);
break;
}
}
}
void fleury(int x)
{
S.push(x);
while (S.size())
{
int flag = 0, x = S.top();
S.pop();
for (int i = 1; i <= n; i++) //寻找与该点连接的边
{
if (g[x][i])
{
flag = 1;
break;
}
}
if (flag)
dfs(x);
else
path[cnt++] = x;
}
}
int main()
{
freopen("in.txt", "r", stdin);
memset(g, 0, sizeof(g));
int x, y;
cin >> n >> m;//输入点数和边数
for (int i = 1; i <= m; i++)
{
scanf("%d%d", &x, &y);
g[x][y] = g[y][x] = 1;
}
fleury(2);
cout << "path: ";
for (int i = 0; i < cnt; i++)
{
cout << path[i] << " ";
}
return 0;
}
通路的路径求法
从奇度点开始用Fleury算法遍历即可。
例题
一笔画问题
一笔画问题加强版(一条边可以通过多次)
例题
拓扑排序
概念和实现算法
将入度为0的点输出,每输出一个入度为0的点后就将该点所连接的所有点的入度减一,然后再输出剩下点中入度为0的点,不断重复。
AOV网
AOV网是一个有向无环图。点与点之间存在先后关系,必须完成所有前驱的点后才能进入下一个点,比如排课(修完了基础课程后才能学习这门课)。
例题
AOE网
在AOV网的基础上增加了边的权值,比如工程问题(权值代表工期)。
例题
图的连通性
概念
连通图:在无向图中从任意结点出发都能达到其他结点
连通分量:具有连通性的无向子图
强连通图:在有向图中从任意结点出发都能达到其他结点
连通分量:具有强连通性的有向子图
连通性判断
一次遍历能经过所有的点即可,dfs+栈
强连通性判断*
kosaraju算法
kosaraju算法是先正向dfs一次,该点的所有边都dfs完后入栈,然后根据栈的顺序反向dfs一遍(反向的意思是原来A→B的方向变为B→A)。
//kosaraju实现代码
#include <bits/stdc++.h>
#define maxn 105
using namespace std;
vector<int> g[maxn], rg[maxn]; //rg中保存的是反图
bool vis[maxn] = {
0};
stack<int> S;
int f[maxn]; //所属的强连通分量
inline void addEdge(int u, int v)
{
g[u].push_back(v);
rg[v].push_back(u);
}
void dfs1(int x)//正向dfs
{
vis[x] = 1;
for (int i = 0; i < g[x].size(); i++)
if (!vis[g[x][i]])
dfs1(g[x][i]);
S.push(x);
}
void dfs2(int x, int k)//反向dfs
{
f[x] = k; //记录该顶点属于哪个强连通分量
vis[x] = 0;
for (int i = 0; i < rg[x].size(); i++)
if (vis[rg[x][i]])
dfs2(rg[x][i], k);
}
int main()
{
//freopen("in.txt", "r", stdin);
int n, m, u, v, cnt = 0;
cin >> n >> m; //输入点数和边数
while (m--)
{
cin >> u >> v;
addEdge(u, v);
}
for (int i = 1; i <= n; i++)
if (!vis[i])
dfs1(i);
while (!S.empty())
{
//正向dfs完后所有的vis都变为1,因此dfs反图时根据vis是否为1进行判断
if (vis[S.top()])
dfs2(S.top(), cnt++);
S.pop();
}
cout << cnt << endl;
return 0;
}
tarjan算法(通用模板见缩点中的tarjan函数)
tarjan算法也是用dfs,用dfn数组来记录一个点被访问到的顺序,用low数组来记录这个点属于哪个强连通分量里(也可以理解为这个强连通分量里最早被访问的点的dfn值)
在dfs的同时还要用一个栈来压入被访问的点,以及一个vis数组来记录某个点是否位于栈中(毕竟不可能将栈中的点一个个弹出来判断某个点是否在里面)一旦dfs中遇到了栈中的点,就更新当前点的low值。如果某个点的dfn和low相同,说明这个强连通分量中所有的点都被访问过了,那么就进行出栈。
B站视频讲解
例题:【NOIP2015】信息传递
//tarjun算法
//【NOIP2015】信息传递
#include <bits/stdc++.h>
#define maxn 200005
using namespace std;
int to[maxn]; //信息传递的下一个人
stack<int> stk;
int low[maxn]; //可以从哪个点到达(也可理解为属于哪个强连通分量里)
int dfn[maxn]; //被访问的顺序
int vis[maxn]; //记录是否在栈中
int tot = 0, ans = 0x3f3f3f3f;
void tarjan(int x)
{
low[x] = dfn[x] = ++tot;
vis[x] = 1;
stk.push(x);
int v = to[x];
//更新low[x]有两种方式,根据v的情况进行选择(画一个图会很好理解)
if (!dfn[v]) //v还没有入栈
{
tarjan(v);
low[x] = min(low[x], low[v]); //如果v能够到达在栈中的点,那么low[v]会更新,low[x]也将随之更新
}
else if (vis[v]) //v已经入过栈
low[x] = min(low[x], dfn[v]);
//回溯到强连通分量的起点处,开始出栈
if (low[x] == dfn[x])
{
int cnt = 1;
while (stk.top() != x)
{
vis[stk.top()] = 0;
stk.pop();
cnt++;
}
stk.pop();
//游戏进行几轮取决于最小的强连通分量中点的个数(前提是点数大于1)
if (cnt > 1) ans = min(ans, cnt);
}
}
int main()
{
int n, v;
cin >> n;
for (int i = 1; i <= n; i++)
{
scanf("%d", &v);
to[i] = v;
}
for (int i = 1; i <= n; i++)
if (!dfn[i]) tarjan(i);
cout << ans << endl;
return 0;
}
缩点
对于一个有向图,求出最少加几条边可以将这个图变成一个强连通图。可以将每个强连通分量看作一个点,然后枚举每一个点,如果这个点能到达另一个连通集的点,那么该点的连通集出度为0,另一点的连通集入度为0,假设入度不为0的连通集个数为a,出度不为0 的连通集个数为b,最后的结果就是max(a,b)
#include <bits/stdc++.h>
#define maxn 100
using namespace std;
int dfn[maxn] = {
0}, low[maxn] = {
0}, group[maxn] = {
0}, tot = 0, cnt = 0, m, n;
bool vis[maxn]