当前位置: 首页 > news >正文

【数据结构】图论与并查集

一、并查集

1.原理

  1. 简单的讲并查集,就是查询两个个元素,是否在一个集合当中,这里的集合的形式进行表示。
  2. 并查集的本质就是森林, 即多棵树。

 我们再来简单的举个例子:

  • 假设此时的你是大一新生,刚进入大学,肯定是先找到宿舍在哪里,然后跟寝室里面的舍友互相认识一下,先形成一个小团体。
  • 假设,宿舍总共6个人,也就是6个人的集合。几乎所有的大学生都是这样先跟周围的人进行联系起来的。
  • 然后辅导员召集班会,这时的你欣然前往,并在讲台上自信的介绍自己,然后吸引或者主动又认识了一群人。这时你或许又跟其它的人进行了关联,或成为了好友,或成为了恋人……

下面我们用如上例子进行展开讨论:

  • 宿舍六人,即六个人,如何判断两个人在同一个集合? 如何进行实现?
  1. 先来解决第一个问题,六个人,选出一个宿舍长,只要两个人的宿舍长是一样的,即可判断两个人在一个集合。
  2. 再来解决第二个问题,既然宿舍长有了,我们都与这个宿舍长产生关联即可,即用树的形式进行表示,至于如何表示,我们可以用双亲表示法进行表示,即每个人记住其宿舍长的名字即可。更为形象的我们可以用下图进行表示:
  3. 更进一步,如何用计算机存储这种结构呢?我们只需对每个人名生成一个下标连续,用计算机进行存储即可。用下图进行直观的理解:
    在这里插入图片描述
  4. 对这张图我们再说明一点,除0下标以外的其他位置存放的是指向代表孙八的下标,这个0处下标存的是集合的所有元素的个数,且存放的是负数形式,这样存有一个好处,我们可以由这个并查集中有多少负数,从而判断这个并查集中有多少个集合。
  • 两个人产生关联,本质上是两个宿舍(集合)之间产生了关联,那两个宿舍如何进行关联起来呢?
  • 下面我们以图的形式更为清晰的进行表述:
    在这里插入图片描述
  • 也就是说因为宿舍的成员是以宿舍长联系起来的,那宿舍与宿舍之间,产生关联(合并),就宿舍长之间认识一下,两个集合就间接的关联起来了。
  • 下图是具体的存储方式:
    在这里插入图片描述

2.基本实现

 根据上面的描述,我们可以作出大致总结:

  1. 数组进行存储表示树形结构。
  2. 数组的下标对应着具体的信息(人名,编号等)。
  3. 我们可以通过一个元素的下标的值不断往上查找,直到找到找到小于0的,即为根节点所在的位置。
  4. 数组中负数的个数代表着集合的个数。
  5. 判断两个元素是否在同一个集合,只需找到根的下标判断是否相等即可。
  6. 将两个不同集合进行合并,其实就是找到根,然后进行更改一个根的指向与改变另一个根的元素个数即可。

由以上信息我们先可以搭建出实现并查集的大致框架:

2.1.基本框架

#include<iostream>
#include<vector>
#include<map>
using namespace std;
template<class T>
class UnionFindSet
{
public:UnionFindSet(const T* arr, size_t size);//构造函数int GetValueIndex(const T& val);//获取val所代表的下标。void GetRoot(const T& val);//获取根节点的下标void Union(const T& x1, const T& x2);//将两个元素的集合进行合并。bool IsSameSet(const T& x1, const T& x2);//判断两个元素是否在同一个集合中int GetSetSize(); //获取集合的元素
private:map<T, int> _indexHash;//map或者unordered_map都可以。用于快速将T转换为对应的下标。vector<T> _createIndex;//用此数组对T类型元素生成下标。vetor<int> _aggregate; //用于存放集合元素,即森林。
};

2.2.构造函数

	UnionFindSet(const T* arr, size_t size){_aggreagte.resize(size, -1);//对存放集合的元素初始化,表示每个元素存放一个元素(负数表示)。_createIndex.resize(size);for (size_t i = 0; i < size; i++){_createIndex[i] = arr[i];_indexHash[arr[i]] = i;//生成下标。}}

2.3.转换元素为下标

	int GetValueIndex(const T& val){auto it = _indexHash.find(val);//最好判断一下val是否存在对应的下标。if (it == _indexHash.end()){throw invalid_argument("不存在所对应的下标");return -1;}return it->second;}

2.4.获取元素根节点下标

	int GetRoot(const T& val){int index = GetValueIndex(val);//找不到小于0的下标指向的位置就一直向上进行找。while (_aggregate[index] >= 0){index = _aggregate[index];}return index;}

2.5.判断元素集合是否相同

	bool IsSameSet(const T& x1, const T& x2)/{int index1 = GetRoot(x1);int index2 = GetRoot(x2);return index1 == index2;}

2.6.合并元素集合

	void Union(const T& x1, const T& x2)//将两个元素的集合进行合并。{if (!IsSameSet(x1, x2)){//不在同一个集合再进行合并。int index1 = GetRoot(x1);int index2 = GetRoot(x2);//进行一步优化,即元素少的合并到元素多的集合当中//此处我们假设index1为元素多的集合,index2为元素少的集合。if (abs(index1) < abs(index2)){swap(index1, index2);}//即将index2(少)合并到index1(多)上//将index2的元素加到index2上_aggregate[index1] += _aggregate[index2];//将index2的父路径指向index1_aggregate[index2] = index1;}}

2.7.获取集合个数

	int GetSetSize()//获取并查集的集合个数{int sum = 0;for (auto e : _aggregate){//计算小于0的元素个数即可。if (e < 0){sum++;}}return sum;}

3.路径压缩

 所谓路径压缩,其实解决存在这样的集合:
在这里插入图片描述
所引发的问题:如果数据足够的多,我们之前写的GetRoot函数的效率会急剧的降低,因此才需要路径压缩帮助我们进行优化。

实现方式也很简单:
在这里插入图片描述

  • 我们只需要找到根节点之后,再找一遍,此时将cur路径上的结点链接到root即可,这样方便了后续的查找。

  • 优化之后的GetRoot

	int GetRoot(const T& val)//获取根节点的下标{int index = GetValueIndex(val);int root = index;//找不到小于0的下标指向的位置就一直向上进行找。while (_aggregate[root] >= 0){root = _aggregate[root];}//路径压缩进行优化。while (index != root){//先保存之前父路径的下标int parent = _aggregate[index];//再将当前结点的父路径改为root_aggregate[index] = root;//继续往上迭代index = parent;}return root;}

4.源码与测试

  • UnionFindSet.hpp
#include<iostream>
#include<vector>
#include<map>
using namespace std;
template<class T>
class UnionFindSet
{
public:UnionFindSet(const T* arr, size_t size){_aggregate.resize(size, -1);//对存放集合的元素初始化,表示每个元素存放一个元素(负数表示)。_createIndex.resize(size);for (size_t i = 0; i < size; i++){_createIndex[i] = arr[i];_indexHash[arr[i]] = i;//生成下标。}}int GetValueIndex(const T& val)//获取val所代表的下标。{auto it = _indexHash.find(val);if (it == _indexHash.end()){throw invalid_argument("不存在所对应的下标");return -1;}return it->second;}int GetRoot(const T& val)//获取根节点的下标{int index = GetValueIndex(val);int root = index;//找不到小于0的下标指向的位置就一直向上进行找。while (_aggregate[root] >= 0){root = _aggregate[root];}//路径压缩进行优化。while (index != root){//先保存之前父路径的下标int parent = _aggregate[index];//再将当前结点的父路径改为root_aggregate[index] = root;//继续往上迭代index = parent;}return root;}void Union(const T& x1, const T& x2)//将两个元素的集合进行合并。{if (!IsSameSet(x1, x2)){//不在同一个集合再进行合并。int index1 = GetRoot(x1);int index2 = GetRoot(x2);//进行一步优化,即元素少的合并到元素多的集合当中//此处我们假设index1为元素多的集合,index2为元素少的集合。if (abs(index1) < abs(index2)){swap(index1, index2);}//即将index2(少)合并到index1(多)上//将index2的元素加到index2上_aggregate[index1] += _aggregate[index2];//将index2的父路径指向index1_aggregate[index2] = index1;}}//判断两个元素是否在同一个集合中bool IsSameSet(const T& x1, const T& x2){int index1 = GetRoot(x1);int index2 = GetRoot(x2);return index1 == index2;}int GetSetSize()//获取并查集的集合个数{int sum = 0;for (auto e : _aggregate){if (e < 0){sum++;}}return sum;}
private:map<T, int> _indexHash;//map或者unordered_map都可以,用于快速将T转换为对应的下标。vector<T> _createIndex;//用此数组对T类型元素生成下标。vector<int> _aggregate; //用于存放集合元素,即森林。
};
  • Test.cpp
#include"UnionFindSet.hpp"
int main()
{string str[] = { "张三","李四","王五","赵六","周七" };UnionFindSet<string> ufs(str, sizeof(str) / sizeof(str[0]));ufs.Union("张三", "李四");ufs.Union("王五", "赵六");cout << "集合数为:" << ufs.GetSetSize() << endl;return 0;
}

运行结果:
在这里插入图片描述

并查集习题:

  1. 省份数量
  2. .等式方程的可满足性
  • 补充一下:
  1. 直接用下标进行抽象,是最常用的,因此这里的生成下标的vector与快速索引的map可以省去,形成一个简化版的并查集,更方便我们使用。
  2. 这里我们将并查集与图论放在一起,是因为并查集可以帮助起到判环的作用,因此我们这里放到一块进行讲解。

二、图论

1.基本概念

  • 图的概念有点凌乱,博主以思维导图的形式呈现出:

在这里插入图片描述

2.存储结构

  • 图有两个基本元素:
  1. 顶点, 我们可以将具体的顶点抽象成下标,从而用下标进行表示。
  2. 边,两个顶点即可确定一条边,因此我们可以用二维矩阵的方式进行表示;每个顶点都有与其相连的边,因此,我们可以单独每个顶点所连接的边抽象成桶的形式(类似于哈希桶)进行表示。
  • 因此我们通常有邻接矩阵和邻接表的形式进行存储。

2.1邻接矩阵

  • 实现代码:
	/*V(vertex) 表示实际存储边的类型,W(weight)表示边的权重,W_MAX 表示权重的不可能取值。Direction false表示是无向的,true表示是有向的。*/template<class V, class W, W W_MAX = INT_MAX, bool Direction = false>class Graph{public:/*构造函数,传入的参数为V类型的指针指向的是V类型数组,以及数组的元素个数。*/Graph(const V* a, size_t n)//有多少个顶点{//初始化边,以及生成边的下标_vertexs.resize(n);for (size_t i = 0; i < n; i++){_vertexs[i] = a[i];_indexMap[a[i]] = i;}//将矩阵进行初始化_matrices.resize(n);for (size_t i = 0; i < n; i++){//没有权值,我们初始化为W_MAX,表示最开始顶点之间不互相连通。_matrices[i].resize(n, W_MAX);}}//将实际的顶点转换为对应的下标int GetVertexIndex(const V& v){auto it = _indexMap.find(v);if (it == _indexMap.end()){//找不到throw invalid_argument("顶点不存在");//抛出异常return -1;}return it->second;}//添加边void AddEdge(const V& src, const V& dst, const W& w){int srci = GetVertexIndex(src);int dsti = GetVertexIndex(dst);_AddEdge(srci, dsti, w);}//这里我们写一个子函数,方便内部接口进行使用。void _AddEdge(int srci, int dsti, const W& w){_matrices[srci][dsti] = w;if (Direction == false){//说明是无向图_matrices[dsti][srci] = w;}}//为了方便进行测试,这里博主将打印函数给出。void Print(){for (size_t i = 0; i < _vertexs.size(); i++){printf("[%d]->", i);cout << _vertexs[i] << endl;//下标对应的边}cout << "    ";for (size_t i = 0; i < _matrices.size(); i++)printf("%-4d", i);cout << endl;for (size_t i = 0; i < _matrices.size(); i++){printf("%-4d",i);for (size_t j = 0; j < _matrices[i].size(); j++){if (_matrices[i][j] != W_MAX)printf("%-4d", _matrices[i][j]);elseprintf("%-4c", '*');}cout << endl;}cout << endl;}vector<V> _vertexs;//顶点map<V, int> _indexMap;//顶点所对应的下标vector<vector<W>> _matrices; //矩阵的英文};
  • 说明:
  1. 如果边带有权值,并且两个节点之间是连通的,边的关系就用权值代替。
  2. 如果两个顶点不通,则使用无穷大代替,即W_MAX。
  • 测试用例:

void TestGraph()
{Graph<char, int, INT_MAX, true> g("0123", 4);g.AddEdge('0', '1', 1);g.AddEdge('0', '3', 4);g.AddEdge('1', '3', 2);g.AddEdge('1', '2', 9);g.AddEdge('2', '3', 8);g.AddEdge('2', '1', 5);g.AddEdge('2', '0', 3);g.AddEdge('3', '2', 6);g.Print();
}
int main()
{TestGraph();return 0;
}

运行结果:
在这里插入图片描述

2.2邻接表

  • 实现代码:
namespace link
{/*因为要存顶点与边的关系,因此我们需要一个结构体来保存对应的相连的顶点与边的权值。*/template<class V,class W>struct Edge{V _dst;//目标顶点W _w;//权值Edge<V, W>* _next;//构造函数Edge(const V& dst, const W w):_dst(dst),_w(w),_next(nullptr){}};template<class V, class W, bool Direction = false>class Graph{public:typedef Edge<V, W> Edge;Graph(const V* a, size_t n)//有多少个顶点{//初始化边,以及生成对应的下标_vertexs.resize(n);for (size_t i = 0; i < n; i++){_vertexs[i] = a[i];_indexMap[a[i]] = i;}//将矩阵进行初始化,为空表示最开始顶点没有边与之相连。_link.resize(n,nullptr);}//添加边void AddEdge(const V& src, const V& dst, const W& w){int srci = GetVertexIndex(src);int dsti = GetVertexIndex(dst);Edge* node = new Edge(dst, w);node->_next = _link[srci];_link[srci] = node;if (Direction == false){//说明是无向图Edge* node = new Edge(src, w);node->_next = _link[dsti];_link[dsti] = node;}}//获取顶点的下标。int GetVertexIndex(const V& v){auto it = _indexMap.find(v);if (it == _indexMap.end()){//找不到throw invalid_argument("顶点不存在");//抛出异常return -1;}return it->second;}//打印的时候我们按照链表的形式打印即可。void Print(){for (size_t i = 0; i < _link.size(); i++){cout << "[" << i << ":" << _vertexs[i] << "]->";Edge* cur = _link[i];while (cur){cout << "[" << cur->_dst << ":" << _indexMap[cur->_dst] << ":" << cur->_w << "]->";cur = cur->_next;}cout << "nullptr" << endl;}cout << endl;}private:vector<V> _vertexs;//顶点map<V, int> _indexMap;//顶点所对应的下标vector<Edge*> _link; //邻接表};
}
  • 测试用例:
void TestGraph()
{string a[] = { "张三", "李四", "王五", "赵六" };Graph<string, int,true> g1(a, 4);g1.AddEdge("张三", "李四", 100);g1.AddEdge("张三", "王五", 200);g1.AddEdge("王五", "赵六", 30);g1.Print();
}

运行结果:

在这里插入图片描述


  • 总结:
  1. 邻接矩阵适合快速查看两个顶点的关系与路径权值。而对于顶点连接的边有多少,是什么,则需要遍历矩阵所在行进行确认。
  2. 邻接表适合直接取所有与点相连的边,而不适合快速查看两个顶点的关系。
  3. 因此邻接矩阵和邻接表是相辅相成的,而综合来看的话,对于较为稀疏的图,即顶点相连的边较少,平分秋色,各有千秋,而对于稠密的完全图来说,邻接矩阵更为合适。因此我们下面统一采用临界矩阵的方式进行实现。

3.遍历方式

3.1广度优先遍历

  • 图解:
    在这里插入图片描述

我们再来分析一下流程,这里是以A为起点,进行广度遍历。

  1. 先遍历A,。
  2. 然后遍历与A相连的BCD。
  3. 其次在遍历与BCD相连的EF,此时就需要注意之前访问过的结点不能在接着继续访问了。
  4. 接着遍历与EF相连的HG,此时也需注意同样的问题。
  5. 最后遍历与H相连的I,此时同理。
  • 因此广度优先遍历,需注意访问的时候不能再访问已经访问过的结点,其次访问时越访问越深的。

实现方式:

  1. 采用队列的结构,不断入与队列元素相连的未访问的结点。
  2. 使用一个vector 记录结点是否已经被访问过了,当入队列时,即将对应的结点的下标标记为true。
void BFS(const V& src)
{int srci = GetVertexIndex(src);int n = _vertexs.size();vector<int> is_visited(n, false);//防止重复结点入队列,以免形成回路。queue<int> que;que.push(srci);is_visited[srci] = true;int levelsize = 1;//第一层就srci.while (!que.empty()){for (int i = 0; i < levelsize; i++){int front = que.front();que.pop();cout << front << ":" << _vertexs[front] << " ";//将与front相关的边进行入队列for (int i = 0; i < n; i++){if (_matrices[front][i] != W_MAX &&is_visited[i] == false){que.push(i);is_visited[i] = true;}}//这一层for循环式暴力遍历矩阵的所在行,确认是否有//没被访问的边。如果是邻接表就直接取较为方便,不过//稠密图倒是矩阵更优一点,能更好的确认两点的关系。					}cout << endl;//更新层结点的个数。levelsize = que.size();}
}
  • 测试用例:
	void TestBFS(){string a[] = { "A", "B", "C", "D", "E","F","G","H","I" };Graph<string, int> g1(a, sizeof(a) / sizeof(string));g1.AddEdge("A", "B", 1);g1.AddEdge("A", "C", 1);g1.AddEdge("A", "D", 1);g1.AddEdge("B", "E", 1);g1.AddEdge("B", "C", 1);g1.AddEdge("C", "F", 1);g1.AddEdge("C", "B", 1);g1.AddEdge("D", "F", 1);g1.AddEdge("E", "G", 1);g1.AddEdge("F", "H", 1);g1.AddEdge("H", "I", 1);g1.BFS("A");}
  • 运行结果:
    在这里插入图片描述

3.2深度优先遍历

  • 图解:
    在这里插入图片描述

我们再来分析一下流程,这里是以A为起点,进行深度遍历。

说明:已经访问过的结点我们是不再进行访问的。

  1. 先访问A相邻的B, 再访问与B相连的C, 再访问与C相连的F, 再访问与F相连的D。
  2. D相邻的A我们是不再进行访问的,因此又回到F, 接着访问H,紧接着访问与H相连的I,I没有访问过的结点,回退到H, H也没有访问过的结点回退到 F。
  3. F也没有与未访问的结点,回退到C,C也没有未访问的结点,于是回退到B。
  4. 接着访问与B相连的E, 更深一步访问与E相连的G,G没有未访问过的结点,回退到E, E此时也没有未访问过的结点回退到B, B此时也没有未访问过的结点,回退到A.
  5. 访问结束。
  • 实现代码:
	void _DFS(int srci,vector<bool>& is_visted){for (size_t i = 0; i < is_visted.size(); i++){if (_matrices[srci][i] != W_MAX && is_visted[i] == false){//此处打印的目的是便于测试。cout << "[" << _vertexs[srci] << "->" << _vertexs[i] << "]" << endl;is_visted[i] = true;_DFS(i, is_visted);}}}void DFS(const V& src){int srci = GetVertexIndex(src);vector<bool> is_visted(_vertexs.size(), false);is_visted[srci] = true;_DFS(srci,is_visted);}
  • 测试用例:
void TestDFS()
{string a[] = { "A", "B", "C", "D", "E","F","G","H","I" };Graph<string, int> g1(a, sizeof(a) / sizeof(string));g1.AddEdge("A", "B", 1);g1.AddEdge("A", "C", 1);g1.AddEdge("A", "D", 1);g1.AddEdge("B", "E", 1);g1.AddEdge("B", "C", 1);g1.AddEdge("C", "F", 1);g1.AddEdge("C", "B", 1);g1.AddEdge("D", "F", 1);g1.AddEdge("E", "G", 1);g1.AddEdge("F", "H", 1);g1.AddEdge("H", "I", 1);g1.DFS("A");
}/*主函数就自由发挥吧。*/
  • 运行结果:
    在这里插入图片描述

4.最小生成树

先来熟悉一下概念:

  • 最小生成树:图的生成树的路径最小。
  • 生成树:一个连通图的最小连通子图称作该图的生成树。有n个顶点的连通图的最小连通子图有n个顶点和n-1条边。
  • 连通图:若从顶点v1到顶点v2有路径,则称顶点v1与顶点v2是连通的。如果图中任意一对顶点都是连通的,则称此图为连通图。
  • 注意:连通图是无向图的概念,也就是说最小生成树的图必须是无向的。强连通图才是有向图的定义。

 简单的说就是从由n个顶点组成的连通图中选择n-1条边,子图连通且所边的权值相加最小。

  实现方法下面介绍克鲁斯卡尔和普里姆两种算法。

4.1Kruskal算法

  • 原理
  1. 首先将所有的边管理起来,每次取出最小的边。
  2. 判断已经选出的边是否构环,如果构成就弃置再从中选最小的边。
  3. (n个顶点构成的图)选择n-1条边即可。
  • 实现关键
  1. 用优先级队列对边进行管理。
  2. 用并查集进行判环。
  • 实现代码:
/*
为方便读者进行阅读,此处博主贴了一份并查集的简略代码。
*/template<class T>class UnionFindSet{public://初始化大小,以及赋初值UnionFindSet(size_t size):_pPath(size, -1){}//将两个数进行合并void Union(int x1, int x2){//找两个数的父结点int index1 = find(x1);int index2 = find(x2);//如果相同则说明已经在同一个集合下,无需进行合并if (index1 == index2) return;//将小的和在大的身上(优化防止路径过长)if (_pPath[index1] < _pPath[index2]){swap(index1, index2);swap(x1, x2);}//此处保证index1的父节点的数量多,index2的数量小_pPath[index1] += _pPath[index2];_pPath[index2] = index1;}//找根int GetValueIndex(int x){//第一步:转换为下标int index = x;//第二步:根据下标找父节点while (_pPath[index] >= 0){index = _pPath[index];}//找到父路径进行返回。//路径压缩while (x != index){int parent = _pPath[x];_pPath[x] = index;x = parent;}return index;}int setsize(){int n = 0;for (int e : _pPath)if (e < 0) n++;return n;}private:vector<int> _pPath;};/*此结构体用于存放边的信息,放入优先级队列中便于进行管理。*/template<class W>struct Edge{int _srci;int _dsti;W _w;Edge(const int srci, const int dsti, const W& w):_srci(srci), _dsti(dsti), _w(w){}bool operator >(const Edge e) const{return _w > e._w;}};W Kruskal(self& min){min._vertexs = _vertexs;//第一步,用优先级队列存放所有的边priority_queue<Edge, vector<Edge>, greater<Edge>> minque;size_t n = _vertexs.size();//无向图,只需存放一半的图的信息即可。for (size_t i = 0; i < n; i++){for (size_t j = 0; j < i; j++){if (_matrices[i][j] != W_MAX){minque.push(Edge(i, j, _matrices[i][j]));}}}//第二步,选边,最小生成树,选择的边为 n-1条边size_t size = 0;UnionFindSet<int> u(n);W total = W();while (!minque.empty() && size != n-1){Edge top = minque.top();minque.pop();if (u.find(top._dsti) != u.find(top._srci)){//说明不构成环,选择此边,并将其加入到并查集和表中//此处是为了方便测试。cout << _vertexs[top._dsti] << "->" << _vertexs[top._srci]<< ":" << top._w << endl;u.Union(top._dsti, top._srci);min._AddEdge(top._dsti, top._srci, top._w);size++;total += top._w;}}//队列为空跳出循环,因此需要判断一下看是否选出了n-1条边。if (size != n - 1){//表明不能选出来return W();}return total;}
  • 测试用例:
	void TestGraphMinTree(){const char* str = "abcdefghi";Graph<char, int> g(str, strlen(str));g.AddEdge('a', 'b', 4);g.AddEdge('a', 'h', 8);//g.AddEdge('a', 'h', 9);g.AddEdge('b', 'c', 8);g.AddEdge('b', 'h', 11);g.AddEdge('c', 'i', 2);g.AddEdge('c', 'f', 4);g.AddEdge('c', 'd', 7);g.AddEdge('d', 'f', 14);g.AddEdge('d', 'e', 9);g.AddEdge('e', 'f', 10);g.AddEdge('f', 'g', 2);g.AddEdge('g', 'h', 1);g.AddEdge('g', 'i', 6);g.AddEdge('h', 'i', 7);Graph<char, int> kminTree(strlen(str));cout << "Kruskal:" << g.Kruskal(kminTree) << endl;}
/*main函数自由发挥吧*/
  • 运行结果:
    在这里插入图片描述

  • 图解:

在这里插入图片描述
说明:

  1. 程序走出的过程可能不一样,比如相同的边谁先选可能由优先级的实现原理决定,但大概率结果是一样的。
  2. 我们走出的只是局部的最优解,全局的最优解,可能还与相同的边的选择顺序有关,相同的边的如果互相影响,则可能会影响后面更大的边的选择。
  3. 因此如果所有的边互不相同那我们可以断定,此算法走出的最小生成树是确定的,即为全局的最小生成树。

4.2Prim算法

  • 原理
  1. 将顶点分为两个集合,设一个集合为X, 一个集合为Y。
  2. 选择一个起始点,放入X集合,剩余的顶点放入Y集合。
  3. 每次选择从Y中选择与X相连的最小的边,并将其相连的顶点放入X集合,从Y中丢弃此顶点。
  4. 直到选择 n - 1条边为止。
  • 实现关键:
  1. 将顶点分为两个集合X, Y,其实就避开了环的问题,产生环的原因本质就是一个集合内的两个顶点连到一块了。
  2. 我们选的是与集合X相连的最小的边,因此还要把X相连的边,放入优先级队列,往后循环可能会有一个集合内的边,我们只需判断边所连的目标顶点不在集合X即可,对于在集合X的我们不选即可。
  3. 除此之外,我们还需要确立一个起始点,用来初始化集合X和集合Y。
  • 实现代码:
	W Prim(self& min,const V& src){size_t n = _vertexs.size();min._vertexs = _vertexs;/*第一步:选择顶点,作为起始顶点。分为两个数组,一个为起始数组,一个为选边数组*/int srci = GetVertexIndex(src);vector<bool> X(n,false);vector<bool> Y(n,true);X[srci] = true;Y[srci] = false;//第二步:将与srci相关的边入队列中。priority_queue<Edge, vector<Edge>, greater<Edge>> minque;for (size_t i = 0; i < n; i++){//将边进行入队列if (_matrices[srci][i] != W_MAX){minque.push(Edge(srci, i, _matrices[srci][i]));}}//第三步进行选边W total = W();size_t size = 0;while (!minque.empty()){Edge front = minque.top();minque.pop();//判断边的终点是否在X中if (X[front._dsti]){//说明构成环。cout << "构成环:";cout << _vertexs[front._srci] << "->" << _vertexs[front._dsti] << endl;}else{cout << _vertexs[front._srci] << "->" << _vertexs[front._dsti] << endl;++size;total += front._w;//将边添加到最小生成树里面,并将与dsti相连的边入队列min._AddEdge(front._srci, front._dsti, front._w);//将desi所在的集合进行删除与添加Y[front._dsti] = false;X[front._dsti] = true;//将dsti所连的边进行入队列for (size_t i = 0; i < n; i++){//避免将已经入过的边再进行入队列if (_matrices[front._dsti][i] != W_MAX && Y[i]){//不在X[i] 即将在Y[i]进行入队列。minque.push(Edge(front._dsti, i,_matrices[front._dsti][i]));}}}}//如果不能生成最小生成树。if (size != n - 1){return W();}return total;}
  • 测试代码:
	void TestGraphMinTree(){const char* str = "abcdefghi";Graph<char, int> g(str, strlen(str));g.AddEdge('a', 'b', 4);g.AddEdge('a', 'h', 8);//g.AddEdge('a', 'h', 9);g.AddEdge('b', 'c', 8);g.AddEdge('b', 'h', 11);g.AddEdge('c', 'i', 2);g.AddEdge('c', 'f', 4);g.AddEdge('c', 'd', 7);g.AddEdge('d', 'f', 14);g.AddEdge('d', 'e', 9);g.AddEdge('e', 'f', 10);g.AddEdge('f', 'g', 2);g.AddEdge('g', 'h', 1);g.AddEdge('g', 'i', 6);g.AddEdge('h', 'i', 7);Graph<char, int> pminTree(strlen(str));cout << "Prim:" << g.Prim(pminTree, 'a') << endl;pminTree.Print();}
/*main 函数只需调用此函数即可*/

运行结果:

在这里插入图片描述

  • 图解:
    在这里插入图片描述

5.最短路径

  • 最短路径是描述两个顶点能连通的情况下,考虑两个顶点之间所经过路径的权值之和的最小值。
  • 举个例子,在现实世界中我们已经不关心两个地方能不能到的问题了,我们主要关系的是两个地方如何规划路程最短或者花费最低,诸如此类的问题,抽象到计算机即转换为了两个顶点所经过的路径的权值之和如何才能最短。

由此,我们引出迪杰斯特拉(Dijkstra), 贝尔曼福特(Bellman-Ford), 弗洛伊德(floyd warshall) 三种算法。

5.1Dijkstra算法

  • 基本认识
  • 此算法主要求的是不带负权值最小路径。

  • 算法思想主要在单源最短路径中进行体现。

  • 算法原理(贪心)
  1. 确定一个起始点,更新与其直接相连的顶点的路径。
  2. 选择路径和最短的那一个,此处确定了第一条路径最短的边。
  • 确定两字我们此处再稍作解释,由于已经选择了起始点直接到路径最短的顶点。因此不可能再出现,从起始点到另一个顶点再经过其它顶点到此点的路径和更短,更简单的表述是两点直接连着已经最短的了,再通过其它点绕远路只会更长,不会更短。
  • 此处用数学的语言进行描述或许更加直观。
  1. 再由最短的那个顶点,再更新(如果更小再进行更新)与其直接相连的边,再确定一条路径最短的边的顶点。由此顶点再进行更新。
  2. 如此往复,直到没有顶点可以更新,就结束。
  • 实现代码:
	void Dijkstra(const V& src, vector<W>& dst, vector<int>& pPath){//将边与路径进行初始化size_t n = _vertexs.size();int srci = GetVertexIndex(src);//值初始化为W_MAXdst.resize(n, W_MAX);//路径初始化为-1pPath.resize(n, -1);//src->src路径值初始化为W(),路径初始化为srcidst[srci] = W();pPath[srci] = srci;//创建一个bool的vector使得每个结点只访问一次vector<bool> is_visted(n, false);for (size_t i = 0; i < n; i++){W min = W_MAX;int vertexi = 0;//先选出没被访问过的最小的边for (size_t j = 0; j < n; j++){if (!is_visted[j] && dst[j] < min){min = dst[j];vertexi = j;}}//选出之后标记为选过的边is_visted[vertexi] = true;//再进行松弛更新与其相连的边for (size_t j = 0; j < n; j++){/*首先得有边,且是顶点没有访问的点,并且 srci->vertex + vertex->j < srci->j,再进行更新*/			if (_matrices[vertexi][j] != W_MAX && !is_visted[j]&& dst[vertexi] + _matrices[vertexi][j] < dst[j]){//更新j的父路径和srci->j的距离pPath[j] = vertexi;dst[j] = dst[vertexi] + _matrices[vertexi][j];}}}}
  • 此处对这里的pPath进行说明一下,是将路径进行压缩从二维降到了一维,但其实也很简单,本质与并查集的路径表示大致一样,下标存的是父节点的下标。
  • 另外,这里打印时因为每个结点表示的是父结点的下标,因此我们还需将路径倒着找到之后,再翻转成正向的,再进行打印。
  • 打印最短路径函数:
void PrinrtShotPath(const V& src, vector<W>& dst, vector<int>& pPath)
{int srci = GetVertexIndex(src);size_t n = _vertexs.size();//先找到路径再进行逆置for (size_t i = 0; i < n; i++){//不能是srci,要不然就陷入环了。if (i != srci){vector<int> path;int parent = i;while (parent != srci){path.push_back(parent);parent = pPath[parent];}//最后将srci根结点入进去path.push_back(srci);//逆转path得到路径reverse(path.begin(), path.end());for (auto index : path){cout << _vertexs[index] << "->";}//最后打印出路径值cout << "最短路径值为:" << dst[i] << endl;}}
}
  • 测试用例:
	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);vector<int> dist;vector<int> parentPath;g.Dijkstra('s', dist, parentPath);g.PrinrtShotPath('s', dist, parentPath);}
  • 运行结果:
    在这里插入图片描述

  • 图解:
    在这里插入图片描述

5.2Bellman-Ford算法

  • 用处:单源最短路径的负权值(不带负权回路)的图

  • 思想:暴力枚举遍历

  1. 由于只会更新出更短的路径,我们可以采取暴力枚举的方法。
  2. 将所有的边进行遍历,之后再遍历 n - 1 次进行修正。
  • 重点就在于: 为什么再遍历n - 1次 ?我们先来讨论一下,假设你再某次更新s->x->t->z 之后,s->x->t 出现了更短的路径(存在负权值,就有可能),更新成了s->y->t,但是原来已经更新的s->x->t->z虽然路径随着s->y->t更新,但是其s->t的权值并没有进行更新,这就导致了数据对不上的问题,因此我们需要再进行更新一轮,使之数据一致。而再次更新,有可能会导致其它最短路径的权值对不上,因此还要再进行更新,直到所有的最短路径都对上为止,因此最多要n-1次,带上最开始的那一次,总共n次。
  • 实现代码:
bool BellmanFord(const V& src, vector<W>& dst, vector<int>& pPath)
{//将边与路径进行初始化size_t n = _vertexs.size();int srci = GetVertexIndex(src);//值初始化为W_MAXdst.resize(n, W_MAX);//路径初始化为-1pPath.resize(n, -1);//src->src路径值初始化为W(),路径初始化为srcidst[srci] = W();pPath[srci] = srci;for (size_t k = 0; k < n; k++){//更新n轮,因为一个路径更新出更短的路径,会影响其它路径的权值,//因此需要再次更新。//一轮之后,更新出最短路径,则其它路径的权值需要暴力更新一遍。//不带第一轮,最多更新n-1轮->其中每一轮都更新出了最短路径。bool update = false;for (size_t i = 0; i < n; i++){for (size_t j = 0; j < n; j++){//边存在,并且 s->i + i->j < s->j if (_matrices[i][j] != W_MAX && dst[i] + _matrices[i][j] < dst[j]){update = true;//更新父路径和权值pPath[j] = i;dst[j] = dst[i] + _matrices[i][j];}}}if (!update){break;}}//检查负权回路//再次更新一轮,检查是否能更新,如果还能更新,则存在负权回路。//如果没有更新,则为false,即bool is_existed = false;for (size_t i = 0; i < n; i++){for (size_t j = 0; j < n; j++){//边存在,并且 s->i + i->j < s->j if (_matrices[i][j] != W_MAX && dst[i] + _matrices[i][j] < dst[j]){is_existed = true;}}}if (is_existed){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);vector<int> dist;vector<int> parentPath;if (g.BellmanFord('s', dist, parentPath)){g.Print();g.PrinrtShotPath('s', dist, parentPath);}else{cout << "存在负权回路" << endl;}}
  • 运行结果:

在这里插入图片描述

  • 图解:

在这里插入图片描述

  • 说明:暴力更新,调试着看数据的变化效果更好。

  • 测试用例2:

	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', 'x', -3);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);vector<int> dist;vector<int> parentPath;if (g.BellmanFord('s', dist, parentPath)){g.PrinrtShotPath('s', dist, parentPath);}else{cout << "存在负权回路" << endl;}}
  • 运行结果:
    在这里插入图片描述
  • 图解:
    在这里插入图片描述
    说明:暴力循环完之后,再更新一次又会引起其它变小,此种情况只会越更新越小,求不出最小路径!

5.3floyd warshall算法

  • 用处:多源最短路径的负权值(不带负权回路)的图

  • 算法思想(dp):

  1. 拆分子问题:分为两种情况
  1. 所有的边经过点K.
  2. 所有的边不经过点K.
  3. 这里的K可能是所有的顶点。
  4. 因此求前两种情况的所有情况的最小值即可。

图解:
在这里插入图片描述

  • 实现代码:
void FloydWarshall(vector<vector<W>>& vvdst, 
vector<vector<int>>& vvpPath)
{size_t n = _vertexs.size();//初始化dst与pPathvvdst.resize(n);vvpPath.resize(n);for (size_t i = 0; i < n; i++){vvdst[i].resize(n, W_MAX);vvpPath[i].resize(n, -1);}//再对边进行初始化,即将i直接到j的边先放在des数组中for (size_t i = 0; i < n; i++){for (size_t j = 0; j < n; j++){if (_matrices[i][j] != W_MAX){vvdst[i][j] = _matrices[i][j];vvpPath[i][j] = i;}if (i == j){//与此同时由于是距离,所以i == j  即 i->i 的距离为0vvdst[i][j] = 0;}}}for (size_t k = 0; k < n; k++){//其中暴力选择k做为中间的边,分析是选择还是不选for (size_t i = 0; i < n; i++){//从中进行选则两端的边for (size_t j = 0; j < n; j++){//选择k作为中间的边,如果i->k,k->j < i->j//即分析是取k小还是不取k小,这里的k采用暴力枚举的方式。if (vvdst[i][k] != W_MAX && vvdst[k][j] != W_MAX&& vvdst[i][k] + vvdst[k][j] < vvdst[i][j]){//则需要更新dst[i][j]的父路径以及权值vvdst[i][j] = vvdst[i][k] + vvdst[k][j];/*i->k 更新 k->j,应为pPath[k][j]如果k->j中间没有其他结点,则说明 pPath[k][j] == k如果k->……->x->j中间经过了其它结点,则 pPath[k][j]==x*/vvpPath[i][j] = vvpPath[k][j];}}}}//此处我们打印出权值和路径的矩阵cout << "   ";for (size_t i = 0; i < n; i++){printf("%-3d", i);}cout << endl;//1.权值矩阵for (size_t i = 0; i < n; i++){printf("%-3d", i);for (size_t j = 0; j < n; j++){if (vvdst[i][j] == W_MAX){printf("%-3c", '*');}else{printf("%-3d", vvdst[i][j]);}}cout << endl;}printf("=============================================\n");//2.路径矩阵cout << "  ";for (size_t i = 0; i < n; i++){cout << i << " ";}cout << endl;for (size_t i = 0; i < n; i++){cout << i << " ";for (size_t j = 0; j < n; j++){cout << vvpPath[i][j] << " ";}cout << 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);vector<vector<int>> vvDist;vector<vector<int>> vvParentPath;g.FloydWarshall(vvDist, vvParentPath);// 打印任意两点之间的最短路径for (size_t i = 0; i < strlen(str); ++i){g.PrinrtShotPath(str[i], vvDist[i], vvParentPath[i]);cout << endl;}}

运行结果:
在这里插入图片描述

  • 图解:
    在这里插入图片描述

说明:这里II的矩阵表示的数字是真实下标对应的数字,我们这里打印的父路径的矩阵表示的数字是下标,因此还需要对不为-1的数加上1才对的上。

总结

  1. 并查集的原理和基本实现。
  2. 图论的基本概念,存储结构(邻接表和邻接矩阵),遍历方式(广度优先和深度优先),最小生成树的两个算法,最短路径的三个算法。
  • 并查集是一个较为简单的数据结构,而图论的表示形式是较为抽象的,需要我们将实际的例子抽象处理,因此不太好理解,关键在于多调试,多画图

尾序

我是舜华,期待与你的下一次相遇!

相关文章:

【数据结构】图论与并查集

一、并查集 1.原理 简单的讲并查集&#xff0c;就是查询两个个元素&#xff0c;是否在一个集合当中&#xff0c;这里的集合用树的形式进行表示。并查集的本质就是森林, 即多棵树。 我们再来简单的举个例子: 假设此时的你是大一新生&#xff0c;刚进入大学&#xff0c;肯定是…...

冲刺港股IPO,速腾聚创「承压」

对于「光鲜」的激光雷达公司来说&#xff0c;当下最难的问题是&#xff1a;如何说服投资者相信&#xff0c;前装市场能够按照预期定点订单兑现。 今年&#xff0c;作为国内高阶智驾头部车企之一的华为&#xff0c;在陆续推出的新车型上开始「降本」。其中&#xff0c;问界智驾版…...

Linux基础知识点(五-信号)

一、信号的基本概念 1.1 信号的概念 信号&#xff08;signal&#xff09;&#xff0c;又称为软中断信号&#xff0c;用于通知进程发生了异步事件&#xff0c;它是Linux系统响应某些条件而产生的一个事件&#xff0c;它是在软件层次上对中断机制的一种模拟&#xff0c;是一种异…...

SpringBoot 一个注解实现数据脱敏

什么是数据脱敏 数据脱敏是指对某些敏感信息&#xff0c;例如姓名、身份证号码、手机号、固定电话、银行卡号、邮箱等个人信息&#xff0c;通过脱敏算法进行数据变形&#xff0c;以保护敏感隐私数据。 数据脱敏通常涉及以下几种主要方法&#xff1a; 替换&#xff1a; 将原始…...

记录:开始学习网络安全

本文持续更新学习进度 背景 在私企干了5年虚拟化、云原生相关的运维&#xff0c;学到了很多&#xff0c;但不成体系。老板是清华毕业法国留学在德勤干过&#xff0c;最后回国创业的野路子。我工作是为了更好的生活&#xff0c;我挺担心老板因为家庭变故或者炒个原油宝&#x…...

C语言—第1次作业:编译与连接基础知识

常做练习巩固知识 本次作业答案链接如下&#xff1a; 答案解析——C语言—第1次作业&#xff1a;编译与连接基础知识 1.字符串的结束标志是&#xff1a;&#xff08; &#xff09; A.是0 B.是EOF C. 是\0 D.是空格 2.关于C语言关键字说法正确的是&#xff1a;( ) A.关…...

not attached to window manager问题解决

关于出现这个问题&#xff0c;一般是因为Activity已经在finish了&#xff0c;但是还在dialog.show()&#xff0c;或者dialog.dismiss().导致window manager无法管理dialog。解决办法如下&#xff1a; /** * 20210913 安全关闭对话框 . * 避免报&#xff1a;not attac…...

影视后期: PR调色处理,调色工具面板介绍

写在前面 整理一些影视后期的相关笔记博文为 Pr 调色处理&#xff0c;涉及调色工具面板简单认知包括 lumetri 颜色和范围面板理解不足小伙伴帮忙指正 元旦快乐哦 _ 名词解释 饱和度 是指色彩的鲜艳程度&#xff0c;也被称为色彩的纯度。具体来说&#xff0c;它表示色相中灰色…...

ARM AArch64的虚拟化(virtualization)详解(上)

目录 一、概述 开始之前 二、虚拟化介绍 为什么虚拟化很重要...

计算机组成原理知识总结

目录 第一章、计算机系统概述知识框架&#xff1a;1.冯诺依曼机和存储程序的概念&#xff1f;2.计算机的工作过程&#xff1f;3.在计算机系统结构中&#xff0c;什么是编译&#xff1f;什么是解释&#xff1f;4.描述一下指令执行过程&#xff1f;1) 取指令&#xff1a; PC 一&g…...

springboot学习(八十五) 解决springboot3.2找不到资源无法抛出404错误的问题

前言 springboot3.2以下可以定义ErrorPageRegistrar将404错误转发到一个接口地址&#xff0c;但升级到springboot3.2&#xff08;spring6.1&#xff09;后,该配置不生效&#xff0c;抛出了500错误。 以前的错误页面处理如下&#xff1a; ConditionalOnClass(ErrorPageRegist…...

OpenHarmony 应用通用签名

一.背景 由于hap包需要经过签名才能安装到设备上&#xff0c;在DevEco Studio可以进行自动签名&#xff0c;但是自动签名只能安装在当前的设备上&#xff0c;在其他设备上不能安装&#xff0c;所以我们需要进行通用的手动签名&#xff0c;手动签名HarmonyOS和OpenHarmony流程是…...

Redis:原理+项目实战——Redis实战1(session实现短信登录(并剖析问题))

&#x1f468;‍&#x1f393;作者简介&#xff1a;一位大四、研0学生&#xff0c;正在努力准备大四暑假的实习 &#x1f30c;上期文章&#xff1a;Redis&#xff1a;原理速成项目实战——Redis的Java客户端 &#x1f4da;订阅专栏&#xff1a;Redis速成 希望文章对你们有所帮助…...

交叉编译aarch64架构支持openssl的curl、libcurl

本文档旨在指导读者在x86_64平台上交叉编译curl和openssl库以支持aarch64架构。在开始之前&#xff0c;请确保您的系统环境已正确配置。 1. 系统环境准备 系统是基于Ubuntu 20.04 LTS&#xff0c;高版本可能会有问题。首页&#xff0c;安装必要的开发工具和库文件。打开终端并…...

扩展名是.KEY的文件可能有不同的存在,打开方式也因此不同

本文解释了使用KEY文件扩展名的所有不同格式&#xff0c;以及如何在可能的情况下打开和转换每种格式。 KEY文件的定义 KEY文件扩展名可能是用于注册软件程序的纯文本或加密的通用许可证密钥文件。不同的应用程序使用不同的KEY文件来注册各自的软件&#xff0c;并证明用户是合…...

软件工程总复习笔记

软件工程课程复习提纲 文章目录 软件工程课程复习提纲一、基本知识点1. 软件工程的概念及目标2. 软件危机的概念及典型表现3. 瀑布模型的概念及特点4. 快速原型模型的特点5. 螺旋模型的基本思想6. 软件生命周期的概念及划分为哪几个阶段7. 软件需求的定义8. 常见的软件需求获取…...

蓝桥杯-每日刷题-030

打印等边三角形 一、题目要求 题目描述 输出等边三角形&#xff1a;输入n值&#xff0c;输出高度为n的等边三角形。输入格式 输入存在多组测试数据。对于每组测试数据输入一个正整数n(1<n<100)。输出格式 对于每组测试数据输出对应的等边三角形。每组测试数据最后输出一…...

AI赋能游戏开发,如何更好地处理随之而来的海量数据,更好地利用开发游戏?

人工智能&#xff08;AI&#xff09;正在改变我们所知的游戏行业。它为3A工作室、独立开发者和业余爱好者提供了工具&#xff0c;让他们能够更轻松地创建以前需要大量时间和资源的项目。尤其是&#xff0c;虚幻引擎的AI工具已经取得了显著的进步。 虚幻引擎AI拥有专门用于游戏…...

Serverless架构学习路线及平台对比

在云计算领域&#xff0c;Serverless架构已经成为了一个重要的趋势。本文将为你提供一条清晰的Serverless架构学习路线&#xff0c;帮助你系统地掌握这个领域的知识&#xff0c;并对比国内外的Serverless平台的优缺点。 一、基础理论学习 首先&#xff0c;我们需要理解Server…...

解决ROS含动态参数的Config文件无法正确识别的错误

问题描述 功能包名为paddle_detection 在工作空间下, 通过catkin_make可以正常通过编译且执行无异常, 可以通过bloom-generate rosdebian生成依赖 但是在将其打包成deb包的过程中fakeroot debian/rules binary报错 fatal error: paddle_detection/paddle_detectionConfig.…...

探索 PyTorch 中的 torch.nn 模块**(1)

目录 引言 torch.nn使用和详解 Parameter 函数作用 使用技巧 使用方法和示例 UninitializedParameter 特点和用途 可进行的操作 使用示例 UninitializedBuffer 特点和用途 可进行的操作 使用示例 Module**&#xff08;重点&#xff09; 关键特性和功能 举例说…...

【WPF.NET开发】预览事件

本文内容 先决条件预览标记为“已处理”的事件通过控件解决事件禁止问题 预览事件&#xff0c;也称为隧道事件&#xff0c;是从应用程序根元素向下遍历元素树到引发事件的元素的路由事件。 引发事件的元素在事件数据中报告为Source 。 并非所有事件场景都支持或需要预览事件。…...

JDBC->SpringJDBC->Mybatis封装JDBC

一、JDBC介绍 Java数据库连接&#xff0c;&#xff08;Java Database Connectivity&#xff0c;简称JDBC&#xff09;是Java语言中用来规范客户端程序如何来访问数据库的应用程序接口&#xff0c;提供了诸如查询和更新数据库中数据的方法。JDBC也是Sun Microsystems的商标。我们…...

ts中的keyof 关键字

const getVal <T,K extends keyof T>(obj:T,key:K) : T[K]>{return obj[key]; }使用了 keyof 关键字。keyof 是 TypeScript 的一个特性&#xff0c;它返回一个字符串字面量类型&#xff0c;表示对象类型的所有属性键的联合类型。 这段代码定义了一个泛型函数 gatVal&…...

Head First Design Patterns - 装饰者模式

什么是装饰者模式 装饰者模式动态地将额外责任附加到对象上。对于拓展功能&#xff0c;装饰者提供子类化的弹性替代方案。 --《Head First Design Patterns》中的定义 为什么会有装饰者模式 根据上述定义&#xff0c;简单来说&#xff0c;装饰者模式就是对原有的类&#xff0c…...

MySQL 执行过程

MySQL 的执行流程也确实是一个复杂的过程&#xff0c;它涉及多个组件的协同工作&#xff0c;故而在面试或者工作的过程中很容易陷入迷惑和误区。 MySQL 执行过程 本篇将以 MySQL 常见的 InnoDB 存储引擎为例&#xff0c;为大家详细介绍 SQL 语句的执行流程。从连接器开始&…...

判断电话号码是否重复-excel

有时候重复的数据不需要或者很烦人&#xff0c;就需要采取措施&#xff0c;希望以下的方法能帮到你。 1.判断是否重复 方法一&#xff1a; 1&#xff09;针对第一个单元格输入等号&#xff0c;以及公式countif(查找记录数的范围&#xff0c;需要查找的单元格&#xff09; 2…...

【Java开发岗面试】八股文—Java虚拟机(JVM)

声明&#xff1a; 背景&#xff1a;本人为24届双非硕校招生&#xff0c;已经完整经历了一次秋招&#xff0c;拿到了三个offer。本专题旨在分享自己的一些Java开发岗面试经验&#xff08;主要是校招&#xff09;&#xff0c;包括我自己总结的八股文、算法、项目介绍、HR面和面试…...

【Linux】Linux 下基本指令 -- 详解

无论是什么命令&#xff0c;用于什么用途&#xff0c;在 Linux 中&#xff0c;命令有其通用的格式&#xff1a; command [-options] [parameter] command&#xff1a;命令本身。-options&#xff1a;[可选&#xff0c;非必填]命令的一些选项&#xff0c;可以通过选项控制命令的…...

Eureka注册及使用

一、Eureka的作用 Eureka是一个服务注册与发现的工具&#xff0c;主要用于微服务架构中的服务发现和负载均衡。其主要作用包括&#xff1a; 服务提供者将自己注册到Eureka Server上&#xff0c;包括服务的地址和端口等信息。服务消费者从Eureka Server上获取服务提供者的地址…...

Ubuntu之修改时区/时间

1、查看当前时间及时区状态 sudo timedatectl status # 显示当前时区为Asia/Shanghai 2、查看当前系统时间 sudo date 3、查看当前系统时间及时区 sudo date -R # 显示当前时间及对应时区&#xff0c;时区为“0800”北京时区 4、修改硬件时间 修改日期格式&#xff1a…...

4、内存泄漏检测(多线程)

4、内存泄漏多线程 多线程下使用Valgrind 工具的memcheck检查. 安装 sudo apt install valgrind使用 valgrind --toolmemcheck --leak-checkfull ./app_main 指令效果如下所示. wqwq-Virtual-Machine:~/work/test_zlog/build$ valgrind --toolmemcheck --leak-checkfull .…...

在使用tcp长连接时,是否还需要再引入重发机制?

一 什么是tcp长连接&#xff1f; 在TCP&#xff08;Transmission Control Protocol&#xff09;中&#xff0c;长连接是指在通信过程中保持连接状态的一种方式&#xff0c;相对于短连接而言。长连接通常用于需要频繁通信的场景&#xff0c;以减少连接建立和断开的开销。在长连接…...

记一次Oracle Cloud计算实例ssh恢复过程

#ssh秘钥丢失# &#xff0c; #Oracle Cloud# 。 电脑上的ssh秘钥文件不知道什么时候丢失了&#xff0c;直到用的时候才发现没有了&#xff0c;这下可好&#xff0c;Oracle Cloud的计算实例连不上了&#xff0c;这个实例只能通过ssh连接上去&#xff1a; 以下是解决步骤&#x…...

2024年01月数据库流行度最新排名

点击查看最新数据库流行度最新排名&#xff08;每月更新&#xff09; 2024年01月数据库流行度最新排名 TOP DB顶级数据库索引是通过分析在谷歌上搜索数据库名称的频率来创建的 一个数据库被搜索的次数越多&#xff0c;这个数据库就被认为越受欢迎。这是一个领先指标。原始数…...

Stable Diffusion API入门:简明教程

Stable Diffusion 是一个先进的深度学习模型&#xff0c;用于创造和修改图像。这个模型能够基于文本描述来生成图像&#xff0c;让机器理解和实现用户的创意。使用这项技术的关键在于掌握其 API&#xff0c;通过编程来操控图像生成的过程。 在探索 Stable Diffusion API 的世界…...

数据结构--二叉搜索树的实现

目录 1.二叉搜索树的概念 2.二叉搜索树的操作 二叉搜索树的插入 中序遍历(常用于排序) 二叉搜索树的查找 二叉搜索树的删除 完整二叉树代码&#xff1a; 二叉搜索树的应用 key/value搜索模型整体代码 1.二叉搜索树的概念 二叉搜索树又称二叉排序树&#xff0c;它或者是一…...

《微信小程序开发从入门到实战》学习六十八

6.6 网络API 6.6.1 网络API 使用wx.request接口可以发起网络请求。该接口接受一个Object参&#xff0c;参数支持属性如下所示&#xff1a; url(必填)&#xff1a;开发者服务器地址 data&#xff1a;请求的参数&#xff0c;类型为string/object/ArrayBuffer header&#xf…...

阿里是如何去“O”的?

大家好&#xff0c;我是老猫&#xff0c;猫头鹰的“猫”。 今天我们来聊聊数据库这个话题。 2009年&#xff0c;阿里提出“去IOE化”的概念&#xff0c;这在当时看起来是天方夜谭&#xff0c;但目前来看可以说是"轻舟已过万重山"。 IOE是传统IT三大件&#xff0c;…...

蓝桥杯备赛 day 1 —— 递归 、递归、枚举算法(C/C++,零基础,配图)

目录 &#x1f308;前言 &#x1f4c1; 枚举的概念 &#x1f4c1;递归的概念 例题&#xff1a; 1. 递归实现指数型枚举 2. 递归实现排列型枚举 3. 递归实现组合型枚举 &#x1f4c1; 递推的概念 例题&#xff1a; 斐波那契数列 &#x1f4c1;习题 1. 带分数 2. 反硬币 3. 费解的…...

87 双指针解验证回文字符串II

问题描述&#xff1a;简单给定一个非空字符串s&#xff0c;最多删除一个字符&#xff0c;判断是否成为回文字符串。 双指针解法&#xff1a;指针1指向开头&#xff0c;指针2指向结尾&#xff0c;定义一个count记录不满足回文串的数量&#xff0c;若超过1&#xff0c;则返回fal…...

【排序算法】【二叉树】【滑动窗口】LeetCode220: 存在重复元素 III

作者推荐 【二叉树】【单调双向队列】LeetCode239:滑动窗口最大值 本文涉及的基础知识点 C算法&#xff1a;滑动窗口总结 题目 给你一个整数数组 nums 和两个整数 indexDiff 和 valueDiff 。 找出满足下述条件的下标对 (i, j)&#xff1a; i ! j, abs(i - j) < indexDi…...

OS 7--DNS配置+Apache发布网站

环境准备 centOS 7 1.配置DNS 1.1 域名为lianxi.com 1.2 为WWW服务器、FTP服务器、NEWS服务器做域名解析 1)安装DNS yum -y install bind bind-utils (如果安装不上&#xff0c;就把磁盘在重洗挂载一下&#xff09; 2&#xff09;修改DNS配置文件 vim /etc/resolv.conf…...

1月2日代码随想录二叉树的最小深度及层序遍历总结

个人认为这么一个层序遍历的章节放这么多基本一样的题目算是很没意思的了 填充每个节点的下一个右侧节点和二叉树最大深度和前面的代码几乎完全一样&#xff0c;所以我就跳过了 代码随想录 (programmercarl.com) 代码随想录 (programmercarl.com) 111.二叉树的最小深度 给…...

RK3568平台开发系列讲解(Linux系统篇)PWM系统编程

🚀返回专栏总目录 文章目录 一、什么是PWM二、PWM相关节点三、PWM应用编程沉淀、分享、成长,让自己和他人都能有所收获!😄 📢本篇将介绍 PWM 的系统编程。 一、什么是PWM PWM,即脉冲宽度调制(Pulse Width Modulation)...

Linux CPU 数据 Metrics 指标解读

过去从未仔细了解过使用 top 和 htop 等命令时显式的CPU信息&#xff0c;本文我们详解解读和标注一下各个数据项的含义&#xff0c;同时和 Ganglia 显式的数据做一个映射。开始前介绍一个小知识&#xff0c;很多查看CPU的命令行工具都是 cat /proc/stat 里的数据&#xff0c;所…...

Ansible自动化运维(一)简介及部署、清单

&#x1f468;‍&#x1f393;博主简介 &#x1f3c5;云计算领域优质创作者   &#x1f3c5;华为云开发者社区专家博主   &#x1f3c5;阿里云开发者社区专家博主 &#x1f48a;交流社区&#xff1a;运维交流社区 欢迎大家的加入&#xff01; &#x1f40b; 希望大家多多支…...

深度学习MLP_实战演练使用感知机用于感情识别_keras

目录 &#xff08;1&#xff09;why deep learning is game changing?&#xff08;2&#xff09;it all started with a neuron&#xff08;3&#xff09;Perceptron&#xff08;4&#xff09;Perceptron for Binary Classification&#xff08;5&#xff09;put it all toget…...

[ffmpeg系列 02] 音视频基本知识

一 视频 RGB&#xff1a; AV_PIX_FMT_RGB24, ///< packed RGB 8:8:8, 24bpp, RGBRGB… Y&#xff1a;明亮度, Luminance或luma, 灰阶图&#xff0c; UV&#xff1a;色度&#xff0c;Chrominance或Chroma。 YCbCr: Cb蓝色分量&#xff0c;Cr是红色分量。 取值范围&#xff…...

【ASP.NET Core 基础知识】--目录

介绍 1.1 什么是ASP.NET Core1.2 ASP.NET Core的优势1.3 ASP.NET Core的版本历史 环境设置 2.1 安装和配置.NET Core SDK2.2 使用IDE&#xff08;Integrated Development Environment&#xff09;&#xff1a;Visual Studio Code / Visual Studio 项目结构 3.1 ASP.NET Core项…...