备战蓝桥杯与csp — 图及树的遍历方式
α. 图的定义以及存储
这里请参考笔者的另一篇文章---->传送门
β. 两种方法
图的遍历就是从图的某一顶点出发对图中所有的顶点访问且仅访问一次,而图的遍历算法是求解图的联通性问题,拓扑排序等算法的基础.
树是一种特殊的图,一般的图中会存在回路,因此需要通过某种操作来控制(visited标记数组),避免多次访问,解决了一般的图遍历问题,数的遍历问题迎刃而解.
β1. 深度优先搜索(dfs)
- 从图中的某一个顶点s出发,访问s
- 找出刚访问过的s的第一个未被访问的邻接点s1(与之相邻),接着访问该顶点,以该顶点作为新的顶点重复第二个步骤,直到刚访问的节点没有邻接点为止结束
- 返回s,接着寻找s的剩下的未被访问的邻接点(s2,s3,s4…),重复2步骤直至无邻接点
以上图为例,以顶点1为s,1的邻接点按照存储先后分别为2,3,4
- 从1出发,标记1被访问过,找到第一个邻接点2,标记其已经被访问过
- 接着2作为出发顶点其邻接点 1 已经被访问过,5 未被访问 ,访问5且标记其已经被访问
- 接着5作为出发顶点,其邻接点2被访问过,返回上一个作为出发顶点的2
- 2作为出发顶点,其邻接点1,5 均被访问过,返回上一个出发顶点1
- 1作为出发顶点,还有3,4未被访问,则按照存储的顺序,3 作为出发顶点
- 3的邻接点为1,4(注意按照存储顺序来,而不是4,1) ,1被访问过,则访问4,标记4已经被访问过,接着4作为出发顶点
- 4的邻接点均被访问过,接着返回上一层3,3作为出发顶点
- 3的邻接点均被访问过,接着返回上一层1,1作为出发顶点
- 1的邻接点均被访问过,遍历结束
- 遍历经过的路径为 1 2 5 3 4
#include <bits/stdc++.h>
using namespace std;
const int maxN = 1e5 +10;
struct edge{
int v;
int w;
edge(int v,int w){
this->v = v;
this->w = w;
}
};
int vis[maxN];
int node,n,m,res;
vector<edge>g[maxN];
int path[maxN]; // 用于存储每一次访问的顶点
int countN = 1;
void Init(){
}
void dfs(int x){
path[countN++] = x;
vis[x] = 1;
for(int i = 0;i < g[x].size();i++){ // 遍历其邻接点
int xx = g[x][i].v;
// int ww = g[x][i].w;
if(!vis[xx]){ // 如果没有被访问则将其作为出发顶点
dfs(xx); // dfs 完之后 进行上一个出发顶点的下一个邻接点
}
}
}
int main(){
cin>>n>>m;
for(int i = 0;i < m;i++){
int u,v,w;
cin>>u>>v>>w;
g[u].push_back(edge(v,w));
g[v].push_back(edge(u,w));
}
dfs(1);
for (int j = 1; j < countN; ++j) {
printf("%d ",path[j]);
}
return 0;
}
/*
*5 5
2 5 1
1 2 1
3 4 1
1 3 1
1 4 1
*/
β2. 广度优先搜索(bfs)
- 从图中的某一个顶点s出发,访问s
- 再接着依次访问s的邻接点
- 分别从这些邻接点出发访问他们的邻接点,重复此步骤,直至全部访问完
以上图为例,以顶点1为s,1的邻接点按照存储先后分别为2,3,4
- 从1出发,标及其已经被访问过,它的三个邻接点依次为2,3,4,我们将其全标记为已经访问过
- 2未被访问过,2的邻接点为1和5,1已经被访问过,我们把5标记为已经访问过
- 2未被访问过,3的邻接点为1和4,二者均被访问过
- 4已经被访问过,遍历结束
- 遍历的顺序为1 2 3 4 5
这里的一个难点就是如果把邻接点按照顺序保存下来,这里我们采用队列,根据它先进先出的特点,来完成算法
#include <bits/stdc++.h>
using namespace std;
const int maxN = 1e5 +10;
struct edge{
int v;
int w;
edge(int v,int w){
this->v = v;
this->w = w;
}
};
int vis[maxN];
int node,n,m,res;
vector<edge>g[maxN];
int path[maxN];
int countN = 1;
void bfs(int x){
vis[x] = 1; // 将第一个出发顶点标记为已经访问过
path[countN++] = x;
queue<int> q;
q.push(x); // 将顶点打入队列
while (!q.empty()){
int xx = q.front();
q.pop();
for(int i = 0;i < g[xx].size();i++){
int u = g[xx][i].v;
// int w = g[xx][i].w;
if(!vis[u]){ // 如果没有被访问过则标及其已经访问过并打入队列
vis[u] = 1;
path[countN++] = u;
q.push(u);
}
}
}
}
int main(){
ios::sync_with_stdio(false);
cin>>n>>m;
for(int i = 0;i < m;i++){
int u,v,w;
cin>>u>>v>>w;
g[u].push_back(edge(v,w));
g[v].push_back(edge(u,w));
}
bfs(1);
for (int j = 1; j <= countN; ++j) {
printf("%d ",path[j]);
}
cin>>n>>m;
for(int i = 0;i < m;i++){
int u,v,w;
cin>>u>>v>>w;
g[u].push_back(edge(v,w));
g[v].push_back(edge(u,w));
}
bfs(1);
for (int j = 1; j < countN; ++j) {
printf("%d ",path[j]); // 1 2 3 4 5
}
return 0;
}
/*
*5 5
2 5 1
1 2 1
3 4 1
1 3 1
1 4 1
*/
γ. 时间复杂度分析
γ1. dfs
使用dfs在遍历时,对图中的每个顶点至多采用一次dfs,因为一旦一个顶点被标记之后我们便不会将其作为出发顶点进行搜索,因此,遍历图的过程实际上是对每一个顶点查找其邻接邻接点过程,耗费时间取决于图的存储结构,如果采用的是邻接表的形式,时间复杂度为O(n+e)(边+点的个数),如果是邻接矩阵则为O(n^2)
γ2. bfs
使用bfs在遍历时,每次顶点至多进入一次队列,因此,实质上遍历结果仍然是一个查找邻接点的过程,时间复杂度和dfs一致,二者的区别可能仅仅是访问的顺序不同