Bellman-Ford
最短路中一定不含环(因为含有负环的最短路不存在,零环和正环可以除去),所以从起点到终点的最短路经过的边数不超过(n-1),所以一定可以通过(n-1)轮松弛得到最短路,每一轮松弛用所有边进行更新
如果第(n)次松弛依然有起点到某个顶点的最短路被更新,说明从起点可以到达一个负环,最短路不存在
可以增加一个优化,如果在某轮松弛中起点到任何顶点的最短路都没有被更新,那么不需要进行下一轮松弛,直接退出
时间复杂度(O(nm)),其中(n)为点数,(m)为边数
const int inf=0x3f,maxn=110,maxm=10010;
int n,m,dis[maxn];
struct Edge{
int u,v,w;
}edge[maxm];
void Bellman_Ford(int s){
memset(dis,0x3f,sizeof(dis));
dis[s]=0;
int check;
for(int i=1;i<=n;i++){
check=0;
for(int j=1;j<=m;j++){
int u=edge[j].u,v=edge[j].v,w=edge[j].w;
if(dis[v]>dis[u]+w){
dis[v]=min(dis[v],dis[u]+w);
check=1;
}
}
if(!check) break;
}
}
SPFA
SPFA是经过队列优化的Bellman-Ford算法,在Bellman-Ford的某轮松弛中,如果起点到某个顶点的距离没有被更新,那么在下一轮中,就不需要从这个顶点出发去松弛其他顶点了
所以通过一个先进先出的队列来保存松弛过的顶点,每次取出队首顶点(u),并且枚举从(u)出发的所有边((u, v)),如果(dis[u]+w(u,v)<dis[v]),则更新(dis[v]),然后判断顶点(v)是否已经在队列中,如果不在就将顶点(v)插入队尾。这样不断从队列中取出队首顶点来进行松弛操作,直至队列为空
任意顶点被更新的次数一定小于(n),如果某个顶点第(n)次被更新,则说明从起点到终点的路径中存在负环
SPFA算法在最坏的情况下,时间复杂度和Bellman-Ford算法一样,为(O(nm))。但是一般不会达到这个上界,一般的期望时间复杂度为(O(km)),其中(k)为常数
const int inf=0x3f3f3f3f;
int dis[maxn];
bool inq[maxn];
queue<int> q;
void spfa(int s){
for(int i=1;i<=n;i++){
dis[i]=(i==s)?0:inf;
inq[i]=(i==s);
}
q.push(s);
while(!q.empty()){
int u=q.front();
q.pop();
inq[u]=false;
for(int i=head[u];~i;i=nxt[i]){
int v=to[i];
int w=weight[i];
if(dis[v]>dis[u]+w){
dis[v]=dis[u]+w;
if(!inq[v]){
q.push(v);
inq[v]=true;
}
}
}
}
}
Dijkstra
Dijkstra算法每次从最短距离已经确定的顶点开始松弛,最短距离已经确定的点就是当前(dis[i])最小的点
如果如中存在负边权,则无法确定某个顶点是否已经取到最短距离,所以Dijkstra算法无法处理含有负边权的图
每次通过遍历所有顶点找到(dis[i])最小的顶点,时间复杂度(O(n^2))
const int maxn=110,inf=0x3f3f3f3f;
int dis[maxn],book[maxn];
void Dijkstra(int s){
for(int i=1;i<=n;i++) dis[i]=(i==s)?0:inf;
memset(book,0,sizeof(book));
book[s]=1;
for(int i=head[s];~i;i=nxt[i]){
int v=to[i],w=weight[i];
dis[v]=min(dis[v],dis[s]+w);
}
for(int k=1;k<n;k++){
int m=inf,id;
for(int i=1;i<=n;i++){
if(!book[i]){
if(m>dis[i]){
m=dis[i];
id=i;
}
}
}
book[id]=1;
for(int i=head[id];~i;i=nxt[i]){
int v=to[i],w=weight[i];
dis[v]=min(dis[v],dis[id]+w);
}
}
}
Dijkstra堆优化
在查找(dis[i])最小的顶点时,可以使用优先队列优化
由于队列中的点有可能重复,插入操作的上限是(m)次,所以队列中最多有(m)个点,时间复杂度(O((n+m)log m))
const int maxn=110,inf=0x3f3f3f3f;
int dis[maxn],book[maxn];
void Dijkstra(int s){
memset(dis,0x3f,sizeof(dis));
dis[s]=0;
memset(book,0,sizeof(book));
priority_queue<PII> pq;
pq.push({0,s});
while(!pq.empty()){
PII p=pq.top();
pq.pop();
int u=p.second;
if(book[u]) continue;
book[u]=1;
for(int i=head[u];~i;i=nxt[i]){
int v=to[i];
int w=weight[i];
if(book[v]) continue;
if(dis[v]>dis[u]+w){
dis[v]=dis[u]+w;
pq.push({-dis[v],v});
}
}
}
}
floyd
计算任意两点间的最短路,基于动态规划,设(dis[k][i][j])表示只允许经过([1,k])中转的情况下,(i)和(j)之间的最短路。所以有两种情况:
1.如果最短路经过(k),则(dis[k][i][j]=dis[k-1][i][k]+dis[k-1][k][j])
2.如果最短路不经过(k),则(dis[k][i][j]=dis[k-1][i][j])
于是有状态转移方程:(dis[k][i][j]=min(dis[k-1][i][j],dis[k-1][i][k]+dis[k-1][k][j]))
时间复杂度(O(n^3)),滚动数组优化之后,空间复杂度(O(n^2))
const int maxn=110;
int dis[maxn][maxn];
void floyd(){
for(int k=1;k<=n;k++){
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(i==j || i==k || k==j) continue;
dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
}
}
}
}