高阶数据结构(1):并查集 与 图
"Head in the clouds"
一、并查集
(1)认识并查集?
在一些问题中需要将n个不同的元素划分成 一些不想交的集合。
开始时,每个元素自成一个单元素集合,然后按一定的规律将归于同一组元素的集合合并。在此过程中要反复用到查询某一个元素归属于那个集合的运算。
例举如下场景:
某高校总共招生10人,成都5人,西安4人,武汉1人。 这些人作为新人进入高校,可能并没有任何关系,互不相识。(一个一个单独的团体)
相反,如果此时要进行分寝,这些人便会自发组织成一个小队。
当然,在语言中,无法实现上述这样的结构。因此 选用数组来 进行模拟。
但,突然寝室单间规模进行更改,不再是单独的四人寝。那么也就意味着,本来已经以集合形式分离的三个队,此时需要进行合并。
此时,仅仅只需让另外一棵“树” 作为其子树即可。
得出以下结论:
1. 数组的下标对应集合中元素的编号
2. 数组中如果为负数,负号代表根,数字代表该集合中元素个数
3. 数组中如果为非负数,代表该元素双亲在数组中的下标
(2)并查集的实现
由此,我们不免 需要对以下问题提供解决方法:
1. 查找元素属于哪个集合
2. 查看两个元素是否属于同一个集合
3. 将两个集合归并成一个集合
4. 集合的个数
①查找
//查找x 属于哪一个集合(返回下标)
size_t FindRoot(int x)
{while (_ufs[x] >= 0)x = _ufs[x];return x;
}
沿着数组表示树形关系以上一直找到根(即:树中中元素为负数的位置)
②合并
void Union(int x1, int x2){//先判断是否x1 x2 已经属于此集合了int root1 = FindRoot(x1);int root2 = FindRoot(x2);//1.属于一个集合if (root1 == root2) return;//2.需要进行合并//用谁去做根呢 ? 答案是都可以//两个棵树的 合并_ufs[root1]+=_ufs[root2];//去做 合并的子树_ufs[root2]=root1;}
1.将两个集合中的元素合并。
2.将一个集合名称改成另一个集合的名称。
③是否在集合里
bool InSet(int x1,int x2){return FindRoot(x1) == FindRoot(x2);}
沿着数组表示的树形关系往上一直找到树的根,如果根相同表明在同一个集合,否则不在
④集合个数
size_t SetSize(){int size = 0;for (auto e : _ufs){if (e < 0) size++;}return size;}
遍历数组,数组中元素为负数的个数即为集合的个数。
测试1:
优化:
①路径压缩:
在这种情况下
//查找x 属于哪一个集合(返回下标)size_t FindRoot(int x){//先找到root 节点int root = x;while (_ufs[root] >= 0)root = _ufs[root];//路径压缩while (_ufs[x] >= 0) //直到更新到 根节点!{//记录x 的parnet 避免被修改int parent = _ufs[x];//直接让x 作为 root的子节点_ufs[x] = root;x = parent;}return x;}
②集合与集合的合并
void Union(int x1, int x2){//先判断是否x1 x2 已经属于此集合了int root1 = FindRoot(x1);int root2 = FindRoot(x2);//1.属于一个集合if (root1 == root2) return;//为什么需要小的集合树 向大的集合树合并?//控制 if (_ufs[root1] < _ufs[root2])swap(root1, root2);_ufs[root1]+=_ufs[root2];//去做 合并的子树_ufs[root2]=root1;}
(3)并查集OJ题
省份数量https://leetcode.cn/problems/bLyHh0/
但是,如果写一个这个题,就得写一整个并查集? 显然很麻烦。
可见,并查集的核心就在于 find
等式方程的可满足性https://leetcode.cn/problems/satisfiability-of-equality-equations/comments/
二、图
(1)认识图
图是由顶点集合及顶点间的关系组成的一种数据结构:G = (V,E)
V:顶点集合= {x|x属于某个数据对象集}是有穷非空集合;
E = {(x,y)|x,y属于V}或者E = {<x, y>|x,y属于V && Path(x, y)}是顶点间关系的有穷集合,也叫
做边的集合;
你是否对G2感到熟悉? 是的没错,树是一种特殊的集合。
图的其他概念:
1.完全图: 无向完全图(G1) 有向完全图(G4);
顾名思义,即任意两个顶点之间有且仅有一条边。
2.邻接顶点:在无向图中G中,若(u, v)是E(G)中的一条边,则称u和v互为邻接顶点。
3.顶点的度:顶点v的度是指与它相关联的边的条数。
4.路径:在图G = (V, E)中,若从顶点vi出发有一组边使其可到达顶点vj,则称顶点vi到顶点vj的顶点序列为从顶点vi到顶点vj的路径。
5.路径长度:在图G = (V, E)中,若从顶点vi出发有一组边使其可到达顶点vj,则称顶点vi到顶点vj的顶点序列为从顶点vi到顶点vj的路径。
简单路径与回路:
径上第一个顶点v1和最后一个顶点vm重合,则称这样的路径为回路或环。
子图:设图G = {V, E}和图G1 = {V1,E1},若V1属于V且E1属于E。
连通图:在无向图中,若从顶点v1到顶点v2有路径,则称顶点v1与顶点v2是连通的。
如果图中任意一对顶点都是连通的,则称此图为连通图。
生成树:一个连通图的最小连通子图称作该图的生成树。有n个顶点的连通图的生成树有n个顶点和n-1条边。
(2)图的存储结构
图的存储结构,实质就是指的是 顶点与 权值的存储结构。
节点保存比较简单,只需要一段连续空间即可,那边关系该怎么保存呢?
①邻接矩阵
因此邻接矩阵(二维数组)即是:先用一个数组将定点保存,然后采用矩阵来表示节点与节点之间的关系。
注:
1.无向图的邻接矩阵是对称的,第i行(列)元素之和,就是顶点i的度。有向图的邻接矩阵则不一定是对称的,第i行(列)元素之后就是顶点i 的出(入)度。
2. 如果边带有权值,并且两个节点之间是连通的,上图中的边的关系就用权值代替,如果两个顶点不通,则使用无穷大代替。
反思:
优点
用邻接矩阵存储图的有点是能够快速知道两个顶点是否连通。
缺陷:
是如果顶点比较多,边比较少时,矩阵中存储了大量的0成为系数矩阵,比较浪费空间,并且要 求两个节点之间的路径不是很好求。
②邻接表
邻接表:使用数组表示顶点的集合,使用链表表示边的关系。
邻接表的实现,很类似于 哈希桶的实现。在每个顶点的后面,挂接直接相连的点。
因此,如果想要知道一个顶点,和哪些顶点存在连通,只需要遍历链表即可。
注:因为 无向图是 双向的, 因此如果存在 i ->j 是连通的,那么 i->j 也是连通的。
(3)图存储结构的实现
①邻接矩阵
对于图而言,其也是一个数据结构。不外于 CURD;
ADD;
size_t GetVertexIndex(const V& key){auto ret = _mapIndex.find(key);if (ret != _mapIndex.end()){return ret->second;}else{//assert(false);cout << "顶点不存在:"<<key << endl;throw invalid_argument("顶点不存在");return -1;}}void _AddEdge(size_t srci, size_t dsti,const W& w){_martex[srci][dsti] = w;if (Direction == false){_martex[dsti][srci] = w;}}void AddEdge(const V& src,const V& dst,const W& w){size_t srci = GetVertexIndex(src);size_t dsti= GetVertexIndex(dst);_AddEdge(srci, dsti, w);}
删除、修改也就不再多言,就是找到 srci 、dsti 去 邻接矩阵里 操作即可。
测试:
void Print(){for (size_t i = 0;i < _vertex.size();++i){cout << "[" << i << "]" << "->" << _vertex[i]<<endl;}cout << " ";for (size_t i = 0;i < _matrix.size();++i){printf("%4d", i);}cout << endl;for (size_t i = 0;i < _matrix.size();++i){cout << i << " "; for (size_t j = 0;j < _matrix[i].size();++j){if (_matrix[i][j] == MAX_W){//cout << "* ";printf("%4c", '*');}else{//cout << _matrix[i][j] << " ";printf("%4d", _matrix[i][j]);}}cout << endl;}cout << endl;}
②邻接表
ADD;
size_t GetVertexIndex(const V& v){auto ret = _mapIndex.find(v);if (ret != -_mapIndex.end()){return ret->second;}else{//assert(false);cout << "顶点不存在:" << key << endl;throw invalid_argument("顶点不存在");return -1;}}void _AddEdge(size_t srci, size_t dsti, const W& w){Edge* eg = new Edge(dsti, w);eg->_next = _table[srci];_table[srci] = eg;if (Direction == false){Edge* eg = new Edge(srci, w);eg->_next = _table[dsti];_table[dsti] = eg;}}void AddEdge(const V& src, const V& dst, const W& w){size_t srci = GetVertexIndex(src);size_t dsti = GetVertexIndex(dst);_AddEdege(srci, dsti, w);}
测试:
(4)图的遍历
给定一个图G和其中任意一个顶点v0,从v0出发,沿着图中各边访问图中的所有顶点,
且每个顶点仅被遍历一次。
①广度(BFS)优先遍历
应用到图的顶点。
②深度(DFS)优先遍历
应用到图:
③具体实现:
BFS;
void BFS(const V& src){size_t srci = GetVertexIndex(src);int n = _vertex.size();queue<int> q;vector<bool> visited(n, false);size_t levelsize = 1;q.push(srci);visited[srci] = true;while (!q.empty()){for (size_t i = 0;i <levelsize;++i){cout << "第" << i << "层";int front = q.front();q.pop();for (size_t i = 0;i < n;++i){if (_matrix[front][i] != MAX_W&& visited[i] == false){q.push(i);visited[i] = true;}}}levelsize = q.size();}}
DFS;
void _DFS(size_t srci, vector<bool>& visited){cout << srci << ":" << _vertex[srci] << endl;visited[srci] = true;for (size_t i = 0;i < _matrix.size();++i){if (_matrix[srci][i] != MAX_W && visited[i] == false){_DFS(i, visited);}}}void DFS(const V& src){size_t srci = GetVertexIndex(src);vector<bool> visited(_matrix.size(),false);_DFS(srci, visited);}
测试;
三、图高阶(选学掌握)
对于图而言,掌握图的概念,图结构的基本优缺点,BFS \ DFS仅够了。
下面内容 知道思想即可。
(1)最小生成树
什么是生成树呢?
连通图中的每一棵生成树,都是原图的一个极大无环子图,即:从其中删去任何一条边,生成树就不在连通;反之,在其中引入任何一条新边,都会形成一条回路。
最小生成树的三大准则;
1. 只能使用图中的边来构造最小生成树。
2.只能使用恰好n-1条边来连接图中的n个顶点
3.选用的n-1条边不能构成回路
最小生成树的算法;
Kruskal算法和Prim算法; 两者都是采用贪心 策略
贪心算法:求 局部最优解。从而实现整体的最优解。
①kruskal克鲁斯卡尔算法
思想:
任给一个有n个顶点的连通网络N={V,E},首先构造一个由这n个顶点组成、不含任何边的图G={V,NULL}。
其中每个顶点自成一个连通分量,其次不断从E中取出权值最小的一条边(若有多条任取其一),若该边的两个顶点来自不同的连通分量,则将此边加入到G中。
如此重复,直到所有顶点在同一个连通分量上为止。
核心:每次迭代时,选出一条具有最小权值,且两端点不在同一连通分量上的边,加入生成树。
W Kruskal(Self& mintree){size_t n = _vertex.size();mintree._vertex = _vertex;mintree._mapIndex = _mapIndex;mintree._matrix.resize(n);for (size_t i = 0;i < n;++i){mintree._matrix[i].resize(n, MAX_W);}priority_queue<Edge, vector<Edge>, greater<Edge>> minque;for (size_t i = 0;i < n;++i){for (size_t j = 0;j < n;++j){if (_matrix[i][j] != MAX_W){minque.push(Edge(i,j,_matrix[i][j]));}}}int size = 0;W totalW = W();dy::UnionFindSet<int> ufs(n);while (!minque.empty()){Edge min = minque.top();minque.pop();if (!ufs.InSet(min._srci,min._dsti)){cout << _vertex[min._srci] << "->" << _vertex[min._dsti]<<":"<<min._w;mintree._AddEdge(min._srci, min._dsti, min._w);ufs.Union(min._srci, min._dsti);++size;totalW += min._w;}else{cout << "构成环:";cout<<_vertex[min._srci]<<"->"<< _vertex[min._dsti]<<":"<< min._w<<endl;}}if (size == n - 1){return totalW;}else{return W();}}
测试:
②Prim(普里姆算法)算法
思想:
Prim算法 和 Dijkastra算法(最短路径) 的算法相似。
集合A的边 总是能构成 一颗树,这棵树可以 从任何节点r开始, 直到覆盖V的所有节点。
算法每一步,就是去找 和 A、A之外所有节点的边,自小的那个,加入到结合A当中。
因为每一步都是 让权值最小的入集合,因此可以形成一个最小生成树。
W Prim(Self& mintree,const V& src){size_t srci = GetVertexIndex(src);size_t n = _vertex.size();mintree._vertex = _vertex;mintree._mapIndex = _mapIndex;mintree._matrix.resize(n);for (size_t i = 0;i < n;++i){mintree._matrix[i].resize(n, MAX_W);}vector<bool> X(n,false);vector<bool> Y(n,true);X[srci] = true;Y[srci] = false;priority_queue<Edge, vector<Edge>, greater<Edge>> minque;for (size_t i = 0;i < n;++i){if (_matrix[srci][i] != MAX_W){minque.push(Edge(srci, i,_matrix[srci][i]));}}cout << "Prim开始选边" << endl;size_t size = 0;W totalW = W();while (!minque.empty()){Edge min = minque.top();minque.pop();if (X[min._dsti]){//cout << "构成换" << min._srci << "->" << min._dsti<<endl;}else{//cout << "不构成" << min._srci << "->" << min._dsti <<":" << min._w;mintree._AddEdge(min._srci, min._dsti, min._w);X[min._dsti] = true;Y[min._dsti] = false;size++;totalW += min._w;if (size == n - 1)break;for (size_t i = 0;i < n;++i){if (_matrix[min._dsti][i] != MAX_W && Y[i]){minque.push(Edge(min._dsti, i, _matrix[min._dsti][i]));}}}}if (size == n - 1){return totalW;}else{return W();}}
测试:
(2)最短路径
最短路径问题:从在带权有向图G中的某一顶点出发,找出一条通往另一顶点的最短路径,最短也就是沿路径各边的权值总和达到最小。
①Dijkstra(迪杰斯特拉)算法
单源最短路径--Dijkstra算法:
S为已确定的最短路径的结点集合。
Q为其余未确定最短路径的结点集合。每次从Q 中找出一个起点到该结点代价最小的结点u ,将u 从Q 中移出,并放入S
中,对u 的每一个相邻结点v 进行松弛操作。注:算法要求图中所有边的权重非负
本质和Prim如出一辙,也是 使用的贪心策略。
void Dijkstra(const V& src, vector<W>& dist, vector<int>& pPath){size_t srci = GetVertexIndex(src);size_t n = _vertex.size();dist.resize(n, MAX_W);pPath.resize(n, -1);dist[srci] = 0;pPath[srci] = srci;vector<bool> S(n, false);for (size_t j = 0;j < n;++j){int u = 0;W min = MAX_W;for (size_t i = 0;i < n;++i){if (S[i] == false && dist[i] < min){u = i;min = dist[i];}}S[u] = true;//松弛for (size_t v = 0;v < n;++v){if (S[v] == false && _matrix[u][v] != MAX_W&& _matrix[u][v] + dist[u] < dist[v]){dist[v] = _matrix[u][v] + dist[u];pPath[v] = u;}}}}
void PrintPathshort(const V& src ,const vector<W>& dist,const vector<int>& pPath){size_t srci = GetVertexIndex(src);size_t n = _vertex.size();for (size_t i = 0;i < n;++i){if (i != srci){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){cout << _vertex[index] << "->";}cout << "权值和:" << dist[i] << endl;}}}
测试:
②单源最短路径--Bellman-Ford算法
对于Dijkstra算法而言,唯一的不足在于,不能很好处理 负权值位的问题。
Bellman-Ford算法:
优点是可以解决有负权边的单源最短路径问题,而且可以用来判断是否有负权回路。它也有明显的缺点,它的时间复杂度 O(N*E)。Bellman-Ford本质是一种 暴力解法
bool BellmanFord(const V& src,vector<W>& dist,vector<int>& pPath){size_t n = _vertex.size();size_t srci = GetVertexIndex(src);dist.resize(n, MAX_W);pPath.resize(n, -1);//srci -> srcidist[srci] = W();bool update = false;for (size_t k = 0;k < n;++k){//i->j 更新一次cout << "更新边:" << endl;for (size_t i = 0;i < n;++i){for (size_t j = 0;j < n;++j){//srci -> i + i->jif (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j]){update = true;cout << _vertex[i] << "->" << _vertex[j] << ":" << _matrix[i][j] << endl;dist[j] = dist[i] + _matrix[i][j];pPath[j] = i;}}}if (update == false) break;}// 还能更新就是带负权回路for (size_t i = 0; i < n; ++i){for (size_t j = 0; j < n; ++j){// srci -> i + i ->jif (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j]){return false;}}}}
测试:
关于bellford的优化 除开上述的 循环跳出
③多源最短路径--Floyd-Warshall算法
Floyd-Warshall算法是解决任意两点间的最短路径的一种算法。
上面 两个算法 主要针对 单源路径。
Floyd-Warshall 也不会受到 负权值的影响。
void FloydWarshall(vector<vector<W>>& vvDist, vector<vector<int>>& vvpPath){size_t n = _vertex.size();vvDist.resize(n);vvpPath.resize(n);// 初始化权值和路径矩阵for (size_t i = 0; i < n; ++i){vvDist[i].resize(n, MAX_W);vvpPath[i].resize(n, -1);}// 直接相连的边更新一下for (size_t i = 0; i < n; ++i){for (size_t 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();}}}// 不经过kfor (size_t k = 0;k < n;++k){for (size_t i = 0; i < n; ++i){for (size_t j = 0; j < n; ++j){//k 作为中间 去更新 i->j// i->k k->jif (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];//因为是从i->k k->j kvvpPath[i][j] = vvpPath[k][j];}}}}}
④
说起三种算法,效率最高的肯定是
Dijkstra算法 的空间复杂度 可以达到O(N^2);
Bellmanford: 时间复杂度最坏可以达到O(N^3) 取决于图的密集程度
Floyd算法和bell 相差无几
总结
①并查集的概念、实际应用
②图的概念、存储结构(邻接表、邻接矩阵)
③图的遍历BFS、DFS
④最小生成树+最短路径
本篇就到此为止啦
感谢你的阅读 ~ 祝你好运
高阶数据结构(1):并查集 与 图相关推荐
- 数据结构-PHP 并查集(Union Find)
文章目录 数据结构-PHP 并查集(Union Find) 1.并查集示意图 2.并查集合并 3.并查集简单的代码示例 3.1 PHP代码定义 3.2 输出演示 数据结构-PHP 并查集(Union ...
- 【算法训练营】 - ⑩ 并查集与图
[算法训练营] - ⑩ 并查集与图 并查集 并查集特征 并查集的优化 图 图结构的表达 图的面试题如何搞定? 图的数据结构 点 边 图 生成图 图算法 广度优先遍历 深度优先遍历 图的拓扑排序算法 最 ...
- 数据结构 之 并查集
并查集是一种树型的数据结构,其保持着用于处理一些不相交集合(Disjoint Sets)的合并及查询问题. 有一个联合-查找算法(union-find algorithm)定义了两个操作用于此数据结构 ...
- 数据结构之并查集Union-Find Sets
1. 概述 并查集(Disjoint set或者Union-find set)是一种树型的数据结构,常用于处理一些不相交集合(Disjoint Sets)的合并及查询问题. 2. 基本操作 并查集 ...
- 数据结构之并查集:UF-Tree优化并查集——19
并查集的优化 在上一节了解到并查集的快速查询,合并,判断归属组等操作,虽然这些操作都非常方便,但是在数据量较大的情况下,并查集的效率并不算高: 上一节中实现代码中使用的合并方法(merge,API设计 ...
- 数据结构之并查集:并查集的介绍与Python代码实现——18
并查集的介绍 并查集(Union-find)数据结构也称作合并查找集(Merge-find set)或者不相交集数据结构(disjoint-set data structure),它是一种记录了由一个 ...
- 数据结构 之 并查集(Disjoint Set)
一.并查集的概念: 首先,为了引出并查集,先介绍几个概念: 1.等价关系(Equivalent Relation) 自反性.对称性.传递性. 假设a和b存在等价关系.记 ...
- 数据结构 7并查集(DISJOINT SET)
并查集(The disjoint set ADT) 等价关系 Relation R:若对于每一对元素(a,b),a,b∈S,aRb或者为true或者为false,则称集合S上定义关系R.如果aRb为t ...
- [重修数据结构0x03]并查集、堆、优先队列(2021.8.11)
前言 在做遍历的题目的时候,发现掌握一些特殊的数据结构和技巧有时对解决题目有着决定性的作用,不可不学.因此特地拿出来两天学习一下并查集.堆.优先队列.以后有更多思考和感悟再加补充吧.内容来自算法笔记, ...
最新文章
- 重磅!库克官宣苹果放弃英特尔,全面采用自研芯片,MAC迎来历史转折点
- Facebook 对前端工程师的要求是啥?一起来看看
- 如何用函数表示数(四)数的彻底消失
- matlab中predictor怎么填,在MATLAB中求解非線性有限元
- 黑客马拉松 招募_我如何赢得第一次黑客马拉松-研究,设计和编码的2个狂野日子
- RabbitMQ Tutorials 3 - Publish/Subscribe 发布/订阅
- C++之安装boost库
- Cannot resolve plugin org.apache.maven.plugins:xxxx
- 二元函数洛必达求极限_(整理)二元函数极限的求法.
- 怎样修改用户的计算机配置文件,计算机本地用户配置文件如何迁移至域账户
- C# 将图片转成字符画
- 11开根号不用计算机,数学开根号有什么方法?不用计算器
- matlab建立机器人模型,matlab 机器人工具箱8-通过URDF建立机器人模型
- java开源项目jeecgboot全解析
- 一款轻巧简单疫情动态网站源码
- 时钟容错同步算法之FTA
- linux下载安装vlc指令,Ubuntu安装VLC播放器的步骤
- APP上短信验证码如何验证?
- 修改数据库的名字和表名
- MTL框架:模型、权重与融合公式