最短路(acwing)

n个点,m条边

n与m同一数量级为稀疏图,用邻接表

n与m平方同一数量级为稠密图,用邻接矩阵

 

 

单源最短路

所有边权都为正数

算法一:朴素dijkstra

例:

 

 

先初始化所有点到起点的距离,起点距离为0,其它设置为正无穷,i循环n次,每次确定一个点的最短距离,i每次循环j从点1找到点n,找到最短距离不确定但目前距离起点最近的那个点,其实确定了(将st变为true),然后利用确定最短距离的该点更新与该点相连的后面点的最短距离

t一开始等于-1,保证t至少换一次等于j

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=510;
int n,m;
int g[N][N];
int dist[N];
bool st[N];
int dijkstra(){
memset(dist,0x3f,sizeof dist);
dist[1]=0;
for(int i=0;i<n;i++){
int t=-1;
for(int j=1;j<=n;j++){
if(!st[j]&&(t==-1||dist[t]>dist[j]))
t=j;
}
st[t]=true;
for(int j=1;j<=n;j++){
dist[j]=min(dist[j],dist[t]+g[t][j]);
}
}
if(dist[n]==0x3f3f3f3f) return -1;
return dist[n];
}
int main()
{
cin>>n>>m;
memset(g,0x3f,sizeof g);
while(m--){
int a,b,c;
cin>>a>>b>>c;
g[a][b]=min(g[a][b],c);
}
int t=dijkstra();
cout<<t<<endl;
return 0;
}

算法二:堆优化dijkstra

例:

小根堆,堆里面按照距离自动升序,一个联合数据类型,第一个代表距离,第二个代表编号,先将起点入队列中,起点距离确定,根据起点更新与它相连的后面的点的最短距离,然后,取队头,也就是距离最小的,然后该点最短距离确定,再去更新后面与之相连的若干点的最短距离,并入队

因为有自环,所以有可能之前确定最短距离过的点可能在出队之后再次入队了,所以要标记一下,true表示该点已经更新与它相连的点了,那么该点就不再用了,即使入队了,也不再用它去更新其他点距离了

 

#include<iostream>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
typedef pair<int,int>PII;
const int N=100010;
int n,m;
int h[N],e[N],ne[N],w[N],idx;
int dist[N];
bool st[N];
void add(int a,int b,int c){
e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
int dijkstra(){
memset(dist,0x3f,sizeof dist);
dist[1]=0;
priority_queue<PII,vector<PII>,greater<PII>>heap;
heap.push({0,1});
while(heap.size()){
auto t=heap.top();
heap.pop();
int ver=t.second,distance=t.first;
if(st[ver]) continue;
st[ver]=true;
for(int i=h[ver];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});
}
}
}
if(dist[n]==0x3f3f3f3f) return -1;
return dist[n];
}
int main()
{
cin>>n>>m;
memset(h,-1,sizeof h);
while(m--){
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
}
int t=dijkstra();
cout<<t<<endl;
return 0;
}

存在负权边

算法一:bellman-ford

例:

 

每次更新一条点的最短距离,最多到k条边,k次循环,每次遍历所有边更新后面的点的距离,但因为有k条边限制,有自环(负环),可能该点指向的下一个点是自己和另一个点,然后,dist该点先负环一下,再去算下一个点就不对了,所以应该事先备份一下该点还没开始更新其它点时的dist,这样,保证k次循环,每次循环都能同时更新该点往后的若干点的最短距离

至于重边,对所有边进行取min,因而重边取的也是小的那个,没有影响

至于为什么不用st判断该点是否已经判断过,是因为每次都是对所有边,根本不关心该点是否已经确定最短距离了,而且有负环的是判断不了最短距离的

利用结构体存储点a到点b的距离w

到了最后可能k条边到不了终点,也有可能到不了终点前的点,如图,到不了点n-1,初始化距离便为0x3f3f3f3f(是一个很大的但是确定的值),点n距离则为0x3f3f3f3f-10000,所以最后n点距离不为0x3f3f3f3f,但也是到不了的

 

 

 

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=510,M=10010;
int n,m,k;
int dist[N],backup[N];
struct Edge{
int a,b,w;
}edges[M];
int bellmanford(){
memset(dist,0x3f,sizeof dist);
dist[1]=0;
for(int i=0;i<k;i++){
memcpy(backup,dist,sizeof dist);
for(int j=0;j<m;j++){
int a=edges[j].a,b=edges[j].b,w=edges[j].w;
dist[b]=min(dist[b],backup[a]+w);
}
}
if(dist[n]>0x3f3f3f3f/2) return -1;
return dist[n];
}
int main()
{
cin>>n>>m>>k;
for(int i=0;i<m;i++){
int a,b,w;
cin>>a>>b>>w;
edges[i]={a,b,w};
}
int t=bellmanford();
if(t==-1) puts("impossilble");
else cout<<t<<endl;
return 0;
}

 

算法二:SPFA

例:

 

从起点开始,放入队列中,出点更新最短距离,并将更新到的点放入队列中(原因是该点更新了,与它相连的出点也会更新,所以放进队列,准备去更新它的出点)放入队列则true表示,弹出队列则false表示,因为有自环,所以刚才放入队列的点有可能重新更新最短距离,因此,要弹出去,标记没在队列中

#include<iostream>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
const int N=100010;
int n,m;
int h[N],e[N],ne[N],w[N],idx;
int dist[N];
bool st[N];
void add(int a,int b,int c){
e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
int spfa(){
memset(dist,0x3f,sizeof dist);
dist[1]=0;
queue<int>q;
q.push(1);
st[1]=true;
while(q.size()){
int t=q.front();
q.pop();
st[t]=false;
for(int i=h[t];i!=-1;i=ne[i]){
int j=e[i];
if(dist[j]>dist[t]+w[i]){
dist[j]=dist[t]+w[i];
if(!st[j]){
q.push(j);
st[j]=true;
}
}
}
}
if(dist[n]==0x3f3f3f3f) return -1;
return dist[n];
}
int main()
{
cin>>n>>m;
memset(h,-1,sizeof h);
while(m--){
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
}
int t=spfa();
if(t==-1) puts("impossible");
else cout<<t<<endl;
return 0;
}

spfa判断负环

例:

 

 

这里不需要初始化dist为正无穷的原因是如果存在负环,dist不管初始化为多少,都会被更新

从某点到该点,每次有最短距离更新,就加一条,如果大于等于n条,说明走了至少n+1个点,而最多n个点,说明有负环

可能从1开始到不了负环,因此,需要将所有数一开始都放入队列中

要考虑的是从该点出发,如果没有负环,不用管,不初始化dist,可能一个点都更新不了,但无所谓,从该点出发走的条数肯定小于等于n-1,但若是有负环,那么肯定有一点,从那点出发,会大于等于n条

 

#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int n,m;
int h[N],ne[N],e[N],w[N],idx;
int dist[N];
int cnt[N];
bool st[N];
void add(int a,int b,int c){
e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
int spfa(){
queue<int> q;
for(int i = 1; i <= n; i ++ ){
st[i] = true;
q.push(i);
}
while(q.size()){
auto t = q.front();
q.pop();
st[t] = false;
for(int i = h[t];i!=-1; i = ne[i]){
int j = e[i];
if(dist[j] > dist[t] + w[i]){
dist[j] = dist[t] + w[i];
cnt[j] = cnt[t] + 1;
if(cnt[j] >= n)
return true;
if(!st[j]){
q.push(j);
st[j] = true;
}
}
}
}
return false;
}
int main()
{
memset(h,-1,sizeof h);
cin >>n>>m;
while(m--){
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
}
if(spfa())
puts("Yes");
else
puts("No");
return 0;
}

多源汇最短路

算法:Floyd

例:

将自环距离初始化为0,其它初始化为INF的原因是需要用到自环,而自环如果距离没给,就是0

其它单源最短路不需要将自环距离初始化为0的原因是dist[x]表示x到起点的最短距离,根本就没用到自环

d[a][b]>INF/2的原因是可能a都到不了b前面的那一个点

动态规划思想(区间dp)

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=210,INF=1e9;
int n,m,Q;
int d[N][N];
void floyd(){
for(int k=1;k<=n;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>>Q;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(i==j) d[i][j]=0;
else d[i][j]=INF;
}
}
while(m--){
int a,b,w;
cin>>a>>b>>w;
d[a][b]=min(d[a][b],w);
}
floyd();
while(Q--){
int a,b;
cin>>a>>b;
if(d[a][b]>INF/2) puts("impossible");
else cout<<d[a][b]<<endl;
}
return 0;
}

INF记为1e9或者0x3f3f3f3f(大概1e9多一点),不能记为2e9,如下图输出 

 

d[2][1]结果为负的,其实是因为超出了int数据类型范围,如下图 

d[3][1]=min(d[3][1],d[3][2]+d[2][1]),而d[3][2]=2e9,d[2][1]=2e9,两者加起来超出int数据类型范围,因而变成了-29497296,再取最小值也就取负的了

一个检查错误的很好的方法就是模拟,用纸笔,也可以在程序中将所有输出出来

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值