【图论】初识最短路

图的基本知识

图的基本元素:点和线

图:{有向图,无向图}

可以将 u<——>v 看作 u——>v 和 v——>u 的结合,把无向图转化成有向图

如果每条边都存在一个边权,则路径长度 = 路径上边的权值的和

最短路:两点之间长度最短的路径(总权值和最小)的路径

Bellman-Ford

(有向图)

主要处理有负权的图,一般正权图就用dijstra

图:{有向图,无向图}

可以将 u<——>v 看作 u——>v 和 v——>u 的结合,把无向图转化成有向图

对于所有边权都 >= 0 的图,任意两点之间的最短路,不会经过重复的顶点或边。

即任意一条最短路经过的顶点数不会超过n个,边不会超过n - 1 条

对于有边权为负的负环,途径负环的最短路没有意义

(因为只要经过负权路就会减小权值,会一直循环)

but,不是说存在负环的图不存在最短路,只要不经过负环就可以找到最短路

核心操作:松弛操作

对边(u, v) ,用 dist(u) 和 l (u, v) 的和尝试更新 dist(v);

dist(v) = min(dist(v), dist(u) + l (u, v))

n:顶点数,m:边数

l (u, v):u到v的边长

s:起点,t:终点

dist[u] :当前的s到u的距离

进行多次迭代操作,每进行一次迭代,就对图上所有的边都进行一次松弛操作,当某次迭代

dist(v) 不变的时候停止

时间复杂度

在最短路存在的情况下,每次迭代的最坏情况也会使最短路的边数至少加1,

s到每个顶点的最短路边数最多是 n - 1 ,所以最多进行n - 1次迭代,每轮迭代m个点

O(nm)

当s点出发能达到一个负环的时候就会进行n轮以上的迭代。

如何在O(nm)复杂度内检测 图上是否存在负环呢?

加一个额外的点,可以到达所有的点,用松弛迭代判断

代码实现

struct Edge {
    int x, y, v;// x到y的边权为v
} edge[M + 1];
int n, m, dist[N + 1], pre[N + 1];
//n个点,m条边,dist[i]:当前s到第i个点的最小距离,pre[i]:第i个点的前面的点(用于输出路径)
int shortestpath(int s, int t) {
    memset(dist, 127, sizeof(dist)); // 其他点的距离赋值为无穷大
    dist[s] = 0; // 将起点的距离赋值为 0 
    for ( ; ; ) {
        bool ok = false;// 判断本轮迭代有没有发生松弛操作
        for (int i = 1; i <= m; i++) {
            int x = edge[i].x, y = edge[i].y, v = edge[i].v;
            if (dsit[x] < 1 << 30) { // 当前的点被更新过
                if (dist[x] + v < dist[y]) {
                    dist[y] = dist[x] + v;
                    pre[y] = x;
                    ok = true;
            	}
            }
        }
        if (!ok)
            break;
    }
    return dist[t];
}

优化方法

队列优化(spfa)不建议使用:(最坏情况会被卡到O(nm))

在每次迭代时,只有在上一次迭代中被更新了距离的点,才有可能去更新其他结点。因此,在每次迭代时,我们将更新过距离的顶点加入一个队列(如果顶点已经在队列里则不加),在下次迭代的时候只需要遍历队列中的顶点连出去的边即可。

Dijkstra

求无负权边图的最短路,并且优化过后比bellman时间复杂度低很多

维护顶点集合c,满足对于所有集合c中的顶点x,我们都找到了起点s到x的最短路,次数dis(x)记录的就是最终最短路的长度。

算法内容

  • 将集合c设置为空,S的距离设置为0,其他顶点的距离都设置为无穷大。

  • 每一轮里面,都将距离起点最近的(dist最小,不能是无穷大)还不在C中的顶点加入C,并且用这个点连出去的边进行松弛操作尝试更新其他点的dist;

  • 当终点T(如果存在的话)或者没有新的点加入C时,算法结束。

由于图上不存在负权边,所以可以证明每次加入C的点都找到了起点到它的最短路。

代码实现

struct Node {
    int y, v;
    Node(int _y, int _v) {y = _y, v = _v;};
};

vector<Node> edge[N + 1];
int n, m, dist[N + 1];
bool b[N + 1]; // 判断该点是否在集合c里面

int Dijkstra(int s, int t) {
    memset(b, false, sizeof(b)); // 初始所有点都不在集合c中
    memset(dist, 127, sizeof(dist)); // 其余点初始化为无穷大
    dist[s] = 0; // s初始0
    
    for ( ; ; ) {
        int x = -1; // 记录不在c中的dist最小的点的编号
        for (int i = 1; i <= n; i++) 
            // 寻找不在c中的已经更新过的点,找dist最小的
            if (!b[i] && dist[i] < 1 << 30)
                	if (x == -1 || dist[i] < dist[x])
                        	x = i;
        // 到了终点,或者没有新的点再加入c了,算法结束
        if (x == t || x == -1)
            break;
        
        b[x] = true;
        for (auto i : edge[x]) // 更新最小距离
            dist[i.y] = min(dist[i.y], dist[x] + i.v);
    }
    
    return dist[t];
}

时间复杂度:O(n2+m)

set堆优化

==时间复杂度:==O((n+m)logn)

观察上面的代码,发现花费大量时间在寻找dist的最小值上面。

每次都找dis最小的,考虑采用最小堆(可以用优先队列)

但是手写堆要比优先队列快一点

优先队列修改中间元素太麻烦,甚至不能修改

所有我们采用set排序,比较好写

q记录不在c中的点的信息,int1,这个点的dis,int2:这个点下标多少?

pair比较大小,先看第一个,第一个一样就再比较第二个数

代码实现

struct Node {
    int y, v;
    Node(int _y, int _v) {y = _y, v = _v;};
};

set<pair<int, int>> q;
vector<Node> edge[N + 1];
int n, m, dist[N + 1];

int Dijkstra(int s, int t) {
    memset(dist, 127, sizeof(dist));
    dist[s] = 0;
    q.clear();
    
    for (int i = 1; i <= n; i++)
        q.insert(make_pair(dist[i], i));
    //或者q.insert({dist[i], i});
    for (; !q.empty(); ) {
        int x = q.begin()->second;
        q.erase(q.begin());
        if (x == t || dist[x] > 1 << 30)
            break;
        
        for (auto i : edge[x]) {
            if (dist[x] + i.v < dist[i.y]) {
                q.erase(make_pair(dist[i.y], i.y));
                dist[i.y] = dist[x] + i.v;
                q.insert(make_pair(dist[i.y], i.y));
            }
        }
    }
    
    return dist[t];
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值