今天我们来专注贝尔曼福德算法,考虑他的优化问题。
依然是这个问题,我们来看如何实现一个改善:
#include<iostream>
#include<vector>
#include<climits>
#include<queue>
#include<list>
using namespace std;
struct Edge{
int to;
int val;
Edge(int t,int v):to(t),val(v){}
};
int main(){
int n,m,s,t,v;
cin>>n>>m;
vector<list<Edge>> grid(n+1);
while(m--){
cin>>s>>t>>v;
grid[s].push_back({t,v});
}
vector<int> minDist(n+1,INT_MAX);
vector<bool> isInQueue(n+1,false);
queue<int> q;
q.push(1);
minDist[1]=0;
isInQueue[1]=true;
while(!q.empty()){
int node=q.front();
q.pop();
isInQueue[node]=false;
for(Edge edge:grid[node]){
int from=node;
int to=edge.to;
int val=edge.val;
if(minDist[from]+val<minDist[to]){
minDist[to]=minDist[from]+val;
if(isInQueue[to]==false){
isInQueue[to]=true;
q.push(to);
}
}
}
}
if(minDist[n]==INT_MAX)cout<<"unconnected"<<endl;
else cout<<minDist[n]<<endl;
return 0;
}
核心的做法是没有太多变化的,最明显的区别就在于我们:一是用邻接表来构建图,二是使用了一个队列和一个bool数组记录了每个点相邻的点,这样我们可以每次只松弛遍历的点的相连的边,也就是把一些无用的松弛操作给取消了。
和之前的迪杰斯特拉的算法一样,这种优化是依赖于图的一个结构的,对于稀疏图来说这样的优化非常有效,但对于稠密图来说就差别不大。
95. 城市间货物运输 II (kamacoder.com)
这个题提出了新的挑战:图中存在负权回路,也就是一系列的道路总权值为负。
显然针对这一系列问题,贪心算法就不用想了:他会陷入负权值的回路无限循环。让我们回味贝尔曼福德算法的核心思路,他会针对总计n个点n-1条边实现一个松弛:其实就是挨个点地更新该点到其他点的最小距离,假如道路中出现负权回路,我们的算法会出现一个什么情况呢?其实不难理解,按理说我们的算法正常情况下经过n-1次松弛之后,再怎么松弛我们的最小距离数组也不会进行更新了,但是出现负权值的回路时他依然会去更新,我们只需要在完成n-1次松弛之后再进行一次松弛判断最小距离数组是否发生变化即可。
#include<iostream>
#include<vector>
#include<climits>
#include<list>
#include<queue>
using namespace std;
struct Edge{
int to,val;
Edge(int t,int v):to(t),val(v){}
};
int main(){
int n,m,s,t,v;
cin>>n>>m;
vector<list<Edge>> grid(n+1);
while(m--){
cin>>s>>t>>v;
grid[s].push_back({t,v});
}
vector<int> minDist(n+1,INT_MAX);
vector<bool> isInQueue(n+1,false);
vector<int> count(n+1,0);
bool flag=false;
queue<int> q;
q.push(1);
minDist[1]=0;
isInQueue[1]=true;
while(!q.empty()){
int node=q.front();
q.pop();
isInQueue[node]=false;
for(Edge edge:grid[node]){
int from=node;
int to=edge.to;
int val=edge.val;
if(minDist[to]>minDist[from]+val){
minDist[to]=minDist[from]+val;
if(!isInQueue[to]){
isInQueue[to]=true;
q.push(to);
count[to]++;
if(count[to]==n){
flag=true;
while(!q.empty())q.pop();
break;
}
}
}
}
}
if(flag)cout<<"circle"<<endl;
else if(minDist[n]==INT_MAX)cout<<"unconnected"<<endl;
else cout<<minDist[n]<<endl;
return 0;
}
上述是优化过的spfa算法,我们也可以用基础的贝尔曼福德算法来实现这个过程:
#include<iostream>
#include<vector>
#include<climits>
using namespace std;
int main(){
int n,m,s,t,v;
cin>>n>>m;
vector<vector<int>> grid;
while(m--){
cin>>s>>t>>v;
grid.push_back({s,t,v});
}
vector<int> minDist(n+1,INT_MAX);
bool flag=false;
minDist[1]=0;
for(int i=1;i<=n;++i){
for(vector<int> &edge:grid){
int from=edge[0];
int to=edge[1];
int val=edge[2];
if(i<n){
if(minDist[from]!=INT_MAX&&minDist[to]>minDist[from]+val){
minDist[to]=minDist[from]+val;
}
}
else{
if(minDist[from]!=INT_MAX&&minDist[to]>minDist[from]+val){
minDist[to]=minDist[from]+val;
flag=true;
}
}
}
}
if(flag)cout<<"circle"<<endl;
else if(minDist[n]==INT_MAX)cout<<"unconnected"<<endl;
else cout<<minDist[n]<<endl;
return 0;
}
96. 城市间货物运输 III (kamacoder.com)
这个题在存在负权回路的基础上,还有一个设定是我们的松弛次数是有上限的。
之前我们对于总计N个点,需要松弛N-1次,因为松弛的本质就是去更新起点到终点的权值,而现在我们最多经过k个城市,那么需要松弛多少次呢?
答案是k+1次:
那我们想到的第一步就是把之前的贝尔曼福德算法的松弛次数从n-1修改为k+1即可。
但是这里涉及到一个贝尔曼福德的性质:
对于有负权回路存在的图,我们的贝尔曼福德算法松弛时遍历的顺序不同会导致结果不同。
这时候我们需要去获得上一次minDist更新的结果,保证在上一次更新的基础上继续更新。
#include<iostream>
#include<vector>
#include<climits>
using namespace std;
int main(){
int n,m,s,t,v;
cin>>n>>m;
vector<vector<int>> grid;
while(m--){
cin>>s>>t>>v;
grid.push_back({s,t,v});
}
int src,dst,k;
cin>>src>>dst>>k;
vector<int> minDist(n+1,INT_MAX);
vector<int> minDist_copy(n+1);
minDist[src]=0;
for(int i=1;i<=k+1;++i){
minDist_copy=minDist;
for(vector<int>& edge:grid){
int from=edge[0];
int to=edge[1];
int val=edge[2];
if(minDist_copy[from]!=INT_MAX&&minDist[to]>minDist_copy[from]+val){
minDist[to]=minDist_copy[from]+val;
}
}
}
if(minDist[dst]==INT_MAX)cout<<"unreachable"<<endl;
else cout<<minDist[dst]<<endl;
return 0;
}