哪些网站是做数据分析的阿里云1m服务器可以搭建网站
web/
2025/10/5 12:47:48/
文章来源:
哪些网站是做数据分析的,阿里云1m服务器可以搭建网站,动画设计需要学什么软件有哪些,国内c2c电子商务平台有哪些一、并查集
1.原理
简单的讲并查集#xff0c;就是查询两个个元素#xff0c;是否在一个集合当中#xff0c;这里的集合用树的形式进行表示。并查集的本质就是森林, 即多棵树。 我们再来简单的举个例子:
假设此时的你是大一新生#xff0c;刚进入大学#xff0c;肯定是…一、并查集
1.原理
简单的讲并查集就是查询两个个元素是否在一个集合当中这里的集合用树的形式进行表示。并查集的本质就是森林, 即多棵树。 我们再来简单的举个例子:
假设此时的你是大一新生刚进入大学肯定是先找到宿舍在哪里然后跟寝室里面的舍友互相认识一下先形成一个小团体。假设宿舍总共6个人也就是6个人的集合。几乎所有的大学生都是这样先跟周围的人进行联系起来的。然后辅导员召集班会这时的你欣然前往并在讲台上自信的介绍自己然后吸引或者主动又认识了一群人。这时你或许又跟其它的人进行了关联或成为了好友或成为了恋人……
下面我们用如上例子进行展开讨论 宿舍六人即六个人如何判断两个人在同一个集合 如何进行实现 先来解决第一个问题六个人选出一个宿舍长只要两个人的宿舍长是一样的即可判断两个人在一个集合。再来解决第二个问题既然宿舍长有了我们都与这个宿舍长产生关联即可即用树的形式进行表示至于如何表示我们可以用双亲表示法进行表示即每个人记住其宿舍长的名字即可。更为形象的我们可以用下图进行表示 更进一步如何用计算机存储这种结构呢我们只需对每个人名生成一个下标连续用计算机进行存储即可。用下图进行直观的理解 对这张图我们再说明一点除0下标以外的其他位置存放的是指向代表孙八的下标这个0处下标存的是集合的所有元素的个数且存放的是负数形式这样存有一个好处我们可以由这个并查集中有多少负数从而判断这个并查集中有多少个集合。 两个人产生关联本质上是两个宿舍(集合)之间产生了关联那两个宿舍如何进行关联起来呢 下面我们以图的形式更为清晰的进行表述 也就是说因为宿舍的成员是以宿舍长联系起来的那宿舍与宿舍之间产生关联(合并)就宿舍长之间认识一下两个集合就间接的关联起来了。下图是具体的存储方式 2.基本实现 根据上面的描述我们可以作出大致总结
数组进行存储表示树形结构。数组的下标对应着具体的信息(人名编号等)。我们可以通过一个元素的下标的值不断往上查找直到找到找到小于0的即为根节点所在的位置。数组中负数的个数代表着集合的个数。判断两个元素是否在同一个集合只需找到根的下标判断是否相等即可。将两个不同集合进行合并其实就是找到根然后进行更改一个根的指向与改变另一个根的元素个数即可。
由以上信息我们先可以搭建出实现并查集的大致框架
2.1.基本框架
#includeiostream
#includevector
#includemap
using namespace std;
templateclass 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:mapT, int _indexHash;//map或者unordered_map都可以。用于快速将T转换为对应的下标。vectorT _createIndex;//用此数组对T类型元素生成下标。vetorint _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
#includeiostream
#includevector
#includemap
using namespace std;
templateclass 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:mapT, int _indexHash;//map或者unordered_map都可以,用于快速将T转换为对应的下标。vectorT _createIndex;//用此数组对T类型元素生成下标。vectorint _aggregate; //用于存放集合元素即森林。
};Test.cpp
#includeUnionFindSet.hpp
int main()
{string str[] { 张三,李四,王五,赵六,周七 };UnionFindSetstring ufs(str, sizeof(str) / sizeof(str[0]));ufs.Union(张三, 李四);ufs.Union(王五, 赵六);cout 集合数为 ufs.GetSetSize() endl;return 0;
}运行结果 并查集习题 省份数量.等式方程的可满足性 补充一下
直接用下标进行抽象是最常用的因此这里的生成下标的vector与快速索引的map可以省去形成一个简化版的并查集更方便我们使用。这里我们将并查集与图论放在一起是因为并查集可以帮助起到判环的作用因此我们这里放到一块进行讲解。
二、图论
1.基本概念
图的概念有点凌乱博主以思维导图的形式呈现出 2.存储结构 图有两个基本元素 顶点, 我们可以将具体的顶点抽象成下标从而用下标进行表示。边两个顶点即可确定一条边因此我们可以用二维矩阵的方式进行表示每个顶点都有与其相连的边因此我们可以单独每个顶点所连接的边抽象成桶的形式(类似于哈希桶)进行表示。 因此我们通常有邻接矩阵和邻接表的形式进行存储。
2.1邻接矩阵
实现代码 /*V(vertex) 表示实际存储边的类型W(weight)表示边的权重W_MAX 表示权重的不可能取值。Direction false表示是无向的true表示是有向的。*/templateclass V, class W, W W_MAX INT_MAX, bool Direction falseclass 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;}vectorV _vertexs;//顶点mapV, int _indexMap;//顶点所对应的下标vectorvectorW _matrices; //矩阵的英文};说明 如果边带有权值并且两个节点之间是连通的边的关系就用权值代替。如果两个顶点不通则使用无穷大代替即W_MAX。 测试用例: void TestGraph()
{Graphchar, 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
{/*因为要存顶点与边的关系因此我们需要一个结构体来保存对应的相连的顶点与边的权值。*/templateclass V,class Wstruct Edge{V _dst;//目标顶点W _w;//权值EdgeV, W* _next;//构造函数Edge(const V dst, const W w):_dst(dst),_w(w),_next(nullptr){}};templateclass V, class W, bool Direction falseclass Graph{public:typedef EdgeV, 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:vectorV _vertexs;//顶点mapV, int _indexMap;//顶点所对应的下标vectorEdge* _link; //邻接表};
}测试用例
void TestGraph()
{string a[] { 张三, 李四, 王五, 赵六 };Graphstring, int,true g1(a, 4);g1.AddEdge(张三, 李四, 100);g1.AddEdge(张三, 王五, 200);g1.AddEdge(王五, 赵六, 30);g1.Print();
}运行结果 总结
邻接矩阵适合快速查看两个顶点的关系与路径权值。而对于顶点连接的边有多少是什么则需要遍历矩阵所在行进行确认。邻接表适合直接取所有与点相连的边而不适合快速查看两个顶点的关系。因此邻接矩阵和邻接表是相辅相成的而综合来看的话对于较为稀疏的图即顶点相连的边较少平分秋色各有千秋而对于稠密的完全图来说邻接矩阵更为合适。因此我们下面统一采用临界矩阵的方式进行实现。
3.遍历方式
3.1广度优先遍历
图解
我们再来分析一下流程这里是以A为起点进行广度遍历。 先遍历A,。然后遍历与A相连的BCD。其次在遍历与BCD相连的EF此时就需要注意之前访问过的结点不能在接着继续访问了。接着遍历与EF相连的HG,此时也需注意同样的问题。最后遍历与H相连的I,此时同理。 因此广度优先遍历需注意访问的时候不能再访问已经访问过的结点其次访问时越访问越深的。
实现方式
采用队列的结构不断入与队列元素相连的未访问的结点。使用一个vector 记录结点是否已经被访问过了当入队列时即将对应的结点的下标标记为true。
void BFS(const V src)
{int srci GetVertexIndex(src);int n _vertexs.size();vectorint is_visited(n, false);//防止重复结点入队列以免形成回路。queueint 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 };Graphstring, 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为起点进行深度遍历。
说明已经访问过的结点我们是不再进行访问的。 先访问A相邻的B, 再访问与B相连的C, 再访问与C相连的F, 再访问与F相连的D。D相邻的A我们是不再进行访问的因此又回到F, 接着访问H紧接着访问与H相连的II没有访问过的结点回退到H, H也没有访问过的结点回退到 F。F也没有与未访问的结点回退到CC也没有未访问的结点于是回退到B。接着访问与B相连的E, 更深一步访问与E相连的GG没有未访问过的结点回退到E, E此时也没有未访问过的结点回退到B, B此时也没有未访问过的结点回退到A.访问结束。 实现代码 void _DFS(int srci,vectorbool 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);vectorbool 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 };Graphstring, 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算法
原理 首先将所有的边管理起来每次取出最小的边。判断已经选出的边是否构环如果构成就弃置再从中选最小的边。(n个顶点构成的图)选择n-1条边即可。 实现关键
用优先级队列对边进行管理。用并查集进行判环。
实现代码
/*
为方便读者进行阅读此处博主贴了一份并查集的简略代码。
*/templateclass Tclass 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:vectorint _pPath;};/*此结构体用于存放边的信息放入优先级队列中便于进行管理。*/templateclass Wstruct 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_queueEdge, vectorEdge, greaterEdge 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;UnionFindSetint 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;Graphchar, 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);Graphchar, int kminTree(strlen(str));cout Kruskal: g.Kruskal(kminTree) endl;}
/*main函数自由发挥吧*/运行结果 图解 说明
程序走出的过程可能不一样比如相同的边谁先选可能由优先级的实现原理决定但大概率结果是一样的。我们走出的只是局部的最优解全局的最优解可能还与相同的边的选择顺序有关相同的边的如果互相影响则可能会影响后面更大的边的选择。因此如果所有的边互不相同那我们可以断定此算法走出的最小生成树是确定的即为全局的最小生成树。
4.2Prim算法
原理 将顶点分为两个集合设一个集合为X, 一个集合为Y。选择一个起始点放入X集合剩余的顶点放入Y集合。每次选择从Y中选择与X相连的最小的边并将其相连的顶点放入X集合从Y中丢弃此顶点。直到选择 n - 1条边为止。 实现关键
将顶点分为两个集合X, Y其实就避开了环的问题产生环的原因本质就是一个集合内的两个顶点连到一块了。我们选的是与集合X相连的最小的边因此还要把X相连的边放入优先级队列往后循环可能会有一个集合内的边我们只需判断边所连的目标顶点不在集合X即可对于在集合X的我们不选即可。除此之外我们还需要确立一个起始点用来初始化集合X和集合Y。
实现代码 W Prim(self min,const V src){size_t n _vertexs.size();min._vertexs _vertexs;/*第一步选择顶点作为起始顶点。分为两个数组一个为起始数组,一个为选边数组*/int srci GetVertexIndex(src);vectorbool X(n,false);vectorbool Y(n,true);X[srci] true;Y[srci] false;//第二步:将与srci相关的边入队列中。priority_queueEdge, vectorEdge, greaterEdge 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;Graphchar, 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);Graphchar, int pminTree(strlen(str));cout Prim: g.Prim(pminTree, a) endl;pminTree.Print();}
/*main 函数只需调用此函数即可*/运行结果 图解
5.最短路径
最短路径是描述两个顶点能连通的情况下考虑两个顶点之间所经过路径的权值之和的最小值。 举个例子在现实世界中我们已经不关心两个地方能不能到的问题了我们主要关系的是两个地方如何规划路程最短或者花费最低诸如此类的问题抽象到计算机即转换为了两个顶点所经过的路径的权值之和如何才能最短。 由此我们引出迪杰斯特拉(Dijkstra), 贝尔曼福特(Bellman-Ford), 弗洛伊德(floyd warshall) 三种算法。
5.1Dijkstra算法
基本认识 此算法主要求的是不带负权值最小路径。 算法思想主要在单源最短路径中进行体现。 算法原理(贪心) 确定一个起始点更新与其直接相连的顶点的路径。选择路径和最短的那一个此处确定了第一条路径最短的边。 确定两字我们此处再稍作解释由于已经选择了起始点直接到路径最短的顶点。因此不可能再出现从起始点到另一个顶点再经过其它顶点到此点的路径和更短更简单的表述是两点直接连着已经最短的了再通过其它点绕远路只会更长不会更短。此处用数学的语言进行描述或许更加直观。 再由最短的那个顶点再更新(如果更小再进行更新)与其直接相连的边再确定一条路径最短的边的顶点。由此顶点再进行更新。如此往复直到没有顶点可以更新就结束。 实现代码 void Dijkstra(const V src, vectorW dst, vectorint 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使得每个结点只访问一次vectorbool 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, vectorW dst, vectorint pPath)
{int srci GetVertexIndex(src);size_t n _vertexs.size();//先找到路径再进行逆置for (size_t i 0; i n; i){//不能是srci要不然就陷入环了。if (i ! srci){vectorint 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;Graphchar, 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);vectorint dist;vectorint parentPath;g.Dijkstra(s, dist, parentPath);g.PrinrtShotPath(s, dist, parentPath);}运行结果: 图解
5.2Bellman-Ford算法 用处单源最短路径的负权值不带负权回路的图 思想暴力枚举遍历 由于只会更新出更短的路径我们可以采取暴力枚举的方法。将所有的边进行遍历之后再遍历 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, vectorW dst, vectorint 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;Graphchar, 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);vectorint dist;vectorint parentPath;if (g.BellmanFord(s, dist, parentPath)){g.Print();g.PrinrtShotPath(s, dist, parentPath);}else{cout 存在负权回路 endl;}}运行结果 图解 说明暴力更新调试着看数据的变化效果更好。 测试用例2 void TestGraphBellmanFord(){// 微调图结构带有负权回路的测试const char* str syztx;Graphchar, 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);vectorint dist;vectorint parentPath;if (g.BellmanFord(s, dist, parentPath)){g.PrinrtShotPath(s, dist, parentPath);}else{cout 存在负权回路 endl;}}运行结果 图解 说明暴力循环完之后再更新一次又会引起其它变小此种情况只会越更新越小求不出最小路径
5.3floyd warshall算法 用处多源最短路径的负权值不带负权回路的图 算法思想(dp) 拆分子问题分为两种情况 所有的边经过点K.所有的边不经过点K.这里的K可能是所有的顶点。因此求前两种情况的所有情况的最小值即可。 图解
实现代码
void FloydWarshall(vectorvectorW vvdst,
vectorvectorint 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;Graphchar, 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);vectorvectorint vvDist;vectorvectorint 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才对的上。 总结
并查集的原理和基本实现。图论的基本概念存储结构(邻接表和邻接矩阵)遍历方式(广度优先和深度优先)最小生成树的两个算法最短路径的三个算法。
并查集是一个较为简单的数据结构而图论的表示形式是较为抽象的需要我们将实际的例子抽象处理因此不太好理解关键在于多调试多画图。
尾序 我是舜华期待与你的下一次相遇!
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/87375.shtml
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!