一、图的概念
图由结点的有穷集合V和边的集合E组成。其中,结点也称为顶点。一对结点(x, y)称为边(edge),表示顶点x连接到顶点y。边可以包含权重/成本,显示从顶点x到y所需的成本。若两个顶点之前存在一条边,就表示这两个顶点具有相邻关系。
二、图的遍历
在图中,需要注意一般都需要通过一个哈希表visited来记录所有节点是否已经遍历过,防止进入死循环。顺便记录下类的学习,在定义类时,需要给类定义初始化函数,再定义类的属性和方法;在应用类时,需要先初始化类,再引用类的属性和方法去解决实际问题。
深度优先遍历(DFS):
- 从图中的一个起始节点开始,沿着一条路径尽可能深地访问,直到到达最深的节点,然后再回溯到之前未访问的节点。
- 通常使用递归或栈来实现,适用于查找连通分量、拓扑排序和解决迷宫等问题。
- 在遍历过程中,深度优先搜索会优先探索最深的分支,直到达到叶子节点,然后再回溯到其他分支。
广度优先遍历(BFS):
- 从图中的一个起始节点开始,先访问起始节点的所有邻居节点,然后依次访问邻居节点的邻居节点,依次类推。
- 通常使用队列来实现,适用于查找最短路径、最小生成树和解决网络分析等问题。
- 在遍历过程中,广度优先搜索会先探索当前节点的所有相邻节点,然后再向外扩展到下一层相邻节点。
#include<iostream>
#include<queue>
#include<stack>
#include<vector>
#include<unordered_set>
using namespace std;
# 定义图节点结构
struct GraphNode
{
int val;
vector<GraphNode*> neighbor;
GraphNode(int value):val(value){}
};
# 定义图
class Graph
{
private:
public:
# 定义图的节点集合
vector<GraphNode*> nodes;
# 添加节点(输入为数组)
void add_nodes(vector<int> values)
{
for(auto val:values)
{ GraphNode* node=new GraphNode(val);
nodes.push_back(node);
}
}
# 添加边(输入为两个顶点)
void add_edges(GraphNode* node1,GraphNode* node2)
{
node1->neighbor.push_back(node2);
node2->neighbor.push_back(node1);
}
# 深度优先遍历的递归方法
void DFS_recursion(GraphNode* cur,unordered_set<GraphNode*>& visited)
{
if(cur==NULL||visited.find(cur)!=visited.end())return;
visited.insert(cur);
cout<<cur->val<<endl;
for(auto neighbor_node:cur->neighbor)
{
DFS_recursion(neighbor_node,visited);
}
}
# 深度优先遍历的非递归方法
void DFS_not_recursion(GraphNode* root)
{
stack<GraphNode*> st;
unordered_set<GraphNode*> visited;
GraphNode* cur;
st.push(root);
while(!st.empty())
{
cur=st.top();
cout<<cur->val<<endl;
st.pop();
visited.insert(cur);
for(auto neighbor_node:cur->neighbor)
{
if(visited.find(neighbor_node)==visited.end())st.push(neighbor_node);
}
}
}
# 广度优先遍历的非递归方法
void BFS(GraphNode* root)
{
GraphNode* cur;
unordered_set<GraphNode*> visited;
queue<GraphNode*> que;
que.push(root);
while(!que.empty())
{
cur=que.front();
cout<<cur->val<<endl;
que.pop();
visited.insert(cur);
for(auto neighbor_node:cur->neighbor)
{
if(visited.find(neighbor_node)==visited.end())que.push(neighbor_node);
}
}
}
};
int main()
{
Graph graph;
vector<int> values={0,1,2,3,4,5};
graph.add_nodes(values);
GraphNode* node_0=graph.nodes[0];
GraphNode* node_1=graph.nodes[1];
GraphNode* node_2=graph.nodes[2];
GraphNode* node_3=graph.nodes[3];
GraphNode* node_4=graph.nodes[4];
GraphNode* node_5=graph.nodes[5];
graph.add_edges(node_0,node_1);
graph.add_edges(node_0,node_2);
graph.add_edges(node_1,node_3);
graph.add_edges(node_1,node_4);
graph.add_edges(node_4,node_5);
unordered_set<GraphNode*> visited;
graph.DFS_recursion(node_0,visited);
graph.DFS_not_recursion(node_0);
graph.BFS(node_0);
}
三、图的应用
3.1 判断图是否为树
判断一个图是否为树的条件是:1)该图是否存在环;2)该图是否连通。
#include<iostream>
#include<unordered_set>
#include<vector>
using namespace std;
struct GraphNode{
int val;
vector<GraphNode*> neighbor;
GraphNode(int value):val(value){}
};
class Graph{
public:
vector<GraphNode*> nodes;
void add_nodes(int value)
{
GraphNode* graphnode=new GraphNode(value);
nodes.push_back(graphnode);
}
void add_edges(GraphNode* node1,GraphNode* node2)
{
node1->neighbor.push_back(node2);
node2->neighbor.push_back(node1);
}
};
// 通过深度优先遍历判断图是否存在环 图是树的条件为:1.没有环 2.连通图
bool isTree(GraphNode* curNode,GraphNode* parentNode,unordered_set<GraphNode*>& visited)
{
// 若访问到已访问的节点,则该图存在环,则该图不是树,返回false
if(visited.find(curNode)!=visited.end())return false;
// visited记录已访问的节点
visited.insert(curNode);
for(auto next:curNode->neighbor)
{
// 继续遍历邻居节点
if(!isTree(next,curNode,visited)&&next!=parentNode)return false;
}
return true;
}
int main()
{
Graph graph;
graph.add_nodes(0);
graph.add_nodes(1);
graph.add_nodes(2);
graph.add_nodes(3);
graph.add_nodes(4);
graph.add_nodes(5);
graph.add_nodes(6);
graph.add_nodes(7);
GraphNode* node0=graph.nodes[0];
GraphNode* node1=graph.nodes[1];
GraphNode* node2=graph.nodes[2];
GraphNode* node3=graph.nodes[3];
GraphNode* node4=graph.nodes[4];
GraphNode* node5=graph.nodes[5];
GraphNode* node6=graph.nodes[6];
GraphNode* node7=graph.nodes[7];
graph.add_edges(node0,node1);
graph.add_edges(node0,node2);
graph.add_edges(node1,node3);
graph.add_edges(node1,node4);
graph.add_edges(node1,node5);
graph.add_edges(node1,node6);
graph.add_edges(node1,node7);
unordered_set<GraphNode*> visited;
bool flag=true;
if(isTree(node0,nullptr,visited))
{
// 判断所有节点是否都已遍历 判断该图是否连通
for(auto node:graph.nodes)
{
if(visited.find(node)==visited.end())
{
flag=false;
break;
}
}
if(flag==true)cout<<"This is a tree.";
else cout<<"This is not a tree.";
}
else cout<<"This is not a tree.";
}
3.2 计算图的边数
通过广度优先遍历来计算图的边数。
#include<iostream>
#include<unordered_set>
#include<vector>
using namespace std;
struct GraphNode{
int val;
vector<GraphNode*> neighbor;
GraphNode(int value):val(value){}
};
class Graph{
public:
vector<GraphNode*> nodes;
void add_nodes(int value)
{
GraphNode* graphnode=new GraphNode(value);
nodes.push_back(graphnode);
}
void add_edges(GraphNode* node1,GraphNode* node2)
{
node1->neighbor.push_back(node2);
node2->neighbor.push_back(node1);
}
int count_edges()
{
// count记录当前边数
int count=0;
// visited记录已访问节点
unordered_set<GraphNode*> visited;
for(auto node:nodes)
{
for(auto neigh:node->neighbor)
{
// 若邻居节点未访问过 则边+1
if(visited.find(neigh)==visited.end())count++;
}
// 将当前节点标记为已访问
visited.insert(node);
}
return count;
}
};
int main()
{
Graph graph;
graph.add_nodes(0);
graph.add_nodes(1);
graph.add_nodes(2);
graph.add_nodes(3);
graph.add_nodes(4);
graph.add_nodes(5);
graph.add_nodes(6);
graph.add_nodes(7);
graph.add_nodes(8);
graph.add_nodes(9);
GraphNode* node0=graph.nodes[0];
GraphNode* node1=graph.nodes[1];
GraphNode* node2=graph.nodes[2];
GraphNode* node3=graph.nodes[3];
GraphNode* node4=graph.nodes[4];
GraphNode* node5=graph.nodes[5];
GraphNode* node6=graph.nodes[6];
GraphNode* node7=graph.nodes[7];
GraphNode* node8=graph.nodes[8];
GraphNode* node9=graph.nodes[9];
graph.add_edges(node0,node1);
graph.add_edges(node0,node2);
graph.add_edges(node0,node3);
graph.add_edges(node0,node4);
graph.add_edges(node1,node5);
graph.add_edges(node1,node6);
graph.add_edges(node1,node7);
graph.add_edges(node2,node8);
graph.add_edges(node2,node9);
graph.add_edges(node5,node6);
cout<<"The number of edges is:"<<graph.count_edges();
}
3.3 找到两个顶点的最短路径
一般通过图的广度优先遍历来找到两个顶点的最短路径。
#include<iostream>
#include<unordered_set>
#include<unordered_map>
#include<vector>
#include<queue>
using namespace std;
struct GraphNode{
int val;
vector<GraphNode*> neighbors;
GraphNode(int value):val(value){}
};
class Graph{
private:
// 用于判断最短路径
int MAX=1000000;
public:
vector<GraphNode*> nodes;
void add_nodes(int value)
{
GraphNode* graphnode=new GraphNode(value);
nodes.push_back(graphnode);
}
void add_edges(GraphNode* node1,GraphNode* node2)
{
node1->neighbors.push_back(node2);
node2->neighbors.push_back(node1);
}
// 广度优先遍历找出所有最短路径
vector<vector<GraphNode*>> shortest_path(GraphNode* node1,GraphNode* node2)
{
// 用于判断最短路径的长度
int min_length=MAX;
// 记录所有路径
queue<vector<GraphNode*>> path_queue;
// 记录满足条件的结果
vector<vector<GraphNode*>> result;
// 记录已访问节点
unordered_set<GraphNode*> visited;
path_queue.push({node1});
while(!path_queue.empty())
{
// 取出当前路径
vector<GraphNode*> curpath=path_queue.front();
path_queue.pop();
// 取出当前路径最后一个节点
GraphNode* curnode=curpath[curpath.size()-1];
// 判断当前路径最后一个节点是否为目标节点
if(curnode==node2)
{
// 如果当前满足条件的路径长度小于最短路径 则清空存放结果的数组后再加入当前路径
if(curpath.size()<min_length)
{
// 清空存放结果的数组
result.clear();
result.push_back(curpath);
min_length=curpath.size();
}
// 如果当前满足条件的路径长度等于最短路径 则直接存放当前路径
else if(curpath.size()==min_length)
{
result.push_back(curpath);
}
}
else
{
// 继续往队列中加入新的路径 即当前路径最后一个节点的邻居
for(auto neighbor:curnode->neighbors)
{
if(neighbor!=curnode&&!visited.count(neighbor))
{
curpath.push_back(neighbor);
path_queue.push(curpath);
curpath.pop_back();
}
}
visited.insert(curnode);
}
}
return result;
}
};
int main()
{
Graph graph;
graph.add_nodes(0);
graph.add_nodes(1);
graph.add_nodes(2);
graph.add_nodes(3);
graph.add_nodes(4);
GraphNode* node0=graph.nodes[0];
GraphNode* node1=graph.nodes[1];
GraphNode* node2=graph.nodes[2];
GraphNode* node3=graph.nodes[3];
GraphNode* node4=graph.nodes[4];
graph.add_edges(node0,node1);
graph.add_edges(node0,node2);
graph.add_edges(node0,node4);
graph.add_edges(node2,node3);
graph.add_edges(node1,node3);
graph.add_edges(node3,node4);
vector<vector<GraphNode*>> result=graph.shortest_path(node0,node3);
cout<<"numbers of shortest path:"<<result.size()<<endl;
int index=1;
for(auto vec:result)
{
cout<<"path"<<index++<<":";
for(auto node:vec)
{
cout<<node->val<<" ";
}
cout<<endl;
}
}
3.4 拓扑排序
拓扑排序不能应用于有环图,因为我们的前提条件是总能至少在图中找到一个入度为0的顶点开始拓扑,但是当图中存在环时,我们在这个环中找不到任意一个入度为0顶的点。
拓扑排序是利用的广度优先的思想,也是借助队列这个辅助数据结构,具体的算法流程是:首先建立一个countArr数组并统计每一个顶点的入度填到对应数组中;通过遍历,首先将所有入度为0的节点入队,并将节点总数vexCount相应减少,然后利用广度优先搜索的思路进行循环,里面注意的操作是:每当出队一个顶点时,我们将以该顶点为弧尾的弧顶顶点对应的入度countArr[i]减一,并判断该countArr[i]是否为0,为零则将该元素入队,并将vexCount减一,循环直至队列为空。最后判断vexCount是否为0,如果不为零,则说明成环了。最后输出的顶点出队顺序就是该图对应的一个拓扑序列。
#include<iostream>
#include<vector>
#include<queue>
using namespace std;
void find_order(int NumVertex,vector<vector<int>> prerequires)
{
// CountArr记录每个顶点的入度
vector<int> CountArr(NumVertex,0);
// 记录每个顶点的入度
for(auto entry:prerequires)
{
CountArr[entry[0]]++;
}
queue<int> que;
queue<int> resque;
for(int i=0;i<NumVertex;i++)
{
if(CountArr[i]==0)que.push(i);
NumVertex--;
}
while(!que.empty())
{
int cur=que.front();
que.pop();
resque.push(cur);
for(auto entry:prerequires)
{
if(cur==entry[1])
{
if(--CountArr[entry[0]]==0)
{
que.push(entry[0]);
NumVertex--;
}
}
}
}
while(!resque.empty())
{
cout<<resque.front();
resque.pop();
}
}
int main()
{
find_order(4,{{1,0},{2,0},{3,1},{3,2}});
cout<<endl;
find_order(3,{{1,0},{2,0},{1,2}});
}
3.5 最短距离
3.5.1 迪杰斯特拉算法
迪杰斯特拉算法是用来求解单源最短路径的算法,采用了贪心的策略,不能处理负权值。
具体过程:
①已知邻接矩阵arr,目标节点source和存放source与其他节点的最短距离的数组shortest。
②每次选择当前集合内与source距离最短的节点index:得到的一定是节点index与source之间的最短距离,因为已经是当前集合内与source距离最短的节点了,经过其他节点时距离只会更远(因此不能处理负权值的情况)。
③将节点index标记为已访问,并将index与source之间的距离加入shortest。
④得到节点index与source之间的最短距离后,计算经过节点index时,其他节点和source之间的距离有没有更短,即arr[source][j]=min(arr[source][j]与arr[source][index]+arr[index][j])。
⑤在所有节点都标记为已访问后,shortest中存放的便是source与其他所有节点之间的最短距离。
3.5.2 佛洛依德算法
佛洛依德算法是用来求解多源最短路径的算法,采用了动态规划的策略,可以处理负权值。
①已知邻接矩阵arr,顶点数V。
②每次设定一个顶点index,计算在经过顶点index时,arr中各顶点之间的距离有没有变短。即arr[i][j]=min(arr[i][j],arr[i][index]+arr[index][j])。
③在执行V次后,矩阵arr中存放的便是顶点之间的最短距离。