【高阶数据结构】图的应用--最短路径算法

文章目录

    • 一、最短路径
    • 二、单源最短路径--Dijkstra算法
    • 三、单源最短路径--Bellman-Ford算法
    • 四、多源最短路径--Floyd-Warshall算法

一、最短路径

最短路径问题:从在带权有向图G中的某一顶点出发,找出一条通往另一顶点的最短路径,最短也就是沿路径各边的权值总和达到最小。

二、单源最短路径–Dijkstra算法

单源最短路径问题:给定一个图G = ( V , E ) G=(V,E)G=(V,E),求源结点s ∈ V s∈Vs∈V到图中每个结点v ∈ V v∈Vv∈V的最短路径。Dijkstra算法就适用于解决带权重的有向图上的单源最短路径问题,同时算法要求图中所有边的权重非负。一般在求解最短路径的时候都是已知一个起点和一个终点,所以使用Dijkstra算法求解过后也就得到了所需起点到终点的最短路径。

针对一个带权有向图G,将所有结点分为两组S和Q,S是已经确定最短路径的结点集合,在初始时为空(初始时就可以将源节点s放入,毕竟源节点到自己的代价是0),Q 为其余未确定最短路径的结点集合,每次从Q 中找出一个起点到该结点代价最小的结点u ,将u 从Q 中移出,并放入S 中,对u 的每一个相邻结点v 进行松弛操作。松弛即对每一个相邻结点v ,判断源节点s到结点u 的代价与u 到v 的代价之和是否比原来s 到v 的代价更小,若代价比原来小则要将s 到v 的代价更新为s 到u 与u 到v 的代价之和,否则维持原样。如此一直循环直至集合Q 为空,即所有节点都已经查找过一遍并确定了最短路径,至于一些起点到达不了的结点在算法循环后其代价仍为初始设定的值,不发生变化。Dijkstra算法每次都是选择V-S中最小的路径节点来进行更新,并加入S中,所以该算法使用的是贪心策略。

Dijkstra算法存在的问题是不支持图中带负权路径,如果带有负权路径,则可能会找不到一些路径的最短路径。

在这里插入图片描述

代码实现:

// 临接矩阵
namespace Matrix
{
    template <class V, class W, W MAX_W = INT_MAX, bool Direction = false>
    class Graph
    {
        typedef Graph<V, W, MAX_W, Direction> Self;

    private:
        std::vector<V> _vertexs;             // 顶点集合
        std::map<V, size_t> _vIndexMap;      // 顶点的下标映射关系
        std::vector<std::vector<W>> _matrix; // 存储边集合的矩阵
        void PrintShortPath(const V &src, const std::vector<W> &dist, const std::vector<int> &pPath)
        {
            size_t srci = GetVertexIndex(src);
            size_t n = _vertexs.size();
            for (size_t i = 0; i < n; ++i)
            {
                if (i != srci)
                {
                    // 找出i顶点的路径
                    std::vector<int> path;
                    size_t parenti = i;
                    while (parenti != srci)
                    {
                        path.push_back(parenti);
                        parenti = pPath[parenti];
                    }
                    path.push_back(srci);
                    reverse(path.begin(), path.end());

                    for (auto index : path)
                    {
                        std::cout << _vertexs[index] << "->";
                    }
                    std::cout << "权值和:" << dist[i] << std::endl;
                }
            }
        }
        // 顶点个数是N  -> 时间复杂度:O(N^2)空间复杂度:O(N)
        void Dijkstra(const V &src, std::vector<W> &dist, std::vector<int> &pPath)
        {
            size_t srci = GetVertexIndex(src);
            int n = _vertexs.size();

            dist.resize(n, MAX_W);
            pPath.resize(n, -1);

            dist[srci] = 0;
            pPath[srci] = srci;

            // 已经确定最短路径的顶点集合
            std::vector<bool> S(n, false);

            // n个节点每个节点都要作为起点
            for (int i = 0; i < n; i++)
            {
                // 选最短路径顶点且不在S更新其他路径
                int u = 0;
                W min = MAX_W;

                for (int j = 0; j < n; j++)
                {
                    if (S[i] == false && dist[i] < min)
                    {
                        u = i;
                        min = dist[i];
                    }
                }

                S[u] = true;
                // 松弛更新u连接顶点v  srci->u + u->v <  srci->v  更新
                for (int v = 0; v < n; v++)
                {
                    if (S[v] == false && _matrix[u][v] != MAX_W && dist[u] + _matrix[u][v] < dist[v])
                    {
                        dist[v] = dist[u] + _matrix[u][v];
                        pPath[v] = u;
                    }
                }
            }
        }
    };
}

测试代码:

void TestGraphDijkstra()
{
    const char *str = "syztx";
    Graph<char, int, INT_MAX, true> g(str, strlen(str));
    g.AddEdge('s', 't', 10);
    g.AddEdge('s', 'y', 5);
    g.AddEdge('y', 't', 3);
    g.AddEdge('y', 'x', 9);
    g.AddEdge('y', 'z', 2);
    g.AddEdge('z', 's', 7);
    g.AddEdge('z', 'x', 6);
    g.AddEdge('t', 'y', 2);
    g.AddEdge('t', 'x', 1);
    g.AddEdge('x', 'z', 4);

    std::vector<int> dist;
    std::vector<int> parentPath;
    g.Dijkstra('s', dist, parentPath);
    g.PrintShortPath('s', dist, parentPath);

    // // 图中带有负权路径时,贪心策略则失效了。
    // // 测试结果可以看到s->t->y之间的最短路径没更新出来
    // const char *str = "sytx";
    // Graph<char, int, INT_MAX, true> g(str, strlen(str));
    // g.AddEdge('s', 't', 10);
    // g.AddEdge('s', 'y', 5);
    // g.AddEdge('t', 'y', -7);
    // g.AddEdge('y', 'x', 3);
    // std::vector<int> dist;
    // std::vector<int> parentPath;
    // g.Dijkstra('s', dist, parentPath);
    // g.PrintShortPath('s', dist, parentPath);
}

三、单源最短路径–Bellman-Ford算法

Dijkstra算法只能用来解决正权图的单源最短路径问题,但有些题目会出现负权图。这时这个算法就不能帮助我们解决问题了,而bellman—ford算法可以解决负权图的单源最短路径问题。它的优点是可以解决有负权边的单源最短路径问题,而且可以用来判断是否有负权回路。它也有明显的缺点,它的时间复杂度 O(NE) (N是点数,E是边数)普遍是要高于Dijkstra算法O(N²)的。像这里如果我们使用邻接矩阵实现,那么遍历所有边的数量的时间复杂度就是O(N^3),这里也可以看出来Bellman-Ford就是一种暴力求解更新。

在这里插入图片描述

代码实现:

// 临接矩阵
namespace Matrix
{
    template <class V, class W, W MAX_W = INT_MAX, bool Direction = false>
    class Graph
    {
        typedef Graph<V, W, MAX_W, Direction> Self;

    private:
        std::vector<V> _vertexs;             // 顶点集合
        std::map<V, size_t> _vIndexMap;      // 顶点的下标映射关系
        std::vector<std::vector<W>> _matrix; // 存储边集合的矩阵
        void PrintShortPath(const V &src, const std::vector<W> &dist, const std::vector<int> &pPath)
        {
            size_t srci = GetVertexIndex(src);
            size_t n = _vertexs.size();
            for (size_t i = 0; i < n; ++i)
            {
                if (i != srci)
                {
                    // 找出i顶点的路径
                    std::vector<int> path;
                    size_t parenti = i;
                    while (parenti != srci)
                    {
                        path.push_back(parenti);
                        parenti = pPath[parenti];
                    }
                    path.push_back(srci);
                    reverse(path.begin(), path.end());

                    for (auto index : path)
                    {
                        std::cout << _vertexs[index] << "->";
                    }
                    std::cout << "权值和:" << dist[i] << std::endl;
                }
            }
        }
        // 时间复杂度:O(N^3) 空间复杂度:O(N)
        bool BellmanFord(const V &src, std::vector<W> &dist, std::vector<int> &pPath)
        {
            size_t n = _vertexs.size();
            size_t srci = GetVertexIndex(src);

            // vector<W> dist,记录srci-其他顶点最短路径权值数组
            dist.resize(n, MAX_W);

            // vector<int> pPath 记录srci-其他顶点最短路径父顶点数组
            pPath.resize(n, -1);

            // 先更新srci->srci为缺省值
            dist[srci] = W();

            // 总体最多更新n轮
            for (int k = 0; k < n; k++)
            {
                bool update = false;
                std::cout << "更新第:" << k << "轮" << std::endl;
                for (int i = 0; i < n; i++)
                {
                    for (int j = 0; j < n; j++)
                    {
                        // srci -> i i -> j
                        if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j])
                        {
                            update = true;
                            std::cout << _vertexs[i] << "->" << _vertexs[j] << ":" << _matrix[i][j] << std::endl;
                            dist[j] = dist[i] + _matrix[i][j];
                            pPath[j] = i;
                        }
                    }
                }
                // 如果这个轮次中没有更新出更短路径,那么后续轮次就不需要再走了
                if (update == false)
                    break;
            }

            // 还能更新就是带负权回路
            for (int i = 0; i < n; i++)
            {
                for (int j = 0; j < n; j++)
                {
                    if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j])
                    {
                        return false;
                    }
                }
            }

            return true;
        }
    };
}

测试代码:

void TestGraphBellmanFord()
{
    // const char *str = "syztx";
    // Graph<char, int, INT_MAX, true> g(str, strlen(str));
    // g.AddEdge('s', 't', 6);
    // g.AddEdge('s', 'y', 7);
    // g.AddEdge('y', 'z', 9);
    // g.AddEdge('y', 'x', -3);
    // g.AddEdge('z', 's', 2);
    // g.AddEdge('z', 'x', 7);
    // g.AddEdge('t', 'x', 5);
    // g.AddEdge('t', 'y', 8);
    // g.AddEdge('t', 'z', -4);
    // g.AddEdge('x', 't', -2);
    // std::vector<int> dist;
    // std::vector<int> parentPath;
    // g.BellmanFord('s', dist, parentPath);
    // g.PrintShortPath('s', dist, parentPath);

    // 微调图结构,带有负权回路的测试
    const char *str = "syztx";
    Graph<char, int, INT_MAX, true> g(str, strlen(str));
    g.AddEdge('s', 't', 6);
    g.AddEdge('s', 'y', 7);
    g.AddEdge('y', 'x', -3);
    g.AddEdge('y', 'z', 9);
    g.AddEdge('y', 'x', -3);
    g.AddEdge('y', 's', 1); // 新增
    g.AddEdge('z', 's', 2);
    g.AddEdge('z', 'x', 7);
    g.AddEdge('t', 'x', 5);
    g.AddEdge('t', 'y', -8); // 更改
    g.AddEdge('t', 'z', -4);
    g.AddEdge('x', 't', -2);
    std::vector<int> dist;
    std::vector<int> parentPath;
    if (g.BellmanFord('s', dist, parentPath))
        g.PrintShortPath('s', dist, parentPath);
    else
        std::cout << "带负权回路" << std::endl;
}

四、多源最短路径–Floyd-Warshall算法

Floyd-Warshall算法是解决任意两点间的最短路径的一种算法。

Floyd算法考虑的是一条最短路径的中间节点,即简单路径p={v1,v2,…,vn}上除v1和vn的任意节点。

设k是p的一个中间节点,那么从i到j的最短路径p就被分成i到k和k到j的两段最短路径p1,p2。p1是从i到k且中间节点属于{1,2,…,k-1}取得的一条最短路径。p2是从k到j且中间节点属于{1,2,…,k-1}取得的一条最短路径。

在这里插入图片描述

在这里插入图片描述

即Floyd算法本质是三维动态规划,D[i][j][k]表示从点i到点j只经过0到k个点最短路径,然后建立起转移方程,然后通过空间优化,优化掉最后一维度,变成一个最短路径的迭代算法,最后即得到所以点的最短路。

在这里插入图片描述

代码实现:

// 临接矩阵
namespace Matrix
{
    template <class V, class W, W MAX_W = INT_MAX, bool Direction = false>
    class Graph
    {
        typedef Graph<V, W, MAX_W, Direction> Self;

    private:
        std::vector<V> _vertexs;             // 顶点集合
        std::map<V, size_t> _vIndexMap;      // 顶点的下标映射关系
        std::vector<std::vector<W>> _matrix; // 存储边集合的矩阵
		void PrintShortPath(const V &src, const std::vector<W> &dist, const std::vector<int> &pPath)
        {
            size_t srci = GetVertexIndex(src);
            size_t n = _vertexs.size();
            for (size_t i = 0; i < n; ++i)
            {
                if (i != srci)
                {
                    // 找出i顶点的路径
                    std::vector<int> path;
                    size_t parenti = i;
                    while (parenti != srci)
                    {
                        path.push_back(parenti);
                        parenti = pPath[parenti];
                    }
                    path.push_back(srci);
                    reverse(path.begin(), path.end());

                    for (auto index : path)
                    {
                        std::cout << _vertexs[index] << "->";
                    }
                    std::cout << "权值和:" << dist[i] << std::endl;
                }
            }
        }
        void FloydWarshall(std::vector<std::vector<W>> &vvDist, std::vector<std::vector<int>> &vvpPath)
        {
            size_t n = _vertexs.size();

            vvDist.resize(n);
            vvpPath.resize(n);

            // 初始化权值和路径矩阵
            for (int i = 0; i < n; i++)
            {
                vvDist[i].resize(n, MAX_W);
                vvpPath[i].resize(n, -1);
            }

            for (int i = 0; i < n; i++)
            {
                for (int j = 0; j < n; j++)
                {
                    if (_matrix[i][j] != MAX_W)
                    {
                        vvDist[i][j] = _matrix[i][j];
                        vvpPath[i][j] = i;
                    }
                    if (i == j)
                    {
                        vvDist[i][j] = W();
                    }
                }
            }

            // abcdef  a {} f ||  b {} c
            // 最短路径的更新i-> {其他顶点} ->j
            for (int k = 0; k < n; k++)
            {
                for (int i = 0; i < n; i++)
                {
                    for (int j = 0; j < n; j++)
                    {
                        // i -> j  i -> k  + k -> j
                        // k 作为的中间点尝试去更新i->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];

                            // 找跟j相连的上一个邻接顶点
                            // 如果k->j 直接相连,上一个点就k,vvpPath[k][j]存就是k
                            // 如果k->j 没有直接相连,k->...->x->j,vvpPath[k][j]存就是x
                            vvpPath[i][j] = vvpPath[k][j];
                        }
                    }
                }
            }

            // 打印权值和路径矩阵观察数据
            for (int i = 0; i < n; i++)
            {
                for (int j = 0; j < n; j++)
                {
                    if (vvDist[i][j] == MAX_W)
                    {
                        printf("%3c", '*');
                    }
                    else
                    {
                        printf("%3d", vvDist[i][j]);
                    }
                }
                std::cout << std::endl;
            }
            std::cout << std::endl;

            for (size_t i = 0; i < n; ++i)
            {
                for (size_t j = 0; j < n; ++j)
                {
                    printf("%3d", vvpPath[i][j]);
                }
                std::cout << std::endl;
            }
            std::cout << "=================================" << std::endl;
        }
    };
}

测试代码:

void TestFloydWarShall()
{
    const char *str = "12345";
    Graph<char, int, INT_MAX, true> g(str, strlen(str));
    g.AddEdge('1', '2', 3);
    g.AddEdge('1', '3', 8);
    g.AddEdge('1', '5', -4);
    g.AddEdge('2', '4', 1);
    g.AddEdge('2', '5', 7);
    g.AddEdge('3', '2', 4);
    g.AddEdge('4', '1', 2);
    g.AddEdge('4', '3', -5);
    g.AddEdge('5', '4', 6);
    std::vector<std::vector<int>> vvDist;
    std::vector<std::vector<int>> vvParentPath;
    g.FloydWarshall(vvDist, vvParentPath);

    // 打印任意两点之间的最短路径
    for (size_t i = 0; i < strlen(str); ++i)
    {
        g.PrintShortPath(str[i], vvDist[i], vvParentPath[i]);
    }
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

14个最佳创业企业WordPress主题

您网站的设计使您能够展示产品的独特卖点。通过正确的主题&#xff0c;您将能够解释为什么客户应该选择您的品牌而不是其他品牌。 在本文中&#xff0c;我们将向您介绍14个初创企业WordPress主题。我们将告诉您每个主题的独特之处以及哪些人应该考虑使用它。让我们开始吧&…

Pinia:Vue 2 和 Vue 3 中更好用的状态管理框架

前言 还在用Vuex? 在Vue应用程序的开发过程中&#xff0c;高效且易于维护的状态管理一直是开发者关注的核心问题之一。随着Vue 3的发布&#xff0c;状态管理领域迎来了一位新星——Pinia&#xff0c;它不仅为Vue 3量身打造&#xff0c;同时也向下兼容Vue 2&#xff0c;以其简…

Django学习第四天

启动项目命令 python manage.py runserver 分页功能封装到类中去 封装的类的代码 """ 自定义的分页组件,以后如果想要使用这个分页组件&#xff0c;你需要做&#xff1a; def pretty_list(request):# 靓号列表data_dict {}search_data request.GET.get(q, &…

谷粒商城-个人笔记(集群部署篇二)

前言 ​学习视频&#xff1a;​Java项目《谷粒商城》架构师级Java项目实战&#xff0c;对标阿里P6-P7&#xff0c;全网最强​学习文档&#xff1a; 谷粒商城-个人笔记(基础篇一)谷粒商城-个人笔记(基础篇二)谷粒商城-个人笔记(基础篇三)谷粒商城-个人笔记(高级篇一)谷粒商城-个…

Excel为数据绘制拆线图,并将均值线叠加在图上,以及整个过程的区域录屏python脚本

Excel为数据绘制拆线图,并将均值线叠加在图上,以及整个过程的区域录屏python脚本 1.演示动画A.视频B.gif动画 2.跟踪鼠标区域的录屏脚本 Excel中有一组数据,希望画出曲线,并且能把均值线也绘制在图上,以下动画演示了整个过程,并且提供了区域录屏脚本,原理如下: 为节约空间,避免…

SpringBoot 启动流程一

SpringBoot启动流程一 我们首先创建一个新的springboot工程 我们不添加任何依赖 查看一下pom文件 我们创建一个文本文档 记录我们的工作流程 我们需要的是通过打断点实现 我们首先看一下启动响应类 package com.bigdata1421.start_up;import org.springframework.boot.Spr…

【Android面试八股文】Android性能优化面试题:怎样检测函数执行是否卡顿?

文章目录 卡顿一、可重现的卡顿二、不可重现的卡顿第一种方案: 基于 Looper 的监控方法第二种方案:基于 Choreographer 的监控方法第三种方案:字节码插桩方式第四种方案: 使用 JVMTI 监听函数进入与退出总结相关大厂的方案ArgusAPMBlockCanaryQQ空间卡慢组件Matrix微信广研参…

linux中与网络有关的命令

本文的命令总览 ifconfig命令 在 Linux 系统中&#xff0c;ifconfig 命令用于配置和显示网络接口的信息&#xff0c;包括 IP 地址、MAC 地址、网络状态等。同时我们也可以利用ifconfig 命令设置网络接口对应的ip地址&#xff0c;子网掩码等 当你使用 ifconfig 命令时&#xf…

DC/AC电源模块为现代电子设备提供稳定的能源

BOSHIDA DC/AC电源模块为现代电子设备提供稳定的能源 DC/AC电源模块是一种重要的电子设备&#xff0c;它为现代电子设备提供稳定的能源。在今天的高科技社会中&#xff0c;电子设备已经成为人们生活和工作的重要组成部分。从家用电器到计算机、手机、汽车和航天航空设备&…

微信小程序毕业设计-球馆预约系统项目开发实战(附源码+论文)

大家好&#xff01;我是程序猿老A&#xff0c;感谢您阅读本文&#xff0c;欢迎一键三连哦。 &#x1f49e;当前专栏&#xff1a;微信小程序毕业设计 精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; &#x1f380; Python毕业设计…

Spring AI 1.0.0 新变化,从 0.8.1 如何升级

Spring AI 1.0.0-M1 版本已经发布&#xff0c;距离 1.0.0 正式版又更近了一步。同时这也意味着&#xff0c;Spring AI 1.0.0 的 API 已经基本确定&#xff0c;不会发生大的改动。这里介绍一下&#xff0c;相对于上一个发布版本 0.8.1&#xff0c;Spring AI 1.0.0 的一些重要的变…

【C语言】—— 文件操作(上)

【C语言】—— 文件操作&#xff08;上&#xff09; 一、 为什么使用文件二、 什么是文件2.1、 程序文件2.2、 数据文件2.3、 文件名2.4、二进制文件与文本文件 三、 文件的打开和关闭3.1、流和标准流&#xff08;1&#xff09;流&#xff08;2&#xff09;标准流 3.2、文件指针…

@PostConstruct注解

1.简介 PostConstruct是java5的时候引入的注解&#xff0c;主要用于标记一个方法&#xff0c;表示该方法应在依赖注入完成后自动调用。通常在使用Java EE或者Spring框架时使用这个注解&#xff0c;以便在Bean初始化之后执行一些初始化工作&#xff0c; 可作为一些数据的常规化…

hadoop集群部署【二】YARN MapReduce 的部署

提前注意&#xff1a;请注意路径是否和我的相同&#xff0c;放置的位置不同&#xff0c;请修改标红处 HDFS部署 HDFS介绍及部署http://t.csdnimg.cn/Q3H3Y 部署说明 Hadoop HDFS分布式文件系统&#xff0c;我们会启动&#xff1a; NameNode进程作为管理节点 DataNode进程…

WRF学习——使用CMIP6数据驱动WRF/基于ncl与vdo的CMIP6数据处理

动力降尺度 国际耦合模式比较计划&#xff08;CMIP&#xff09;为研究不同情景下的气候变化提供了大量的模拟数据&#xff0c;而在实际研究中&#xff0c;全球气候模式输出的数据空间分辨率往往较低&#xff08;>100Km&#xff0c;缺乏区域气候特征&#xff0c;为了更好地研…

K8s 集群(kubeadm) CA 证书过期解决方案

Author&#xff1a;Arsen Date&#xff1a;2024/07/04 目录 一、现象描述二、解决方案三、集群验证 一、现象描述 之前有篇文章《K8s Token 过期解决方案&#xff08;Kubeadm&#xff09;》提到了默认生成的 Token 有效期只有 24 小时&#xff0c;过期后 Token 将不可用&#…

C# 类型转换之显式和隐式

文章目录 1、显式类型转换2. 隐式类型转换3. 示例4. 类型转换的注意事项5. 类型转换的应用示例总结 在C#编程中&#xff0c;类型转换是一个核心概念&#xff0c;它允许我们在程序中处理不同类型的数据。类型转换可以分为两大类&#xff1a;显式类型转换&#xff08;Explicit Ca…

18. JAVA 多线程锁介绍

1. 前言 本节内容主要是对 Java 多线程锁进行介绍&#xff0c;是对锁的一个全方位的概述&#xff0c;为我们对后续深入学习不同的锁的使用方法奠定一个良好的基础。本节内容的知识点如下&#xff1a; 乐观锁与悲观锁的概念&#xff0c;以及两种锁之间的区别&#xff0c;这是并…

文华财经T9多空波段趋势量化交易策略模型源码

// 定义变量 Vars Numeric STEP1,MVALUE1,SARVAL,C; Numeric SARLINE,COND,ZBMA1,ZBMA2; Begin CCLOSE; STEP13/11; MVALUE120/22; SARVALSAR(4, STEP1, MVALUE1); PlotLine("",IIF(SARVAL>0,SARVAL,InvalidNumeric),RED,Circledot); PlotLine("&q…

今晚19点,《语音和心理健康》开讲!

《2024GAS声学大讲堂—音频产业创新技术公益讲座》面向医疗健康的声音与音乐技术系列专题讲座 第五讲 将于 今晚 19点 开讲&#xff0c;本次邀请了 湖南大学 教授 张子兴 演讲&#xff0c;讲座主题&#xff1a;《语音和心理健康》。此次直播方式为腾讯会议、小鹅通和中国电子音…