Bellman-Ford算法&SPFA算法(队列优先)
(一):Bellman-Ford算法理解
Bellman - ford算法是求含负权图的单源最短路径的一种算法,效率较低,代码难度较小。其原理为连续进行松弛,在每次松弛时把每条边都更新一下,若在n-1次松弛后还能更新,则说明图中有负环,因此无法得出结果,否则就完成。
图的任意一条最短路径既不能包含负权回路,也不会包含正权回路,因此它最多包含|v|-1条边。
其次,从源点s可达的所有顶点如果 存在最短路径,则这些最短路径构成一个以s为根的最短路径树。Bellman-Ford算法的迭代松弛操作,实际上就是按顶点距离s的层次,逐层生成这棵最短路径树的过程。
在对每条边进行1遍松弛的时候,生成了从s出发,层次至多为1的那些树枝。也就是说,找到了与s至多有1条边相联的那些顶点的最短路径;对每条边进行第2遍松弛的时候,生成了第2层次的树枝,就是说找到了经过2条边相连的那些顶点的最短路径……。因为最短路径最多只包含|v|-1条边,所以,只需要循环|v|-1 次。
每实施一次松弛操作,最短路径树上就会有一层顶点达到其最短距离,此后这层顶点的最短距离值就会一直保持不变,不再受后续松弛操作的影响。(但是,每次还要判断松弛,这里浪费了大量的时间,这就是Bellman-Ford算法效率底下的原因,也正是SPFA优化的所在)。
spfa算法模板(邻接矩阵): void spfa(int s){ for(int i=0; i<=n; i++) dis[i]=99999999; //初始化每点i到s的距离 dis[s]=0; vis[s]=1; q[1]=s; 队列初始化,s为起点 int i, v, head=0, tail=1; while (head队列非空 head++; v=q[head]; 取队首元素 vis[v]=0; 释放队首结点,因为这节点可能下次用来松弛其它节点,重新入队 for(i=0; i<=n; i++) 对所有顶点 if (a[v][i]>0 && dis[i]>dis[v]+a[v][i]){ dis[i] = dis[v]+a[v][i]; 修改最短路 if (vis[i]==0){ 如果扩展结点i不在队列中,入队 tail++; q[tail]=i; vis[i]=1; } } } }
②:测试数据:
Sample Input
3 2 1 20.0
1 2 1.00 1.00 1.00 1.00
2 3 1.10 1.00 1.10 1.00
Sample Output
YES
③:Bellman_Ford算法代码:
#include
}
进行不停地松弛,每次松弛把每条边都更新一下,若n-1次松弛后还能更新,则说明图中有负环,无法得出结果,否则就成功完成。Bellman-ford算法有一个小优化:每次松弛先设一个flag,初值为FALSE,若有边更新则赋值为TRUE,最终如果还是FALSE则直接成功退出
④:SPFA算法代码
#include
#include
#include
#include
#include
using namespace std;
int n,m,s;
double v;
double dis[101],rate[1001][1001], cost[1001][1001];
bool spfa(int start)
{
bool flag[101];
//两个数组必须都要初始化
memset(dis,0,sizeof(dis));
memset(flag,0,sizeof(flag));
dis[start]=v;
//开始进行队列处理
queue
Q.push(start);
flag[start]=true;
while(!Q.empty())
{
int x=Q.front();
Q.pop();
flag[x]=false;
for(int i=1;i<=2*m;i++)
{
if(dis[i]<(dis[x] - cost[x][i]) * rate[x][i])
{
dis[i]=(dis[x] - cost[x][i]) * rate[x][i];
if(dis[start]>v)
return true;
if(!flag[i])
{
Q.push(i);
flag[i]=true;
}
}
}
}
return false;
}
int main()
{
int a,b;
double ar,ac,br,bc;
while(~scanf("%d%d%d%lf",&n,&m,&s,&v))
{
/*for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
{
if(i == j)
rate[i][j] = 1;
else
rate[i][j] = 0;
cost[i][j] = 0;
}*///不用初始化也可以;
for(int i=0;i
//读入处理输入的数
scanf("%d%d%lf%lf%lf%lf",&a,&b,&ar,&ac,&br,&bc);
rate[a][b] = ar;
rate[b][a] = br;
cost[a][b] = ac;
cost[b][a] = bc;
}
if(spfa(s))
printf("YES\n");
else
printf("NO\n");
}
return0;
}
2、NEFU 207 最小树
这个例题是从网看到的比较好的
http://blog.csdn.net/hjd_love_zzt/article/details/26739593
题目与分析:
这一道题,抽象一下,描述如下:“求从a到b的最短路径的距离”。
floyd:解决多源最短路径问题。求任意两个点之间的最短路径。这当然也就包含了“从a到b的这种情况”。所以这道题也可以使用floyd来解决
dijkstra:解决单源最短路径问题 。最典型的就是解决“从a到b的最短路径的距离”的这种问题了。
具体讲解dijkatra算法和floyd算法会在其他博客中描述,先大体看一下(代码写得很好)。
以下分别给出这两种算法的解题方法
1)使用floyd
#include
#include
using namespace std;
const int maxn = 105;
const int inf = 99999999;
int e[maxn][maxn];
int n,m;
void initial(){
int i;
int j;
for(i = 1 ; i <= n ; ++i){
for(j = 1 ; j <= n ; ++j){
if(i == j){
e[i][j] = 0;
}else{
e[i][j] = inf;
}
}
}
}
void floyd(){
int i;
int j;
int k;
for(k = 1 ; k <= n ; ++k){
for(i = 1 ; i <= n ; ++i){
for(j = 1 ; j <= n ; ++j){
if(e[i][j] > e[i][k] + e[k][j]){
e[i][j] = e[i][k] + e[k][j];
}
}
}
}
}
int main(){
while(scanf("%d%d",&n,&m)!=EOF){
initial();
int i;
for(i = 1 ; i <= m ; ++i){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
e[a][b] = e[b][a] = c;
}
floyd();
printf("%d\n",e[1][n]);
}
return 0;
}
2)使用dijkstra
#include
#include
using namespace std;
const int maxn = 105;
const int inf = 9999999;
int s[maxn];//用来记录某一点是否被访问过
int map[maxn][maxn];//地图
int dis[maxn];//从原点到某一个点的最短距离(一开始是估算距离)
int n;
int target;
/**
* 返回从v---->到target的最短路径
*/
int dijkstra(int v){
int i;
for(i = 1 ; i <= n ; ++i){//初始化
s[i] = 0;//一开始,所有的点均为被访问过
dis[i] = map[v][i];
}
for(i = 1 ; i < n ; ++i){
int min = inf;
int pos;
int j;
for(j = 1 ; j <= n ; ++j){//寻找目前的最短路径的最小点
if(!s[j] && dis[j] < min){
min = dis[j];
pos = j;
}
}
s[pos] = 1;
for(j = 1 ; j <= n ; j++){//遍历u的所有的邻接的边
if(!s[j] && dis[j] > dis[pos] + map[pos][j]){
dis[j] = dis[pos] + map[pos][j];//对边进行松弛
}
}
}
return dis[target];
}
int main(){
int m;
while(scanf("%d%d",&n,&m)!=EOF){
int i;
int j;
for(i = 1 ; i <= n ; ++i){
for(j = 1 ; j <= n ; ++j){
if(i == j){
map[i][j] = 0;
}else{
map[i][j] = inf;
}
}
}
for(i = 1 ; i <= m ; ++i){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
map[a][b] = map[b][a] = c;//这里默认是无向图。。所以要两个方向都做处理,只做一个方向上的处理会WA
}
target = n;
int result = dijkstra(1);
printf("%d\n",result);
}
return 0;
}
三、使用bellman-ford算法
bellmen-ford算法介绍:
思想:其实bellman-ford的思想和dijkstra的是很像的,其关键点都在于不断地对边进行松弛。而最大的区别就在于前者能作用于负边权的情况。其实现思路还是在求出最短路径后,判断此刻是否还能对便进行松弛,如果还能进行松弛,便说明还有负边权的边
实现:
bool bellmen_ford(){
int i;
for(i = 1 ; i <= n ; ++i){//初始化
dis[i] = inf;
}
dis[source] = 0;//源节点到自己的距离为0
int j;
for(i = 1 ; i < n ; ++i){//计算最短路径
for(j = 1 ; j <= m ; ++j){
if(dis[edge[j].v] > dis[edge[j].u] + edge[j].weight){
dis[edge[j].v] = dis[edge[j].u] + edge[j].weight;
}
if(dis[edge[j].u] > dis[edge[j].v] + edge[j].weight){
dis[edge[j].u] = dis[edge[j].v] + edge[j].weight;
}
}
}
for(j = 1 ; j <= m ; ++j){//判断是否有负边权的边
if(dis[edge[j].v] > dis[edge[j].u] + edge[j].weight){
return false;
}
}
return true;
}
基本结构:
struct Edge{
int u;
int v;
int weight;
};
Edge edge[maxm];//用来存储边
int dis[maxn];//dis[i]表示源点到i的距离.一开始是估算距离
条件:其实求最短路径的题目的基本条件都是点数、边数、起点、终点
一下给出这一道题的bellman-ford的实现方法
#include
#include
using namespace std;
const int maxn = 105;
const int maxm = 105;
struct Edge{
int u;
int v;
int weight;
};
Edge edge[maxm];//用来存储边
int dis[maxn];//dis[i]表示源点到i的距离.一开始是估算距离
const int inf = 1000000;
int source;
int n,m;
bool bellmen_ford(){
int i;
for(i = 1 ; i <= n ; ++i){//初始化
dis[i] = inf;
}
dis[source] = 0;//源节点到自己的距离为0
int j;
for(i = 1 ; i < n ; ++i){//计算最短路径
for(j = 1 ; j <= m ; ++j){
if(dis[edge[j].v] > dis[edge[j].u] + edge[j].weight){
dis[edge[j].v] = dis[edge[j].u] + edge[j].weight;
}
if(dis[edge[j].u] > dis[edge[j].v] + edge[j].weight){
dis[edge[j].u] = dis[edge[j].v] + edge[j].weight;
}
}
}
for(j = 1 ; j <= m ; ++j){//判断是否有负边权的边
if(dis[edge[j].v] > dis[edge[j].u] + edge[j].weight){
return false;
}
}
return true;
}
int main(){
while(scanf("%d%d",&n,&m)!=EOF){
int i;
for(i = 1 ; i <= m ; ++i){
scanf("%d%d%d",&edge[i].u,&edge[i].v,&edge[i].weight);
}
source = 1;
bellmen_ford();
printf("%d\n",dis[n]);
}
return 0;
}
四、使用spfa算法来解决。
思想:用于求单源最短路径,可以适用于负边权的情况。spfa(Shortest Path Faster Algorithm)算法其实不是什么很难理解的算法,它只是bellman-ford的队列优化而已。
模板:
#include
#include
#include
using namespace std;
const int N = 105;
const int INF = 99999999;
int map[N][N], dist[N];
bool visit[N];
int n, m;
void init() {//初始化
int i, j;
for (i = 1; i < N; i++) {
for (j = 1; j < N; j++) {
if (i == j) {
map[i][j] = 0;
} else {
map[i][j] = map[j][i] = INF;
}
}
}
}
/**
* SPFA算法.
* 使用spfa算法来求单元最短路径
* 参数说明:
* start:起点
*/
void spfa(int start) {
queue
int i, now;
memset(visit, false, sizeof(visit));
for (i = 1; i <= n; i++){
dist[i] = INF;
}
dist[start] = 0;
Q.push(start);
visit[start] = true;
while (!Q.empty()) {
now = Q.front();
Q.pop();
visit[now] = false;
for (i = 1; i <= n; i++) {
if (dist[i] > dist[now] + map[now][i]) {
dist[i] = dist[now] + map[now][i];
if (visit[i] == 0) {
Q.push(i);
visit[i] = true;
}
}
}
}
}
这道题的代码如下:
#include
#include
#include
using namespace std;
const int N = 105;
const int INF = 99999999;
int map[N][N], dist[N];
bool visit[N];
int n, m;
void init() {//初始化
int i, j;
for (i = 1; i < N; i++) {
for (j = 1; j < N; j++) {
if (i == j) {
map[i][j] = 0;
} else {
map[i][j] = map[j][i] = INF;
}
}
}
}
/**
* SPFA算法.
* 使用spfa算法来求单元最短路径
* 参数说明:
* start:起点
*/
void spfa(int start) {
queue
int i, now;
memset(visit, false, sizeof(visit));
for (i = 1; i <= n; i++){
dist[i] = INF;
}
dist[start] = 0;
Q.push(start);
visit[start] = true;
while (!Q.empty()) {
now = Q.front();
Q.pop();
visit[now] = false;
for (i = 1; i <= n; i++) {
if (dist[i] > dist[now] + map[now][i]) {
dist[i] = dist[now] + map[now][i];
if (visit[i] == 0) {
Q.push(i);
visit[i] = true;
}
}
}
}
}
int main(){
while(scanf("%d%d",&n,&m)!=EOF){
init();
while(m--){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
if(map[a][b] > c){
map[a][b] = map[b][a] = c;
}
}
spfa(1);
printf("%d\n",dist[n]);
}
return 0;
}
(四):知识点:
松弛操作:
松弛操作是指对于每个顶点v∈V,都设置一个属性d[v],用来描述从源点s到v的最短路径上权值的上界,称为最短路径估计(shortest-path estimate)。
经过初始化以后,对所有v∈V,π[v]=NIL,对v∈V-{s},有d[s]=0以及d[v]=∞。
在松弛一条边(u,v)的过程中,要测试是否可以通过u,对迄今找到的v的最短路径进行改进;如果可以改进的话,则更新d[v]和π[v]。一次松弛操作可以减小最短路径估计的值d[v],并更新v的前趋域π[v](S到v的当前最短路径中v点之前的一个点的编号)。
每个单源最短路径算法中都会调用INITIALIZE-SINGLE-SOURCE,然后重复对边进行松弛的过程。另外,松弛是改变最短路径和前趋的唯一方式。各个单源最短路径算法间区别在于对每条边进行松弛操作的次数,以及对边执行松弛操作的次序有所不同。在Dijkstra算法以及关于有向无回路图的最短路径算法中,对每条边执行一次松弛操作。在Bellman-Ford算法中,每条边要执行多次松弛操作。
这段代码是从上边复制过来的不理解变量的可以看一下上边。
for(int i=1;i<=n-1;i++)//n-1 轮松弛操作
{
bool flag=false;//标记是否松弛
for(int j=0;j<2*m;j++)
{
//下面的四行是赋值为了下面的松弛更简便一些
//若是没有的话就执行消去的代码
int a=edge[j].a;
int b=edge[j].b;
double ar=edge[j].rate;
double ac=edge[j].commission;
if(dis[b]<(dis[a]-ac)*ar)//松弛【也就是走这条路,钱变多】
//if(dis[edge[j].b]<(dis[edge[j].a]-edge[j].commission)*edge[j].rate)
{
//dis[edge[j].b]=(dis[edge[j].a]-edge[j].commission)*edge[j].rate;
dis[b]=(dis[a]-ac)*ar;
flag=true;
}
}
}
最通俗的模板:
if (d[y] > d[x] + w[x][y]){
d[y] = d[x] + w[x][y];
fa[y] = x;
}
这就是松弛;。