Djisktra + 链式前向星建图 + PriorityQueue堆优化【附Java代码模板题解】

简介: Djisktra + 链式前向星建图 + PriorityQueue堆优化【附Java代码模板题解】

Djisktra堆优化(链式前向星)

                       🌺本文将讲解dijsktra堆优化思路,并以c++和Java代码为例讲解“链式前向星”,文末附Java实战题目代码💧      

前言

朴素算法的时间复杂度是n的平方,而一般题目给的数据都是差不多le5,这时候肯定会爆


朴素dijsktra算法可以参考这位博主的文章:算法之迪杰斯特拉(dijkstra)非常详细介绍


用堆优化来降低时间复杂度

时间复杂度降到nlogn+m

堆优化了每次找离起点最近的点的时间复杂度。


何为“链式前向星”?

如果说邻接表是不好写但效率好,邻接矩阵是好写但效率低的话,前向星就是一个相对中庸的数据结构。前向星固然好写,但效率并不高。而在优化为链式前向星后,效率也得到了较大的提升。虽然说,世界上对链式前向星的使用并不是很广泛,但在不愿意写复杂的邻接表的情况下,链式前向星也是一个很优秀的数据结构。 ——摘自《百度百科》


链式前向星其实就是静态建立的邻接表;

时间效率为O(n)、空间效率也为O(n)、遍历效率也为O(n);

对于下面的数据:第一行5个顶点、7条边。接下来是边的起点,终点和权值。如:边1 -> 2 权值为1。

    5 7
    1 2 1
    2 3 2
    3 4 3
    1 3 4
    4 1 5
    1 5 6
    4 5 7

链式前向星存的是以【1,n】为起点的边的集合,对于上面的数据输出就是:

1 //以1为起点的边的集合
1 5 6
1 3 4
1 2 1
2 //以2为起点的边的集合
2 3 2
3 //以3为起点的边的集合
3 4 3
4  //以4为起点的边的集合
4 5 7
4 1 5
5 //以5为起点的边不存在

我们先对上面的7条边进行编号第一条边是0以此类推编号【0~6】。

然后我们要知道两个变量的含义:


Next,表示与这个边起点相同的上一条边的编号。

head[ i ]数组,表示以 i 为起点的最后一条边的编号。

head数组一般初始化为-1, 为什么是 -1后面会讲到。加边函数是这样的:

//c++版本
void add_edge(int u, int v, int w)//加边,u起点,v终点,w边权
{
    edge[cnt].to = v; //终点
    edge[cnt].w = w; //权值
    edge[cnt].next = head[u];//以u为起点上一条边的编号,也就是与这个边起点相同的上一条边的编号
    head[u] = cnt++;//更新以u为起点上一条边的编号
}
//java版本
static int total = 0;//++total:记录从第一条边到最后一条边
private static void add(int start, int end, long weight) {//链式前向星的创建方法
    ends[total] = end;
    weights[total] = weight;
    next[total] = head[start];//以start为起点的上一条边的编号,即:与这个边起点相同的上一条边的编号
    head[start] = total++;//更新以start为起点的上一条边的编号
}

我们只要知道next,head数组表示的含义,根据上面的数据就可以写出下面的过程:

对于1 2 1这条边:end[0] = 2; next [0] = -1; head[1] = 0;

对于2 3 2这条边:end[1]= 3; next [1]= -1; head[2] = 1;

对于3 4 3这条边:end[2] = 4; next [2]= -1; head[3] = 2;

对于1 3 4这条边:end[3] = 3; next [3]= 0; head[1] = 3;

对于4 1 5这条边:end[4] = 1; next [4]= -1; head[4] = 4;

对于1 5 6这条边:end[5] = 5; next [5]= 3; head[1] = 5;

对于4 5 7这条边:end[6] = 5; next [6]= 4; head[4] = 6;

遍历函数是这样的:

//c++版本
for(int i = 1; i <= n; i++)//n个起点
    {
        cout << i << endl;
        for(int j = head[i]; j != -1; j = edge[j].next)//遍历以i为起点的边
        {
            cout << i << " " << edge[j].to << " " << edge[j].w << endl;
        }
        cout << endl;
    }
//java版本
static int[] head;//表示以 i 为起点的最后一条边的编号
static int[] next;//存储与当前边起点相同的上一条边的编号
static int[] ends;//存储边的终点
static long[] weights;//权值
//链式前向星的遍历方法,遍历出以x为起点的所有边
for (int i = head[x]; i != -1; i = next[i]) {//i表示:第 i 条边
    System.out.println(i + "这条边的终点:" + ends[i] + "这条边的权值:" + weights[i]);
}

第一层for循环是找每一个点,依次遍历以【1,n】为起点的边的集合。第二层for循环是遍历以 i 为起点的所有边,k首先等于head[ i ],注意head[ i ]中存的是以 i 为起点的最后一条边的编号。然后通过next[ j ]来找下一条边的编号。我们初始化head为-1,所以找到你最后一个边(也就是以 i 为起点的第一条边)时,你的next[ j ]为 -1作为终止条件。

堆优化dijsktra代码实现

C++实现步骤(参考其他博主,并非本人总结)

priority_queue默认的是大根堆

priority_queue支持小根堆的一种方法

priority_queue<int,vector<int>,greater<int>> q;

可以看到,我们把距离放在pair对的第一个,把对应的点放在pair对的第二个,这是因为小根堆的比较对象是pair对的第一个元素

而我们正是要对距离排序,把离起点最近的距离放在堆顶

算法思路:

1.链式前向星存边

2.采用优先队列和c++中的二元组pair,它相当于一个包含两个变量的结构体,所以这里也可以用结构体实现,使用重载运算符(结构体的内嵌比较函数),但是我不太会,所以就没用

3.每次离原点最近的点都放在堆顶,并且用vis数组记录访问过的点,防止来回兜圈

4.弹出堆顶,搜索堆顶的所有连边,如果从起点到v的距离大于从从起点到u的距离+从u到v的距离,那么就更新最小距离,把它对应的点和距离加入队列

5.遍历,输出值即可

代码实现:

#include<bits/stdc++.h>
#define inf 0x3f3f3f3f
using namespace std;
const int maxn = 2e5;
struct node
{
    int v,next,w;
}edge[maxn];
int cnt;
bool vis[maxn];
int dis[maxn],head[maxn];
void add(int u,int v,int w)//起点,终点,距离,链式前向星建图 
{
    edge[cnt].w=w;
    edge[cnt].v=v;
    edge[cnt].next=head[u];
    head[u]=cnt++;
}
void dijkstra(int n)
{
    priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int,int>>>q;
    memset(dis,inf,sizeof dis);
    q.push({0,1});
    dis[1]=0;
    while(!q.empty())
    {
        pair<int,int> temp = q.top();//记录堆顶,即堆内最小的边并将其弹出 
        q.pop();
        int u = temp.second;//点 
        if(vis[u]) continue;//如果被访问过,就跳过 
        vis[u]=true;//标记 
        for(int i = head[u];i!=-1;i=edge[i].next)//搜索堆顶的所有连边 
        {
            int v = edge[i].v;
            if(dis[v]>dis[u]+edge[i].w)//松弛操作 
            {
                dis[v]=dis[u]+edge[i].w;
                q.push({dis[v],v});//把新遍历的点加到堆中 
            }
        }
    }
    //if(dis[n]==inf) cout<<-1<<endl;
    //else cout<<dis[n]<<endl;
    for(int i=1;i<=n;i++)
    cout<<dis[i]<<endl;
}
int main()
{
    int n,m,u,v,w;
    cin>>n>>m;
    memset(head,-1,sizeof(head));
    for(int i = 0;i<m;i++)
    {
        cin>>u>>v>>w;
        add(u,v,w);
    }
    dijkstra(n);
    return 0;
}
/*  
    测试数据 
    6 9
  1 2 1
  1 3 12
  2 3 9
  2 4 3
  3 5 5
  4 3 4
  4 5 13
  4 6 15
  5 6 4*/

Java实现步骤 以一道蓝桥杯模板题为例进行讲解(代码含注释)

题目:蓝桥王国 【dijsktra】
代码详解

import java.util.*;
public class Main {
    static int[] head, next, ends;
    static long[] weights, dis;//权值和结果集
    static int n, m, total;//n个顶点,m条边,++total:从第一条边到最后一条边
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        n = scanner.nextInt();
        m = scanner.nextInt();
        head = new int[m + 1];//表示以 i 为起点的最后一条边的编号
        next = new int[m + 1];//存储与当前边起点相同的上一条边的编号
        ends = new int[m + 1];//存储边的终点
        weights = new long[m + 1];//存储边的权值
        dis = new long[n + 1];//存储最终结果
        Arrays.fill(head, -1);//初始化
        for (int i = 0; i < m; i++) {
            int start = scanner.nextInt();
            int end = scanner.nextInt();
            long weight = scanner.nextLong();
            add(start, end, weight);
        }
        dijkstra(1);
        for (int i = 1; i <= n; i++)
            if (dis[i] == Long.MAX_VALUE)
                System.out.print(-1 + " ");
            else
                System.out.print(dis[i] + " ");
    }
    private static void dijkstra(int startPoint) {
        for (int i = 1; i <= n; i++) {
            dis[i] = Long.MAX_VALUE;//初始化时,应当赋上最坏的情况
        }
        Queue<Node> queue = new PriorityQueue<>((o1, o2) -> (int) (o1.dis - o2.dis));
        queue.offer(new Node(startPoint, weights[startPoint]));
        dis[startPoint] = 0;//起始位置,应当赋上最好的情况
        while (!queue.isEmpty()) {
            Node x = queue.poll();//当前点
            //链式前向星的遍历方法,遍历出以x为起点的所有边
            for (int i = head[x.num]; i != -1; i = next[i]) {//i表示:第 i 条边
                int j = ends[i];//第 i 条边的终点
                if (dis[j] > dis[x.num] + weights[i]) {//如果length(起点-->终点) > length(起点 --> 当前点) + length(当前点 --> 终点)
                    dis[j] = dis[x.num] + weights[i];//更新起点到终点的最短距离
                    queue.offer(new Node(j, dis[j]));//并将这个终点入队,以便之后通过该点访问其他顶点
                }
            }
        }
    }
    static class Node {
        int num;
        long dis;
        public Node(int num, long dis) {
            this.num = num;
            this.dis = dis;
        }
    }
    private static void add(int start, int end, long weight) {//链式前向星的创建方法
        ends[total] = end;
        weights[total] = weight;
        next[total] = head[start];//以start为起点的上一条边的编号,即:与这个边起点相同的上一条边的编号
        head[start] = total++;//更新以start为起点的当前边的编号
    }
}

总结

堆优化可以有效降低朴素dijsktra的时间复杂度,OI必备,希望大家仔细理解后将其掌握。

相关文章
|
1天前
|
Java
代码实例演示Java字符串与输入流互转
代码实例演示Java字符串与输入流互转
|
2天前
|
存储 安全 Java
掌握8条泛型规则,打造优雅通用的Java代码
掌握8条泛型规则,打造优雅通用的Java代码
掌握8条泛型规则,打造优雅通用的Java代码
|
2天前
|
安全 Java 程序员
【Java多线程】面试常考——锁策略、synchronized的锁升级优化过程以及CAS(Compare and swap)
【Java多线程】面试常考——锁策略、synchronized的锁升级优化过程以及CAS(Compare and swap)
6 0
|
2天前
|
Java 程序员 图形学
程序员教你用代码制作飞翔的小鸟--Java小游戏,正好拿去和给女神一起玩
《飞扬的小鸟》Java实现摘要:使用IntelliJ IDEA和JDK 16开发,包含小鸟类`Bird`,处理小鸟的位置、速度和碰撞检测。代码示例展示小鸟图像的加载、绘制与旋转。同时有`Music`类用于循环播放背景音乐。游戏运行时检查小鸟是否撞到地面、柱子或星星,并实现翅膀煽动效果。简单易懂,可直接复制使用。
|
2天前
|
缓存 Java 数据库
Java并发编程中的锁优化策略
【5月更文挑战第9天】 在高负载的多线程应用中,Java并发编程的高效性至关重要。本文将探讨几种常见的锁优化技术,旨在提高Java应用程序在并发环境下的性能。我们将从基本的synchronized关键字开始,逐步深入到更高效的Lock接口实现,以及Java 6引入的java.util.concurrent包中的高级工具类。文中还会介绍读写锁(ReadWriteLock)的概念和实现原理,并通过对比分析各自的优势和适用场景,为开发者提供实用的锁优化策略。
3 0
|
3天前
|
数据库连接
java+ssm+vue代码视频学习讲解
java+ssm+vue代码视频学习讲解
5 0
|
3天前
|
SQL 缓存 算法
优化你的Java代码:性能调优技巧
优化你的Java代码:性能调优技巧
8 0
|
1月前
|
Java
使用Java代码打印log日志
使用Java代码打印log日志
253 1
|
Java BI API
在Java代码中打日志需要注意什么?
日志是什么?日志是你在代码运行时打印出来的一些数据和记录,是快速排查问题的好帮手,是撕逼和甩锅的利器!
328 0
|
缓存 Java 网络架构
别在 Java 代码里乱打日志了,这才是正确的打日志姿势!
别在 Java 代码里乱打日志了,这才是正确的打日志姿势!
135 0