时间:2022-07-28
本文章向大家介绍图,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

图及其表示方式

图由两个部分组成,一是点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);
            }
        }
    }
}

应用:

  1. 无权图的最小生成树和最短路径。
  2. 点对点网络。在点对点网络中,比如BitTorrent,广度优先搜索用于查找所有邻居节点。
  3. 搜索引擎中的爬虫。
  4. 社交网站:在社交网络中,我们可以找到某个特定的人距离为“K”的所有人。
  5. GPS导航:使用广度优先搜索查找所有邻近位置。
  6. 网络广播:在网络中,广播机制是优先搜索所有相邻可达到节点。
  7. 垃圾收集
  8. 无向图的环检测:在无向图中,BFS或DFS可以用来检测循环。在有向图中,只有深度首先可以使用搜索。
  9. 在Ford-Fulkerson算法中,可以使用广度先或深度先遍历,找到最大流。优先考虑BFS,时间复杂度更小。
  10. 判断一个图是否是可以二分,既可以使用广度优先,也可以使用深度优先遍历。
  11. 判断两个点之间是否存在路径。
  12. 从给定节点中,查找可以访问的所有节点。

图的深度优先遍历及应用

从源点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);
}

应用

  1. 对于无权图,DFS可以生成最小生成树。
  2. 检测图中是否有循环。
  3. 查找给定节点uv之间是否有路径
  4. 拓扑排序
  5. 判断一个图是否可以二分
  6. 寻找图的强连通分量
  7. 迷宫问题

深度优先遍历的非递归实现

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),顶点uv之前。例如,下面图的拓扑排序是“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)所关联的两个顶点ij分别属于这两个不同的顶点集(V_A,U_B),则称图是一个二分图。

如果一个图是二分图,那么可以使用两种颜色将节点划分到两个集合中(每个集合中节点的颜色一样)。

胃酸法:开始对任意一未染色的顶点染色,之后判断其相邻的顶点中,若未染色则将其染上和相邻顶点不同的颜色, 若已经染色且颜色和相邻顶点的颜色相同则说明不是二分图,若颜色不同则继续判断,bfs和dfs可以搞定!