是AcWing算法基础课关于基本图论算法的笔记。图片和引用来自给出原链接的“参考”。
AcWing 永远滴神!
Dijkstra的使用条件是:边权非负即可
朴素版Dijkstra-AcWing 849. Dijkstra求最短路 I(稠密图)
步骤:
- 初始化,dis[1]=0,dis[n]=0x3f3f3f3f.——起点到起点的距离为0,到其他点的距离为正无穷(这里1是起点)
- 迭代:n次迭代确定每个点到起点的最小值
- 输出答案
其中:
dist[n]表示起点到点n的距离
st[n]表示点n到起点的最短距离是否已经确定
每次找还没有确定最短距离的点中的最短的那个进行更新:
可以看这里的演示
如:
(举例子的图都来自上面的链接!)
有一个图,我们想知道家到学校的距离:
1.进行初始化:不与家相连的都为无穷大。(也可以全都初始化为无穷大,一样的)
2.此时所有的点都是未确定最短路的点。 我们选择当前从起点出发最短路径的点。
是 家->V2.
此时确定的集合:括号内是路径长度
V2(3)
不确定的集合:
V1(4),V3(8),V6(15),V4(INF),V5(INF),学校(INF)
3.然后是家->V1
确定的集合:
V2(3),V1(4)
不确定的集合:
V3(7),V6(15),V4(12),V5(INF),学校(INF)
4.然后是家->V3:
确定的:
V2(3),V1(4),V3(7)
不确定的:
V6(15),V4(13),V5(22),学校(INF)
…以此类推。大概就是这个意思。
其中,如果确定了某个点已经有到起点x的最短路了,我们就让st[x]=1;
然后用这个点去更新其他所有点的距离。
后更新的不会影响先更新的点,因为后更新的点就算与先更新的点有路,也会比它大。
注意:若有重边,要保留最短的边。
模板题:
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=500+10;
int n,m;
int g[N][N];//存图
int st[N];//点N是否确定了最短距离
int dist[N];//从起点到点N的最短距离
void dij()
{
memset(dist,0x3f,sizeof(dist));
dist[1]=0;
//st[1]=1;
//这里万万不能st[1]=1;
//因为如果=1了,在后面的更新中就不会从1这个起点开始;
//而只有1这个起点的dist[1]=0是最小的,其他都是正无穷
for(int i=1;i<=n;i++)
{
//找到当前未确定最短距离的点——找当前距离最小的
int t=-1;
for(int j=1;j<=n;j++)
{
//t==-1表示还没找到t 第二个条件是找最短的t
if(st[j]==0&&(t==-1||dist[t]>dist[j]))
{
t=j;
}
}
st[t]=1;
for(int j=1;j<=n;j++)
{
dist[j]=min(dist[j],dist[t]+g[t][j]);
//dist[t]+g[t][j]就是用点t去更新所有相连的点
}
}
}
int main()
{
cin>>n>>m;
memset(g,0x3f,sizeof(g));//先初始化为最大
while(m--)
{
int x,y,z;
cin>>x>>y>>z;
g[x][y]=min(g[x][y],z);//如果重边,留最短的
}
dij();
if(dist[n]==0x3f3f3f3f) cout<<-1;
else cout<<dist[n];
return 0;
}
堆优化版Dijkstra-AcWing 850. Dijkstra求最短路 II(稀疏图)
引用和参考来源:参考1和参考2
关于如何判断稠密/稀疏图:m是n^2级别的是稠密图,是n级别的是稀疏图。
思路:
- 一号点的距离dist[1]=0,其他初始化为0x3f3f3f3f
- 将一号点放入堆中
- 不断循环,直到堆空。每一次循环的操作为:
弹出堆顶(与朴素版找到S外距离最短的点相同,并标记该点的最短路径已经确定),用该点更新临界点的距离,更新成功就加入堆中。
关于时间复杂度:
时间复杂度 O(mlogn)
每次找到最小距离的点沿着边更新其他的点,若dist[j] > distance + w[i],表示可以更新dist[j],更新后再把j点和对应的距离放入小根堆中。由于点的个数是n,边的个数是m,在极限情况下(稠密图m=n(n−1)2)最多可以更新m回,每一回最多可以更新n个点(严格上是n - 1个点),有m回,因此最多可以把n2个点放入到小根堆中,因此每一次更新小根堆排序的情况是O(log(n2)),一共最多m次更新,因此总的时间复杂度上限是O(mlog((n2)))=O(2mlogn)=O(mlogn)
堆优化版的Dijkstra用邻接表来存,就无所谓重边,因为算法会保证我们得到最短的边。
关于用数组模拟邻接表的引用和参考:数组模拟邻接表
h数组的下标为结点的编号,e,w,nxt数组的下标为边的编号,eidx为边的编号
h[u]表示u这个结点的第一条边的编号
e[eidx]表示边的终点,w[eidx]表示边的权重,nxt[eidx]表示下一条边。
所以模板是:
const int N = 1010, M = 1010;
int h[N], e[M], w[M], nxt[M], eidx;
void add(int u, int v, int weight) // 添加有向边 u->v, 权重为weight
{
e[eidx] = v; // 记录边的终点
w[eidx] = weight; // 记录边的权重
nxt[eidx] = h[u]; // 将下一条边指向结点u此时的第一条边
h[u] = eidx; // 将结点u的第一条边的编号改为此时的eidx
eidx++; // 递增边的编号edix, 为将来使用
}
邻接表添加边之前要全初始化为-1(邻接表的性质)。
题:
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=150000+10,M=150000+10;
int h[N],w[M],e[M],ne[M],idx;
//点对应的边,边的权重,边结束的点,边的下一条边,一个编号
void add(int a,int b,int c)//往邻接表里加点
{
w[idx]=c;//存权重
e[idx]=b;//存边的结尾
ne[idx]=h[a];//存下一条边:头插法,当前的下一条边其实是之前的该点的第一条边
h[a]=idx++;//idx要更新
}
typedef pair<int,int>PII;
int dist[N],st[N];
int n,m;
void dijkstra()
{
memset(dist,0x3f,sizeof(dist));
dist[1]=0;
priority_queue<PII,vector<PII>,greater<PII>>heap;//小根堆
//heap的数据类型是PII,first用来存权重,second用来存点
//pair的排序排的是first,所以必须要权重放在前面
heap.push({0,1});
while(heap.size())
{
PII temp=heap.top();
heap.pop();
int distance=temp.first,id=temp.second;
if(st[id]) continue;//该点已经找到了最短路
st[id]=1;
for(int i=h[id];i!=-1;i=ne[i])//遍历该点的所有边
{
int j=e[i];
if(dist[j]>distance+w[i])
{
dist[j]=distance+w[i];
heap.push({dist[j],j});
}
}
}
}
int main()
{
memset(h,-1,sizeof(h));
cin>>n>>m;
while(m--)
{
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);//存图
}
dijkstra();
if(dist[n]==0x3f3f3f3f) cout<<-1;
else cout<<dist[n];
return 0;
}
bellman-ford-AcWing 853. 有边数限制的最短路
擅长解决有边数限制的最短路问题
参考和引用:
参考1和参考2
为什么dijkstra不能解决负边权的最短路问题?
如图所示:
最左边的点到其右下的点,它只会记录1的距离,而不会记录其到右上方100的距离,就自然不会记录-200的距离。
因此无法解决负边权的最短路问题。
什么是bellman-ford算法:
Bellman - ford 算法是求 含负权图的单源最短路径 的一种算法,效率较低,代码难度较小。其原理为连续进行松弛, 在每次松弛时把每条边都更新一下,若在 n-1 次松弛后还能更新,则说明图中有负环,因此无法得出结果,否则就完成。
(通俗的来讲就是:假设 1 号点到 n 号点是可达的,每一个点同时向指向的方向出发,更新相邻的点的最短距离,通过循环 n-1 次操作,若图中不存在负环,则 1 号点一定会到达 n 号点,若图中存在负环,则在 n-1 次松弛后一定还会更新)
一些名词:
松弛操作:dist[b]=min(dist[b],dist[a]+w)
三角不等式:dist[b]<=dist[a]+w;
若是图中存在负权边,如图:那么从1点到2点的距离会变成负无穷——12342这样一直转。
也就是说:如果能通过bellman-ford求出最短路,则路径中是没有负权回路的(负环不在最短路径中就不影响,可以在其他无所谓的地方)。
(一个对比,spfa是一定要求图中不含负环
)
因此,迭代的次数会有实际意义:假如迭代了k次,那么意义就是从1号点,经过不超过k条边,走到每个点的最短距离。
也就是说,如果在第n次迭代的时候又更新了
,那么存在一条最短路径,它上面有n条边,即意味着有n+1
个点,则这条路径上一定存在环,且是负环
(因为更新过了,不是负的怎么会往回走呢)。
所以bellman-ford算法可以用来找负环(但时间复杂度较高),不过一般都用spfa来找。
时间复杂度:O(nm)
伪代码:
for n次 //n
for 所有边 a,b,w (松弛操作) //m
dist[b] = min(dist[b],back[a] + w)
back数组是对dist数组的备份。如果不加back数组,可能会发生串联
。
如图:
开始的时候:
1 2 3
dist 0 INF INF
迭代一次:3号点是根据2号点得来的,所以得到了2,但实际它应该是3(1->3为3)。这里的2相当于迭代了2次(2条边)
1 2 3
dist 0 1 2
这里的错误就是串联。
所以需要一个back数组来存放迭代前的dist数组。
关于边数限制的含义:如上图,如果边数限制是1,则1-3的最短路是3.
关于判断是否能达到终点n:是否能到达n号点的判断中需要进行if(dist[n] > INF/2)判断,而并非是if(dist[n] == INF)判断,原因是INF是一个确定的值,并非真正的无穷大,会随着其他数值而受到影响,dist[n]大于某个与INF相同数量级的数即可
如图:dist[x]不等于0x3f3f3f3f,但是还是到不了。
所以我们的判断条件就变为:if(dist[n] > INF/2) cout<<-1;
题:
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=500+10,M=10000+10;
struct node
{
int a,b,c;
}nodes[M];//M条边
int dist[N];
int back[N];
int n,m,k;
void bellman_ford()
{
memset(dist,0x3f,sizeof(dist));
dist[1]=0;
for(int i=0;i<k;i++)//k次循环
{
memcpy(back,dist,sizeof(dist));
//把dist的全部复制给back
for(int j=0;j<m;j++)
{
int a=nodes[j].a,b=nodes[j].b,c=nodes[j].c;
dist[b]=min(dist[b],back[a]+c);
//这里是back[a]+c,即这一次迭代前的dist[a],否则会串联
}
}
}
int main()
{
cin>>n>>m>>k;
for(int i=0;i<m;i++)
{
int a,b,c;
cin>>a>>b>>c;
nodes[i]={a,b,c};
}
bellman_ford();
if(dist[n]>0x3f3f3f3f/2) cout<<"impossible";
else cout<<dist[n];
return 0;
}
spfa
AcWing 851. spfa求最短路
spfa算法是对bellman-ford算法的优化。
bellman-ford会更新所有的边,实际上没有必要。我们只需要遍历那些到起点的距离变小的点所相连的边
即可。
因此,做法:创建一个队列,每次放入距离被更新的结点。
注意:
st数组的作用
标记是否已经在队列中。如果已经在队列中,就无需再push进队列,只需更新值。
spfa与dijkstra的不同
- dijkstra中的st[x]=1表示,点x的最短路已经确定,因此x点的dist值不会改变;而spfa的st[x]=1表示,x点在队列里,但dist[x]还是可能更新的。
- dijkstra中使用优先队列保存当前未确定的点中的最短路,而spfa用的队列,是保存当前发生过更新的点。
spfa与bellman-ford的不同:判断没有最短路条件
bellman-ford中表示没有最短路(也就是不连通)的条件是if(dist[n]>0x3f3f3f3f/2)
,原因是所有的路径bellman-ford都会遍历,所以尽管不连通,也会参与计算,0x3f3f3f3f可能会改变,判断条件就不能是if(dist[n]==0x3f3f3f3f)
.
而spfa中表示没有最短路的条件仍是if(dist[n]==0x3f3f3f3f)
,原因是,若不连通,则不会发生更新,0x3f3f3f3f不会改变。
spfa与bellman-ford的不同:关于负环
bellman-ford可以存在负权回路,因为它会限制路径长度,不会产生死循环。
spfa不行,它用队列存储,只要更新就不断入队,会死循环。
时间复杂度
spfa由bellman-ford优化而来,最坏情况下的时间复杂度跟bellman-ford一样,O(nm)。
求负环一般使用spfa
能用dijkstra尽量别用spfa,因为spfa的时间复杂度带有常数,比dijkstra更容易TLE
题:
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;
const int N=100010,M=100010;
int h[N],e[M],w[M],ne[M],idx;
void add(int a,int b,int c)
{
e[idx]=b;
w[idx]=c;
ne[idx]=h[a];//联想一下头插法就好理解
h[a]=idx++;
}
int n,m;
int dist[N],st[N];
void spfa()
{
memset(dist,0x3f,sizeof(dist));
dist[1]=0;
queue<PII>q;
q.push({0,1});//first是w,second是结点编号
st[1]=1;
while(q.size())
{
PII temp=q.front();
q.pop();
int id=temp.second;
st[id]=0;
//这里是更新所有与队头元素相连的边
for(int i=h[id];i!=-1;i=ne[i])
{
int j=e[i];
//是w[i],不是temp.first 后者是固定不变的
if(dist[j]>dist[id]+w[i])
{
dist[j]=dist[id]+w[i];
if(!st[j])
{
st[j]=1;
q.push({dist[j],j});
}
}
}
}
}
int main()
{
scanf("%d%d",&n,&m);
memset(h,-1,sizeof(h));//邻接表要初始化为-1
while(m--)
{
int a,b,c;scanf("%d%d%d",&a,&b,&c);
add(a,b,c);
}
spfa();
if(dist[n]==0x3f3f3f3f) cout<<"impossible";
else cout<<dist[n];
return 0;
}
AcWing 852. spfa判断负环
参考和引用:这里
用spfa算法解决是否存在负环有两种方法:
- 统计每个点的入队次数,如果某个点入队次数为n次,则说明存在环
- 统计当前每个点的最短路中所包含的边数,如果某点的最短路所包含的边数大于等于n,则说明存在环。
一般用方法2,因为方法1可能会超时。
spfa求负环与spfa求最短路的区别:
不需要初始化为0x3f
因为不是求距离。而且,如果存在负环,不管初始化为多少,都会被更新(至无穷小)。
要把所有的点都放进去
求是否存在负环,但负环可能没法从1号点得到。所以开始的时候要把所有点放进去。就当作:
在原图的基础上建立一个虚拟源点,从该点向其他所有点连一条权值为0的有向边。
dist[x]记录虚拟源点到x点的距离
cnt[x]记录当前x点到虚拟源点的最短路边数。若出现cnt[x]>=n,说明存在负环。(因为只有n个点,而1-n的点的个数应该最多只有n-1个才是,多了说明有环,而因为求的是最短路,如果环是正的,就不会往回拐,所以一定是负环)
时间复杂度 一般:O(m) 最坏:O(nm)
题:
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;
const int N=2010,M=10010;
int h[N],e[M],w[M],ne[M],idx;
void add(int a,int b,int c)
{
e[idx]=b;
w[idx]=c;
ne[idx]=h[a];//联想一下头插法就好理解
h[a]=idx++;
}
int n,m;
int dist[N],st[N],cnt[N];
int flag=0;//判断是否有负环
void spfa()
{
queue<int>q;
for(int i=1;i<=n;i++)
{
q.push(i);
st[i]=1;
}
while(q.size())
{
int id=q.front();
q.pop();
st[id]=0;
//这里是更新所有与队头元素相连的边
for(int i=h[id];i!=-1;i=ne[i])
{
int j=e[i];
//是w[i],不是temp.first 后者是固定不变的
if(dist[j]>dist[id]+w[i])
{
dist[j]=dist[id]+w[i];
cnt[j]=cnt[id]+1;
if(cnt[j]>=n)
{
flag=1;
return;
}
if(!st[j])
{
st[j]=1;
q.push(j);
}
}
}
}
}
int main()
{
scanf("%d%d",&n,&m);
memset(h,-1,sizeof(h));//邻接表要初始化为-1
while(m--)
{
int a,b,c;scanf("%d%d%d",&a,&b,&c);
add(a,b,c);
}
spfa();
if(flag==1) puts("Yes");
else puts("No");
return 0;
}
Floyd-AcWing 854. Floyd求最短路
要点全在这里:
这个算法要注意的点:
状态转移
d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
i到j的距离为min(自己,i到k的距离+k到j的距离)。
判断是否存在最短路的条件
if(d[x][y]>0x3f3f3f3f/2)
因为Floyd算的是所有点的距离,会把没法连到起点的点也算进去。所以要这样判断。
注意全初始化为无穷大和d[x][x]=0
题:
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=200+10;
int d[N][N];//d[i][j]表示从i到j的最短距离
int n,m,k;
void Floyd()
{
for(int k=1;k<=n;k++)//k相当于跳板
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
{
d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
}
}
int main()
{
cin>>n>>m>>k;
memset(d,0x3f,sizeof(d));
for(int i=1;i<=n;i++) d[i][i]=0;
while(m--)
{
int a,b,c;cin>>a>>b>>c;
d[a][b]=min(d[a][b],c);
}
Floyd();
while(k--)
{
int x,y;cin>>x>>y;
if(d[x][y]>0x3f3f3f3f/2) puts("impossible");
else cout<<d[x][y]<<endl;
}
return 0;
}