在引入图的深度优先搜索之前,为了更加容易理解.先考究一种特殊的图---二叉树的深度优先搜索算法---即二叉树的递归遍历方法.
二叉树的前序遍历算法:
void TreeWalk(node* root) { if(root) { visit(root); TreeWalk(root->left); TreeWalk(root->right); } }对于二叉树来说,步骤如下:
1.如果root不为空,先访问root,
2再递归下降地访问root->left和root->right.
在二叉树中,与root邻接的结点只有left and right.所以,每次只需要递归下降访问这两个节点即可.
对于图来说,可以类比二叉树(特殊情况),总结出一般规律:
1先选定任意一个点vex做为生成树的根结点.访问该根点,
2再递归下降地访问与vex邻接的而且未被访问过的所有结点.
在图中,与vex邻接的结点是不像二叉树一样只有left 和 right,因为是不确定的,因此需要循环地查找判断哪些点邻接并且尚未被访问过.
伪代码:
void DFS(Graph& g,int vex) { Visit vex and Mark vex is visited; for w=FirstAdjacencyVertex to LastAdjacencyVertex(which is not visited before) DFS(g,w) }对应的C++代码:
//深度优先算法,可以形成一棵树 void DFS(Graph& g,int vex) { cout<<vex<<' '; visited[vex]=true;//is visited int w=FirstAdjVertex(g,vex); while(w>0) { DFS(g,w); w=NextAdjVertex(g,vex,w); } }其中,FirstAdjVertex(g,vex)可以返回vex的第一个未被访问的邻接结点w.NextAdjVertex(g,vex,w)可以返回vex的下一个未被访问的邻接结点,在编号上在w之后.(小编号结点优先原则).
//找第一个未被访问过的邻接结点,找不到返回0 int FirstAdjVertex(Graph& g,int vex) { for(int i=1;i<=g.vnum;++i)//从点1开始找与vex邻接的第一个结点 { if(g.edge[vex][i]==1 && !visited[i]) return i; } return 0; } //找下一个未被访问过的邻接结点 int NextAdjVertex(Graph& g,int vex,int w) { for(int i=w;i<=g.vnum;++i)//从w开始,找下一个未被访问过的邻接结点 { if(g.edge[vex][i]==1 && !visited[i])//若i与vex邻接且i未访问过,下一次遍历,就从这个点往下. return i; } return 0;//点都从1开始,返回0则所以点已访问过 }对于无向连通图或者强有向强连通图,可以调用DFS形成一棵深度优先搜索树.但是,对于非连通图或者非强连通图.需要在生成一棵树之后,继续寻找是否还存在未被访问到的点,在这些点上继续调用DFS,最终形成森林.
void DFS_traver(Graph& g) { for(int i=1;i<=g.vnum;++i) { if(!visited[i])//如果i结点未被访问过,形成森林 DFS(g,i); } }
#include<iostream> using namespace std; const int MAX=20;//最大点数 bool visited[MAX]={false};//点标记域:标志是否被访问过. struct Graph { int vertex[MAX];//点 int edge[MAX][MAX];//边 int vnum;//点数 }; //创建图 void CreateGraph(Graph& g) { cin>>g.vnum; for(int i=1;i<=g.vnum;++i) g.vertex[i]=i; for(int i=1;i<=g.vnum;++i) { for(int j=1;j<=g.vnum;++j) cin>>g.edge[i][j]; } } //打印图 void PrintGraph(Graph& g) { for(int i=1;i<=g.vnum;++i) cout<<g.vertex[i]<<' '; cout<<endl; for(int i=1;i<=g.vnum;++i) { for(int j=1;j<=g.vnum;++j) cout<<g.edge[i][j]<<' '; cout<<endl; } } //找第一个未被访问过的邻接结点,找不到返回0 int FirstAdjVertex(Graph& g,int vex) { for(int i=1;i<=g.vnum;++i)//从点1开始找与vex邻接的第一个结点 { if(g.edge[vex][i]==1 && !visited[i]) return i; } return 0; } //找下一个未被访问过的邻接结点 int NextAdjVertex(Graph& g,int vex,int w) { for(int i=w;i<=g.vnum;++i)//从w开始,找下一个未被访问过的邻接结点 { if(g.edge[vex][i]==1 && !visited[i])//若i与vex邻接且i未访问过,下一次遍历,就从这个点往下. return i; } return 0;//点都从1开始,返回0则所以点已访问过 } //深度优先算法,可以形成一棵树 void DFS(Graph& g,int vex) { cout<<vex<<' '; visited[vex]=true;//is visited int w=FirstAdjVertex(g,vex); while(w>0) { DFS(g,w); w=NextAdjVertex(g,vex,w); } } void DFS_traver(Graph& g) { for(int i=1;i<=g.vnum;++i) { if(!visited[i])//如果i结点未被访问过,形成森林 DFS(g,i); } } void main() { Graph g; CreateGraph(g); PrintGraph(g); DFS_traver(g); }
#include<iostream> using namespace std; const int MAX=6; struct node { int value; node* next; bool visited; node(int v):value(v),visited(false),next(NULL){} }; struct Graph { node* A[MAX];//定义一个指针数组,数组的每一个元素都是指针 Graph() { for(int i=0;i<MAX;++i) { A[i]=NULL; } } }; node* insertList(node* head,node* x) { if(!head)//if(!head)=if(head==NULL) { head=x; return head; } head->next=insertList(head->next,x); return head; } void CreateGraph(Graph& g) { int inter; for(int i=1;i<MAX;++i) { while(cin>>inter && inter !=0)//以0为结束 g.A[i]=insertList(g.A[i],new node(inter));//g.A[i]是表头 } } void printGraph(Graph& g) { node* p=NULL; for(int i=1;i<MAX;++i) { p=g.A[i]; while(p) { cout<<p->value<<' '; p=p->next; } cout<<endl; } } node* FindAdjVertex(Graph& g,node* vex) { while(vex)//从点1开始找与vex邻接的第一个结点 { if(!g.A[vex->value]->visited)//查看vex是否被访问过,访问标志保存在表头g.A[i] return vex; vex=vex->next; } return NULL; } /* //找下一个未被访问过的邻接结点 node* FindAdjVertex(Graph& g,node* w) { while(w)//从w开始,找下一个未被访问过的邻接结点 { if(!g.A[w->value]->visited)//若i与vex邻接且i未访问过,下一次遍历,就从这个点往下. return w; w=w->next; } return NULL;//点都从1开始,返回0则所有点已访问过 } */ void DFS(Graph& g,node* vex) { cout<<vex->value<<' '; //vex->visited=true;//is visited g.A[vex->value]->visited=true;//设置标志 node* w=FindAdjVertex(g,g.A[vex->value]); while(w) { DFS(g,w); w=FindAdjVertex(g,g.A[w->value]); } } void DFS_traver(Graph& g) { for(int i=1;i<MAX;++i) { if(!g.A[i]->visited)//如果i结点未被访问过,形成森林 DFS(g,g.A[i]); } } void main() { Graph g; CreateGraph(g); printGraph(g); //DFS(g,g.A[1]); DFS_traver(g); }
/*从顶点i开始对图进行深度优先搜索遍历,图由其邻接表adjlist表示*/ void DFS2(Graph& g,node* vex) { node* p=vex; stack<node*> st; while (p || !st.empty()) { while (p) { if(!g.A[p->value]->visited)//如果未被访问过 { cout<<p->value<<' ';//visit g.A[p->value]->visited=true;//设为已访问,纵深探索 st.push(p);//同时压栈 p=g.A[p->value];//找下一个点 } else p = p->next;//接着找未被访问过的节点 } if(!st.empty()) { p=st.top(); st.pop(); p = p->next; } } }
图片来自算法导论第二版:
广度优先搜索算法:来源:点击打开链接
图的广度优先遍历
图的广度优先遍历BFS算法是一个分层搜索的过程,和树的层序遍历算法类同,它也需要一个队列以保持遍历过的顶点顺序,以便按出队的顺序再去访问这些顶点的邻接顶点。
1.连通图的广度优先遍历算法思想。
(1)顶点v入队列。
(2)当队列非空时则继续执行,否则算法结束。
(3)出队列取得队头顶点v;访问顶点v并标记顶点v已被访问。
(4)查找顶点v的第一个邻接顶点col。
(5)若v的邻接顶点col未被访问过的,则col入队列。
(6)继续查找顶点v的另一个新的邻接顶点col,转到步骤(5)。直到顶点v的所有未被访问过的邻接点处理完。转到步骤(2)。
【例】下面以图( a )为例说明广度优先搜索的过程。首先从起点 v1 出发访问 v1。v1 有两个未曾访问的邻接点 v2 和 v3 。先访问 v2 ,再访问 v3 。然后再先访问 v2 的未曾访问过的邻接点 v4、v5 及 v3 的未曾访问过的邻接 v6 和 v7 ,最后访问 v4 的未曾访问过的邻接点 v8 。至此图中所有顶点均已被访问过。得到的顶点访问序列为:
代码:特别鸣谢@tommyhu111
void BFS(Graph& g,int vex) { queue<int> q; visited[vex] = true; //标记已被访问过 q.push(vex); //初始化队列 while(!q.empty()) { vex=q.front(); q.pop(); //取出队头数据 cout<<vex<<' '; for ( int j=1; j<=g.vnum; ++j ) { if( g.edge[vex][j] == 1 && !visited[j] ) //邻接未被访问过的结点入队 { visited[vex] = true; //标记已被访问过 q.push(j); } } } } void BFS_traver(Graph& g) { for(int i=1;i<=g.vnum;++i) { if(!visited[i]) //如果i结点未被访问过,形成森林 BFS(g,i); } }
广度优先搜索的经典应用:Dijkstra算法。Dijkstra算法的思想是BFS+贪心策略。即从源结点向外辐射状地扩展结点,并且每一次都选择最优的结点进行扩展。如果有n个结点,则循环n次就可以将所有结点的最短路径求出来,缺点是无法对权重为负数的图进行并且搜索的结点多效率相对较低。
先直接贴出代码,后面再解释算法运行的原理。
#include<stdio.h> #define MAX_VERTEX_NUM 100 //最大顶点数 #define MAX_INT 10000 //无穷大 typedef int AdjType; typedef char VType; //设顶点为字符类型 struct PathType { int route[MAX_VERTEX_NUM];//存放v到vi的一条最短路径 int End; }; //邻接矩阵表示的图 struct Graph { VType V[MAX_VERTEX_NUM]; //顶点存储空间 AdjType A[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; //邻接矩阵 }; //Dijkstra算法 //求G(用邻接矩阵表示)中源点v到其他各顶点最短路径,n为G中顶点数 void Dijkstra(Graph * G,PathType path[],int dist[],int v,int n) { int i,j,count,min,minIndex; //visited[n]用来标志源点到某顶点的最短路径是否求出 int *visited=new int[n]; for(i=0;i<n;i++) { //1.初始化 visited[i]=0; dist[i]=G->A[v][i];//v到它的邻接顶点的权为当前最短路径,用这个权值初始化dist[],优先队列(每次找小) path[i].route[0]=v; //开始结点都是v path[i].End=0; //路径末尾索引 } dist[v]=0; visited[v]=1;//源点到源点本身的最短路径已经求出 count=1; while(count<=n-1) { //求n-1条最短路径 min=MAX_INT;// MAX_INT为无穷大值,需要实际情况设置 //这一部分实现的是优先队列的功能,找最小值 for(j=0;j<n;j++) { //找当前最短路径长度 //如果j未被访问过且当前j的距离更短,更新min和minIndex if( !visited[j] && dist[j]<min ) { minIndex=j; min=dist[j]; } } //当visited[]数组全是1,即合被访问过时,可以退出了。 if(min==MAX_INT) break;//最短路径求完(不足n-1)条,跳出while循环,考虑非连通图 //贪心选择,选择当前最min的路径 int pEnd = ++path[minIndex].End; path[minIndex].route[pEnd]=minIndex ; visited[minIndex]=1;//表示V到minIndex最短路径求出,标记为已被访问过 for(j=0;j<n;j++) { //minIndex求出后,修改dist和path向量,只更新那些未处理过的点 if(!visited[j] && dist[minIndex]+G->A[minIndex][j] < dist[j] ) { dist[j]=dist[minIndex]+G->A[minIndex][j];//更新最短路径长度 path[j]=path[minIndex];//更新为更短的路径 } } count++; } delete []visited; } int main() { int i,j,v=0,n=6;//v为起点,n为顶点个数 Graph G; //v到各顶点的最短路径向量 PathType* path = new PathType[n]; //v到各顶点最短路径长度向量 int* dist =new int[n]; AdjType a[MAX_VERTEX_NUM][MAX_VERTEX_NUM]= { {MAX_INT,7,9,MAX_INT,MAX_INT,14}, {7,MAX_INT,10,15,MAX_INT,MAX_INT}, {9,10,MAX_INT,11,MAX_INT,2}, {MAX_INT,15,11,MAX_INT,6,MAX_INT}, {MAX_INT,MAX_INT,MAX_INT,6,MAX_INT,9}, {14,MAX_INT,2,MAX_INT,9,MAX_INT} }; for(i=0;i<n;i++) { for(j=0;j<n;j++) { G.A[i][j]=a[i][j]; } } Dijkstra(&G,path,dist,v,n); for(i=0;i<n;i++) { printf("%d到%d的最短路径:",v+1,i+1); for(j=0;j<=path[i].End;j++) { printf("%d ",path[i].route[j]+1); } printf("\n"); printf("最短路径长度:%d",dist[i]);//输出为MAX_INT则表示两点间无路径 printf("\n"); } delete []path; delete []dist; return 0; }用一个具体的图作为例子:
图的领接矩阵(带权短阵):
{MAX_INT,7,9,MAX_INT,MAX_INT,14},
{7,MAX_INT,10,15,MAX_INT,MAX_INT},
{9,10,MAX_INT,11,MAX_INT,2},
{MAX_INT,15,11,MAX_INT,6,MAX_INT},
{MAX_INT,MAX_INT,MAX_INT,6,MAX_INT,9},
{14,MAX_INT,2,MAX_INT,9,MAX_INT}
图如下:
Dijkstra算法的第一步:初始化
将所有的结点visited[i]设为0,即各个结点还没有计算出最短路径。
dist[i]表示结点i的权值,也可以看作是从源点v到结点i的最短路径长度。v到它的邻接顶点的权为当前最短路径.初始化比如,dist[0]=0,dist[1]=7,dist[2]=9,dist[5]=14,dist[4]=MAX,dist[3]=MAX(结点从0数起)
path[i]的开头结点都设为源结点v,路径末尾索引都设为0,因为当前只有一个元素。
并且dist[v] =0 ,visited[v]=1,源点到源点的最短路径已经求出。
初始化后是:
第二步:贪心选择当前未被计算出最短路径的dist[i]的最小值,如上图,现在dist[]数组是{0,7,9,14,MAX,MAX},0已被访问过,则从其余结点中贪心选择最短的一个结点1,(这一部分实现的是一个优先队列的功能,可以用STL的优先队列实现)
第三步:第二步贪心选择好一个结点后,需要更新dist[]数组和path[]数组,因为基于启发式搜索,有可能新发现的路径比原来的路径更短。
(如图:在7,9,14,MAX,MAX中贪心选择最短路径7,即结点1.标记为已计算出最短路径visited[i]=1,然后更新未visited过的结点的路径.对于结点3,有7+15<MAX,则用22更新结点3的权,同时更新最短路径paht[]。对于结点2,7+10>9,新发现的路径比原来的更小,无须更新。对4,5这些和1并不邻接的结点有MAX+7 > MAX,无须更新,同时MAX不能设为INT_MAX)
第四步:重复上第二步和第三步的步骤n-1次,就可以求出源点到所有结点的最短路径。
最短路练手:HDU1874
#include<iostream> #include<string.h> #include<limits.h> using namespace std; #define MAX_INT 2139062143 int map[205][205]; int n,s,v; int visited[205],dist[205]; void Dijkstra() { //第一步:初始化 int i,min,v; memset(visited,false,sizeof(visited)); memset(dist,127,sizeof(dist));//强大的近似初始化最大值 dist[s] = 0; while(1) { //第二步:找最小 min = MAX_INT,v = s; for(i=0; i<n; ++i) { if(!visited[i] && dist[i] < min) min = dist[i],v = i; } if(min == MAX_INT) break; visited[v] = true; //第三步:更新路径长度 for(i=0; i<n; ++i) { if(!visited[i] && map[v][i]<MAX_INT && dist[v] + map[v][i] < dist[i]) dist[i] = dist[v] + map[v][i]; } } } int main() { int m,i,x,y,res; while(cin>>n>>m) { memset(map,127,sizeof(map)); for(i=0; i<m; ++i) { cin >> x >> y >> res; if(map[x][y]>res)//注意重边,选最小的边 map[x][y] = map[y][x] = res; } cin >> s >> v; Dijkstra(); if( dist[v] == MAX_INT ) cout << "-1" << endl; else cout << dist[v] << endl; } return 0; }