图论总结——最短路
算法
记号
为了方便叙述,这里先给出下文将会用到的一些记号的含义。
为图上点的数目, 为图上边的数目; 为最短路的源点; 为 点到 点的 实际 最短路长度; 为 点到 点的 估计 最短路长度。任何时候都有 。特别地,当最短路算法终止时,应有 。 为 这一条边的边权。
注:在使用此算法时应默认存在条件:图的边权非负
算法思想
贪心或
算法流程
把当前这个图分为两个集合,一个为已确定最短路的
正确性证明
证明的命题为:每个节点只会更新一次,即每次从
从正面直接攻破貌似很困难,考虑反证法。假设存在路径使得点
case1: 路径 上所有点都在 集合中
设最后路径
case2:路径 上部分点都在 集合中,且第一个在 集合中的点为
因为
故得证,每次从
推理
dijkstra第k次从T集合取出的点为到起点第k小(非严格)的节点
证明:
考虑算法流程,每次从未求出最短路径的点集中取出距离起点最近的点,然后以这个点为跳板刷新其他点才符合贪心(或
小优化
只要搜到终点后就停止搜索,正确性显然。
时间复杂度分析
朴素的
朴素
void dijkstra(int s)
{
memset(dis,0x3f,sizeof(dis));
dis[s]=0;
for(int i=1;i<=n;i++)
{
int u=0,res=0x3f3f3f3f;
for(int j=1;j<=n;j++)
if(!vis[j]&&dis[j]<res)res=dis[j],u=j;
vis[u]=1;
for(int j=head[u];j;j=nxt[j])
{
int v=to[j];
if(dis[v]>dis[u]+res)dis[v]=dis[u]+res;
}
}
}
我们发现维护最大值可以用优先队列来维护,所以采用
struct node{
int val,pos;
bool operator>(const node &x)const{
return val>x.val;
}
};
priority_queue<node,vector<node>,greater<node> >q;
void dijkstra(int s)
{
memset(dis,0x3f,sizeof(dis));
memset(vis,0,sizeof(vis));
dis[s]=0;
q.push(node{dis[s],s});
while(!q.empty())
{
node t=q.top();q.pop();
if(vis[t.pos])continue;
vis[t.pos]=1;
for(int i=head[t.pos];i;i=nxt[i])
{
int v=to[i];
if(dis[v]>dis[t.pos]+w[i])
{
dis[v]=dis[t.pos]+w[i];
q.push(node{dis[v],v});
}
}
}
return;
}
算法流程
对于边
这么做的含义是显然的:我们尝试用
Bellman–Ford 算法所做的,就是不断尝试对图上每一条边进行松弛。我们每进行一轮循环,就对图上所有的边都尝试进行一次松弛操作,当一次循环中没有成功的松弛操作时,算法停止。
每次循环是
在最短路存在的情况下,由于一次松弛操作会使最短路的边数至少
但还有一种情况,如果从
代码实现
struct edge {
int v, w;
};
vector<edge> e[maxn];
int dis[maxn];
const int inf = 0x3f3f3f3f;
bool bellmanford(int n, int s) {
memset(dis, 63, sizeof(dis));
dis[s] = 0;
bool flag;
for (int i = 1; i <= n; i++) {
flag = false;
for (int u = 1; u <= n; u++) {
if (dis[u] == inf) continue;
for (auto ed : e[u]) {
int v = ed.v, w = ed.w;
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
flag = true;
}
}
}
if (!flag) break;
}
return flag;
}
算法优化 ——
考虑只有每次更新操作更中被新的节点才会进入下一轮松弛,所以建一个队列每次从队首取出一个元素进行更新,再将更新成功的节点放入队尾。这个优化称为
松弛的思想很重要,以后做题时如果图不是 当然 )
代码实现
void spfa(int s)
{
memset(dis,0x3f,sizeof(dis));
memset(vis,0,sizeof(vis));
dis[s]=0;
queue<int> q;
q.push(s);
while(!q.empty())
{
int u=q.front();q.pop();
vis[u]=0;
for(int i=head[u];i;i=nxt[i])
{
int v=to[i];
if(dis[v]>dis[u]+w[i])
{
dis[v]=dis[u]+w[i];
if(vis[v]==0)
{
vis[v]=1;
q.push(v);
}
}
}
}
}
判负环
如果不存在负环,每个节点的最短路径(包括起点和终点)最多
也可以用另一种方式理解:
代码实现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=2005,MAXM=20005;
int n,m,cnt0;
int dis[MAXN],vis[MAXN],head[MAXN],cnt[MAXN];
struct Edge{
int nxt,to,dis;
}edge[MAXM];
void add(int u,int v,int w)
{
edge[++cnt0].nxt=head[u];
edge[cnt0].to=v;
edge[cnt0].dis=w;
head[u]=cnt0;
}
bool spfa()
{
queue<int> q;
for(int i=1;i<=n;i++)
{
dis[i]=0x3f3f3f3f;
vis[i]=0;
}
q.push(1);
dis[1]=0;
vis[1]=1;
cnt[1]++;
while(!q.empty())
{
int u=q.front();
q.pop();
vis[u]=0;
for(int i=head[u];i;i=edge[i].nxt)
{
int v=edge[i].to;
if(dis[v]>dis[u]+edge[i].dis)
{
dis[v]=dis[u]+edge[i].dis;
if(vis[v]==0)
{
q.push(v);
vis[v]=1;
cnt[v]++;
if(cnt[v]>n)return 1;
}
}
}
}
return 0;
}
void init()
{
memset(dis,0,sizeof(dis));
memset(vis,0,sizeof(vis));
memset(cnt,0,sizeof(cnt));
memset(edge,0,sizeof(edge));
memset(head,0,sizeof(head));
cnt0=0;
}
int main()
{
int T;
scanf("%d",&T);
init();
for(int a=1;a<=T;a++)
{
init();
scanf("%d %d",&n,&m);
for(int i=1;i<=m;i++)
{
int u,v,w;
scanf("%d %d %d",&u,&v,&w);
add(u,v,w);
if(w>=0)add(v,u,w);
}
if(spfa())printf("YES\n");
else printf("NO\n");
}
return 0;
}
算法思想
采用动态规划的思路。设
考虑如何进行状态更新。显然从
从
从
显然对于第一类路径,最短者的长度就是
然后只需要依次枚举
代码非常简洁,如下所示:
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
dis[i][j][k]=min(dis[i][j][k-1],dis[i][k][k-1]+dis[k][j][k-1]);
时间复杂度
显然
空间复杂度变为
代码实现:
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
闲话
好像不管
你就说它能不能求最短路吧
说白了算法思想还是 如果考场上没想到,也可以用万能的
它是一种由于边权相等的情况下使用,比如题目中常给的边权为
1).边权相等
本质上是类似与
算法流程
每次从队中取出队顶元素,再把更新后的节点放到队尾。这样就可以保证每次从队顶取出的节点一定是
注:在
代码实现:
void bfs(int s)
{
memset(dis,0x3f,sizeof(dis));
memset(vis,0,sizeof(vis));
queue<int> q;
dis[s]=0;
q.push(s);
while(!q.empty())
{
int u=q.front();q.pop();
for(int i=head[u];i;i=nxt[i])
{
int v=to[i];
if(dis[v]>dis[u]+w[i])
{
dis[v]=dis[u]+w[i];
q.push(v);
}
}
}
}
2).边权为 和
算法流程
不说了,直接上模板,大体上就是用个双端队列维护队列中
代码实现
void bfs(int s)
{
memset(dis,0x3f,sizeof(dis));
memset(vis,0,sizeof(vis));
deque<int>q;
dis[s]=0;
q.push(s);
while(!q.empty())
{
int u=q.front();q.pop();
for(int i=head[u];i;i=nxt[i])
{
int v=to[i];
if(dis[v]>dis[u]+w[i])
{
dis[v]=dis[u]+w[i];
if(w[i]==0)q.push_front(v);
else q.push_back(v);
}
}
}
}
时间复杂度
显然一个点只会进队一次,整张图会被遍历一次,故时间复杂度为
题型
常用技巧
- 求所有点到一个点的最短路,可以用反图的方式解决,脑补一下,所有点到一个点,相当于这个点反向建边倒着这走回去。
- 二进制分组
二进制分组处理多源多汇最短路例题:
做法
做法
考虑直接将
那我们看到我们要求
那么最直接的做法是随机讲这
做法
做法
在这之前先引入一个概念二进制分组,因为要使
但值得注意的是本题的边为有向边,所以在划分时有可能把终点的
判环
判负环
用
判无向图的最小环
考虑
于是,
- 有编号不超过
的节点构成。 - 经过节点
上式中相当于枚举了与
对于所有的整数
判有向图的最小环
用
最短路计数
前置知识
- 最短路的一个很好的性质:从
到 的最短路上的一个节点 ,都满足 到 的路径是关于 单源最短路的最短路
证明:
反证法,假设
故得证。
- 在
到 的最短路径上一定不存在环
证明:
考虑反证法,因为这个图的边权非负,所以环的权值为非负数,所以一定不会比不经过这个环更优。
故得证。
注: 上述结论是在图的边权非负时才成立的,如果图的边权为负数上述结论就不一定成立了。
例题[HAOI2012] 道路
直接队每一条边进行枚举肯定会
设
很显然,对于一条边
时间复杂度
分层图最短路
概述
分层图最短路,如:有
其中
概念理解:分层图最短路往往是与
例题
例
做法
简单来说,本题是在无向图上求出一条从
可以仿照前面所说的方法,用
显然,我们刚才设计的状态转移是有后效性的(因为本题是按照动态规划的思想解决的,动态规划对状态空间的遍历构成一张有向无环图。注意一定是有向无环图!遍历顺序就是该有向无环图的一个拓扑序。 有向无向图中的节点对应问题中的“状态”,图中的边对应状态之间的“转移”,转移的选取就是动态规划中的“决策”。 在本题中,比如有三个点
从最短路径问题的角度去理解,图中的节点也不仅限于“整数编号”,可以扩展到二维,用二元组
例
做法
由于购买机票不需要花钱,所以肯定不会多次重复乘坐同样的航线或者多次访问到同一个城市。如果
例
考试真题。
做法
这道题就是一个很显然的二维最短路(特殊的分层图最短路),设
很显然可以用
也可以为
还有种情况
答案为
补图最短路
是一种很套路的考法,一般做法的主题思想都是不变的。
补图的定义
补图是相对于完全图定义的, 对于一个图
通俗的理解即为有
所以补图的有一个性质为点还是原图的点,但边完全不一样。
补图最短路
前置条件:补图中的边权必须相等
补图最短路在补图求最短路,但如果原图的
一种常见的做法为创建两个
然后在原图跑最短路,遍历到一个点
习题 :Sparse Graph
习题 :神秘力量
算法:二维最短路+补图最短路
和上一题一样,这题做法为在正图上跑最短路,然后再在补图进行更新,很显然,如果上一条边为普通边,那就直接普普通通地进行更新即
为什么可以直接删去?,因为边权为
具体实现过程为:
设
-
如果是通过
出队的,那么对于 的相邻节点 ,如果连接的边为普通边有 ,而如果为特殊边则有 。 -
如果是通过
出队的,那么对于 的相邻节点 ,如果连接的边为普通边有 ,而如果为特殊边则有 ,然后在 里面删去 ,把它放入 中。最后遍历 中的元素,直接进行更新 ,再删去。
杂题
题 (最短路+拓扑排序)
解法
由题可知,一个城市
具体过程为设
所以先考虑
,其中 为与 相邻的节点。显然 不能更新 ,因为 表示只是到达了 点,但可能还没有进入 点,所以不能这样更新。
然后再来考虑
, 表示保护 的节点编号。如果 表明到达 点可能还没有进入的最早时间比攻破 点的时间早,那么显然此时 应该由 决定。如果 表明到达 点可能还没有进入的最早时间比攻破 点的时间晚,而因为此时保护 点的节点都被攻破了,所以由 来决定,即 。
转移方程想出来了,于是现在考虑用怎样的顺序能正确更新
题 ( 判环+简单 +单源最短路)
解法
建出反图,再在正图上算出以
考虑怎么转移
所以方程为
考虑到这个方程转移边界会很复杂,可以用记忆化搜索来实现。
初始化
题 (排序+多次单源最短路)
解法
对所有边进行排序,然后只用取前
然后对于这
题 红灯(图论建模)
解法
按照题意模拟即可,注意要把边看成点进行建模。具体地:看每个点连出去的四条边,设当前点的编号为
总结
只要是是图论,建模一定排在第一位,其次是算法特征与思想,最后才是套路,三者一结合起来那么你在图论这个版块就一定不会很差。
有时候图论题还需要考察你对模型的抽象化,有可能第一眼看上去和图论一点关系没有,结果最后是图论建模后就解决了,这类题型只能靠见多识广,再无其他门路。
一些图论套路题也要多见识,才能在考场上遇见这些题目不慌张。还有平时打图论模板也要一个字一个字的打,切忌不要直接复制,因为我们是
最后就是不要小看一些看上去复杂度很高很没有优势的算法,它们在特定的题型中还是会发挥它们自己的作用的。比如
当然有时候一个题目需要运用到多种算法,这就很考验一个选手的综合能力与基本功了,所以我们需要及时复习以前学过的算法,以便临近考试时不慌张。
总之一句话,多见识题型,多训练思维,及时回顾算法思想与解题套路,这样就能在
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具