初学图论,先更最短路,后续更些基础题缓缓。
先说单源最短路中求只有正权的。
先说dijkstra,一定不能用于有负权边的!!!正权算法完全基于贪心,每个点只拓展一次,且找当前dist最小的点,找到一次就解决一个点的最短路(见下);之所以dijkstra不能用于负权,因权值可能为负,每个点可能需要多次拓展所以最坏情况会很慢(可能后面再拓展时由于负权边使dist大的点去拓展dist小的点)。
1 ACW849
先是朴素dij,不妨先将点分为两部分:S集合(包含已确定最短路的点)和T集合(包含还未确定最短路的点)。主要思路是n个点,每个点只扩展1次求得所有点的最短路,以下方法可以保证每个点只扩展一次(因为每个点拓展一次后就确定了,将它加入S,再也用不到它扩展了):每次从T集合中的所有点中选出一个距离当前源点距离dist最短的点,该点一定是所有点中的源点到其距离dist最短的点(画个图很好理解,边无负权,没有再通过更新其它点可以使该点dist变小),所以将该点标记为true,再用该加入S的点为先决条件遍历是否可以更新还未确定到源点的当前最短距离(即在T集合中的点的最短距离)。

//朴素dij O(n^2)
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=550;
int dist[N],n,m;
bool st[N];
int g[N][N];//稠密图
void dijkstra()
{
memset(dist,0x3f,sizeof dist);
dist[1]=0;
for(int i=1;i<=n;i++)
{
int t=-1;
for(int j=1;j<=n;j++)
{
if(st[j]==false && (t==-1 || dist[j]>dist[t]))
t=j;
}
st[t]=true;
for(int j=1;j<=n;j++)
{
if(dist[j]>dist[t]+g[t][j])
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 x,y,z;
cin>>x>>y>>z;
if(g[x][y]>z)
g[x][y]=z;//若有多条重复边选权值最小的
}
cout<<dijkstra();
return 0;
}
2 ACW850
优先队列模拟一个小根堆始终保持堆顶最小,所以不需要找当前dist最小的点,任务只剩在邻接表中对应的某一链表中更新其中点的dist(关于邻接表画个图很易懂):

//堆优化dij O(mlogn)
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
typedef pair<int,int> PII;
const int N=2e5+10;
const int M=2e5+10;
const int INF=0x3f3f3f3f;
int dist[N],n,m;
bool st[N];
int e[M],ne[M],w[M],h[N],idx;//以边数和点数开的数组是要分开的
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())
{
PII t=heap.top();
heap.pop();
int id=t.second,dis=t.first;
if(st[id]==true)
continue;
st[id]=true;
for(int i=h[id];i!=-1;i=ne[i])
{
int j=e[i];
if(dist[j]>dis+w[i])
{
dist[j]=dis+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);//千万不能丢!!!邻接表中每个链表的头结点初始指向-1表示以此点为起点的链表为空
while (m -- )
{
int x,y,z;
cin>>x>>y>>z;
add(x,y,z);
}
cout<<dijkstra();
return 0;
}
单源最短路开始有负权了。
在负权边中,所谓找最短路的前提是DAG,有环的图中找最短路没意义。
3 ACW853
Bellman—Ford只走规定走k步时才用,一般限制走k步是因为图中有负权回路,若不限制步数会死循环。
①不需存储图,开个结构体存边即可。
②共k步,每一步用bak做最新dist的备份数组。即由于k步的限制,更新时只能用上一次的结果,可防止发生“串联”(画个图很易懂,即用刚更新的dist值又更新其它点的dist,会乱)。
//Bellman-Ford O(mn)
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=550;
const int M=1e5+10;
const int INF=0x3f3f3f3f;
int dist[N],n,m,k,bak[N];
bool st[N];
struct bian
{
int x,y,z;
}s[M];
void bellmanford()
{
memset(dist,0x3f,sizeof dist);
dist[1]=0;
while(k--)
{
memcpy(bak,dist,sizeof dist);
for(int i=1;i<=m;i++)
{
int a=s[i].x,b=s[i].y,c=s[i].z;
if(dist[b]>bak[a]+c)
dist[b]=bak[a]+c;
}
}
}
int main()
{
cin>>n>>m>>k;
for(int i=1;i<=m;i++)
{
int a,b,c;
cin>>a>>b>>c;
s[i].x=a,s[i].y=b,s[i].z=c;
}
bellmanford();
if(dist[n]>INF/2)//有负权,不一定是INF了
cout<<"impossible"<<endl;
else
cout<<dist[n]<<endl;
return 0;
}
4 ACW851
SPFA比较类似于BFS。要找到负权图最短路径,类比堆优化dij,最直接的办法是解除每个点只扩展一次的限制,扩展一次进一次堆排个序再找堆顶,只要堆不为空一直取堆顶。但此时取堆顶元素并没有意义(如果过几步又是这个点扩展的可能更小),而堆每次排序还要O(logn),所以干脆不排序了(不排序了pair里的dist没意义了,直接存编号更好),直接用queue,时间复杂度还能降到O(1)。此时,判断该点是否已拓展过的bool数组便失去意义可删去,且queue队列里,同一个点在数组里也没意义,所以新加一个bool数组并含义为“判断当前queue中是否有两个相同元素,有则跳过”。
//SPFA O(m)~O(n*m)
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
const int N=1e5+10;
const int M=1e5+10;
const int INF=0x3f3f3f3f;
int dist[N],n,m;
bool st[N];
int e[M],ne[M],w[M],h[N],idx;
void add(int a,int b,int c)
{
e[idx]=b;
w[idx]=c;
ne[idx]=h[a];
h[a]=idx++;
}
void 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]==false)
{
q.push(j);
st[j]=true;
}
}
}
}
}
int main()
{
cin>>n>>m;
memset(h, -1, sizeof h);
while (m -- )
{
int x,y,z;
cin>>x>>y>>z;
add(x,y,z);
}
spfa();
if(dist[n]>INF/2)
cout<<"impossible";
else
cout<<dist[n];
return 0;
}
除此之外,SPFA常用于判断是不是有负权环。
用一个cnt数组记录到当前点时已经走了几步,若cnt初值为0,找到cnt>=n,则说明n个点中一定有两个点被重复走过了,必然存在负权环。需要注意,负环可能由某一点到不了(假如图中有多个连通分量,只有其中一个有环),所以需要将所有n个点都加入队列中进行初始化。
5 ACW852
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
const int N=2e3+10;
const int M=1e4+10;
const int INF=0x3f3f3f3f;
int dist[N],n,m,cnt[N];
bool st[N];
int e[M],ne[M],w[M],h[N],idx;
void add(int a,int b,int c)
{
w[idx]=c;
e[idx]=b;
ne[idx]=h[a];
h[a]=idx++;
}
void f()
{
memset(dist,0x3f,sizeof dist);
dist[1]=0;
queue<int>q;
for(int i=1;i<=n;i++)
{
q.push(i);
st[i]=true;
}
while(q.size())
{
int t=q.front();
st[t]=false;
q.pop();
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(st[j]==false)
{
q.push(j);
st[j]=true;
}
}
if(cnt[j]>n-1)
{
cout<<"Yes";
return;
}
}
}
cout<<"No";
}
int main()
{
cin>>n>>m;
memset(h, -1, sizeof h);
while (m -- )
{
int x,y,z;
cin>>x>>y>>z;
add(x,y,z);
}
f();
return 0;
}
以上都是求单源最短路,下面求多源最短路。
6 ACW854
//Floyd O(n^3)
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=220;
const int INF=0x3f3f3f3f;
int g[N][N];
int n,m,k;
void floyd()
{
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if(g[i][j]>g[i][k]+g[k][j])
g[i][j]=g[i][k]+g[k][j];
}
int main()
{
cin>>n>>m>>k;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
if(i==j)
g[i][j]=0;
else
g[i][j]=INF;
}
}
while (m -- )
{
int x,y,z;
cin>>x>>y>>z;
if(g[x][y]>z)
g[x][y]=z;
}
floyd();
while(k--)
{
int x,y;
cin>>x>>y;
if(g[x][y]>INF/2)
cout<<"impossible"<<endl;
else
cout<<g[x][y]<<endl;
}
return 0;
}
这篇博客介绍了图论中的最短路问题,包括单源最短路算法如Dijkstra和Bellman-Ford,以及它们在处理正权和负权边时的应用。Dijkstra不适用于负权边,而Bellman-Ford能处理含有负权回路的图。同时,文章还提到了SPFA算法,用于负权图的最短路径计算,并可用于检测负权环。
763

被折叠的 条评论
为什么被折叠?



