【第五节】C/C++数据结构之图

news2024/10/7 7:28:21

目录

一、图的基本概念

1.1 图的定义

1.2 图的其他术语概念

二、图的存储结构

2.1 邻接矩阵

2.2 邻接表

三、图的遍历

3.1 广度优先遍历

3.2 深度优先遍历

四、最小生成树

4.1 最小生成树获取策略

4.2 Kruskal算法

4.3 Prim算法

五、最短路径问题

5.1 Dijkstra算法

5.2 Bellman-Ford算法

5.3 Floyd-Warshall算法

六、AOV网络和AOE网络

6.1 AOV网络(Activity On Vertex Network)

6.2 AOE网络(Activity On Edge Network)

6.3 异同点

七、总结


一、图的基本概念

1.1 图的定义

        数据结构中图的定义是:图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为G(V, E),其中G表示一个图,V表示顶点的集合(也称为顶集或Vertices Set),E表示顶点之间边的集合(也称为边集或Edges Set)。

  • 顶点(Vertex):图中的数据元素,也称为节点或点。
  • 边(Edge):顶点之间的逻辑关系,用来表示两个顶点之间的连接关系。在无向图中,边没有方向,用无序偶对(u, v)来表示;在有向图中,边具有方向,用有序偶<u, v>来表示,其中u称为弧尾(Tail),v称为弧头(Head)。

此外,图还有以下一些相关的概念和定义:

  • 有向图(Directed Graph):图中任意两个顶点之间的边都是有向边。
  • 无向图(Undirected Graph):图中任意两个顶点之间的边都是无向边。
  • 阶(Order):图G中点集V的大小称作图G的阶。
  • 子图(Sub-Graph):当图G'=(V', E'),其中V'包含于V,E'包含于E,则G'称作图G=(V, E)的子图。
  • 度(Degree):一个顶点的度是指与该顶点相关联的边的条数。在无向图中,顶点的度就是其边的数量;在有向图中,顶点的度分为入度和出度,入度是指以其为终点的边数,出度是指以该顶点为起点的边数。

1.2 图的其他术语概念

完全图:在无向图中,假设顶点数量为N,那么有N*(N-1)/2,条边,即任意两个顶点之间都有边相连,那么就称其为无向完全图。在有向图中,任意两个顶点之间都有两条指向相反的连接线,即有N个顶点的有向图有N*(N-1)条边,称这样的图结构为有向完全图。

邻接顶点:在无向图中,若存在边(A,B),则顶点A与顶点B互为邻接顶点。在有向图中,若存在边<A,B>,则称顶点A邻接到顶点B,而顶点B邻接自顶点A,表示A指向B的连接关系。

顶点的度:顶点的度定义为与该顶点相连的边的数量,记作deg(V),代表顶点V的度。在有向图中,顶点V的度为其入度与出度之和。出度是以V为起点的边的数量,记作outdeg(V);入度是以V为终点的边的数量,记作indeg(V)。因此,deg(V) = outdeg(V) + indeg(V)。在无向图中,由于边无方向,顶点的度等同于其出度和入度,即deg(V) = outdeg(V) = indeg(V)。

路径:在图G = { V, E }中,如果从顶点vi出发,能够经过一系列顶点到达顶点vj,则这一系列顶点构成的序列称为从顶点vi到顶点vj的路径。

路径长度:对于无权图,路径长度指的是从源顶点到目标顶点所经过的边的数量。而在带权图中,路径长度则是源顶点到目标顶点所经过的边的权值之和。权值通常作为边的附加信息,用于表示某种特定的度量或属性。

简单路径与回路:假设顶点v1和vm相连,路径v1, v2, ... , vm没有重复的顶点,那么称v1, v2, ... , vm为简单路径,如果v1,v2, ..., v1,路径从起始点开始又回到了起始点,那么就是回路。

无向图的连通性

路径:在无向图 G=(V,{E}) 中由顶点 v v‘’ 的顶点序列。
回路或环:第一个顶点和最后一个顶点相同的路径。
简单回路或简单环:除第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路。
连通:顶点 v v‘’ 之间有路径存在
连通图:无向图图 G 的任意两点之间都是连通的,则称 G 是连通图。
连通分量:极大连通子图

有向图的连通性

路径:在有向图 G=(V,{E}) 中由顶点 v 经有向边至 v‘’ 的顶点序列。
回路或环:第一个顶点和最后一个顶点相同的路径。
简单回路或简单环:除第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路。
连通:顶点 v v‘’ 之间有路径存在
强连通图:有向图图 G 的任意两点之间都是连通的,则称 G 是强连通图。
强连通分量:极大连通子图

最小生成树:对于连通图,能够将每个顶点连接在一起的最小连通子图,称为最小生成树,对于有N个顶点的连通图,其最小生成树应该有N-1条边。

二、图的存储结构

图的存储结构主要有两种:邻接矩阵(Adjacency Matrix)和邻接表(Adjacency List)。

2.1 邻接矩阵

  • 定义:邻接矩阵使用一个二维数组来存储图中顶点间的关系(边或弧)。对于无向图,邻接矩阵是对称的;对于有向图,邻接矩阵可能不是对称的。
  • 特点:
    • 无向图的邻接矩阵对称且唯一。
    • 有向图的邻接矩阵的第i行非零元素个数为第i个顶点的出度;第j列非零元素个数为第j个顶点的入度1。
    • 对于带权图,邻接矩阵的元素可以用来存储权值;如果两结点无连接,可以用无穷大(∞)表示。
  • 适用场景:稠密图(即边数较多的图)更适合用邻接矩阵存储。

邻接矩阵的优缺点:邻接矩阵能够快速查找两个顶点是否直接相连,但是如果边较少的时候,邻接矩阵中会有大量的\oe\infty浪费空间,且使用邻接矩阵不容易求得两个顶点之间的路径。

代码实现

#include <iostream>
#include <vector>
#include <unordered_map>
#include <optional>
#include <stdexcept>

namespace Matrix
{
    // Graph 类模板定义
    // V - 顶点类型
    // W - 权重类型
    // MAX_W - 权重的最大值,默认为 INT_MAX
    // Direction - 是否为有向图,默认为无向图 (false)
    template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
    class Graph
    {
    public:
        // 构造函数,初始化图的顶点
        // arr - 顶点数组
        // size - 顶点数组的大小
        Graph(const V* arr, size_t size)
        {
            for (size_t i = 0; i < size; ++i)
            {
                _vertex.push_back(arr[i]);
                _valIndexMap[arr[i]] = i;
            }
            
            // 初始化邻接矩阵,所有权重设为 MAX_W
            _edges.resize(size, std::vector<W>(size, MAX_W));
        }

        // 获取顶点值对应的索引
        // val - 顶点值
        // 返回顶点值对应的索引,如果顶点不存在则返回 std::nullopt
        std::optional<size_t> GetIndex(const V& val) const
        {
            auto pos = _valIndexMap.find(val);
            if (pos != _valIndexMap.end())
            {
                return pos->second;
            }
            return std::nullopt; // 顶点不存在
        }

        // 添加边到图中
        // src - 源顶点
        // dst - 目标顶点
        // w - 边的权重
        void AddEdge(const V& src, const V& dst, const W& w)
        {
            auto srci = GetIndex(src);
            auto dsti = GetIndex(dst);

            // 检查顶点是否存在
            if (!srci || !dsti)
            {
                throw std::runtime_error("One or both vertices not found in the graph.");
            }

            // 添加边
            _edges[*srci][*dsti] = w;

            // 如果是无向图,还需要添加反向边
            if (!Direction)
            {
                _edges[*dsti][*srci] = w;
            }
        }

        // 打印邻接矩阵
        void Print() const
        {
            size_t n = _vertex.size();
            for (size_t i = 0; i < n; ++i)
            {
                for (size_t j = 0; j < n; ++j)
                {
                    // 如果权重为 MAX_W,则打印 '*' 表示无边
                    if (_edges[i][j] == MAX_W) std::cout << "*  ";
                    else std::cout << _edges[i][j] << "  ";
                }
                std::cout << std::endl;
            }
        }

    private:
        std::vector<V> _vertex; // 顶点数组
        std::unordered_map<V, size_t> _valIndexMap; // 顶点到索引的映射
        std::vector<std::vector<W>> _edges; // 邻接矩阵
    };
}

 

        整个类的设计侧重于使用邻接矩阵来表示图,这在顶点数量较少时很有效,但对于边数远少于顶点对数的稀疏图,这种表示方法可能会浪费大量内存。此外,类模板的灵活性允许用户定义顶点和边权重的数据类型,并选择图的方向性。

2.2 邻接表

  • 定义:邻接表是一种顺序分配和链式分配相结合的存储结构。如果表头结点所对应的顶点存在相邻顶点,则把相邻顶点依次存放于表头结点所指向的单向链表中。
  • 特点:
    • 邻接表是为了节省存储空间而引入的,对于稀疏图(即边数较少的图),相对于邻接矩阵,无需耗费大量存储空间。
    • 对于有向图,还有逆邻接表的概念,逆邻接表可以得到图的入度。
  • 适用场景:稀疏图更适合用邻接表存储

代码示例

#include <iostream>
#include <vector>
#include <unordered_map>
#include <optional>
#include <stdexcept>

namespace LinkTable
{
    // Edge 结构体代表图中的边
    template<class W>
    struct Edge
    {
        size_t _dsti;  // 目标顶点在数组中的下标
        W _w;          // 边的权重
        Edge* _next;   // 链表中的下一条边

        // 构造函数初始化边
        Edge(size_t dsti, const W& w)
            : _dsti(dsti), _w(w), _next(nullptr)
        { }
    };

    // Graph 类模板定义
    template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
    class Graph
    {
    public:
        typedef Edge<W> EdgeType;

        // 构造函数,初始化图的顶点
        Graph(const V* arr, size_t size)
        {
            for (size_t i = 0; i < size; ++i)
            {
                _vertex.emplace_back(arr[i]);
                _valIndexMap[arr[i]] = i;
            }

            // 初始化邻接表
            _edges.resize(size, nullptr);
        }

        // 析构函数,负责释放所有动态分配的边
        ~Graph()
        {
            for (auto& edge : _edges)
            {
                while (edge)
                {
                    EdgeType* temp = edge;
                    edge = edge->_next;
                    delete temp;
                }
            }
        }

        // 获取顶点值对应的索引
        std::optional<size_t> GetIndex(const V& val) const
        {
            auto pos = _valIndexMap.find(val);
            if (pos != _valIndexMap.end())
            {
                return pos->second;
            }
            return std::nullopt; // 顶点不存在
        }

        // 添加边到图中
        void AddEdge(const V& src, const V& dst, const W& w)
        {
            auto srci = GetIndex(src);
            auto dsti = GetIndex(dst);

            if (!srci || !dsti)
            {
                throw std::runtime_error("One or both vertices not found in the graph.");
            }

            // 添加边从src到dst
            EdgeType* edge1 = new EdgeType(*dsti, w);
            edge1->_next = _edges[*srci];
            _edges[*srci] = edge1;

            // 如果是无向图,添加边从dst到src
            if (!Direction)
            {
                EdgeType* edge2 = new EdgeType(*srci, w);
                edge2->_next = _edges[*dsti];
                _edges[*dsti] = edge2;
            }
        }

        // 打印邻接表
        void Print() const
        {
            size_t n = _edges.size();
            for (size_t i = 0; i < n; ++i)
            {
                std::cout << _vertex[i] << ":";
                EdgeType* cur = _edges[i];
                while (cur)
                {
                    std::cout << " -> [" << _vertex[cur->_dsti] << ":" << cur->_w << "]";
                    cur = cur->_next;
                }
                std::cout << " -> nullptr" << std::endl;
            }
        }

    private:
        std::vector<V> _vertex;   // 顶点数组
        std::unordered_map<V, size_t> _valIndexMap;   // 顶点到索引的映射
        std::vector<EdgeType*> _edges;   // 邻接表
    };
}

 

        这个 Graph 类的设计使用邻接表来表示图,这比邻接矩阵更适合表示稀疏图,因为它可以减少内存占用,并可能提高遍历边的效率。与之前的邻接矩阵实现相比,这种实现方式在处理大量顶点和边时通常更高效。

三、图的遍历

3.1 广度优先遍历

        广度优先遍历(Breadth-First Search, BFS)是一种用于遍历或搜索树或图的算法。这个算法从图的某一顶点(源顶点)开始,首先访问起始顶点,然后访问其所有相邻顶点,接着再访问这些相邻顶点的未访问过的相邻顶点,依此类推,直到所有顶点都被访问为止。

广度优先遍历通常使用队列(Queue)来实现。下面是广度优先遍历的基本步骤:

  1. 创建一个队列Q,并将起始顶点v加入队列Q。
  2. 创建一个集合visited来记录已被访问的顶点,并将v标记为已访问。
  3. 当队列Q非空时,重复以下步骤:
    a. 从队列Q中取出一个顶点u。
    b. 访问顶点u。
    c. 对于u的每一个未被访问过的相邻顶点v,将v加入队列Q,并标记v为已访问。
  4. 当队列Q为空时,算法结束。此时,所有可达的顶点(从起始顶点开始)都已被访问。

示例代码:

#include <iostream>
#include <vector>
#include <queue>
#include <unordered_map>
#include <optional>

// ...(其他代码保持不变)...

template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
class Graph
{
    // ...(其他成员和方法保持不变)...

    // 图的广度优先遍历,src为遍历起点
    void BFS(const V& src)
    {
        std::optional<size_t> srcIndexOpt = GetIndex(src);  // 获取起点的索引
        if (!srcIndexOpt.has_value()) {
            throw std::runtime_error("The source vertex does not exist in the graph.");
        }
        size_t srcIndex = srcIndexOpt.value();  // 起点索引

        size_t n = _vertex.size();  // 顶点个数
        std::vector<bool> visited(n, false);  // 记录每个顶点是否已访问
        std::queue<size_t> q;  // 队列,用于存储将要访问的顶点索引
 
        q.push(srcIndex);  // 将起点索引入队
        visited[srcIndex] = true;  // 标记起点为已访问

        size_t level = 0;  // 当前层级
        // 当队列不为空时,循环执行
        while (!q.empty())
        {
            size_t levelSize = q.size();  // 当前层的顶点数量
            std::cout << "第 " << level << " 层:";
            for (size_t i = 0; i < levelSize; ++i)
            {
                size_t currentVertexIndex = q.front();  // 获取队列前端的顶点索引
                q.pop();  // 将当前顶点索引从队列中移除
                std::cout << _vertex[currentVertexIndex] << " ";  // 打印当前顶点

                // 遍历当前顶点的所有邻接边
                for (EdgeType* edge = _edges[currentVertexIndex]; edge != nullptr; edge = edge->_next)
                {
                    size_t adjacentIndex = edge->_dsti;  // 获取邻接顶点的索引
                    // 如果邻接顶点未被访问,则将其加入队列
                    if (!visited[adjacentIndex])
                    {
                        visited[adjacentIndex] = true;  // 标记邻接顶点为已访问
                        q.push(adjacentIndex);  // 将邻接顶点索引入队
                    }
                }
            }
            std::cout << std::endl;
            level++;  // 层级加一
        }
    }
};

// ...(其他代码保持不变)...

3.2 深度优先遍历

        图的深度优先遍历(Depth-First Search, DFS)是一种用于遍历或搜索树或图的算法。这个算法会尽可能深地搜索图的分支。当节点v的所在边都已被探寻过,搜索将回溯到发现节点v的那条边的起始节点。这一过程一直进行到已发现从源节点可达的所有节点为止。如果还存在未被发现的节点,则选择其中一个作为源节点并重复以上过程,整个进程反复进行直到所有节点都被访问为止。

        深度优先遍历通常使用栈(Stack)来实现,但也可以使用递归。以下是深度优先遍历的基本步骤:

  1. 创建一个集合visited来记录已被访问的顶点。
  2. 选择一个起始顶点v,并将其标记为已访问。
  3. 递归地(或使用栈)访问v的所有未访问过的相邻顶点。对于每个这样的顶点u,如果u未被访问过,则标记u为已访问,并递归地(或使用栈)访问u的所有未访问过的相邻顶点。
  4. 当所有可访问的顶点都已被访问时,算法结束。

代码示例:

#include <iostream>
#include <vector>
#include <unordered_map>
#include <optional>

// ...(其他代码保持不变)...

template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
class Graph
{
    // ...(其他成员和方法保持不变)...

    // 深度优先遍历算法子函数
    // curi为当前遍历节点的下标,visited为记录节点是否被遍历过的数组
    void _DFS(size_t curi, std::vector<bool>& visited)
    {
        // 标记当前节点为已访问
        visited[curi] = true;
        // 输出当前节点
        std::cout << _vertex[curi] << " ";

        // 遍历与当前节点相连的所有节点
        for (EdgeType* edge = _edges[curi]; edge != nullptr; edge = edge->_next)
        {
            size_t u = edge->_dsti;  // 获取相连节点的索引
            // 如果相连节点未访问,则递归调用DFS
            if (!visited[u])
            {
                _DFS(u, visited);
            }
        }
    }

    // 深度优先遍历函数,src为起始点
    void DFS(const V& src)
    {
        // 获取起始点的索引,如果不存在则抛出异常
        std::optional<size_t> srcIndexOpt = GetIndex(src);
        if (!srcIndexOpt.has_value()) {
            throw std::runtime_error("The source vertex does not exist in the graph.");
        }
        size_t srcIndex = srcIndexOpt.value();

        // 初始化访问标记数组
        size_t n = _vertex.size();
        std::vector<bool> visited(n, false);

        // 从起始点开始执行DFS
        _DFS(srcIndex, visited);
    }
};

// ...(其他代码保持不变)...

四、最小生成树

4.1 最小生成树获取策略

        所谓最小生成树,是对于无向连通图的概念,即:路径权值和最小的、连通的子图。这就要求最小生成树满以下条件:

    如果原图有N个顶点,那么其最小生成树有N-1条边。
    最小生成树中的边不能构成回路。
    必须是满足前两个条件,边权值和最小的生成树。

        获取最小生成树的算法有Kruskal算法(克鲁斯卡尔算法)和Prim算法(普里姆算法),这两种算法都是采用“贪心”策略,即寻找局部最优解,即:当前图中满足一定条件的权值最小的边。但是要注意,Kruskal算法和Prim算法都是局部贪心算法,能够取得局部最优解,但是不一定获取的是全局最优解,它们获取的结果只能说是非常接近于最小生成树,而不一定就是最小生成树。


4.2 Kruskal算法

        Kruskal算法的思想就是在整个图的所有边中,筛选出权值最小的边,同时在选边的过程中避免构成环,等到筛选出N-1条边后,就可以获取最小生成树。图4.1为Kruskal算法的选边过程,其中红色加粗的线为被选择的边。

Kruskal算法核心:每次都筛选权值最小的、且不构成回路的边,加入生成树。

        通过Kruskal算法获取最小生成树需要使用 小根堆 + 并查集 来辅助进行,其中小根堆负责每次在所有尚未选取的边中筛选权值最小的边,并查集用于避免生成回路(环)。需要定义struct Edge类来记录边的属性信息,struct Edge的成员包括起始顶点下标srci、目标顶点下标dsti以及权重w,重载> 运算符,用于比较权重大小。在Kruskal算法的代码中首先要将所有的边插入小根堆,每次从堆顶拿出一条边,使用并查集检查两个顶点是否会构成环(属于同一个集合),如果不会构成环,那么就将这条边添加到生成树中去。之后,将此时的srci和dsti归并到并查集的同一集合中去以避免成环,然后选边计数器+1,进行权重累加。假设总共有N个顶点,如果选出生成树有N-1条边,说明成功获得了最小生成树,返回每个边的权重之和,否则就是获取最小生成树失败,返回MAX_W。

下面代码为Kruskal算法及其配套被调函数及自定义类型的实现,其中Graph的其余不相关函数省略

#include "UnionFindSet.hpp"
#include <iostream>
#include <vector>
#include <queue>
#include <unordered_map>
#include <algorithm>
#include <cassert>

namespace Matrix
{
    // 自定义类型 -- 顶点与顶点之间的边
    template<class W>
    struct Edge
    {
        size_t _srci;   // 源顶点下标
        size_t _dsti;   // 目标顶点下标
        W _w;           // 权重

        // 构造函数
        Edge(size_t srci, size_t dsti, const W& w)
            : _srci(srci), _dsti(dsti), _w(w)
        { }

        // 大于比较运算符重载函数,用于构建小根堆
        bool operator>(const Edge<W>& w) const
        {
            return _w > w._w;
        }
    };

    template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
    class Graph
    {
        typedef Edge<W> Edge;
        typedef Graph<V, W, MAX_W, Direction> Self;

    public:
        // 强制生成默认构造函数
        Graph() = default;

        // ....
        // 与Kruskal算法不相关的成员函数全部省略

        // 根据下标添加边的函数
        void _AddEdge(size_t srci, size_t dsti, const W& w)
        {
            _edges[srci][dsti] = w;
            if (!Direction) // 如果图是无向的,则需要在邻接矩阵中添加两个方向的边
            {
                _edges[dsti][srci] = w;
            }
        }

        // Kruskal算法获取最小生成树
        // 返回值为最小生成树的权值和,minTree为输出型参数,用于获取最小生成树
        // 如果无法获取最小生成树,那么就返回MAX_W
        W Kruskal(Self& minTree)
        {
            // 初始化minTree中的每个成员
            size_t n = _vertex.size();
            minTree._vertex = _vertex;
            minTree._valIndexMap = _valIndexMap;
            minTree._edges.resize(n, std::vector<W>(n, MAX_W));

            // 将所有边的信息(源顶点、目标顶点、权值)插入到小根堆中去
            std::priority_queue<Edge, std::vector<Edge>, std::greater<Edge>> minHeap;
            for (size_t i = 0; i < n; ++i)
            {
                for (size_t j = i + 1; j < n; ++j)
                {
                    if (_edges[i][j] != MAX_W)
                    {
                        minHeap.emplace(i, j, _edges[i][j]);
                    }
                }
            }

            UnionFindSet ufs(n);    // 用于避免构成回路的并查集
            size_t count = 0;       // 计数器,用于统计选取了多少条边
            W totalW = W();         // 总权值计数器

            std::cout << "Kruskal开始选边:" << std::endl;
            while (!minHeap.empty() && count < n - 1)
            {
                // 小根堆堆顶为当前尚未被筛选且权值最小的边
                Edge curEdge = minHeap.top();
                minHeap.pop();

                // 检查当前两个节点是否位于同一并查集的集合中
                if (!ufs.InSet(curEdge._srci, curEdge._dsti))
                {
                    std::cout << "[" << _vertex[curEdge._srci] << "->" << _vertex[curEdge._dsti] << "]:" << curEdge._w << std::endl;

                    // 向最小生成树中添加srci->dsti的边
                    minTree._AddEdge(curEdge._srci, curEdge._dsti, curEdge._w);

                    // 将srci和dsti归为同一集合
                    ufs.Union(curEdge._srci, curEdge._dsti);

                    // 选边计数器+1,权值累加
                    ++count;
                    totalW += curEdge._w;
                }
                else
                {
                    std::cout << "构成环  " << "[" << _vertex[curEdge._srci] << "->" << _vertex[curEdge._dsti] << "]:" << curEdge._w << std::endl;
                }
            }

            // 如果选择了n-1条边,那么说明获取了最小生成树,否则获取最小生成树失败
            if (count == n - 1) {
                return totalW;
            }
            else {
                return MAX_W;
            }
        }

    private:
        std::vector<V> _vertex;    // 存储顶点值的一维数组
        std::unordered_map<V, size_t> _valIndexMap;   // 顶点值与其在数组下标中的映射关系
        std::vector<std::vector<W>> _edges;        // 邻接矩阵
    };
}

并查集的实现代码如下

#pragma once

#include <vector>
#include <algorithm>

class UnionFindSet {
public:
    // 构造函数,初始化n个元素的并查集
    UnionFindSet(size_t n) : _ufs(n, -1) {}

    // 合并两个元素所在的集合
    void Union(int x1, int x2) {
        int root1 = FindRoot(x1);
        int root2 = FindRoot(x2);

        // 如果两个元素已经在同一个集合中,则无需合并
        if (root1 == root2)
            return;

        // 按秩合并,将秩较小的根节点合并到秩较大的根节点上
        if (abs(_ufs[root1]) < abs(_ufs[root2]))
            std::swap(root1, root2);

        // 更新集合的秩,并将root2的根节点指向root1
        _ufs[root1] += _ufs[root2];
        _ufs[root2] = root1;
    }

    // 查找元素x的根节点
    int FindRoot(int x) {
        int root = x;
        // 寻找根节点
        while (_ufs[root] >= 0) {
            root = _ufs[root];
        }

        // 路径压缩,将查找路径上的每个节点直接连接到根节点
        while (_ufs[x] >= 0) {
            int parent = _ufs[x];
            _ufs[x] = root;
            x = parent;
        }

        return root;
    }

    // 检查两个元素是否属于同一集合
    bool InSet(int x1, int x2) {
        return FindRoot(x1) == FindRoot(x2);
    }

    // 获取并查集中集合的数量
    size_t SetSize() {
        size_t size = 0;
        for (size_t i = 0; i < _ufs.size(); ++i) {
            if (_ufs[i] < 0) {
                // 集合的根节点的值为负数,其绝对值表示集合的大小
                size++;
            }
        }
        return size;
    }

private:
    std::vector<int> _ufs; // 并查集数组,非负值表示父节点的索引,负值的绝对值表示集合的大小
};

4.3 Prim算法

        Prim算法(普里姆算法)的思路与Kruskal算法基本一致,采用的都是贪心策略,与Kruskal算法不同的是,Prim算法会选定一个起始点src,并将已经连通的顶点和尚未被连通的顶点划分到两个集合中去,分别记为S和U,每一次筛选,都会选出从si->ui的边中权值最小的那个,由于对已经连通和尚未连通的顶点进行了划分,因此选边建立连接的过程中不需要并查集来辅助就能够避免成环。下图为Prim算法的选边过程,红色加粗的实线为被选择的边。

Prim算法的实现

// Prim算法获取最小生成树
template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
W Graph<V, W, MAX_W, Direction>::Prim(const V& src, Self& minTree) {
    // 初始化minTree的每个成员
    size_t n = _vertex.size();
    minTree._vertex = _vertex;
    minTree._valIndexMap = _valIndexMap;
    minTree._edges.resize(n, std::vector<W>(n, MAX_W));

    // 检查源顶点是否存在
    auto it = _valIndexMap.find(src);
    if (it == _valIndexMap.end()) {
        throw std::runtime_error("源顶点不存在!");
    }
    size_t srci = it->second;

    // visited数组记录每个顶点是否已经被访问
    std::vector<bool> visited(n, false);
    visited[srci] = true; // 标记源顶点为已访问

    // 使用小根堆选取最短边
    std::priority_queue<Edge, std::vector<Edge>, std::greater<Edge>> minHeap;
    // 将源顶点的所有邻边加入小根堆
    for (size_t i = 0; i < n; ++i) {
        if (_edges[srci][i] != MAX_W) {
            minHeap.emplace(srci, i, _edges[srci][i]);
        }
    }

    size_t count = 0; // 已选择的边数
    W totalW = W();   // 最小生成树的总权重

    std::cout << "Prim开始选边:" << std::endl;
    // 循环直到所有顶点都被访问或者堆为空
    while (!minHeap.empty() && count < n - 1) {
        // 获取堆顶元素(最短边)
        Edge curEdge = minHeap.top();
        minHeap.pop();

        size_t u = curEdge._srci;
        size_t v = curEdge._dsti;
        W w = curEdge._w;

        // 如果终点v未被访问,则这条边是最小生成树的一部分
        if (!visited[v]) {
            std::cout << "[" << _vertex[u] << "->" << _vertex[v] << "]:" << w << std::endl;

            // 在minTree中添加这条边
            minTree._AddEdge(u, v, w);

            // 更新访问状态,边数和总权重
            visited[v] = true;
            ++count;
            totalW += w;

            // 将新访问到的顶点v的所有邻边加入小根堆
            for (size_t k = 0; k < n; ++k) {
                if (!visited[k] && _edges[v][k] != MAX_W) {
                    minHeap.emplace(v, k, _edges[v][k]);
                }
            }
        }
    }

    // 如果选取的边数等于顶点数减一,则成功构建了最小生成树
    if (count == n - 1) {
        return totalW;
    } else {
        throw std::runtime_error("无法构建最小生成树!");
    }
}

五、最短路径问题

        在所有类型的图上,最短路径问题都是寻找从一个顶点(或一组顶点)到另一个顶点(或一组顶点)的路径,使得该路径上所有边的权重之和最小,权值非负情况。这通常通过使用适当的算法(如Dijkstra、Bellman-Ford、Floyd-Warshall等)来实现。

5.1 Dijkstra算法

        Dijkstra算法(迪杰斯特拉算法),用于求单源最短路径,即:给定一个起点,计算以这个顶点为起点,图中其余任意顶点为终点的路径中,权值之和最小的那一条路径。注意,Dijkstra算法要求不能带有负权值。

        Dijkstra算法的核心思想是贪心算法,其大致的流程为:将一个有向图G中的顶点分为S和Q两组,其中S为已经确定了最短路径的顶点,Q为尚未确定最短路径的顶点,最初先将处源顶点srci以外所有顶点都加入Q,源顶点srci加入S。每次从Q中找出一个源顶点到该顶点最小的顶点u,将其从Q中移出放入到S中,对与u相邻的顶点v进行松弛操作。所谓松弛操作,就是比较srci->u + s->v的和是否比原来srci->v的路径和小,如果是,那么就更新srci->v的最短路径,反复进行松弛操作,直到Q集合中没有顶点。下图为Dijkstra算法松弛迭代的过程,黑色填充的顶点为已经确定最短路径的顶点,灰色填充为本轮遍历的源顶点。

代码实现

        Dijkstra算法的具体实现,该函数接收三个参数,分别为起始点、最小路径dist(输出型参数)、每个顶点的父亲顶点pPath(输出型参数),这里使用pPath的目的是为了避免存储全部的路径,达到节省空间,降低算法编码难度的目的。为了观察结果,实现了PrintPath函数,用于打印顶点src到任意顶点的最短路径。

// Dijkstra算法求最短路径
// dist为路径和,pPath为每个顶点前导顶点的下标
template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
void Graph<V, W, MAX_W, Direction>::Dijkstra(const V& src, std::vector<W>& dist, std::vector<size_t>& pPath) {
    size_t n = _vertex.size();
    dist.assign(n, MAX_W);
    pPath.assign(n, std::numeric_limits<size_t>::max());

    // 获取源顶点的下标
    auto srci = GetIndex(src);
    if (srci >= n) {
        throw std::runtime_error("源顶点不存在!");
    }
    dist[srci] = 0;
    pPath[srci] = srci;

    std::vector<bool> visited(n, false);

    for (size_t k = 0; k < n; ++k) {
        // 找出未访问顶点中dist最小的
        W minDist = MAX_W;
        size_t u = std::numeric_limits<size_t>::max();
        for (size_t i = 0; i < n; ++i) {
            if (!visited[i] && dist[i] < minDist) {
                minDist = dist[i];
                u = i;
            }
        }
        
        // 所有顶点都访问过或者剩下的顶点都不可达
        if (u == std::numeric_limits<size_t>::max()) break;

        visited[u] = true;

        for (size_t v = 0; v < n; ++v) {
            if (!visited[v] && _edges[u][v] != MAX_W && dist[u] + _edges[u][v] < dist[v]) {
                dist[v] = dist[u] + _edges[u][v];
                pPath[v] = u;
            }
        }
    }
}

// 路径打印函数
template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
void Graph<V, W, MAX_W, Direction>::PrintPath(const V& src, const std::vector<W>& dist, const std::vector<size_t>& pPath) {
    size_t srci = GetIndex(src);
    size_t n = _vertex.size();

    for (size_t i = 0; i < n; ++i) {
        if (dist[i] == MAX_W) {
            std::cout << _vertex[srci] << "->" << _vertex[i] << " 不可达" << std::endl;
            continue;
        }

        std::vector<size_t> path;
        for (size_t v = i; v != srci; v = pPath[v]) {
            if (pPath[v] == std::numeric_limits<size_t>::max()) {
                std::cout << _vertex[srci] << "->" << _vertex[i] << " 不可达" << std::endl;
                break;
            }
            path.push_back(v);
        }
        path.push_back(srci);
        std::reverse(path.begin(), path.end());

        for (size_t j = 0; j < path.size(); ++j) {
            std::cout << _vertex[path[j]];
            if (j < path.size() - 1) std::cout << "->";
        }
        std::cout << " 权重:" << dist[i] << std::endl;
    }
}

5.2 Bellman-Ford算法

        Dijkstra算法不能解决带有负权的图的问题,为此,Bellman-Ford算法(贝尔曼-福特算法)被提了出来,这种算法可以解决带有负权的图的最小路径问题,这种算法也是用于解决单源最短路径问题的,即:给定一个起始点src,获取从src到每一个顶点的最短路径。

        Bellman-Ford算法实际上是一种暴力求解的算法,对于有N个顶点的图,要暴力搜索顶点vi和顶点vj,迭代更新最短路径。Bellman-Ford算法的时间复杂度为O(N^3),而Dijkstra算法的时间复杂度为O(N^2),因此对于不带有负权的图,应当使用Dijkstra求最短路径而非使用Bellman-Ford算法

        Bellman-Ford算法无法解决负权回路,所谓负权回路,就是图结构中的某个环,其所有边的权值累加起来小于0,就是负权回路。如下所示的图,a->b->d->a就是一个负权回路,a->b->d->a的权值加起来为-2,这样就存在一种诡异的现象,即每一次从a出发再回到a,路径权值之和都会变小,这样理论上a->a的路径可以无限小,对于存在负权回路的图,没有任何办法可以解决其最小路径问题。

代码实现:Bellman-Ford算法的实现

// BellmanFord算法求单源最短路径
// dist为路径长度数组,pPath为各顶点的前导顶点下标
template<class V, class W, W MAX_W = std::numeric_limits<W>::max()>
bool Graph<V, W, MAX_W>::BellmanFord(const V& src, std::vector<W>& dist, std::vector<size_t>& pPath) {
    size_t n = _vertex.size();
    dist.assign(n, MAX_W);
    pPath.assign(n, std::numeric_limits<size_t>::max());

    size_t srci = GetIndex(src);    // 获取源顶点的下标
    if (srci >= n) {
        throw std::runtime_error("源顶点不存在!");
    }
    dist[srci] = 0;                 // 源点到自身的距离为0
    pPath[srci] = srci;             // 源点的前导节点设为自身

    // 进行n-1轮松弛操作,确保所有的最短路径都被找到
    for (size_t k = 0; k < n - 1; ++k) {
        bool updated = false;       // 用于标记本轮是否有更新
        // 遍历所有边进行松弛操作
        for (size_t i = 0; i < n; ++i) {
            for (size_t j = 0; j < n; ++j) {
                if (dist[i] != MAX_W && _edges[i][j] != MAX_W &&
                    dist[i] + _edges[i][j] < dist[j]) {
                    dist[j] = dist[i] + _edges[i][j];
                    pPath[j] = i;
                    updated = true;
                }
            }
        }
        // 如果本轮没有更新,则提前退出
        if (!updated) {
            break;
        }
    }

    // 检查负权回路,如果存在则返回false
    for (size_t i = 0; i < n; ++i) {
        for (size_t j = 0; j < n; ++j) {
            if (_edges[i][j] != MAX_W && dist[i] + _edges[i][j] < dist[j]) {
                return false;  // 存在负权回路
            }
        }
    }

    return true;  // 不存在负权回路
}

5.3 Floyd-Warshall算法

        Floyd-Warshall算法(弗洛伊德算法),是用于计算多源最短路径的算法,其基本原理为三维动态规划算法:

设D_{ijk}为,从顶点i到定点j,仅以 {1,2,...,k}顶点为中间顶点的情况下的最短路径和。

    若i->j的最短路径经过k,那么D_{i,j,k} = D_{i,j,k-1}+D_{k,j,k-1}
    如i->j的最短路径不经过k,那么D_{i,j,k}=D_{i,j,k-1}

状态转移方程为:D_{i,j,k}=min(D_{i,j,k-1}+D_{k,j,k-1}, D_{i,j,k-1})

        Floyd-Warshall算法的本质是三维动态规划算法,D[i][j][k]表示的是从顶点i到顶点j,在只经过0~k个中间顶点的情况下的最短路径。通过优化将最后一维k优化掉,这是只需要二维数组D[i][j]就可以计算出多源最短路径,Floyd-Warshall算法的时间复杂度为O(N^3),空间复杂度为O(N^2),且Floyd-Warshall算法可以解决带有负权的图的问题。

代码实现:Floyd-Warshall算法的实现

// FloydWarshall算法计算所有顶点对之间的最短路径
template<class V, class W, W MAX_W = std::numeric_limits<W>::max()>
void Graph<V, W, MAX_W>::FloydWarshall(std::vector<std::vector<W>>& vvDist, std::vector<std::vector<size_t>>& vvPath) {
    size_t n = _vertex.size();
    vvDist.assign(n, std::vector<W>(n, MAX_W));
    vvPath.assign(n, std::vector<size_t>(n, std::numeric_limits<size_t>::max()));

    // 初始化距离和路径矩阵
    for (size_t i = 0; i < n; ++i) {
        for (size_t j = 0; j < n; ++j) {
            if (_edges[i][j] != MAX_W) {
                vvDist[i][j] = _edges[i][j];
                vvPath[i][j] = i;
            }
            if (i == j) {
                vvDist[i][j] = 0;  // 顶点到自身的距离为0
                vvPath[i][j] = i;  // 顶点到自身的路径是自己
            }
        }
    }

    // 使用动态规划方法更新所有顶点对之间的最短路径
    for (size_t k = 0; k < n; ++k) {
        for (size_t i = 0; i < n; ++i) {
            for (size_t j = 0; j < n; ++j) {
                if (vvDist[i][k] != MAX_W && vvDist[k][j] != MAX_W &&
                    vvDist[i][k] + vvDist[k][j] < vvDist[i][j]) {
                    vvDist[i][j] = vvDist[i][k] + vvDist[k][j];
                    vvPath[i][j] = vvPath[k][j];  // 更新路径
                }
            }
        }
    }
}

六、AOV网络和AOE网络

        AOV网络和AOE网络是数据结构中用于表示和分析工程计划和实施过程的有向无环图(DAG)的两种不同方式。DAG图差异如下所示

以下是关于这两种网络的详细解释:

6.1 AOV网络(Activity On Vertex Network)

定义:AOV网络是用顶点表示活动,用有向边表示活动之间的先后关系的有向图123。在实际应用中,例如工程或项目的计划中,各个子工程或任务被表示为图中的顶点,而它们之间的依赖关系或执行顺序则用有向边来表示。

特点

  • 顶点表示活动(或任务)。
  • 有向边表示活动之间的先后关系。
  • 可以通过拓扑排序获得活动的执行顺序。
  • 在AOV网中,并发活动可以被表示为互不相连的顶点。

应用:AOV网络常被用于现代化管理,来描述和分析一项工程的计划和实施过程,形象地反映出整个工程中各个活动之间的先后关系。

6.2 AOE网络(Activity On Edge Network)

定义:AOE网络是在带权有向图中,用顶点表示事件(即活动的起始和结束时间),用有向边表示活动,边上的权值表示活动的持续时间。这样的图用来估算工程的最短工期以及哪些活动是影响工程进展的关键。

特点

  • 顶点表示事件(活动的起始和结束时间)。
  • 有向边表示活动,边上的权值表示活动的持续时间。
  • 需要进行关键路径的计算,以确定整个项目的最短完成时间和关键活动。
  • 在AOE网中,并发活动可以通过将多个活动指向同一个事件节点来表示。

应用:AOE网络用于描述由许多交叉活动组成的复杂计划和工程的方法,如计算工程的最短工期和识别关键路径等。

6.3 异同点

  • 定义:AOV网将活动表示为图中的顶点,活动之间的依赖关系表示为有向边;而AOE网将活动表示为图中的边,边上的权值表示活动的持续时间,顶点表示事件。
  • 表示方式:在C语言中,AOV网常用邻接表或邻接矩阵来表示;而AOE网则需要引入事件节点,用邻接表表示图。
  • 拓扑排序:AOV网可以通过拓扑排序获得活动的执行顺序;而AOE网则需要进行关键路径的计算。

总之,AOV网络和AOE网络在数据结构中的表示和计算方式上有一些不同。AOV网更关注活动的依赖关系和执行顺序,而AOE网更关注活动的持续时间和项目的最短完成时间。有兴趣的可以自行了解各方面的应用。

七、总结

        图是一种用于存储顶点和边之间关系的数据结构,记作G={V,E},其中V代表顶点的集合,E代表边的集合。根据边是否带有权重以及边的方向性,图可以进一步细分为带权图与无权图、有向图与无向图。

        图的存储结构主要有两种:邻接表和邻接矩阵。这两种方式各有其适用场景和优缺点。在表示稀疏图时,邻接表因其节省空间的特性而常用;然而,在表示稠密图时,由于邻接矩阵的索引方式简单直观,且便于计算图中任意两点之间的路径,因此通常选择邻接矩阵作为存储结构。

        在无向连通图中,最小生成树是一个特殊的子图,它包含了原图中的所有顶点,并且这些顶点之间通过边相连,形成一个没有回路的树形结构。同时,这棵树的边权值之和是所有可能的树中最小的。计算最小生成树常用的算法有Kruskal算法和Prim算法,这两种算法都采用了局部贪心的策略。

        Dijkstra算法和Bellman-Ford算法是解决单源最短路径问题的经典算法。Dijkstra算法适用于边权值为非负的图,能够高效地计算出从指定源点到图中其他所有顶点的最短路径。然而,当图中存在负权边时,Dijkstra算法将不再适用。Bellman-Ford算法则能够处理带有负权边的图,但相对于Dijkstra算法,其时间复杂度较高。Dijkstra算法的时间复杂度为O(N^2)(其中N为顶点的数量),而Bellman-Ford算法的时间复杂度为O(N^3)。

        Floyd-Warshall算法则用于解决多源最短路径问题,即计算图中任意两点之间的最短路径。该算法基于动态规划的思想,能够处理带有负权边的图。其时间复杂度为O(N^3),其中N为顶点的数量。

        AOV网络和AOE网络都是用于描述和分析工程项目中活动之间关系的有向无环图数据结构。
        AOV网络侧重于活动之间的依赖关系和执行顺序,通过拓扑排序确定活动的合理执行顺序。
        AOE网络侧重于活动的持续时间和项目的最短完成时间,通过计算关键路径来估算工程完成时间和确定关键活动。在实际应用中,根据项目需求的不同,可以选择使用AOV网络或AOE网络来进行项目规划和分析。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1893390.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

springboot+vue人事管理系统 +LW +PPT+源码+讲解

3系统分析 3.1可行性分析 在开发系统之前要进行系统可行性分析&#xff0c;目的是在用最简单的方法去解决最大的问题&#xff0c;程序一旦开发出来满足了员工的需要&#xff0c;所带来的利益也很多。下面我们将从技术、操作、经济等方面来选择这个系统最终是否开发。 3.1.1技…

米国政府呼吁抛弃 C 和 C++

在开始前刚好我有一些资料&#xff0c;是我根据网友给的问题精心整理了一份「C的资料从专业入门到高级教程」&#xff0c; 点个关注在评论区回复“888”之后私信回复“888”&#xff0c;全部无偿共享给大家&#xff01;&#xff01;&#xff01; 很多观点认为C 或 C永远不可被…

C++那些事之小项目实战-进程间通信

小项目实战之进程间通信 进程间通信是一个非常重要的话题&#xff0c;特别是像一些大型项目都有它的影子&#xff0c;例如&#xff1a;PostgreSQL使用了管道完成copy的进程间通信&#xff0c;那么本节也将基于这个主题&#xff0c;使用C去搭建一个进程间通过管道通信的demo出来…

Angluar 实现pdf页面预览以及编辑

之前用过一个pdf预览的lib&#xff0c;并且还支持在线编辑&#xff0c;和直接下载编辑之后的pdf和直接打印&#xff0c;还不错&#xff0c;记录下 PdfShowcase 首先安装依赖 npm install ngx-extended-pdf-viewer 然后引入 import { NgxExtendedPdfViewerModule } from &q…

软件研发标准化流程文件

为了规范化系统开发流程&#xff0c;我们精心制定了一套详尽的规范文档。该文档旨在通过标准化、系统化的方法来显著提升开发效率与项目质量。流程始于明确需求阶段&#xff0c;通过深入细致的设计规划来确保解决方案既可行又具有前瞻性。随后&#xff0c;我们进入高效的编码实…

【懒删除堆 优先队列】1172. 餐盘栈

本文涉及知识点 懒删除堆 优先队列 LeetCode1172. 餐盘栈 我们把无限数量 ∞ 的栈排成一行&#xff0c;按从左到右的次序从 0 开始编号。每个栈的的最大容量 capacity 都相同。 实现一个叫「餐盘」的类 DinnerPlates&#xff1a; DinnerPlates(int capacity) - 给出栈的最大…

Linux开发讲课29---Linux USB 设备驱动模型

Linux 内核源码&#xff1a;include\linux\usb.h Linux 内核源码&#xff1a;drivers\hid\usbhid\usbmouse.c 1. BUS/DEV/DRV 模型 "USB 接口"是逻辑上的 USB 设备&#xff0c;编写的 usb_driver 驱动程序&#xff0c;支持的是"USB 接口"&#xff1a; US…

向量数据库、主键存储引擎、高速网络 RDMA 框架……DolphinDB 版本更新啦!

盛夏已至&#xff0c;炎热的七月伊始&#xff0c;DolphinDB 也迎来了版本的更新。此次更新的 3.00.1 与 2.00.13 版本从多个维度进行了优化扩展&#xff0c;进一步深化了 DolphinDB 在机器学习、数据分析等领域的尝试与探索。 为了响应用户日益增长的 AI 运算需求&#xff0c;…

XJTUSE-数据结构-homework2

当时写的还挺痛苦的 不过现在看&#xff0c;原老师布置的作业真的有水平 现在来看大二数据结构的作业&#xff0c;真的很锻炼代码能力。有些题目&#xff0c;我现在写也不一定能很快写出来hhhh 当时写的作业感觉还是存在问题的&#xff01; 任务概述 任务 1 &#xff1a;指定的…

JSON字符串中获取一个指定字段的值

一、方式一&#xff0c;引用gson工具 测试报文&#xff1a; {"account":"yanxiaosheng","password":"123456" } 引入pom <!-- https://mvnrepository.com/artifact/com.google.code.gson/gson --> <dependency><gr…

无人机测绘需要注意什么?

无人机测绘是一项高精度的测量工作&#xff0c;需要注意以下四点&#xff1a; 一、作业前准备&#xff1a;沟通相关事宜&#xff0c;现场勘查&#xff0c;飞行环境检查等&#xff1b; 二、航线规划与像控点布设&#xff1a;航线规划是任务规划的核心内容&#xff0c;需要综合…

【web APIs】快速上手Day05(Bom操作)

目录 Web APIs - 第5天笔记js组成window对象BOM定时器-延迟函数案例-5秒钟之后消失的广告 JS执行机制location对象案例-5秒钟之后跳转的页面 navigator对象histroy对象 本地存储&#xff08;今日重点&#xff09;localStorage&#xff08;重点&#xff09;sessionStorage&#…

交换机需要多大 buffer

有点违背直觉&#xff0c;但是真事儿&#xff0c;交换机过境的流越多&#xff0c;所需 buffer 越小&#xff0c;这是为什么&#xff1f; 范氏(范雅各布森&#xff0c;van jacobson)管道的 aimd 流建议 buffer_size 为 bdp&#xff0c;这很容易理解&#xff0c;因为 aimd 流最小…

适合弱电行业的项目管理软件!找企智汇软件!

随着科技的不断发展&#xff0c;弱电行业对于项目管理的需求日益增强。为满足这一需求&#xff0c;企智汇推出了一款专为弱电行业打造的工程项目管理系统。 企智汇弱电行业工程项目管理系统以其专业性、高效性和智能性&#xff0c;赢得了业界的广泛认可。该系统深入融合了弱电…

pycharm配置conda解释器

假如我新建了一个conda虚拟环境&#xff0c;名为python3.8

Ubuntu设置nacos开机以单机模式自启动

首先&#xff0c;需要安装jdk Ubuntu 安装JDK 创建Systemd服务单元文件 sudo vim /etc/systemd/system/nacos.service按i进入编辑模式&#xff0c;写入下面信息 [Unit] Descriptionnacos server Afternetwork.target[Service] Typeforking Environment"JAVA_HOME/opt/j…

AI 芯片之战:开启智能新时代的关键角逐

在科技发展的浪潮中&#xff0c;一场围绕 AI 芯片的激烈竞争正在全球范围内如火如荼地展开。多家巨头纷纷投身其中&#xff0c;使得这场混战已然进入白热化阶段。 AI 芯片&#xff0c;作为推动人工智能发展的核心硬件&#xff0c;其作用举足轻重。它能够高效地处理海量的数据&a…

IODD简介(1)

目录 1 IODD简介 1.1基本概述 1.2主要结构 1.3 数据类型 1 IODD简介 1.1基本概述 IODD&#xff08;IO Device Description&#xff09;是一组文件&#xff0c;该文件的作用&#xff1a;被工程工具用于PLC或主站。用于识别、配置、定义用于过程数据交换的数据结构参数化和…

软件测试常见的面试题(46道)

01、您所熟悉的测试用例设计方法都有哪些&#xff1f;请分别以具体的例子来说明这些方法在测试用例设计工作中的应用。 答&#xff1a;有黑盒和白盒两种测试种类&#xff0c;黑盒有等价类划分法&#xff0c;边界分析法&#xff0c;因果图法和错误猜测法。白盒有逻辑覆盖法&…

Android的高校讲座预约管理系-计算机毕业设计源码21634

摘 要 本系统旨在设计和实现一个基于Android平台的高校讲座预约管理系统&#xff0c;以提供管理员和普通用户便捷的讲座预约服务和全面的管理功能。系统将包括在线讲座发布、讲座预约、座位安排、签到信息记录等功能模块&#xff0c;旨在提高高校讲座活动的组织效率和用户体验。…