先上代码:
BFS:
bool vis[MAXN];
int dis[MAXN];
void bfs(int s)
{
queue<int> Q;
Q.push(s);
vis[s] = true;
while(!Q.empty())
{
int u = Q.front();
Q.pop();
for(int e = first[u]; e; e = nxt[e])
{
int v = go[e];
if(vis[v]) continue;
dis[v] = dis[u] + 1;
vis[v] = true;
Q.push(v);
}
}
}
SPFA:
int dis[MAXN];
bool vis[MAXN];
void SPFA(int s)
{
for(int i = 1; i <= n; ++i) dis[i] = inf;
dis[s] = 0;
queue<int> Q;
Q.push(s);
vis[s] = true;
while(!Q.empty())
{
int u = Q.front();
Q.pop();
vis[u] = false;
for(int e = first[u]; e; e = nxt[e])
{
int v = go[e];
if(dis[v] > dis[u] + val[e])
{
dis[v] = dis[u] + val[e];
if(!vis[v])
{
Q.push(v);
vis[v] = true;
}
}
}
}
}
Dijkstra:
struct node
{
int u, ds;
friend bool operator<(const node& n1, const node& n2)
{
return n1.ds > n2.ds;
}
};
bool vis[MAXN];
int dis[MAXN];
void Dijkstra(int s)
{
memset(dis, 0x3f, sizeof(dis));
dis[s] = 0;
priority_queue<node> Q;
Q.push({s, 0});
while(!Q.empty())
{
int u = Q.top().u;
Q.pop();
if(vis[u]) continue;
vis[u] = true;
for(int e = first[u]; e; e = nxt[e])
{
int v = go[e];
if(dis[v] > dis[u] + val[e])
{
dis[v] = dis[u] + val[e];
if(!vis[v]) Q.push({v, dis[v]});
}
}
}
}
分析对比
在BFS算法中,当Q.push()调用时顺便设置vis数组。原因是,BFS只能应用于有向无环图(Directed Acyclic Graph, DAG),节点v第一次被搜索到时的dis值就是s→v的最短路。因此,在后面又访问到v时,根本不需要干任何事情,既不需要加入队列,也不需要更新dis值。因此,vis数组确保每个点只入队一次、dis值只被更新一次。
在SPFA(队列优化的Bellman-Ford算法)中,vis数组标记了节点v是否在Q中。元素出队时vis设置为false,入队时设置为true。如果节点v已经在队列中,则不需要再次入队。SPFA可以看作时BFS的衍生算法,它可以用于任意图,包括多重图、含有自环的图。对于已经访问过的节点,如果形成环路,后面的节点可以更新前面节点的dis值。其实可以去掉vis数组,但是这样会导致某一时刻队列中包含多个同一元素,极大地降低了算法的效率。因为同一时刻队列中有多个同一元素和只有一个该元素的效果是一样的。
Dijkstra算法是一个较为复杂的算法。根据《算法导论》,
Dijkstra算法在运行过程中维持的关键信息是一组结点集合S。从源结点s到该集合中每个结点之间的最短路径已经被找到。算法重复从结点集V-S中选择最短路径估计最小的结点u,将u加入到集合S,然后对所有从u出发的边进行松弛。(P383)
算法维持的不变式为Q=V-S。(P384)(注:V是所有结点的集合,Q是优先队列,S为已经求出最短路的结点集合)
Dijkstra的暴力算法会维持该循环不变式。但我们的算法并不会,因为只有被松驰过的结点才有可能加入队列。理由是,没有被松驰过的结点其dis值一定是inf。Dijkstra还满足一个定理:
定理 队列Q中dis值最小的结点u,其dis值就是s→u的最短路。
因此,我们的vis数组的意义是标志结点u是否属于集合S,即结点u的最短路是否已经被计算出来。当u出队时,根据定理,其最短路一定已经被计算出来了,因此标记vis[u]=true。在这之前,我们写道:
if(vis[u]) continue;
不加这句话会TLE。原因何在?为什么同一个结点会出队两次?因为它可能入队两次。或者说,它出队之前被连续松弛了超过一次,导致它在队里有好几个。因此我们使用vis数组,保证不走冤枉路。(亲测,if(!vis[v]) Q.push({v, dis[v]})中if(!vis[v])可以去掉)
区别Dijkstra和SPFA的一个方法是:Dijkstra末有4个右花括号,SPFA有5个