本文章为参照这位博主图论总结进行学习的笔记 引用了部分该博主的内容
欧拉通路与欧拉回路问题
基本概念
欧拉通路:通过图中所有边一次且仅一次行遍所有顶点的通路
欧拉回路:通过图中所有边一次且仅一次行遍所有顶点的回路
欧拉图:具有欧拉回路的图
半欧拉图:具有欧拉通路而无欧拉回路的图
欧拉通路/回路的判定
无向图通路
除了起点和终点外都是偶度点
无向图回路
全都是偶度点
有向图通路
除了两个顶点(一个入度=出度+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] = {0}, in[maxn], out[maxn];
vector<int> g[maxn];
stack<int> S;
void tarjan(int x)
{
dfn[x] = low[x] = ++tot;
S.push(x);
vis[x] = 1;
for (int i = 0; i < g[x].size(); i++)
{
int v = g[x][i];
if (!vis[v]) //这个点还没有被访问过
{
tarjan(v);
low[x] = min(low[x], low[v]);
}
else if (!group[v]) //这个点不属于任何一个连通集(说明还在栈中)
{
low[x] = min(low[x], dfn[v]);
}
}
if (low[x] == dfn[x]) //该顶点为连通集的第一个被访问元素
{
cnt++; //连通集个数加一
while (S.top() != x)
{
group[S.top()] = cnt; //记录所属的连通集
S.pop();
}
group[x] = cnt;
S.pop();
}
}
int shrink()
{
if (cnt == 1) return 0;
for (int i = 1; i <= cnt; i++)
in[i] = out[i] = 1;
for (int i = 0; i < n; i++) //枚举所有的点
{
for (int j = 0; j < g[i].size(); j++)
{
int v = g[i][j];
if (group[i] != group[v])
{
out[group[i]] = 0;
in[group[v]] = 0;
}
}
}
int a = 0, b = 0;
for (int i = 1; i <= cnt; i++)
{
a += in[i];
b += out[i];
}
return a > b ? a : b;
}
int main()
{
int u, v;
cin >> n >> m;
for (int i = 0; i < m; i++)
{
scanf("%d%d", &u, &v);
g[u].push_back(v);
}
for (int i = 0; i < n; i++)
if (!vis[i]) tarjan(i);
int res = shrink();
cout << "有" << cnt << "个连通集" << endl;
cout << "最少需要添加" << res << "条边" << endl;
return 0;
}
割点
在一个无向图中,如果去掉一个点和它所连出去的的所有边,使得剩下的点不连通(即分成一个以上的强连通分量)时,这个点被称为割点。
求割点: 将图看成树的样子,同时沿用tarjan算法中的low和dfn数组对点进行标记。任选一个点作为根进行dfs,如果根与两个连通分量相连,那么这个点就是割点(两个连通分量之间的点不通过该点不能互相到达,即至少进行两次dfs才能将该点所连的边都遍历到)。如果子结点能再连接一个连通分量,那么也是割点。具体实现方式见代码注释。
//割点模板
#include <bits/stdc++.h>
#define maxn 100
using namespace std;
int low[maxn], dfn[maxn] = {0}, tot = 0;
vector<int> g[maxn];
bool sign[maxn] = {0}; //记录是否为割点
void tarjan(int x, int root)
{
int cnt = 0; //记录根节点所连的连通集个数
dfn[x] = low[x] = ++tot;
for (int i = 0; i < g[x].size(); i++)
{
int v = g[x][i];
if (!dfn[v]) //该点还没访问过
{
tarjan(v, root);
low[x] = min(low[x], low[v]);
if (x != root && low[v] >= dfn[x]) //子结点所能到达的最早的点也在x之后到达,说明和之前的点没有边相连,是一个独立的连通分量
sign[x] = 1;
else if (x == root) //如果是根结点,那么连通集个数加一
cnt++;
}
else //遇到访问过的点,更新low
low[x] = min(low[x], dfn[v]);
}
if (cnt >= 2)
sign[root] = 1;
}
int main()
{
int n, m, u, v;
vector<int> ans;
cin >> n >> m;
for (int i = 0; i < m; i++)
{
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
for (int i = 0; i < n; i++)
if (!dfn[i]) tarjan(i, i);
for (int i = 0; i < n; i++)
if (sign[i]) ans.push_back(i);
cout << "割点个数为:" << ans.size() << endl;
for (int i = 0; i < ans.size(); i++)
cout << ans[i] << " ";
return 0;
}
/*
sample input:
7 8
1 2
1 0
2 0
3 4
0 3
4 5
5 6
6 3
sample output:
割点个数为:2
0 3
*/
桥(割边)
在一个无向图中,如果去掉一条边,使得剩下的点不连通,则这条边就是桥。
割边求法:
解释1: 需要加一个pre数组来记录当前的点的前一个点,防止回退。然后枚举所有的边,如果一个边的low[x]!=low[y]那么就是割边。
解释2: 从当前点x的边出发访问未访问的点y,如果low[y]>dfn[x],那么xy就是一条割边。
//割边模板
#include <bits/stdc++.h>
#define maxn 100
using namespace std;
struct edge
{
int x, y;
};
int low[maxn], dfn[maxn] = {0}, pre[maxn], tot = 0;
vector<int> g[maxn];
vector<edge> ans;
void tarjan(int x)
{
dfn[x] = low[x] = ++tot;
for (int i = 0; i < g[x].size(); i++)
{
int v = g[x][i];
if (pre[x] == v) continue; //防止回退访问重复的点
if (!dfn[v]) //该点还没访问过
{
pre[v] = x;
tarjan(v);
low[x] = min(low[x], low[v]);
if (low[v] > dfn[x]) ans.push_back(edge{x, v});
}
else //遇到访问过的点,更新low
low[x] = min(low[x], dfn[v]);
}
}
int main()
{
int n, m, u, v;
cin >> n >> m;
for (int i = 0; i < m; i++)
{
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
for (int i = 0; i < n; i++)
if (!dfn[i])
{
pre[i] = i;
tarjan(i);
}
cout << "割边数为:" << ans.size() << endl;
for (int i = 0; i < ans.size(); i++)
printf("(%d,%d) ", ans[i].x, ans[i].y);
return 0;
}
/*
sample input:
7 8
1 2
1 0
2 0
3 4
0 3
4 5
5 6
6 3
sample output:
割边数为:1
(0,3)
*/
2-SAT算法*
学习该算法需要先学习强连通分量的判断
2-SAT问题学习笔记+POJ3648例题题解+3道例题
最短路径算法
Dijkstra算法普通版本
// dijkstra最短路径算法,适用于单源最短路径,指定一个起点,计算所有点到该点的最短路径长度
// 有两个集合,分别记录已经确定最短路径的点和未确定的点
// 将起点加入已经确定的点的集合中,然后从未确定的集合中找距离起点最近的点,将其移动到已经确定的集合中
// 将已经确定的集合中的点同未确定的集合中的所有点进行比较,将未确定的集合中的点中最短路径的点移动到已经确定的集合中
// 重复以上过程,直到一个集合为空
void dijkstra(int start)//若不传入终点,将计算起点到所有点的最短距离
{
int mlen, id, jj;
len[start] = 0;
visted.push_back(start);//加入已经确定的点集中
rest.erase(rest.begin() + start);//从未确定的点集中删除起点
while (rest.size())
{
mlen = inf;
jj = -1;
for (int i = 0; i < visted.size(); i++)//从已经确定的点集中取点
{
int x = visted[i], y;
for (int j = 0; j < rest.size(); j++)//从未确定的点集中取点
{
y = rest[j];
if (len[x] + g[x][y] < mlen)//找到了距离更短的点
{
p[y] = x;
mlen = len[x] + g[x][y];//更新最小距离
id = y;//记录距离最小的点
jj = j; //jj记录后面要从rest中删除的下标
}
}
}
}
if (jj == -1)//不存在新的点,或者已经搜索到终点
return;
len[id] = mlen;//最后将确定的点更新信息即可,其余的点不用管
rest.erase(rest.begin() + jj); //将路径最短的点删去
visted.push_back(id); //加入新的点
}
Dijkstra算法堆优化版本
由于普通版本的Dijkstra算法复杂度为O(n*n),在大多数的算法竞赛题中都会超时,因此堆优化版本的Dijkstra使用率更高。
堆优化借助一个优先队列(一个小根堆)和一个状态表示结构体(记录点的下标和距离),每次可以用O(logn)的时间找到剩余点中距离最小的点,对于稀疏图的时间复杂度为O(nlogn),但是在稠密图中可能会退化到O(n*n),此时和普通版本的Dijkstra差别不大。
#include <bits/stdc++.h>
#define mem(a, v) memset(a, v, sizeof(a))
const int inf = 2e9 + 5;
const int N = 1e5 + 5;
using namespace std;
struct edge //链式前向星方式存边
{
int to; //与之有边相连的点
int next; //下一条边的下标
int distance; //边权
} e[N * 2];
struct node //堆优化的Dijkstra算法状态记录结构体
{
int index;
int dis;
node() {}
node(int i, int d) { index = i, dis = d; }
};
bool operator<(node a, node b) { return a.dis > b.dis; } //运算符重载,用于实现优先队列的排序
int head[N], cnt = 0;
int dis[N];
bool vis[N];
void addEdge(int u, int v, int dis)
{
e[++cnt].to = v;
e[cnt].distance = dis;
e[cnt].next = head[u];
head[u] = cnt;
}
void Dijkstra(int s, int n)
{
for (int i = 1; i <= n; i++)
dis[i] = inf;
mem(vis, 0);
priority_queue<node> q;
q.push(node(s, 0));
dis[s] = 0;
while (q.size())
{
int x = q.top().index; //取堆顶的点作为下一个确定的点
int d = q.top().dis;
q.pop();
if (vis[x]) continue; // x是确定的点,跳过
vis[x] = 1;
for (int i = head[x]; i; i = e[i].next)
{
int to = e[i].to;
if (vis[to]) continue; //已经确定的点就跳过
if (d + e[i].distance < dis[to])
{
dis[to] = d + e[i].distance;
q.push(node(to, dis[to])); //将更新的点状态入堆
}
}
}
}
int main()
{
mem(head, 0);
int n, m, s, u, v;
int w;
scanf("%d%d%d", &n, &m, &s);
while (m--)
{
scanf("%d%d%d", &u, &v, &w);
addEdge(u, v, w); //添加有向边
}
Dijkstra(s, n);
for (int i = 1; i <= n; i++)
printf("%d ", dis[i]);
return 0;
}
Floyd算法
弗洛伊德最短路径算法,适用于多源最短无向图路径,可以求出任意两点间的最短路径
一般采用临接矩阵来存储路径长度,复杂度为O(n^3),因此点数不易过多
算法实现原理:类似动态规划
选定一个中转点k,比较x->y和x->k->y的路径哪个更短,然后更新路径
用三重循环,第一重是中转点k,第二重是起点x,第三重是终点y,三重循环过后即可算出任意两点间的最短路径长度
//核心算法,三重循环
for (int k = 1; k <= n; k++) //k表示中转点
for (int i = 1; i <= n; i++) //i表示起点
for (int j = 1; j <= n; j++) //k表示终点
if (dis[i][j] > dis[i][k] + dis[k][j]) //走中转点的路径更短,那么就更新路径
dis[i][j] = dis[i][k] + dis[k][j];
Bellman-Ford 算法与 SPFA
Bellman-Ford 算法可以看作是Dijkstra算法的改进,最大的特点是可以处理负边权的情况(Dijkstra算法在遇到负边权时就会失效)。Ford 算法是每次更新所有的边,从而确定一个点的最短距离。在Bellman-Ford算法中,边是有方向的(即不能往回走,否则负边权两点间可以重复来回使得距离不断减小)
优化一: 判断负权回路,如果在进行了n-1次更新后还可以继续更新,那么必定存在负权回路。
优化二: SPFA是加了负回路判断的队列版本(可以继续用优先队列进行优化)。其利用队列以进行 Ford 算法的过程,初始时将起点加入队列,每次从队列中取出一个元素,并对所有与它相邻的点进行修改,若相邻的点修改成功,则将其入队,直到队列为空时算法结束。
//bellman-ford算法原始版
#include <bits/stdc++.h>
#define maxn 105
#define inf 0x3f3f3f3f
using namespace std;
struct edge
{
int x, y, w;
};
vector<edge> vedge;
int dis[maxn]; //距离起点的距离
void init(int n) //初始化
{
vedge.clear();
for (int i = 0; i <= n; i++)
dis[i] = inf;
}
void bellman_ford(int start, int n) //算法主体
{
dis[start] = 0;
for (int k = 0; k < n; k++)
{
for (int i = 0; i < vedge.size(); i++)
{
int u = vedge[i].x;
int v = vedge[i].y;
int w = vedge[i].w;
dis[v] = min(dis[v], dis[u] + w);
}
}
}
int main()
{
freopen("bellman_in.txt", "r", stdin);
int n, m, u, v, w, start;
cin >> n >> m; //输入点和边数
cin >> start;
init(n);
for (int i = 0; i < m; i++)
{
scanf("%d%d%d", &u, &v, &w);
vedge.push_back(edge{u, v, w});
}
bellman_ford(start, n);
for (int i = 0; i < n; i++)
if (i != start) printf("dis[%d]=%d , ", i, dis[i]);
return 0;
}
优化一:(判断负权回路),只需要在n-1次松弛后再加一轮循环即可
int flag = 0;
for (int i = 0; i < vedge.size(); i++)
{
int u = vedge[i].x;
int v = vedge[i].y;
int w = vedge[i].w;
if (dis[v] > dis[u] + w)
{
flag = 1;
break;
}
}
if (flag) printf("存在负权回路!");
优化二:SPFA(队列版本) 实际操作中往往不需要n-1次循环,并且一些点更新一次即可,无需每次都枚举所有的边,如果用优先队列优化每次先更新小的更容易先更新完,但是入队出队的时间增加到了log级别,需要权衡
//bellman-ford算法SPFA带负环判断
#include <bits/stdc++.h>
#define maxn 105
#define inf 0x3f3f3f3f
using namespace std;
struct edge
{
int v, w;
};
vector<edge> g[maxn]; //记录该点所出的边
bool vis[maxn]; //记录一个点是否在队列中
int dis[maxn]; //距离起点的距离
int cnt[maxn]; //记录一个点松弛的次数(用于判断负环)
void init(int n) //初始化
{
memset(vis, 0, sizeof(vis));
for (int i = 0; i <= n; i++)
{
dis[i] = inf;
cnt[i] = 0;
}
}
bool bellman_ford(int start, int n) //算法主体
{
dis[start] = 0;
queue<int> q;
q.push(start);
vis[start] = 1;
while (!q.empty())
{
int u = q.front();
q.pop();
vis[u] = 0;
for (int i = 0; i < g[u].size(); i++)
{
int v = g[u][i].v;
int w = g[u][i].w;
if (dis[v] > dis[u] + w)
{
dis[v] = dis[u] + w;
if (!vis[v])
{
q.push(v);
vis[v] = 1;
}
if (++cnt[v] >= n) return false; //如果一个边松弛次数超过了n-1次,则存在负环
}
}
}
return true;
}
int main()
{
freopen("bellman_in.txt", "r", stdin);
int n, m, u, v, w, start;
cin >> n >> m; //输入点和边数
cin >> start;
init(n);
for (int i = 0; i < m; i++)
{
scanf("%d%d%d", &u, &v, &w);
g[u].push_back(edge{v, w});
}
if (bellman_ford(start, n))
{
for (int i = 0; i < n; i++)
if (i != start)
printf("dis[%d]=%d , ", i, dis[i]);
}
else
printf("存在负环");
return 0;
}
最小生成树
prime算法
与Dijkstra算法类似,先选定一个起点,遍历与这个点相连的所有边,选择最短的一条将其加入,然后继续从已经加入的点中遍历相连的边,选择最短的加入。n个点,则需要加入n-1条边,适用于稠密图,对于稀疏图的效率不是很高。
kruskal算法
先将所有的边按从小到大排序,选择其中最小的一条边,将其加入,然后从剩余的边中继续选择最小的边加入,在加入边时,边的两端点不能位于同一棵树中,即不能构成回路,为了判断是否位于同一棵树,需要用到并查集。
最小生成树例题(选自PTA)
AC代码
prime版本:
//采用prime算法,用时47ms,对于稀疏图效率不是很高,算法与dijskar算法类似
#include <bits/stdc++.h>
using namespace std;
#define mem(a, n) memset(a, n, sizeof(a))
#define inf 0x3f3f3f3f
typedef unsigned long long ull;
int len[1001][1001];
int n, m, x, y, l, cnt = 0, vis[1001] = {0}, sum = 0;
vector<int> g[1001];
vector<int> visted; //保存已经加入最小树的点
int main()
{ //ios::sync_with_stdio(false);cin.tie(0);
vis[1] = 1;
cin >> n >> m;
visted.push_back(1);
while (m--)
{
cin >> x >> y >> l;
g[x].push_back(y);
g[y].push_back(x);
len[x][y] = l;
len[y][x] = l;
}
//prime算法主体
while (cnt < n - 1)
{
int x, y, id = -1, mlen = inf, xx;
for (int i = 0; i < visted.size(); i++) //从已经确定的点中找与其连接的最短路径
{
x = visted[i];
for (int j = 0; j < g[x].size(); j++) //遍历与该点链接的所有边
{
y = g[x][j];
if (!vis[y] && len[x][y] < mlen) //如果没有加入最小树并且距离比当前的最小距离更小就更新
{
id = y;
xx = x;
mlen = len[x][y];
}
}
}
if (id == -1) //不能生成最小树
{
cnt = -1;
break;
}
else
{
cnt++;
sum += len[xx][id];
visted.push_back(id);
vis[id] = 1;
}
}
if (cnt == -1)
cout << -1;
else
cout << sum;
return 0;
}
kruskal版本
//采用kruskal算法,用时7ms,对于稀疏图的效率较高,需要用到并查集(堆可以不用)
#include <bits/stdc++.h>
using namespace std;
#define mem(a, n) memset(a, n, sizeof(a))
#define inf 100000000
int fa[1001];
struct side
{
int x;
int y;
int len;
} s[3001];
bool cmp(side a, side b)
{
return a.len < b.len;
}
int Find(int x)
{
if (fa[x] == x)
return x;
return fa[x] = Find(fa[x]);
}
void merge(int x, int y)
{
int f1 = Find(x), f2 = Find(y);
if (f1 != f2)
{
fa[f1] = fa[f2];
}
}
int main()
{ //ios::sync_with_stdio(false);cin.tie(0);
int n, m, x, y, l, cnt = 0, sum = 0, left = 0;
cin >> n >> m;
for(int i=1;i<=n;i++) fa[i]=i;
for (int i = 0; i < m; i++)
{
cin >> x >> y >> l;
s[i].x = x;
s[i].y = y;
s[i].len = l;
}
sort(s, s + m, cmp); //将所有边按权重进行升序排列
while (cnt < n - 1 && left < m)//找最短边,如果该边的两点在同一个集合中,就跳过
{
side t = s[left];
if (Find(t.x) != Find(t.y))
{
merge(t.x, t.y);
sum += t.len;
cnt++;
}
left++;
}
if(cnt==n-1) cout<<sum;
else cout<<-1;
return 0;
}
次小生成树
最小瓶颈生成树
所谓瓶颈生成树,即对于图 G 中的生成树树上最大的边权值在所有生成树中最小。最小瓶颈生成树不一定是最小生成树。无向图中的最小生成树一定是最小瓶颈生成树。在有向图中采用kruskal算法即可。
二分图
参考文章
二分图的定义:一个图中的所有结点可以划分为两个集合,划分后,所有边的两端都分别位于两个集合中。即不存在一条边的两个端点在同一个集合内。
匹配
匹配是一个边的集合,集合中没有两条边存在公共点,下面是两个匹配的示意图,其中右边既是最大匹配也是完美匹配。如果所有的顶点都是匹配点,则称为完美匹配。完美匹配一定是最大匹配,但是完美匹配不一定存在。
匈牙利算法求最大匹配
原理: 将两个集合划分为X和Y,每次选择X中的一个点x1,去Y中寻找一个点y1与X进行匹配。
y1会存在以下两种情况:
- y1还没有被匹配过,则将y1与x1进行匹配,结束。
- y1已经被匹配了,那么从y1的匹配点x2开始,尝试找一个新的匹配点y2与x2进行匹配,从而令y1能够与x1进行匹配。对于x2,执行同样的做法。
以POJ1469为例介绍匈牙利算法的两种版本
题目大意:一共有P门课程和N个学生,每个学生可以选候选列表里的一门课或不选,能否保证每门课都有人选,且选课的学生中没有人选相同的课。很明显是一道求最大匹配的问题。
DFS版本
#include <cstdio>
#include <cstring>
#include <vector>
#define maxn 1005
using namespace std;
int p, n;
int vis[maxn]; //记录在dfs中是否被访问过
int cp[maxn]; //记录匹配点
vector<int> g[maxn]; //记录每门课的候选学生
bool dfs(int x) //找x的匹配点,一旦找到了就返回true
{
for (int i = 0; i < g[x].size(); i++) //遍历所有能和x匹配的点
{
int y = g[x][i];
if (vis[y]) continue; //已经访问过了,就不用重复走这条路了
vis[y] = true;
// 1、y还没有被匹配,
// 2、y的匹配点z可以去匹配其他的点使得y与x能够匹配
if (cp[y] == -1 || dfs(cp[y]))
{
cp[y] = x; //更改y的匹配点
return true;
}
}
return false;
}
bool check() //检查每门课是否都能找到匹配
{
for (int i = 1; i <= p; i++)
{
memset(vis, 0, sizeof(vis));
if (!dfs(i))
return 0;
}
return 1;
}
int main()
{
int t;
scanf("%d", &t);
while (t--)
{
for (int i = 0; i < maxn; i++)
{
g[i].clear();
cp[i] = -1;
}
scanf("%d%d", &p, &n);
for (int i = 1; i <= p; i++)
{
int temp, stu;
scanf("%d", &temp);
while (temp--)
{
scanf("%d", &stu);
g[i].push_back(stu);
}
}
if (p > n) //课程数大于学生数,肯定无解
printf("NO\n");
else
{
if (check())
printf("YES\n");
else
printf("NO\n");
}
}
return 0;
}
BFS版本
BFS版本实际上就是去模拟DFS的递归过程,相比DFS可以减少一些递归上的时间开销
#include <cstdio>
#include <cstring>
#include <queue>
#include <vector>
#define maxn 1005
using namespace std;
int p, n; //记录左右侧点的数量
int lcp[maxn], rcp[maxn]; //记录左侧元素的匹配点以及右侧元素的匹配点
int pre[maxn]; //记录前驱结点,在回溯更新匹配点时用到
int vis[maxn]; //记录该点被哪个点访问过,避免重复访问
vector<int> g[maxn]; //临接表存图
int Hungarian()
{
memset(vis, 0, sizeof(vis));
for (int i = 0; i < maxn; i++)
lcp[i] = rcp[i] = -1;
for (int i = 1; i <= p; i++) //给每个点找匹配
{
if (lcp[i] != -1) continue; //如果该点已经匹配,进行下一轮循环
queue<int> q;
q.push(i);
pre[i] = -1;
bool flag = false; //是否找到匹配
while (q.size() && !flag)
{
int x = q.front();
q.pop();
for (int j = 0; j < g[x].size(); j++)
{
int y = g[x][j];
if (vis[y] == i) continue; //如果之前从i点到过y点,说明这条路无解,不用继续走了
vis[y] = i;
//如果右侧的点已经匹配,将对应的左侧匹配点入队
if (rcp[y] != -1)
{
q.push(rcp[y]);
pre[rcp[y]] = x;
}
else //如果右侧有未匹配的点,则匹配成功
{
flag = true;
//进行回溯更新匹配边
while (x != -1)
{
int temp = lcp[x]; //记录原来的匹配点
lcp[x] = y;
rcp[y] = x;
x = pre[x];
y = temp;
}
break;
}
}
}
}
//统计有多少点找到了匹配点
int cnt = 0;
for (int i = 1; i <= p; i++)
if (lcp[i] != -1) cnt++;
return cnt;
}
int main()
{
int t;
scanf("%d", &t);
while (t--)
{
for (int i = 0; i < maxn; i++)
g[i].clear();
scanf("%d%d", &p, &n);
for (int i = 1; i <= p; i++)
{
int temp, stu;
scanf("%d", &temp);
while (temp--)
{
scanf("%d", &stu);
g[i].push_back(stu);
}
}
if (Hungarian() == p)
printf("YES\n");
else
printf("NO\n");
}
return 0;
}
网络流
FF算法(最大流基础)
B站介绍视频FF算法
用一道例题来辅助理解
洛谷例题
//FF算法(效率较低,容易被卡),此题有两个测试点会TLE
#include <bits/stdc++.h>
#define maxm 20005
#define maxn 505
#define mem(a) memset(a, 0, sizeof(a))
#define inf 0x3f3f3f3f
typedef long long ll;
using namespace std;
struct edge
{
int to, next;
ll cap;
} e[maxm];
int head[maxn] = {0}, cnt = 1, S, T;
bool vis[maxn] = {0};
//链式前向星方式存边
void add_edge(int u, int v, ll cap)
{
e[++cnt] = edge{v, head[u], cap};
head[u] = cnt;
}
ll dfs(int u, ll flow)
{
if (u == T) return flow;
vis[u] = 1; //防止dfs往回走
for (int i = head[u]; i != 0; i = e[i].next)
{
int v = e[i].to;
if (!e[i].cap || vis[v]) continue;
ll delta = dfs(v, flow < e[i].cap ? flow : e[i].cap);
if (!delta) continue; //delta=0说明不是增广路
e[i].cap -= delta; //正向边减去流量
e[i ^ 1].cap += delta; //反向边加流量
//关于i^1的解释:每次加边的时候都是正向边和反向边一起加,因此正向边和反向边的下标相邻
//e的有效下标从2开始,因此如果正向边是2*n,那么反向边就是2*n+1,也就是(2*n)^1的结果
//反之如果正向边是2*n+1,那么反向边就是2*n,而(2*n+1)^1=2*n
return delta;
}
return 0; //如果这个点的所有边都不是增广路 返回0
}
ll getmaxflow()
{
ll maxflow = 0, delta;
while (1)
{
mem(vis);
delta = dfs(S, inf);
if (!delta) break; //没有增广路了 退出
maxflow += delta;
}
return maxflow;
}
int main()
{
int n, m, u, v;
ll w;
cin >> n >> m >> S >> T;
for (int i = 0; i < m; i++)
{
scanf("%d%d%lld", &u, &v, &w);
add_edge(u, v, w);
add_edge(v, u, 0);
}
printf("%lld", getmaxflow());
return 0;
}
Dinic算法(对FF算法的改进)
Dinic算法作了三个改进:
1.多路增广,即一次BFS后可以找到多条增广路
2.将点进行分层(根据BFS的遍历顺序进行编号)
3.弧优化(可以应对一个点连很多边的情况)
//Dinic算法 可以通过全部测试点
#include <bits/stdc++.h>
#define maxm 20005
#define maxn 505
#define mem(a) memset(a, 0, sizeof(a))
#define inf 0x3f3f3f3f
typedef long long ll;
using namespace std;
struct edge
{
int to, next;
ll cap;
} e[maxm];
int head[maxn] = {0}, deep[maxn], cur[maxn], cnt = 1, n, m, S, T;
//链式前向星方式存边
void add_edge(int u, int v, ll cap)
{
e[++cnt] = edge{v, head[u], cap};
head[u] = cnt;
}
bool bfs()
{
queue<int> q;
q.push(S);
int u, v;
mem(deep);
deep[S] = 1;
while (q.size())
{
u = q.front();
q.pop();
for (int i = head[u]; i; i = e[i].next)
{
v = e[i].to;
if (deep[v] || e[i].cap == 0) continue; //已经访问过或者没有残余流量,也算弧优化的一种
q.push(v);
deep[v] = deep[u] + 1; //将点进行分层
}
}
//先将cur数组初始化
for (int i = 0; i <= n; i++)
cur[i] = head[i];
return deep[T] != 0; //如果T点无法到达说明没有增广路了
}
ll dfs(int u, ll flow)
{
if (u == T) return flow;
for (int i = cur[u]; i != 0; i = e[i].next)
{
cur[u] = i;
//弧优化,记录当前这个点所dfs到的点,可以避免重复访问前面的点
//如果是从head[u]开始的化要把与u连接的所有点都遍历到,大大增加了时间
//对于是否会漏点的问题,由于dfs在调用的过程中存在一个隐式的栈,
//这一层的dfs访问不到的点会在上一层中被访问
int v = e[i].to;
if (deep[u] + 1 != deep[v] || !e[i].cap) continue;
ll delta = dfs(v, flow < e[i].cap ? flow : e[i].cap);
if (!delta) continue; //delta=0说明不是增广路
e[i].cap -= delta; //正向边减去流量
e[i ^ 1].cap += delta; //反向边加流量
return delta;
}
return 0; //如果这个点的所有边都不是增广路 返回0
}
ll getmaxflow()
{
ll maxflow = 0, delta;
while (bfs())
while (delta = dfs(S, inf))
maxflow += delta;
return maxflow;
}
int main()
{
int u, v;
ll w;
cin >> n >> m >> S >> T;
for (int i = 0; i < m; i++)
{
scanf("%d%d%lld", &u, &v, &w);
add_edge(u, v, w);
add_edge(v, u, 0);
}
printf("%lld", getmaxflow());
return 0;
}