先介绍BFS和DFS的大致模板:
BFS:
void BFS(Graph *g , int v){
queue<int> q;//用于存储图中每个顶点
visited[g->v] = {0};//0表示未访问
q.push(v);
while(!q.empty()){//若stk不为空,则一直循环
int cur = q.front();//取出队列第一个元素
q.pop();
for(map<int,int>::iterator it = g->ag[front].mp.begin(); it != g->ag[front].mp.end(); it++){
if(g->visited[it->first] == 0){//如果节点未被访问过
g->visited[it->first] = 1; //1表示已经在队列中,待访问
q.push(it->first);
}
}
}
}
DFS:(递归实现)
visited[max] = {0};
void DFS(Graph* g, int v){
visited[v] = 1;
for(map<int,int>::iterator it = g->ag[front].mp.begin(); it != g->ag[front].mp.end(); it++){
if(g->visited[it->first] == 0)//如果节点未被访问过
DFS(g, it->first);
}
}
如下几个应用使用深度优先或者广度优先都是可以实现的
原因是:这几个应用用到了深搜和广搜的顶点到顶点沿着边遍历的性质。
1.求每个联通分量中顶点的个数
2.路径问题
1)单源路径
2)点到点路径(递归改进)
3.环检测(递归改进)
4.二分图检测
1.求每个联通分量中顶点个数
不管是深搜还是广搜,遍历时能遍历一个联通分量的所有顶点,每次访问顶点时,将每个顶点保存起来
利用这性质
int visited[max] = {0};
void dfs(G* g,int v, vector<int> node){
g->visited[v] = 1;
node.push_back(v);// 将节点v存储到vector中
for(map<int,int>::iterator it = g->ag[v].mp.begin(); it != g->ag[v].mp.end(); it++){
if(!g->visited[it->first])
dfs(g, it->first,node);
}
}
vector<vector<int> > CCNode_1(G* g){
vector<vector<int> > nodes; //这个vector是用来存储 装有其他节点的连通分量的
for(int i = 0; i < g->v; i++){
if(!g->visited[i]){
vector<int> node;
dfs(g, i, node);
nodes.push_back(node);//先将每个联通分量的顶点求出来
//再将用vector.size()很容易就能求出数量了
}
}
return nodes;
}
上面这种方法有个很明显的确定,需要的空间比较大
下面我们对其进行改进
通过观察可以发现,在上一个的dfs_1()中,
g->visited[v] = 1;
node.push_back(v);
这两句话会同时被执行,也就是说访问过该顶点就会把该顶点置为1,且把该顶点放入对应的vector中而每次遍历完一个vector时,就会传入新的vector,那么改进visited数据的逻辑,让vector能够存储更多数据,在每个联通分量中,在访问时,相同的联通分量中给visited赋给相同的值,就能实现对连通分量的里的顶点进行区分。然后我们只需要计算visited中有几个不一样的值,就完成了。
void dfs_2(G* g,int v, int flag){
g->visited[v] = flag;
data.push_back(g->ag[v].data);
for(map<int,int>::iterator it = g->ag[v].mp.begin(); it != g->ag[v].mp.end(); it++){
if(!g->visited[it->first])
dfs_2(g, it->first,flag);
}
}
void CCNode_2(G* g){
for(int i = 0; i < max; i++){
g->visited[i] = -1;//-1表示未访问
}
int flag = -1;
for(int i = 0; i < g->v; i++){
if(!g->visited[i]){
flag++;
//每次要传入的flag即为赋给visited的值,
//在visited中具有相同的值即为在同一个连通分量
dfs_2(g, i, flag);
}
}
}
以上代码使用DFS实现的,用BFS同样可以实现,这里不给出其代码了。
2.路径问题
路径问题分为两类
一类是单源路径(Single Path):即求某一个点到其他所有点的路径
另一类是求某一点到另一点的路径或者距离。
这里需要强调的一点是:BFS求得的路径是最短路径,而DFS不是,这是BFS特有的性质,仅在无权图中。
要求路径,只要从源点出发,每访问一个节点时,记录下该节点编号即可。
void bfs(G* g, int v){
queue<int> q;
q.push(v);//入队
while(!q.empty()){
int cur = q.front(); //取出第一个元素
q.pop(); //弹出第一个元素
for(map<int,int>::iterator it = g->ag[cur].mp.begin(); it != g->ag[cur].mp.end(); it++){//遍历所有的边
if(g->visited[it->first] == 0){//如果节点未被访问过
mpath[it->first] = cur;//将访问的节点保存在起来
//如果检索mpath[v1] == v2;代表
//v1到v2之间存在一条边
g->visited[it->first] = 1; //1表示已经在队列中
q.push(it->first);
}
}
}
}
vector<int> Path(G* g, int v, int d){
bfs(g,v);
vector<int> vtr;//用于存储v到d点的路径
if(mpath.find(d) == mpath.end()){
return vtr;//在mpath找不到d说明v和d不在一个联通分量里
}
else{
vtr.push_back(d);
int next = d;
while(next != v){
vtr.push_back(mpath[next]);
next = mpath[next];
}
}
vector<int> rev;
vector<int>::reverse_iterator reit;
for(reit = vtr.rbegin(); reit != vtr.rend(); reit++){
rev.push_back(*reit);
}
return rev;
}
点到点求路径:如果我们已经找到了目标点,那么提前终止遍历即可
这里我使用的是DFS代码,会涉及到递归函数结构上的修改。
声明函数时:void DFS(Graph * g, int v);
可以理解下这个函数的语义,在图g中,以v为顶点进行DFS,我们知道这个是一个递归调用函数,也就是说每次访问一个顶点时都会调用DFS。好了我妈来理解下这句话 “每次访问一个顶点时都会调用DFS”,也就是说,每次访问一个顶点时,如果这个这个顶点就是我们要找的顶点,那么此时是不是就可以停止继续遍历了,此时我们可以发出一个信号,告诉上一层函数,已经找到了这条路径,由于DFS的特性,是“一条路走到黑”,当他在往图的深度不断遍历时,突然找到了目标顶点,就可以把这个信号返回,一直返回到源节点,从而中止这条路径上的顶点再去访问其他顶点了。 如果找到了目标顶点,返回一个true,如果这条走到黑的路径没有找到目标顶点就返回false。所以函数声明应该修改为 bool DFS(Graph *g, int v);
下面看函数具体实现
bool dfs(G* g,int v, int d, int parent){
g->visited[v] = 1;
if(v != parent) mpath[parent] = v; //源节点没有父节点
if(v == d) return true; //提前终止递归
for(map<int,int>::iterator it = g->ag[v].mp.begin(); it != g->ag[v].mp.end(); it++){
if(!g->visited[it->first]){
if(dfs(g, it->first, d, v)) return true;//按照递归语义:若在顶点it->first 到d顶点存在一条路径,则返回true
}
}
return false; //这个return是接在for后面理解语义的,若在顶点it->first 到d顶点不存在一条路径,则返回false
}
bool isConnectedTo(G* g, int s,int d){
return dfs(g,s,d,s);
}
vector<int> path(G* g, int s, int d){
vector<int> vt;
if(isConnectedTo(g,s,d)){
vt.push_back(s);
int next = s;
while(next != d){
vt.push_back(mpath[next]);
next = mpath[next];
}
}
for(int i = 0; i < vt.size()-1; i++){//输出路径
cout<< vt[i] <<"->";
}
cout << d<<endl;
return vt;
}
虽然在代码DFS函数接口中,参数多了一个int parent
,这主要是为了处理刚源节点传进去时,和其他节点的统一。
3.环检测
如何检测环?以上图为例,如果按DFS遍历的方式,遍历顺序为 1-> 2->4->3 当访问顶点3时,他将会去访问顶点1,但是顶点1已经被访问过了,再去访问其父节点4,其父节点必然被访问过了, 然后进行回溯。从这个过程中我们可以发现,若3顶点和1顶点之间没有边,那么这个图就不会有环,而在顶点3访问1的过程中,顶点3访问了一个已被访问的非父顶点,也就是说顶点0除了0-1这条边能回到顶点0,还能通过另外一条边回到顶点顶点0,所以产生了环。
所以使用DFS时,只需要判断每个顶点在遍历他的所有边时,会不会访问到已被访问非父的顶点。
顶点在遍历边时,需要区分这条边的另一条顶点是不是他的父节点。所有我们需要在DFS函数声明时需要传入parent参数。void DFS(Graph* g, int v, int parent);
下面是具体函数实现
bool dfs(G* g,int v, int parent){
g->visited[v] = 1;
for(map<int,int>::iterator it = g->ag[v].mp.begin(); it != g->ag[v].mp.end(); it++){//遍历v节点的相邻节点
if(!g->visited[it->first]){
dfs(g, it->first,v);
}
else if(it->first != parent){ //由于上一个if()条件为假,所以这个顶点必然是被访问过的,
return true; //若这个结点不是上一个父节点,则必然存在环。按照求点到点的路径方法
//此刻可以停止循环了,返回真
}
}
}
bool CycleDetection(G* g){//环检测
for(int i = 0; i < g->v; i++){
if(!g->visited[i]){ //考虑图可能是不连通的
if(dfs(g, i, i))//源顶点的父节点是其本身
return true;
}
}
return false;
}
4.二分图检测
首先介绍二分图有什么特点:
所有的顶点可以分成两部分,这每个部分中所有顶点之间都不存在边
所有的边的两个端点隶属于不同的部分
针对这种应用,我们采用的是染色,颜色种类只有两种,我们将相邻的节点染成不同的颜色,也就刚刚说的 一条边隶属于不同的两个顶点集,同色的为一种顶点集,最后发现某一条边的两个端点的颜色为同色,则肯定不是二分图。
void dfs(G* g,int v, int parent){//通过bfs遍历,将其染色
//通过思考可以发现,染色的过程和访问顶点的过程是一样的,只是每次染色给visited数组赋了两种值(这种想法是错误的)
//正确的:其染色原理是遍历的时候,每条边的两个顶点染成不同的颜色,通过DFS机制,应该将下一个节点染成
//与其父节点相异的颜色,这里要区分一下的是,在DFS中一个节点父节点不是他的上一个节点,因为在DFS中有
//回退机制(递归回退),所以要新添parent形参
if(g->visited[parent] == 1) g->visited[v] =2;
else g->visited[v] =1;
for(map<int,int>::iterator it = g->ag[v].mp.begin(); it != g->ag[v].mp.end(); it++){//遍历v节点的相邻节点
if(!g->visited[it->first]){
dfs(g, it->first,v);
}
}
}
bool BipartitionDetection(G* g){
for(int i = 0; i < g->v; i++){
if(!g->visited[i]){
g->visited[i] = 1;
dfs(g, i,i);
}
}
//判断图中的两个端点的颜色是否相异 ,这里只需要遍历结构体中的那个数组
for(int i = 0; i < g->v; i++){
int color = g->visited[i];
for(map<int,int>::iterator it = g->ag[i].mp.begin(); it != g->ag[i].mp.end(); it++){
if(g->visited[it->first] == color){//若有相同颜色,则不是二分图
return false;
}
}
}
//若没有检测出异样,则是二分图
return true;
}
以上代码并不是最优实现,因为我是将所有的顶点都染上色在进行判断的。而在DFS中,再遍历的过程中可以直接判断,当访问的这个非父顶点的颜色和自身颜色相同时就可直接判断这个图不可能是二分图了。