文章目录
1.基本概念
2.常用算法
图论隶属于离散数学,因此在不管是刷题还是系统学习都会经常遇到离散数学中的术语和概念,因此系统学习对基础很有帮助。
Graph, vertex(-es,顶点), edge
存储图的方式一般耳熟能详的有三种:
1.邻接矩阵(NXN,N个定点)
2.邻接表
3.链式向前星
矩阵存图一般在小数据适用,写起来十分简单。
G[ u ][ v ] = w ; 问题抽象为点 u -> v 这条边的权值是 w。
有向图(Directed Graph)和无向图(Undirected Graph)映射到矩阵上区别是无向图一定是是对称矩阵。
线性代数中有提到矩阵G自己累乘有对应的现实意义。e.g.一个记录飞机场能不能互通的矩阵,
存储这飞机场经过一个中转站的连通情况。
邻接表是有一列head,head后面一个挨着一个挂着。
vector<int>head[MAXN];
最多被使用的是链式前向星,
链式:模拟链表,头节点后面一条链,链子上是和头节点简连通的边。
向前:因为是模拟链表,所以我们数组存储的是链子头的位置,所以将边插入在链子前面最高效。
星:我觉的挺好听的,真的。
下面是模板
struct edge
{
int v, w;//u在head里
int next;
}E[MAXN];
int head[MAXN], tot;;
inline void add(int u, int v, int w)
{
E[++tot].v = v;//新边total+1
E[tot].w = w;
E[tot].next = head[u];//模拟插入链表
head[u] = tot;//head[u]指向E[tot+1]
}
前向星很常用很重要。
学会存图是最基本的要求,而如何熟练去操作它们,如何把问题进行正确的抽象才是至关重要的。
一般比赛常用的
DFS(Depth First Search)
深度优先
一般是先写一个或者两个direct数组(方向移动对吧),然后模拟无脑递归(回溯)
我一开始觉得DFS比较简单好上手,但是只能处理数据较少的。
但是只要记忆化数组(比如状态数组)用的好,剪枝就很高效(一棵生成树都快剪秃了,遍历几个树枝非常快)。
我只能说DFS几乎都能用,上下限都很高。
BFS(Breadth First Search)
BFS经典应用:
一个迷宫(maze),里面有障碍物,问从左上角到右下角(一个点到另一个点)最小花费是多少。
使用Queue,因为广度优先是类似对一棵树的层次遍历,一层一层的遍历。
因此可以确保结果的正确性。类似于向迷宫灌水,生物学中的霉菌生长。
利用visited数组等额外维护来解决实际问题。
最短路问题:
1.Floyd
2.dijkstra(不能有负边)
3.Bellman-ford
1.Floyd比较简单,容易理解,复杂度,三个循环,
可以求解任意两个点之间的最短路径。
void Floyd()
{
for (int k = 1; k <= n; k++)
{
for (int i = 1; i <=n ; i++)
{
for (int j = 1; j <=n ; j++)
{
G[i][j] = min(G[i][j], G[i][k] + G[k][j]);
}
}
}
}
Floyd-Warshall算法可以用于解决包含负权边的图的最短路径问题,但不包括负权环。Floyd-Warshall算法是一种动态规划算法,它可以计算图中任意两个顶点之间的最短路径长度。
2.dijkstra
因为最短路的子路一定也是最短路,所以dijkstra本质是贪婪(心)。
简单证明一下,如果子路不是最短,一定有更短的子路,后缀路径不变,所以很显然。
dijkstra很常用,实现方法也很多,有利于理解的朴素实现;
int g[MAXN][MAXN];
int dis[MAXN];各点距离当前搜索路径的距离
bool vis[MAXN];是不是已经最短
int n, m, s;
void dijkstra() {
for (int i = 1; i <= n; ++i) {
dis[i] = g[s][i];
vis[i] = false;
}
//随便找一个s作为第一个开始点
vis[s] = true;
dis[s] = 0;
//自己到自己为0
for (int i = 1; i < n; ++i) {
int min_dis = INF, u;
for (int j = 1; j <= n; ++j) {
if (!vis[j] && dis[j] < min_dis) {
min_dis = dis[j];
u = j;
}
}
//要从目前最近的开始
vis[u] = true;
for (int v = 1; v <= n; ++v) {
if (!vis[v] && dis[u] + g[u][v] < dis[v]) {
dis[v] = dis[u] + g[u][v];//relax操作
}
}
更新dis
}
}
如果存在负权边,Dijkstra算法可能会产生错误的最短路径或者陷入无限循环。这是因为在算法过程中,它可能会选择一个已经被访问的顶点。
而Bellman-Ford算法可以在负权环下处理最短路问题,并且检查是否存在;
代码并不重要家人们,重要的是理解,这几个最短路,最小生成树都一个味。
不得不提优化:
图的存储改用前向星,节省空间。
dis采用最大/小堆(priority_queue),节省搜索时间。
3.Bellman-ford
遍历所有可能的松弛操作。
检查是不是依然存在可行松弛操作。如果存在就是负权环。
(u,v)第k次松弛是u->v经过最多k条边的带来的最短路径,所有最多n-1次就保证了正确性。
Bellman-Ford算法的基本思想之一就是通过遍历所有可能的松弛操作来更新顶点的最短路径估计值,并检查是否存在可行的松弛操作。如果在算法的最后一轮迭代中仍然存在可以进行松弛操作的情况,那么就说明图中存在负权环。
Bellman-Ford算法不仅能够计算出单源最短路径,还能够检测图中是否存在负权环,这使得它成为解决包含负权边的图的最短路径问题的一种有用的算法。
最小生成树:
1.kruskal
2.prim
最小生成树:覆盖所有节点的权值最小的图(一定是n个点,n-1个边,不能有环)
连通;任意两个点至少有一条路
连通分量:无向图极大连通子图
强连通分量:有向图中的连通分量
1.kruskal好理解一些:
需要用到并查集(因为每次插入一条当前最小权值边都要用并查集检查有没有环);
并查集不难理解,pre数组记载共同前辈,join函数,find函数;
将所有m条边都存放并排序(堆),然后一个一个的插入图中,直到n-1条,或者不存在。
void find(int x)
{
if (x == pre[x])return;
return pre[x] = find(pre[x]);
}
void kruskal()
{
sort(E + 1, E + n + 1);
for (int i = 0; i < m; i++)
{
int preu = find(E[i].u),
int prev = find(E[i].v);
if (preu == prev)continue;//一样的pre代表成环
cot++;
join(prev, preu);
res += E[i].w;
if (cot == n - 1)break;
}
}
适用稀疏图(边的个数m少一些);
2.Prim
随便找一点开始,然后开始一步一步生成树,记录下没在树中的点到树的最小距离。在树中的点肯定是最短了,标记下来。每一次选择最近的点插入,并且更新dist和visited。
和dijkstra有一些类似,但是目标不一样。
Dijkstra算法主要用于解决单源最短路径问题,即从一个顶点出发,找到到图中所有其他顶点的最短路径。
Prim算法则主要用于解决最小生成树问题,即在一个加权连通图中找到一个最小的生成树。
感谢观看,恭侯交流与指点。