【模板】最短路
图论总结
最短路
| 最短路算法 | Floyd | Bellman–Ford | Dijkstra | Johnson |
|---|---|---|---|---|
| 最短路类型 | 每对结点之间的最短路 | 单源最短路 | 单源最短路 | 每对结点之间的最短路 |
| 作用于 | 任意图 | 任意图 | 非负权图 | 任意图 |
| 能否检测负环? | 能 | 能 | 不能 | 能 |
| 时间复杂度 | \(O(N^3)\) | \(O(NM)\) | \(O(M\log M)\) | \(O(NM\log M)\) |
注:表中的 Dijkstra 算法在计算复杂度时均用 priority_queue 实现。
Floyd
适用范围:无论有向图还是无向图,无论是否有负边。(正常运行)不能有负环。
定义数组f[k][x][y],表示只允许经过1-k的节点,x到y的最短路径长度。(x,y不一定要在1-k中)
本质是dp。第一维可压。
初始化:边权,或0(x=y),或inf(不连接)。
int e[maxn][maxn], f[maxn][maxn];
void floyd()
{
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
{
f[i][j] = (e[i][j] == -1 ? inf : e[i][j]);
if (i == j)
f[i][j] = 0;
}
for (int k = 1; k <= n; k++)
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
f[i][j] = min(f[i][j], f[i][k] + f[k][j]);
}
Bellman–Ford
适用范围:允许有负边,可以判断负环。
松弛操作:dis(v) = min(dis(v), dis(u) + w(u, v)) 。
关于负环:
若存在最短路(没有负环),由于每一次松弛会使得最短路边数至少 +1,而最短路的边数最多为 n-1,因此整个算法最多执行 n-1 轮松弛操作。若第n轮还能继续,则存在负环。
需要注意,从S出发能跑出最短路,不代表一定没有负环(如有向图,或不属于同一个分量的情况)。正确做法是建立一个超级原点S',从它向每个点连一条边权为0的边,然后从这个点开始跑。(不过如果只是找S的最短路也无所谓了)
只有上一次被松弛的结点,所连接的边,才有可能引起下一次的松弛操作。所以有了SPFA,即队列优化。
SPFA会被卡,所以一般情况用Dijkstra。
bool SPFA()//返回true代表有最短路,否则有负环
{
memset(dis,0x3f,sizeof(dis));//由于memset的妙妙特性,dis会变成0x3f3f3f3f
queue<int> qu;
dis[s]=0,inque[s]=1;
qu.push(s);
while (!qu.empty())
{
int u=qu.front();
qu.pop();inque[u]=0;
for(edge ed:e[u])//相当于for(int i=0;i<e[u].size();i++)edge ed=e[u][i]
{
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,有负环
if(!inque[v])
qu.push(v), inque[v]=1;
}
}
}
return true;
}
Dijkstra
适用范围:非负权图(可以有0)
思路:将结点分成两个集合:已确定最短路长度的点集(记为 S 集合)的和未确定最短路长度的点集(记为 T 集合)。一开始所有的点都属于 T 集合。每次从T中选取一个最短路长度最小的结点,移到 S 集合中,并进行松弛操作。
本质为贪心。
struct edge{
int u,v,w;
};
struct node{
int u,dis;
bool operator > (const node& b) const {return dis>b.dis;};//用于greater;const不能少
};
vector<edge> e[maxn];
int dis[maxn], vis[maxn];
priority_queue<node, vector<node>, greater<node>> pq;
void dijkstra()
{
memset(dis, 0x3f, (n + 1) * sizeof(int));
memset(vis, 0, (n + 1) * sizeof(int));
dis[s]=0;
pq.push({s,0});
while (!pq.empty())
{
int u=pq.top().u;
pq.pop();
if(vis[u])continue;
vis[u]=1;
for(edge ed:e[u])
{
int v=ed.v,w=ed.w;
if(dis[v]>dis[u]+w)
{
dis[v]=dis[u]+w;
pq.push({v,dis[v]});
}
}
}
}
Johnson
适用范围:全源最短路,可以有负边,不能有负环。
跑n遍Dijkstra实现全源最短路的复杂度是\(O(nm\log m)\),在稀疏图上比Floyed更优。但是Dijkstra不能解决负边。因此需要将值全部调整为正的。
新建虚拟节点0,从0向所有节点连一条权为0的边。(注:即使是无向图,也只用添加单向的边)接下来从0开始求最短路,记为h[i]。如果一条边从u到v,权为w,则新的权值为为 \(w+h_u-h_v\)。然后从每个节点跑一次Dijkstra。
模板:洛谷P5905
//公共部分
const int maxn = 3e3+10;
int n,m;
struct edge{
int u,v,w;
};
vector<edge> e[maxn];
//spfa变量区
int h[maxn],cnt[maxn],inque[maxn];
const int inf = 1e9;
bool SPFA(int s)//返回true代表有最短路,否则有负环
{
for(int i=1;i<=n;i++)
h[i]=inf;
queue<int> qu;
h[s]=0,inque[s]=1;
qu.push(s);
while (!qu.empty())
{
int u=qu.front();
qu.pop();inque[u]=0;
for(edge ed:e[u])//相当于for(int i=0;i<e[u].size();i++)edge ed=e[u][i]
{
int v=ed.v,w=ed.w;
if(h[v]>h[u]+w)
{
h[v]=h[u]+w;
cnt[v]=cnt[u]+1;
if(cnt[v]>=n+1)//注意在Johnson中是n+1,因为有0号原点
return false;//大于等于n+1,有负环
if(!inque[v])
qu.push(v), inque[v]=1;
}
}
}
return true;
}
//dijkstra变量区
struct node{
int u,dis;
bool operator > (const node& b) const {return dis>b.dis;};//用于greater;const都不能少
};
int dis[maxn], vis[maxn];
priority_queue<node, vector<node>, greater<node>> pq;
void dijkstra(int s)
{
for(int i=1;i<=n;i++)
dis[i]=inf;
memset(vis, 0, (n + 1) * sizeof(int));
dis[s]=0;
pq.push({s,0});
while (!pq.empty())
{
int u=pq.top().u;
pq.pop();
if(vis[u])continue;
vis[u]=1;
for(edge ed:e[u])
{
int v=ed.v,w=ed.w;
if(dis[v]>dis[u]+w)
{
dis[v]=dis[u]+w;
pq.push({v,dis[v]});
}
}
}
}
void johnson()
{
for(int i=1;i<=n;i++)
e[0].push_back({0,i,0});
bool y=SPFA(0);
if(!y)
{
cout<<-1<<endl;
return;
}
for(int i=1;i<=n;i++)
for(edge& ed:e[i])
ed.w=ed.w+h[ed.u]-h[ed.v];//重新赋值
for(int i=1;i<=n;i++)
{
dijkstra(i);
int ans=0;
for(int j=1;j<=n;j++)//本题要求是这么处理,其他题看情况
{
if(dis[j]==inf)
ans+=j*inf;//这里需要注意,最后不要用dis[j]+h[j]-h[i]对inf算
else
ans+=j*(dis[j]+h[j]-h[i]);//别忘记了
}
cout<<ans<<endl;
}
}

浙公网安备 33010602011771号