暑假集训D5 2023.7.28 补题
首先来回顾一下 \(dijkstra\) 和 \(SPFA\) 里面 \(vis\) 数组的作用和区别,以及不用 \(vis\) 数组的影响.(今天发现之前写堆优化的 \(Dijkstra\) 都不加 \(vis\) 数组...)
-
\(Dijkstra\) 算法中,每次取出距离源点最近的一个点来更新与他相连的其他点,置 \(vis\) 数组为 \(true\) . 如果在更新之前发现已经 \(vis\) 过了,直接 \(continue\) ,原因是这个点早就是距离源点最近的点了,如果没有负权边,那么不可能再有其他点来更新这个点的 \(dist\) ,并且已经用这个点更新过与他相连的点.因此再取出这个点不会再更新到其他点的信息了,额外浪费了时间.
-
\(SPFA\) 算法中, \(vis\) 数组主要是标记这个点是否已经在队列里.那么就很好理解什么时候置 \(vis\) 为 \(false\) \(or\) \(true\) ,什么时候置为 \(true\) .其主要作用是防止一个元素重新入队.
其实可以去掉vis数组,但是这样会导致某一时刻队列中包含多个同一元素,极大地降低了算法的效率。因为同一时刻队列中有多个同一元素和只有一个该元素的效果是一样的。
B.[USACO06DEC] Wormholes G
容易看出本题求负环就可以了.先给出 OIWIKI 上的 SPFA 求负环的模板代码
struct edge {
int v, w;
};
vector<edge> e[maxn];
int dis[maxn], cnt[maxn], vis[maxn];
queue<int> q;
bool spfa(int n, int s) {
memset(dis, 63, sizeof(dis));
dis[s] = 0, vis[s] = 1;
q.push(s);
while (!q.empty()) {
int u = q.front();
q.pop(), vis[u] = 0;
for (auto ed : e[u]) {
int v = ed.v, w = ed.w;
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
cnt[v] = cnt[u] + 1; // 记录最短路经过的边数
if (cnt[v] >= n) return false;
// 在不经过负环的情况下,最短路至多经过 n - 1 条边
// 因此如果经过了多于 n 条边,一定说明经过了负环
if (!vis[v]) q.push(v), vis[v] = 1;
}
}
}
return true;
}
在应用 \(SPFA\) 求负环时,一定要注意看一下图是不是连通的,如果不是连通的,一定要把所有点都用一遍 \(SPFA\) .
可以用下面两种方法:
for(int i = 1;i<=n;i++)
{
if(!vis[i])
{
spfa(n,i);
}
}
第二种方法是预先把所有点都加进队列中.
bool spfa()
{
queue<int> q;
memset(vis, 0, sizeof vis);
memset(cnt, 0, sizeof cnt);
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
//把所有点预先加进去.这样就不用担心不连通了.
for(int i =1;i<=n;i++)
{
q.push(i);
vis[i] =1;
}
while (q.size())
{
int u = q.front();
q.pop(), vis[u] = 0;
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if (dist[j] > dist[u] + c[i])
{
dist[j] = dist[u] + c[i];
cnt[j] = cnt[u] + 1;
//cout<<u<<" "<<j<<dist[j]<<endl;
//system("pause");
if (cnt[j] > n)return false;
if (!vis[j]) q.push(j), vis[j] = 1;
}
}
}
return true;
}