Ford(福特)算法和SPFA算法和Dijkstra以及例题实战+全能模板

之前将的Floyd呢是求出所有两点之间的最短距离,复杂度n三次方,只能处理小规模问题,但实际上我们很多时候没必要知道所有两点之间的最短距离,我们只想知道所有节点到a的最短距离,那接下来的Ford、SPFA、Dijkstra就是解决这3个问题的,这三种算法中,SPFA是Ford的进阶版,Ford能处理nm<10的7次方的规模,而SPFA和Dijkstra可以处理更大的,但是呢SPFA可以处理权值为负的,Dijkstra不能处理权值为负的,但是Dijkstra比SPFA更稳定,有时候题目会特意卡SPFA的数据,就可以用Dijkstra来实现了,他们各有各的优点,在这里呢由于我接触SPFA和Dijkstra比较早,并且SPFA各方面都比Ford好,所以Ford我在这里不作详细解释,网上大佬们的精髓到处都有。

Ford

Ford算法的思想大致就是,第一轮,对所有的边都遍历一遍,然后对邻居节点进行操作,比如a->c,那么遍历到这条边的时候,c就问a到起点最近的距离是多少,如果c+a到起点的距离比当前c到起点的距离近,我们就更新c。
如果写成代码呢就是:

void bellman() {
    int s = 1;
    int d[NUM];
    for (int i = 1; i <= n; i++) d[i] = INF;
    d[s] = 0;
    for (int k = 1; i <= n; k++) {
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= n; j++) 
                if (d[j] > d[i] + graph[i][j])
                    d[j] = d[i] + graph[i][j];
    }
    printf("%d\n", d[n]);
}

但是这里我们发现邻接矩阵发挥不了Ford的有点,枚举边的时候复杂度达到了n²,我们要用邻接表来实现就会简单很多。
下面是一套板子,包含了打印路径和判断负圈。
Ford的打印路径的有点是,对于每一个点它没必要知道它到起点的路径是怎么走的,它只需要知道它的上一次节点是多少。
判断负圈:我们可以想到我们每一次循环,都没枚举一次边,那么最多我们枚举n次以后,就不会再有点更新最短路径了,如果有,就一定是有负圈了。

const int INF = 1e6;
struct edge{int u, v, w;} e[10005];//起点u终点v
int pre[NUM], d[NUM];
void print_path(int s, int t) {
	if (s == t) {printf("%d", s); return;}
	print_path(s, pre[t]);
	printf("%d", t)
} 
void bellman(s) {
	for (int i = 1; i <= n; i++) d[i] = INF;
	d[s] = 0;
	int k = 0;
	bool update = true;
	whiel(update) {
		k++;
		update = false;
		if (k > n) {printf("有负圈"); return;}
		for (int i = 0; i < cnt; i++) {
		int x = e[i].u, y = e[i].v;
		if (d[y] > d[x] + e[i].w) {
			update = true;
			d[y] = d[x] + d[i].w;
			pre[y] = x;
		}
    }
	}
}

SPFA

进入正题,在上面的Ford中,我们发现,我们每次枚举所有的边的时候,有很多边根本就和起点没什么关系,明明还离起点很远,但是我们还是去遍历它,这就造成了浪费。在SPFA中,我们用了队列,每次只更新当前操作点的邻居,这样就能加快收敛的过程。SPFA很像BFS,我们下面来模拟一下过程。
(1)起点s入队,计算它所有邻居到s的最短距离(当前最段距离,不是全局最段距离)。下面我把更新最段路径简称为更新。把s出队,把刚才状态有更新的邻居入队,没更新的不入队。这样循环下去,队列里的点都是状态有变化的点,只有这些点才会影响最段路径的计算。
(2)现在队列的头部是s的一个邻居u。弹出u,更新其所有邻居的状态,把其中有状态变化的邻居入队列。
(3)这里有一个问题,弹出u之后,在后面的计算中u可能会再次更新状态(后来发现,u借道其他节点去s,路更近)。所以,u可能需要重新入队列。这一点很容易做到:在处理一个新的节点v时,它的邻居可能就是以前处理过的u,如果u的状态变化了,把u重新加入队列就行了。
(4)继续以上过程知道队列为空,这也意味着所有节点的状态都不再更新。最后的状态就是道起点s的最短路径。
上面第(3)决定了SPFA的效率。有可能只有很少节点重新进入队列,也有可能很多,这取决于图的特征,即使两个图的节点和边的数量一样,但是边的权值不同,他们的SPFA队列可能也会差别很大,所以SPFA是不稳定的。

在比赛时,有的题目可能故意卡 SPFA 的不稳定性:如果一个题目的规模很大,并且边的权值为非负数,它很可能故意设置了不利于 SPFA 的测试数据。此时不能冒险用 SPFA,而是用下一节的 Dijkstra算法。Dijkstra 是一种稳定的算法,一次迭代至少能找到一个结点到 5的最短路径,最多只需要 m(边数)次迭代即可完成。
SPFA和Ford一样,邻接表比邻接矩阵好太多了。所以还是以hdu2544为例。

#include <iostream>
#include <vector>
#include <cstring>
#include <queue>

using namespace std;
const int INF = 1e9 + 7;
const int maxn = 105;
struct edge {
    int to, w;
    edge(){};
    edge(int &a, int &b) {to = a; w = b;}
};
vector<vector<edge>> edges(10005);
bool inq[maxn];
int dis[maxn];
int _size = 0;

int n,m;
int spfa(int s) {
    memset(inq, false, sizeof(inq));
    memset(dis, 0x3f3f3f3f, sizeof(dis));
    queue<int> que;
    que.push(s);
    inq[s] = true;
    dis[s] = 0;
    while(!que.empty()) {
        int u = que.front(); que.pop();
        inq[u] = false;
        for (int i = 0; i < edges[u].size(); i++) {
            int to = edges[u][i].to, w = edges[u][i].w;
            if (dis[u] + w < dis[to]) {
                dis[to] = dis[u] + w;
                if (!inq[to]) inq[to] = true, que.push(to);
            }
        }
    }
    return dis[n];
}

int main()
{
    while(~scanf("%d%d", &n, &m)) {
        if (!n && !m) break;
        for (int i = 1; i <= n; i++) edges[i].clear();
        _size = 0;
        for (int i = 0; i < m; i++) {
            int a,b,c;
            scanf("%d%d%d", &a, &b, &c);
            edges[a].push_back(edge(b,c));
            edges[b].push_back(edge(a,c));
        }
        printf("%d\n", spfa(1));
    }
    return 0;
}

判断负圈 SPFA也适用于有负权值的图,也能判断负圈。如果有一个点进队列超过n次,那就说明图中存在负圈,因为引起一个节点得到更新的顶多就剩下的n-1个节点。具体见Neg[]有关的部分。
打印最段路径和Ford一摸一样的基本上,都是用pre[]记录前驱,不做详细解释。

上面我们用的是邻接表,邻接表我们一般用vector来存图,但是在图特别大的情况下,用邻接表也会超空间限制,这里我们就要用到链式前向星来存图了。
链式前向行的知识往这里走:添加链接描述

那下面我贴一个模板,是链式前向星实现的,有打印路径、判断负圈功能的SPFA:

const int INF = 
const int NUM = 
struct edge{
    int to, w, next;
}edges[NUM];
int n,m,cnt;
int head[NUM], dis[NUM], Neg[NUM], pre[NUM];
bool inq[NUM];
void print_path(int s, int t) {
    if (s == t) {printf("%d ",s); return;}
    print_path(s, pre[t]);
    printf("%d ", t);
}

void init() {
    for (int i = 0; i < NUM; i++) {
        edges[i].next = -1;
        next[i] = -1;
    }
    cnt = 0;
}

void add(int u, int v, int w) {
    edges[cnt].to = v;
    edges[cnt].w = w;
    edges[cnt].next = head[u];
    head[u] = cnt++;
}

int spfa(int s) {
    memset(Neg, 0, sizeof(Neg));
    Neg[s] = 1;
    for (int i = 1; i <= n; i++) {dis[i] = INF; inq[i] = false;}
    dis[s] = 0;
    queue<int>que;
    que.push(s);
    inq[s] = true;
    while(!que.empty()) {
        int u = que.front(); que.pop(); inq[u] = false;
        for (int i = head[u]; ~i; i = edges[i].next) {
            int v = edges[i].to, w = edges[i].w;
            if (dis[v] > dis[u] + w) {
                dis[v] = dis[u] + w;
                pre[v] = u;
                if (!inq[v]) {
                    inq[v] = true;
                    que.push(v);
                    if (++Neg[v] > n) return 1;
                }
            }
        }
    }
    printf("%d\n", dis[n]);
    //path_path(s,n);
    return 0;
}

例题hdu1535,这里有100个点,如果用邻接表会MLE,必须要链式前向星来实现。
题意是点1是起点,现在我们要从点1到另一个点,再从这个点回来,直到每个点都去过。其实就是求点1到剩下点的最短距离+剩下所有点到点1的最短距离(来回)。点1到剩下所有点的最短距离我们spfa一遍就行了,那剩下所有点到点1的最短距离呢,这里就有个技巧,我们可以倒着存图,本来是3->4,我们就存成4->3,这样我们拿这张图再spfa一遍,就可以得到剩下所有点到点1的距离了。
贴个ac代码:

#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
typedef long long ll;
const ll INF = 1e18;
const int NUM = 1000000 + 50;
struct edge{
    int to, w, next;
}edges[NUM], edges2[NUM];
int n,m,cnt, cnt2;
ll dis[NUM];
int head[NUM], Neg[NUM], pre[NUM];
int head2[NUM];
bool inq[NUM];

void init() {
    for (int i = 0; i < NUM; i++) {
        edges[i].next = edges2[i].next = -1;
        head[i] = head2[i] = -1;
    }
    cnt = cnt2 = 0;
}

void add(int u, int v, int w) {
    edges[cnt].to = v;
    edges[cnt].w = w;
    edges[cnt].next = head[u];
    head[u] = cnt++;
}

void add2(int u, int v, int w) {
    edges2[cnt2].to = v;
    edges2[cnt2].w = w;
    edges2[cnt2].next = head2[u];
    head2[u] = cnt2++;
}

ll spfa(int s, edge* edges, int* head) {
    memset(Neg, 0, sizeof(Neg));
    Neg[s] = 1;
    for (int i = 1; i <= n; i++) {dis[i] = INF; inq[i] = false;}
    dis[s] = 0;
    queue<int>que;
    que.push(s);
    inq[s] = true;
    while(!que.empty()) {
        int u = que.front(); que.pop(); inq[u] = false;
        for (int i = head[u]; ~i; i = edges[i].next) {
            int v = edges[i].to, w = edges[i].w;
            if (dis[v] > dis[u] + w) {
                dis[v] = dis[u] + w;
                if (!inq[v]) {
                    inq[v] = true;
                    que.push(v);
                    if (++Neg[v] > n) return 1;
                }
            }
        }
    }
    ll ans = 0;
    for (int i = 1; i <= n; i++) ans += dis[i];
    return ans;
}

int main()
{
    int T; scanf("%d", &T);
    while(T--) {
        init();
        scanf("%d%d", &n, &m);
        int a,b,c;
        for (int i = 0; i < m; i++) {
            scanf("%d%d%d", &a, &b, &c);
            add(a,b,c);
            add2(b,a,c);
        }
        printf("%lld\n", spfa(1, edges, head) + spfa(1, edges2, head2));
    }
     return 0;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值