前言
这期是蓝桥杯常考点的最后一章了,其中的dijkstra算法更是蓝桥杯中的高频考点
图的基本相关概念
有向图和无向图 自环和重边 稠密图和稀疏图
对于不带权的图,一条路径的路径长度是指该路径上各边权值的总和
对于带权的图,一条路径长度时指该路径上各个边权值得总和
顶点的度是指和它相关联的边的条数,由该顶点发出的边称为顶点的出度,到达该顶点的边称为顶点的入度
无向图中才有联通图和联通分量这些概念
考的时候:有些数据输入格式就是按图给的
(eg:u和v之间有一条长度为k的边)
如果没有说图没有重边和自环,一般默认测试点的图会有重边和自环
有向无环图可以搭配上动态规划用(eg:拓扑+动态规划)(在问最短路有几条时,常要用到动态规划)
图的存储
有两种方式:邻接矩阵和邻接表
1.邻接表的存储方式和树的孩子表示法一样,用vector数组和链式前向星都可以实现
2.邻接矩阵是用一个二维数组,其中edge[i][j]存储顶点i与顶点j之间的边的信息
c++
邻接矩阵:
1.对于带权图而言,若顶点i和顶点j之间有边相连,则邻接矩阵中对应项存放着该边对应的权值
若顶点i和顶点j之间不相连,则用无穷大(有时用其他)代表这两顶点间无边
2.对于不带权的图,可以创建一个二维的bool类型的数组,来标记顶点i和j之间有边相连
时间复杂度为:O(n平方),因此适合存稠密图
注意:邻接矩阵如果有重边,一般存其最小值
代码展示:
int edges[N][N];//一般需要初始化,vector那个则不用
for(int i = 1;i<=m;i++)
{
int a,b,c;cin>>a>>b>>c;
//a和b之间有一条边,权值为c
edges[a][b] = c;
//如果是无向边,需要反过来再存一下
edges[b][a] = c;
}
c++
邻接表:
自己一般喜欢使用vector数组去存
如果存在边权的话,vector数组里面需要放结构体或者pair
pair自己喜欢第一个数据记录边的终点,第二个数据记录边权值
vector数组的下标表示边的起点
代码展示:
vector<pair<int,int>> edges[N];
for(int i=1;i<=m;i++)
{
int a,b,c;cin>>a>>b>>c;
//a和b之间有一条边,权值为c
edges[a].push_back{(b,c)};
//如果是无向边,需要反过来再存一下
edges[b].push_back{(a,c)};
}
图的遍历
图的遍历有DFS和BFS,和树的遍历的实现方法一样
c++
自己常用dfs来搞:
1.邻接矩阵:
void dfs(int u)
{
cout<<u<<endl;
st[u] = true;
for(int v =1;v<=n;v++)
{
//如果存在u->v的边,并且没有遍历过
if(edges[u][v]!=-1&&st[v])
dfs(v);
}
}
main函数里面有memset(edges,-1,sizeof edges);
2.vector数组:
void dfs(int u)
{
cout<<u<<endl;
st[u] = true;
for(auto&t:edges[u])
{
//u->v的一条边,权值为w
int v = t.first,w=t.second;
if(!st[v])
{
dfs[v];
}
}
}
最小生成树
一般用普利姆(Prim)算法和克鲁斯卡尔(Kruskal)算法去构造最小生成树
最小生成树面向的是无向图,如果有向图有无向图返回的性质,也可以用此
c++
Prim算法:
核心:不断加点
步骤:
1.从任意一点开始构造最小生成树(一般选1为起点,dist[1] = 0)
2.将距离该树权值最小且不在树中的顶点,加入到生成树中。(记得判断是否联通)
然后更新与该点相连的点到生成树的最短距离(不要忘了考虑重边和自环!)
3.重复2操作n次,直到所有顶点都加入为此
Kruskal算法:(运行时间和空间时间允许的话,建议用这个)
核心:不断加边
步骤:
1.所有边按照权值排序
2.每次选出权值最小且两端顶点不连通的一条边,直到所有顶点都联通
时间复杂度:m*logm(m是边数)
这个算法不用图,只用存边,用结构体存即可(也不算邻接表)
c++
定理:最小生成树就是瓶颈生成树
瓶颈生成树:所有生成树中,最大的边权的值最小的那棵树
拓扑排序
拓扑排序的目标是将有向无环图中的所有结点排序
适用于有要完成了前置项才能走的点的图(AOV网)
(eg:一个摄像头能被砸毁的条件是该摄像头所在位置不被其他摄像头监视)
实现方法:
1.将图中所有入度为0的点,加入到队列中
2.取出队头元素,删除与该点相连的边。如果删除之后的后继结点入度变为0,加入到队列中
3.重复2操作,直到图中没有点或者没有入度为0的点为止
需要搞个vectoredges[N]存N的后继
int in[N]存入度信息
eg: edges[i].push_back(j);//i的后继为j
in[j]++;//统计入度信息
例题: 洛谷 B3644 【模板】拓扑排序/家谱树
c++
拓扑排序判断是否有环:
跑一遍拓扑排序,如果有结点没有进队,那么表明有环
单源最短路
概念:图中一个顶点到其他各顶点的最短路径
有向图,无向图都能用
c++
常见版dijkstra算法:
流程:
1.创建一个长度为n的dist数组,其中dist[i]表示从起点到i结点的最短路
2.创建一个长度为n的bool数组st,其中st[i]表示i点是否已经确定了最短路
3.初始化:dist[i] = 0,其余结点的dist值为无穷大,表示还没有找到最短路
4.在所有没有确定最短路的点中,找出最短路长度最小的点u。打上确定最短路的标记,
然后对u的出边进行松弛操作
松弛操作:
if(dist[u]+w<dist[v])
dist[v] = dist[u]+w;
堆优化版的dijkstra算法:
1.创建一个长度为n的dist数组,其中dist[i]表示从起点到i结点的最短路
创建一个长度为n的bool数组st,其中st[i]表示i点是否已经确定了最短路
创建一个小根堆,维护更新后的结点.(也就是需要确定最短路的结点)
eg:priority_queue<PII,vector<PII>,greater<PII>>heap
2.初始化:dist[i]=0,然后讲{0,s}加到堆里,其余结点的dist值为无穷大,表示还找到最短路
3.弹出堆顶元素,如果该元素已经标记过,就跳过;如果没有,打上标记,进行松弛操作
bellman-ford算法(简称BF算法):
核心思想:不断尝试对图上的每一条边进行松弛,直到所有的点都无法松弛为止
1.创建一个长度为n的dist数组,其中dist[i]表示从起点到i结点的最短路
2.初始化:dist[i]=0,其余结点的dist值为无穷大,表示还没有找到最短路
3.每次都对所有的边进行一次松弛操作(一般按结点编号顺序来找边去松弛)
4.重复上述操作,直到所有边都不需要松弛为止
这个算法也不需要存图
spfa算法:(本质是用队列对BF算法做优化)
1.创建一个长度为n的dist数组,其中dist[i]表示从起点到i结点的最短路
创建一个长度为n的bool数组st,其中st[i]表示i点是否已经在队列中
2.初始化:标记dist[i]=0,同时1入队;其余结点的dist值无穷大,表示还没找到最短路
3.每次拿出队头元素u,去掉在队列中的标记,同时对u所有相连的点v进行松弛操作
如果结点v被松弛,那就放进队列中
4.重复上述操作,直到队列中没有结点为止
c++
例题:洛谷 P3385 【模板】负环
1.BF判断负环:
执行n轮松弛操作,如果第n轮还存在松弛操作,那么就有负环
2.spfa算法判断负环:
维护一个cnt数组记录从起点到该点所经过的边数,如果cnt[i]>=m,说明有负环
c++
单源路算法总结:(n为结点个数,m为边数)
1.dijkstra算法:时间复杂度:O(n平方)
2.堆优化的dijkstra算法: 时间复杂度:O(m*logm,m为边数)
没有负边权的话用这俩
有负边权就用BF算法和spfa算法
3.BF算法:时间复杂度:nm
4.spfa算法:时间复杂度:km~nm(k要具体题去分析)
5.普通BFS:处理边权全部相同并且非负的单源最短路
6.01BFS:处理边权要么为0,要么为1的单元最短路
c++
问最短路有几条的问题:
例题: 洛谷 P1144 最短路计数
1.这里的松弛操作和上面的有些不同:(要分情况了)
dist[u]+w<dist[v]的话:
f[v] = f[u]
dist[u]+w=dist[v]的话
f[v]+=f[u]
2.而且这里的BFS不能用st数组了,其他算法可以
一般做法使用dijkstra算法或者BFS(边权相等才可BFS这个方法)+动态规划
多源最短路
和搜索那里的多源最短路区分(那里是多个起点)
这里是分阶段求最短路(加点求,加点求)--用floyd算法解决
floyd算法适用于任何图,但是不能含负环
c++
floyd算法:其本质是动态规划。其实就是分阶段,逐步更新出我们的最终结果
思路:
1.状态表示:
f[k][i][j]表示:
仅仅经过[1,k]这些点,结点i走到结点j的最短路径的长度
2.状态转移方程:
第一种情况,不选新来的点:f[k][i][j]=f[k-1][i][j]
第二种情况,选择新来的点:f[k][i][j]=f[k-1][i][j]+f[k-1][i][j]
取这两种的min
3.空间优化:可以优化掉第一维
4.初始化:
f[i][i]=0;
f[i][j]为初始状态下i到j的距离,如果没有边则为无穷
5.填表顺序:
一定要先枚举j,再枚举i和j
例题: 洛谷 B3647 【模板】Floyd
如果题目有限制加点的时机,那就把floyd算法里面的k那一层循环拆了改成判断条件即可
例题: 洛谷 P1119 灾后重建
c++
无向图的最小环问题:
例题:洛谷 P6175 ⽆向图的最⼩环问题
floyd算法循环到k的时候,这个环的最小长度为f[i][j]+e[i][k]+e[k][j]
核心部分:
for(int k =1;k<=n;k++)
{
//最小环
for(int i=1;i<k;i++)
for(int j=i=1,j<k,j++)
ret = min(ret,f[i][j]+e[i][k]+e[k][j]);
//最短距离
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
f[i][j]=min(f[i][j],f[i][k]+f[k][j]);
}
例题的跳转链接汇总
洛谷 B3644 【模板】拓扑排序/家谱树
洛谷 P3385 【模板】负环
洛谷 P1144 最短路计数
洛谷 B3647 【模板】Floyd
洛谷 P1119 灾后重建
洛谷 P6175 ⽆向图的最⼩环问题