迪杰斯特拉

 

void d(int begin){
    init();
    dist[begin]=0;
    while(1){
        int i,u=-1,v;
        int min=INF;
        for(i=1;i<=n;++i){
            if(!sure[i]&&dist[i]<min){//找到最小的
                min=dist[i];
                u=i;
            }
        }
        if(u==-1){
            break;//结束没有了
        }
        //已经找到了最小的点
        //现在将这个点加入
        sure[u]=1;
        //更新
        for(v=1;v<=n;++v){
            if(g[u][v]!=-1&&dist[v]>dist[u]+g[u][v]){
                dist[v]=dist[u]+g[u][v];
            }
        }

    }

}

单源最短路

该博客的单源最短路算法要解决的就是在一个没有负权边的图上,找出所有点与源点 ss 的最短路径,这样一个问题。这里介绍迪杰斯特拉 O(n2)O(n2) 解法与堆优化 O(mlogm)O(mlog⁡m) 解法,其中 nn 为图上节点数量,mm 为图上边的数量。 
不介绍也不推荐玄学复杂度的 spfaspfa 解法。

朴素写法思路:贪心

  1. 首先将所有点与源点的距离设为无穷大(记录在 disdis 数组中),ss 与自己的距离初始化为 00,还有一个表示“已经确定最短路径长度”的节点集合 SetSet。
  2. 然后不断从不属于 SetSet 集合的点中找到一个 disdis 最小的点 xx,那么这个点在以后的操作中一定不会再更新(由于边权非负,所以如果后面最短路径将通过其他点 yy 到达 xx,那么这个值最小为 disy+disy→⋯→x≥disydisy+disy→⋯→x≥disy,由于 disy≥disxdisy≥disx,所以可以保证在后续运行过程中 disxdisx 不会被更新为更小的值),将这个点加入集合 SetSet 中,然后更新所有与 xx 相邻节点的 distdist 为 min(dist,disx+disx→t)min(dist,disx+disx→t)。
  3. 不断重复第二步,直到所有点的最短路径都被找到 / 所有点都在 SetSet 集合中。

朴素写法时间复杂度

从上面的算法可以看出,大循环要将所有点都加入集合一次,而每次寻找这个“不在集合中且 disxdisx 最小的点”需要一次 O(n)O(n) 的循环,接着更新所有与 xx 邻接的点(最多有 nn 个点),这里也需要 O(n)O(n),所以大循环为 O(n)O(n),小循环为 O(n+n)O(n+n),整体时间复杂度为 O(n×(n+n))=O(n2)O(n×(n+n))=O(n2)。

O(n2)O(n2) 代码

const int maxn = 1000 + 100;
struct Node {
    int pos;
    int dis;
    Node() {}
    Node(int p, int d) {
        pos = p;
        dis = d;
    }
};
int n;
int dis[maxn];
bool vis[maxn];
vector<Node> G[maxn];

void dij(int s) {
    // 距离初始化
    fill(dis, dis + n + 1, INT_MAX);
    dis[s] = 0;
    int cnt = n;
    // cnt 表示不在集合中的节点个数
    while(cnt > 0) {
        int Min = INT_MAX;
        int x;
        // 寻找不在集合中的,距离 s 最近的节点
        for(int i = 1; i <= n; ++i) {
            if(!vis[i] && dis[i] < Min) {
                Min = dis[i];
                x = i;
            }
        }
        // 将节点 x 加入集合
        --cnt;
        vis[x] = true;
        // 更新节点 x 的邻接节点
        int len = G[x].size();
        for(int i = 0; i < len; ++i) {
            int pos = G[x][i].pos;
            int d = G[x][i].dis;
            dis[pos] = min(dis[pos], dis[x] + d);
        }
    }
}

堆优化

上面能够优化的时间复杂度是在“找到不在集合中的离 ss 最近的点”(每个点必须要进入集合才算更新结束,所以大循环的 O(n)O(n) 是无法减小的),我们能否通过一些排序操作,使得能够快速地找到这个点 xx 呢? 
这里就可以用到一个“小顶堆”的数据结构,它能够在 O(logn)O(log⁡n)(其中 nn 为堆中的元素数量)的时间复杂度内完成插入、删除最小值的操作,在 O(1)O(1) 的时间复杂度内完成取堆内最小值的操作。于是我们可以将上面的查找这一步操作放入到堆中,时间复杂度就能下降到 O(logn)O(log⁡n)。 
但这里要注意一点,在我们查找之后,是可以更新这个点的最短距离的,但是小顶堆不允许访问、更改堆内元素,只能访问堆顶元素,所以如果将点与当前最短距离放入堆内,将存在一些多余的点(更新时该点还未从堆顶弹出),而这些所有数据最多有 mm 个,mm 为图上边的数量。 
这里可以标记某个点 xx 已经在 SetSet 集合中,这样在其他指向 xx 的点更新到 xx 的时候,就不必再重复判断。 
但是这种做法是多余的,因为如果 xx 已经在 SetSet 集合中,则指向 xx 的 yy 点必然无法更新 disxdisx 的值,所以实际上可以放任不管。

堆优化时间复杂度

从上面可以看出,这个代码应该类似于 bfsbfs 的代码,只是将队列改为优先队列(小顶堆),这样就能保证优先弹出的是距离 ss 最短的(但不能保证不在集合 SetSet 中),整体的大循环应该是每个点都进入队列(不只一次)然后出队列,这个次数由图上的边数决定,为 O(m)O(m)(其中 mm 为图上边的数量),而在堆中,最多会被存放 mm 个数据点,所以小循环内应该是 O(logm)O(log⁡m),所以整体的时间复杂度为 O(mlogm)O(mlog⁡m)。

O(mlogm)O(mlog⁡m) 代码

const int maxn = 1000 + 100;
struct Node {
    int pos;
    int dis;
    Node() {}
    Node(int p, int d) {
        pos = p;
        dis = d;
    }
};
// 由于优先队列默认为大顶堆,所以重载小于号要用 a.dis > b.dis,来起到小顶堆的作用
bool operator<(const Node &a, const Node &b) {
    return a.dis > b.dis;
}
int n;
int dis[maxn];
vector<Node> G[maxn];
priority_queue<Node> que;

void dij(int s) {
    // 初始化距离
    fill(dis, dis + n + 1, INT_MAX);
    dis[s] = 0;
    que.push(Node(s, 0));
    while(!que.empty()) {
        Node tmp = que.top();
        que.pop();
        // 更新节点 tmp 的邻接节点,若邻接节点被更新,则将节点和 dis 加入小顶堆
        int len = G[tmp.pos].size();
        for(int i = 0; i < len; ++i) {
            int pos = G[tmp.pos][i].pos;
            int d = G[tmp.pos][i].dis;
            if(dis[pos] > tmp.dis + d) {
                dis[pos] = tmp.dis + d;
                que.push(Node(pos, dis[pos]));
            }
        }
    }
}

两种写法的比较

两种写法最大的区别在于时间复杂度,第一种是 O(n2)O(n2),第二种是 O(mlogm)O(mlog⁡m),在大多数情况下都可以用第二种写法(例如题目给出 mm 的数据范围在 [1,106][1,106] 内),但是在节点数 nn 比较大的完全图(完全图的边数为 m=C2n=n×(n−1)2m=Cn2=n×(n−1)2,)下,第二种的时间复杂度就为 O(n2logn2)O(n2log⁡n2),这样第二种写法的时间复杂度就会比第一种大,在 nn 超过 10001000 时,完全图情况下就只能用第一种写法,因此朴素写法并不是没有用的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值