图
图及其表示方式
图由两个部分组成,一是点node,二是边edge。 图的表示方法由邻接表法和邻接矩阵法。当然还有其他的方式。 如下所示,有一无向图,其邻接表和邻接矩阵示意图为:
class Graph
{
int V;
list<int> *adj;
public:
Graph(int V);
void addEdge(int v, int w);
};
Graph::Graph(int V)
{
this->V = V;
adj = new list<int>[V];
}
void Graph::addEdge(int v, int w)
{
adj[v].push_back(w);
}
图的广度优先遍历及应用
如图所示:,从源点2开始且标记访问,与2相邻的0,3入队,并标记已经访问过。结束后,0出队,与0相邻的1,入队,由于2已经标记访问过了,不在入队。3也是如此。遍历结果2,0,3,1.
void BFS(int s)
{
// 标记未被访问过的节点
bool *visited = new bool[V];
for(int i = 0; i < V; i++)
visited[i] = false;
// 用于BFS的队列
list<int> queue;
// 标记当前节点已被访问并且入队
visited[s] = true;
queue.push_back(s);
list<int>::iterator i;
while(!queue.empty()){//开始BFS
s = queue.front();
queue.pop_front();
//遍历与s相连接的顶点,这些点存在adj[s]中
for (i = adj[s].begin(); i != adj[s].end(); ++i){
if (!visited[*i]){
visited[*i] = true;
queue.push_back(*i);
}
}
}
}
应用:
- 无权图的最小生成树和最短路径。
- 点对点网络。在点对点网络中,比如BitTorrent,广度优先搜索用于查找所有邻居节点。
- 搜索引擎中的爬虫。
- 社交网站:在社交网络中,我们可以找到某个特定的人距离为“K”的所有人。
- GPS导航:使用广度优先搜索查找所有邻近位置。
- 网络广播:在网络中,广播机制是优先搜索所有相邻可达到节点。
- 垃圾收集
- 无向图的环检测:在无向图中,BFS或DFS可以用来检测循环。在有向图中,只有深度首先可以使用搜索。
- 在Ford-Fulkerson算法中,可以使用广度先或深度先遍历,找到最大流。优先考虑BFS,时间复杂度更小。
- 判断一个图是否是可以二分,既可以使用广度优先,也可以使用深度优先遍历。
- 判断两个点之间是否存在路径。
- 从给定节点中,查找可以访问的所有节点。
图的深度优先遍历及应用
从源点2开始,并标记已经访问2了,之后查找它的所有相邻顶点,重复上面操作。下面的访问顺序之一为2,0,1,3。
void DFS(int v, bool visited[])
{
visited[v] = true;
list<int>::iterator i;
for(i= adj[v].begin(); i != adj[v].end(); ++i)
if (!visited[*i])
DFS(*i, visited);
}
应用
- 对于无权图,DFS可以生成最小生成树。
- 检测图中是否有循环。
- 查找给定节点uv之间是否有路径
- 拓扑排序
- 判断一个图是否可以二分
- 寻找图的强连通分量
- 迷宫问题
深度优先遍历的非递归实现
void DFS(int s, vector<bool> &visited)
{
stack<int> stack;
stack.push(s);
while (!stack.empty())
{
s = stack.top();
stack.pop();
if (!visited[s]) visited[s] = true;
for(auto i = adj[s].begin();i != adj[s].end();++i)
if (!visited[*i])
stack.push(*i);
}
}
值得注意的是,当图不是完全有向图的,需要对每个节点,重复调用DFS,这样才能遍历到每个节点。
检测有向图中是否有环
如在上图中,是存在0->2->0这样的环。3->3的环。当且仅当存在一条后向边才可以认为图中有环。后向边(u,v)是指节点u连接到其在深度优先搜索树中的一个祖先节点v这样的一条边。3->3这样的自循环也可以认为是一条后向边。
为了检测图中的后向边,对DFS递归函数的中递归栈进行跟踪。如果我们当前遍历的顶点出现在递归栈中,那么就认为存在一条后向边,图中存在循环。
bool Graph::isCyclic(int v, vector<bool>&visited, vector<bool>&recStack)
{
//recStack表示递归栈
if (visited[v] == false)
{
visited[v] = true;
recStack[v] = true;
for(auto i=adj[v].begin(); i != adj[v].end();++i)
{
//如果节点v所连接的另一边节点i还没有被访问,
//并且在节点i处存在环,那么真的存在环
if(!visited[*i])
{
if(isCyclic(*i, visited, recStack))
return true;
}
//如果节点i已经被访问了,且在递归栈中有i,那么有环
else if (recStack[*i]) return true;
}
}
recStack[v] = false;
return false;
}
上面函数仅能够判断从节点v出发判断是否存在环,若要对整个图判断一下,需要对图中每个节点都调用一次。
检测无向图中是否存在环
很明显,在图中是存在一个环的。对于一个正在访问的节点V,如果它的相连接的节点u已经访问过,并且不是v的父节点,那么就可以认为图中存在环。
比如在图中,从节点0出发,使用DFS进行遍历。访问节点1,此时节点0是1的父节点。在访问节点2,1是2的父节点,但0不是2的父节点,并且0已经被访问过了,此时就可以判定图中存在环。
bool isCyclic(int v, vector<bool>&visited, int parent)
{
visited[v] = true;
for (auto i = adj[v].begin(); i != adj[v].end(); ++i)
{
if (!visited[*i])
{//这个if一定得这么写,不得两个if用&&连起来
if (isCyclic(*i, visited, v))
return true;
}
else if (*i != parent)
return true;
}
return false;
}
还是只能检测联通的图,如果不连通,每个位置统统调用检测一遍。
并查集(在无向图中检测是否存在环)
并查集一种数据结构,它跟踪一组被划分为多个没有交集的子集中的元素。并查集有两个主要操作, 查找(find):确定某个元素所在的子集,确定两个元素是否在同一个子集中。 联合(union):将两个子集连接成一个子集。 并查集算法可用于检测无向图是否有环。此方法需要假设图不包含任何自循环,设置一个父数组parent。如
使用图的每一个顶点创建子集。parent数组的所有元素都初始化为-1(意味着每个槽就是一个子集)。如果两个顶点都在同样的子集,就可以找到一个循环。
0 |
1 |
2 |
---|---|---|
-1 |
-1 |
-1 |
现在逐个处理每条边。首先是0-1边:找到顶点0和1所在的子集。由于它们属于不同的子集,故要取它们的并集。对于取并集(union),可以让节点1作为节点0的父节点,反之也可以。数组就更新为下面这样
0 |
1 |
2 |
---|---|---|
1 |
-1 |
-1 |
然后是1-2边:1在子集1中,2在子集2中,不在同一个子集,于是union起来,将子集1置于子集2下面。结果如下
0 |
1 |
2 |
---|---|---|
1 |
2 |
-1 |
最后是0-2边:0在子集2中(0在子集1中,子集1在子集2中),2也在子集2中。那么加上这条边就形成一个环。
int find(vector<int>parent, int i)
{//并查集查找,查找i所在的子集
if (parent[i] == -1)
return i;
return find(parent, parent[i]);
}
void Union(vector<int>&parent, int x, int y)
{//并查集的并
int xset = find(parent, x);
int yset = find(parent, y);
if (xset != yset) {
parent[xset] = yset;
}
}
bool Graph::isCycle()
{//环判定,值得一提是,这里检测的是无向图,但图的定义在添加边的时候添加单向边
vector<int>parent(this->v, -1);//?数组,
for (int v = 0; v < this->V; v++)
{
for (auto i = adj[v].begin(); i != adj[v].end(); ++i)
{//v和*i是一条边的两端
int x = find(parent, v);
int y = find(parent, *i);
if (x == y)
return true;
Union(parent, x, y);
}
}
return false;
}
拓扑排序
拓扑排序是有向无环图所有顶点的线性排序,满足对于每一条有向边(u,v),顶点u在v之前。例如,下面图的拓扑排序是“5 4 2 3 1 0”,拓扑排序次序并不唯一。
拓扑排序过程:将DFS修改一下就行了。首先需要一个栈,暂时保存结果,从某个源点S开始,对源点S相邻的点递归调用拓扑排序,结束之后再把S压入栈中。最后将栈内元素全部出战即可。
void topologicalSortUtil(int v,vector<bool>&visited,stack<int> &Stack)
{
visited[v] = true;
for(auto i = adj[v].begin(); i != adj[v].end(); ++i)
if(!visited[*i]) topologicalSortUtil(*i, visited, Stack);
Stack.push(v);
}
void topologicalSort()
{
stack<int> Stack;//保存拓扑结果
vector<bool>visited(V, false);
for(int i = 0; i < V; i++)//对于每个节点,若没有被访问过,都要进行拓扑排序
if (visited[i] == false)
topologicalSortUtil(i, visited, Stack);
while (Stack.empty() == false)
Stack.pop();
}
有向无环图(DAG)的最长路径
描述:给出一个带权有向无环图(DAG)和其中的一个源点s,求出 s到图中所有其它顶点的最长距离。 众所周知,一般图最长路径问题是NPH problem。但对于DAG的最长路径问题有一个线性时间解。使用拓扑排序可以求解。
求解过程:首先初始化源点S到其他顶点的距离为无穷小,源点S到S的距离为0。之后对整个图DAG进行拓扑排序。按照拓扑排序后的节点顺序,更新到源点距离就行了。
如图:对图a进行拓扑排序结果为r,s,t,x,y,z。如图b所示,并标出图中所有的边。1.如图c所示,更新r到其他点的距离。2.如图d所示,更新s到其他点的距离。3.如图e所示,更新t到其他点的距离。4.如图f所示,更新x到其他点的距离。5.如图g所示,更新y到其他点的距离。6.如图h所示,更新z到其他点的距离。
void longestPath(int s)
{
stack<int> Stack;//保存拓扑排序的结果
vector<int>dist(this->V);//距离数组
vector<bool>visited(this->V, false);
//对每个位置进行拓扑排序,得到结果
for (int i = 0; i < V; i++)
if (visited[i] == false)
topologicalSortUtil(i, visited, Stack);
//初始化所有距离为负无穷,源点距离为0
for (int i = 0; i < V; i++)
dist[i] = INT_MIN;
dist[s] = 0;
//不断出栈,并且更新出栈元素,到其相邻的边的最长长度
while (Stack.empty() == false) {
int u = Stack.top();
Stack.pop();
list<AdjListNode>::iterator i;
if (dist[u] != INT_MAX) {
for (i = adj[u].begin(); i != adj[u].end(); ++i)
if (dist[i->getV()] < dist[u] + i->getWeight())
dist[i->getV()] = dist[u] + i->getWeight();
}
}
//打印最后结果
for (int i = 0; i < V; i++)
if (dist[i] == INT_MIN)
cout << "INF ";
else cout << dist[i] << " ";
}
判断图是否可以二分
若有无向图G=(V,E),其顶点V可分割为两个互不相交的子集(A,B),并且图中的每条边(i,j)所关联的两个顶点i和j分别属于这两个不同的顶点集(V_A,U_B),则称图是一个二分图。
如果一个图是二分图,那么可以使用两种颜色将节点划分到两个集合中(每个集合中节点的颜色一样)。
胃酸法:开始对任意一未染色的顶点染色,之后判断其相邻的顶点中,若未染色则将其染上和相邻顶点不同的颜色, 若已经染色且颜色和相邻顶点的颜色相同则说明不是二分图,若颜色不同则继续判断,bfs和dfs可以搞定!
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- leetcode树之N叉树的前序遍历
- Spring Security 中的 hasRole 和 hasAuthority 有区别吗?
- python常见的import导包技巧
- 真正了解贪心算法,这是一篇精华入门总结...
- MGR修改max_binlog_cache_size参数导致异常
- 【技术创作101训练营】TensorFlow Lite的 GPU 委托(Delegate)加速模型推理
- 弄懂这 5 个问题,拿下 Python 迭代器!
- 1500字,8个问题,彻底理解堆!
- Python画王者荣耀英雄能力雷达图
- Python语言的精华:Itertools库
- MySQL为什么lsof会看到这么多临时文件
- IE浏览器主页被劫持,如何解决主页被篡改问题?
- 参与国际化项目一定要遵循的java命名规范
- 威胁事件告警分析技巧及处置(二)
- 组复制安全 | 全方位认识 MySQL 8.0 Group Replication