dijkstra+堆优化

昨天看jupyter操作时,偶然翻到了markdown这种标记格式,正好今天算法课老师在贪心模块讲到了Dijkstra算法。一时兴起,用它写我的第一篇blog。本文代码部分大多解约刘汝佳的紫书。如有疏漏,敬请指正。

dijkstra原理

dijkstra用于求解单源最短路径问题。同时适用于有向图与无向图

首先是几个概念

  1. dijkstra算法只能用于正权图,也就是说,边的距离都是正数。
  2. 我们定义集合V是全部节点的集合,集合E是全部边的集合集合,S是(已经找>到了原点F到它的最短距离的节点构成的集合。那么问题的求解就是使S逐渐扩大,当S=V时问题解决。
  3. dis数组记录各个节点到原点F的最短距离; 通过标记数组vis在代码中区分S与V-S集合; pre数组记录每个节点的前一个节点,可以回溯得到对应的最短路径(类似递归,你要找U->V到最短路径,不必一口气知道路径上一次是哪些节点,只需要知道V的前一个节点是什么,再利用它做V,不断得到前一个节点就行,当到达U时可以退出递归)

Dijkstra是一个贪心模型。其贪心选择性质表示如下:

为完成任务我们要不断地选择节点加入S中。 每次选择的节点T具有这样的性质:T是集合V-S中离原点F最近的点,即dis[T]最小。因为是正权图,因此不会有中间节点U的存在使S->T之间的距离比当前更短(单单S->U的距离就大于S->T了)。即我们选择T是全局正确的。
选择出T后要将T放入集合S,同时利用T可以作为路途上的中间节点去更新dis数组与pre数组。

每次这样选择|V|次T就可以得到全局最优解


未优化的Dijkstra

for (int i = 0; i < n; i++)
 {
     //寻找dis最小的节点u,m存放当前最短距离
     int m = inf, u;
     for (int j = 1; j <= n; j++)
     {
         if (!vis[j] && dis[j] < m)
         {
            m = dis[j];
            u = j;  
         }
     }
     //标记u并利用它更新dis数组,标记其前一个节点
     vis[u] = 1;
     for (int j = 1; j <= n; j++)
     {
         if (dis[j] > dis[u] + map[u][j])
         {
             pre[j] = u;
             dis[j] = dis[u] + map[u][j];
         }
     }
 }

外层for循环O(n),循环内第一个for寻找dis最小的节点用时O(n),第二个for循环做更新用时O(n),时间复杂度为O(n^2) + O(n^2), 可通过邻接表存储图,使第二个for循环复杂度降低,但最终时间复杂度仍为O(n^2)


堆优化的Dijkstra

可以看出制约Dijkstra的时间复杂度的环节在找dis最小的节点上。为此我们可以引入一个最大堆,STL为priority_queue,方便每次取节点

图的存储

边中存放起点s,终点e和边权len,Edges是所有边的集合,可利用边在Edges中的下标作为边的序号。二维vector G是邻接表,每一行记录与节点U关联的边的序号。这种方式拓展到别的图论算法也十分适用。
节点中存放编号和dis信息。

为配合优先队列的使用,要自定义一个小于运算符。优先队列默认是一个最大 堆,若i < j, i排在后面j后面

priority_queue<Node> que;

struct Edge
{
    int s, e, len;
    Edge(int a, int b, int c) : s(a), e(b), len(c) {}
    Edge() {}
} edges[maxn];
vector<int> G[maxn];

struct Node
{
    int num, dis;
    Node(int a, int b) : num(a), dis(b) {}
    bool operator<(const Node &t) const
    {
        return dis < t.dis;
    }
};

代码实现

void dijkstra(int s)
{
    //记得初始化上面的几个数组
    memset(dis, inf, sizeof(dis));
    memset(vis, 0, sizeof(vis));
    memset(pre, 0, sizeof(pre));
    pre[s] = -1;
    dis[s] = 0;
    //与bfs代码结构十分类似
    que.push(Node(s, 0));
    while (!que.empty())
    {
        Node tmp = que.top();
        que.pop();
        int u = tmp.num;
        //需要强调的是,节点中很可能重复放入同一个节点,出现这种情况时只需处理一次即可
        if (vis[u])
            continue;
        vis[u] = 1;
        for (int i = 0; i < G[u].size(); i++)
        {
            //注意边是怎么选出来的
            Edge& nowE = edges[G[u][i]];
            int v = nowE.e;
            if (dis[v] > dis[u] + nowE.len)
            {
                dis[v] = nowE.len + dis[u];
                que.push(Node(v, dis[v]));
                pre[v] = u;
            }
        }
    }
}

时间复杂度

O(Elog(V))。while循环体内的操作可近似视为只有for循环操作。因为是邻接表存储,要进行E次dis更新,最差情况下,每次更新都要进行优先队列的插入删除操作,优先队列操作是**O(logV)**的。
这里没有采用外循环乘内循环的方式,而是将两个循环合并看做一种操作

两种trace_back

递归的程序开销大,可以将其转为迭代,用vector存放路上的节点之后倒序输出。


void trace_back(int u)
{
    if (pre[u] == -1)
    {
        cout << u;
        return; 
    }
    else
    {
        trace_back(pre[u]);
        cout << "->" << u;
    }
}
void trace_back(int u)
{
    vector<int> F;
    while (u != -1)
    {
        F.push_back(u);
        u = pre[u];
    }
    bool first = 1;
    for (int i = F.size() - 1; i >= 0; i--)
    {
        if (first)
        {
            first = 0;
            cout << F[i];
        }
        else
            cout << "->" << F[i];

    }
    cout << endl;
}

又臭又长,谢谢您的观看。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值