寒假训练营 第十三节 搜索与图论(四)总结

Bellman-ford(单源最短路)

基本原理:

不断尝试对图上每一条边进行松弛。我们每进行一轮循环,就对图上所有的边都尝试进行一次松弛操作,当一次循环中没有成功的松弛操作时,算法停止。

特点:

边权没有要求,可正可负,还可判断负权边.时间复杂度为O(nm),但是经过队列优化,时间复杂度是<=O(nm)。

算法思想:

图的任意一个条最短路,既不会包含负权回路,也不会包含正权回路,最多包含n-1边。那么,从源点s开始,可以达到的节点,如果存在最短路,则最短路构成了一颗以s为根的最短路树。因此,可以按照距离根s的层次,逐层生成达到每个点的最短路(松弛操作);所以整个过程,就是创建最短路树的过程;需要一个辅助数组d[n]和v[n]来记录最短路距离和跟踪寻迹;从边的角度来考虑,每次迭代要遍历每条边;循环n-1次后,第n次循环如果所有d[n]值不更新,则跳出循环;如果第n次还存在路径更新,则说明存在负环;Bellman-Ford算法也可以求解最长路和用来判断正环,只要在递推关系选择最大的更新就好。

Bellman-Ford算法还能用来求最长路或者判断正环。

Bellman-ford与Dijkstra算法的区别:
迪杰斯特拉(Dijkstra)算法是借助贪心思想,每次选取一个未处理的最近的结点,去对与他相连接的边进行松弛操作;贝尔曼福特(Bellman-ford)算法是直接对所有边进行N-1遍松弛操作。
迪杰斯特拉(Dijkstra)算法要求边的权值不能是负数;贝尔曼福特(Bellman-ford)算法边的权值可以为负数,并可检测负权回路。

void bellman_ford(int s)
{
    memset(dis,MM,sizeof(dis));
    dis[s] = 0;
    bool change;
    for (int i = 0; i < n - 1; i++)
    {
        change = false;
        for (int k = 0; k < m; k++)
        {
            if (dis[edge[k].to] > dis[edge[k].from] + edge[k].value)
            {
                change = true;
                dis[edge[k].to] = dis[edge[k].from] + edge[k].value;
                patch[edge[k].to]=edge[k].from;
            }
        }
        if (change==false)
        {
            break;
        }
    }
}

Bellman-Ford算法可以大致分为三个部分:
初始化所有点。每一个点保存一个值,表示从原点到达这个点的距离,将原点的值设为0,其它的点的值设为无穷大(表示不可达)。
进行循环,循环下标为从1到n-1(n等于图中点的个数)。在循环内部,遍历所有的边,进行松弛计算。
遍历途中所有的边(edge(u,v)),判断是否存在这样情况: d(v) > d (u) + w(u,v),若存在,则返回false,表示图中存在从源点可达的权为负的回路。
之所以需要第三步的原因,是因为,如果存在从源点可达的权为负的回路,则将因为无法收敛而导致不能求出最短路径。

SPFA(Bellman-ford的队列优化版)

基本原理:

bellman-ford中有很多重复无用的松弛操作,只有上一次被松弛的结点,所连接的边,才有可能引起下一次的松弛操作。所以,我们用队列来储存被松弛的结点,就可以访问必须访问的边了。

特点:

边权没有要求,可正可负,还可判断负权边(如果没有负边权,推荐使用Dijkstra算法)。时间复杂度大多数情况情况快,最坏可达O(nm)。(与bfs算法比较,复杂度相对稳定)

SPFA算法有四个优化策略:堆优化、栈优化、SLF和LLL

  • 堆优化:将队列换成堆,与 Dijkstra 的区别是允许一个点多次入堆。在有负权边的图可能被卡成指数级复杂度。
  • 栈优化:将队列换成栈(即将原来的 BFS 过程变成 DFS),在寻找负环时可能具有更高效率,但最坏时间复杂度仍然为指数级。
  • SLF:Small Label First 策略,设要加入的节点是j,队首元素为i,若dist(j)<dist(i),则将j插入队首,否则插入队尾;
  • LLL:Large Label Last 策略,设队首元素为i,队列中所有dist值的平均值为x,若dist(i)>x则将i插入到队尾,查找下一元素,直到找到某一i使得dist(i)<=x,则将i出队进行松弛操作。

SLF 和 LLL 优化在随机数据上表现优秀,但是在正权图上最坏情况为 O(VE),在负权图上最坏情况为达到指数级复杂度。

struct edge {
  int v, w;
};

vector<edge> e[maxn];
int dis[maxn], cnt[maxn], vis[maxn];
queue<int> q;

bool spfa(int n, int s) {
  memset(dis, 63, sizeof(dis));
  dis[s] = 0, vis[s] = 1;
  q.push(s);
  while (!q.empty()) {
    int u = q.front();
    q.pop(), vis[u] = 0;
    for (auto ed : e[u]) {
      int v = ed.v, w = ed.w;
      if (dis[v] > dis[u] + w) {
        dis[v] = dis[u] + w;
        cnt[v] = cnt[u] + 1;  // 记录最短路经过的边数
        if (cnt[v] >= n) return false;
        // 在不经过负环的情况下,最短路至多经过 n - 1 条边
        // 因此如果经过了多于 n 条边,一定说明经过了负环
        if (!vis[v]) q.push(v), vis[v] = 1;
      }
    }
  }
  return true;
}

题目详情: 洛谷P3371 【模板】单源最短路径(弱化版)

如题,给出一个有向图,请输出从某一点出发到所有点的最短路径长度。

输入格式:

第一行包含三个整数 n , m , s n,m,s n,m,s,分别表示点的个数、有向边的个数、出发点的编号。
接下来 m m m 行每行包含三个整数 u , v , w u,v,w u,v,w,表示一条 u → v u \to v uv 的,长度为 w w w 的边。

输出格式:

输出一行 n n n 个整数,第 i i i 个表示 s s s 到第 i i i 个点的最短路径,若不能到达则输出 2 31 − 1 2^{31}-1 2311

提示:

【数据范围】
对于 20 % 20\% 20% 的数据: 1 ≤ n ≤ 5 1\le n \le 5 1n5 1 ≤ m ≤ 15 1\le m \le 15 1m15
对于 40 % 40\% 40% 的数据: 1 ≤ n ≤ 100 1\le n \le 100 1n100 1 ≤ m ≤ 1 0 4 1\le m \le 10^4 1m104
对于 70 % 70\% 70% 的数据: 1 ≤ n ≤ 1000 1\le n \le 1000 1n1000 1 ≤ m ≤ 1 0 5 1\le m \le 10^5 1m105
对于 100 % 100\% 100% 的数据: 1 ≤ n ≤ 1 0 4 1 \le n \le 10^4 1n104 1 ≤ m ≤ 5 × 1 0 5 1\le m \le 5\times 10^5 1m5×105 1 ≤ u , v ≤ n 1\le u,v\le n 1u,vn w ≥ 0 w\ge 0 w0 ∑ w < 2 31 \sum w< 2^{31} w<231,保证数据随机。

#include<bits/stdc++.h>
using namespace std;
const int NR=100000;
int d[NR],n,m,s;	//	d[NR]:最短路径存储列表
vector< pair<int,int> > adj[NR];//一个结构体队列
void spfa(int s){//spfa函数
    memset(d,0x3f,sizeof(d));
    queue<int> q;
    q.push(s);d[s]=0;
    while(!q.empty()){
        int u=q.front();q.pop();
        for(int i=0;i<adj[u].size();i++){
            int v=adj[u][i].first;
            int w=adj[u][i].second;
            if(d[u]+w<d[v]){
                d[v]=d[u]+w;
                q.push(v);
            }
        }
    }
}
int main(){
    scanf("%d%d%d",&n,&m,&s);
    for(int i=0;i<m;i++){
        int st,en,w;
        scanf("%d%d%d",&st,&en,&w);
        adj[st].push_back(make_pair(en,w));
    }
    spfa(s);
    for(int i=1;i<=n;i++){//输出
        if(d[i]>=1e9)printf("2147483647 ");
        else printf("%d ",d[i]);
    }
    return 0;
}

时间复杂度分析:

  • Floyed算法:求多源最短路,可以处理负边;时间复杂度为O(n3);
  • Dijkstra算法:求单源最短路,不能处理负边;时间复杂度为O(n2);
  • Bellman-Ford算法:求单源最短路,可以处理负权边;时间复杂度为O(NM);
  • SPFA算法:求单源最短路,Bellman-ford算法优化版本,可以处理负权边;时间复杂度为O(kM)~O(NM); k为较小常数;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值