dfs及其简单应用
这里记录一下老师上课讲过的dfs.
主要思路是,我们对于每一个节点都设置白色、灰色和黑色三种颜色,分别表示三种状态:未访问、正在访问和访问完毕。这三种状态的标准是根据某一个具体节点的邻居节点来定的,就是说,对于某一个具体节点,如果还没有访问完该节点的所有邻居节点,那么就表示这个节点处于正在访问状态,如果所有邻居节点都访问完毕,那就表示这个节点已经访问完毕。
那么首先,为了简单起见,我们直接用一个二维数组来表示边,且在初始化的时候,所有节点到其他任何节点之间的距离都设置为无穷大,包括节点到它自己之间的距离。
1 void init() 2 { 3 for (int i = 1; i <= n; ++i) { 4 for (int j = 1; j <= n; ++j) { 5 edge[i][j] = INT_MAX; 6 } 7 } 8 }
然后,就可以开始dfs了。这个函数的参数只需要设置一个,表示当前正在访问的节点。首先,我们当然就是需要把这个节点的颜色设置为灰色啦!然后,我们开始访问它的所有邻居节点,一旦发现一个邻居节点并且这个邻居节点的颜色状态也是白色(表示未访问),那么就以这个邻居节点为基准,继续dfs.这就体现了深度的特点。等到全部访问完后,也就是这个节点的所有邻居节点都访问完毕了,那么就可以把这个节点的颜色设置为黑色了,最后就把它打印出来就可以了。
1 void dfs(int u) 2 { 3 color[u] = "grey"; 4 for (int j = 1; j <= n; ++j) { 5 if (edge[u][j] != INT_MAX && color[j] == "white") { 6 dfs(j); 7 } 8 } 9 color[u] = "black"; 10 cout << u << " "; 11 }
最后,为了防止访问的这个图是一个森林,我们还要来一个函数,遍历一遍这个图的所有节点,一旦发现有节点是白色的,那就访问。
void GraphTravel() { for (int i = 1; i <= n; ++i) { if (color[i] == "white") { dfs(i); } } }
最后来一波完整代码:
#include <iostream> #include <algorithm> #include <queue> #define N 100 using namespace std; queue<int> q; int edge[N][N]; string color[N]; int n, edges; void init() { for (int i = 1; i <= n; ++i) { for (int j = 1; j <= n; ++j) { edge[i][j] = INT_MAX; } } } void dfs(int u) { color[u] = "grey"; for (int j = 1; j <= n; ++j) { if (edge[u][j] != INT_MAX && color[j] == "white") { dfs(j); } } color[u] = "black"; cout << u << " "; } void GraphTravel() { for (int i = 1; i <= n; ++i) { if (color[i] == "white") { dfs(i); } } } int main() { int start, end, weight; cout << "输入点的数量:" << endl; cin >> n; for (int i = 1; i <= n; ++i) { color[i] = "white"; } cout << "输入边的数量:" << endl; cin >> edges; cout << "依次输入起点、终点和边的权值:" << endl; init(); for (int i = 1; i <= edges; ++i) { cin >> start >> end >> weight; edge[start][end] = weight; edge[end][start] = weight; } GraphTravel(); return 0; }
再来一波运行效果:

对了,这个图长这样:

而如果要求这个图中有多少个不同的连通块,只要稍微改一下代码就可以了:
1 #include <iostream> 2 #include <algorithm> 3 #include <queue> 4 #define N 100 5 using namespace std; 6 queue<int> q; 7 int edge[N][N]; 8 int color[N]; 9 int n, edges; 10 void init() 11 { 12 for (int i = 1; i <= n; ++i) { 13 for (int j = 1; j <= n; ++j) { 14 edge[i][j] = INT_MAX; 15 } 16 } 17 } 18 19 void dfs(int u) 20 { 21 color[u] = 2; 22 for (int j = 1; j <= n; ++j) { 23 if (edge[u][j] != INT_MAX && color[j] == 1) { 24 dfs(j); 25 } 26 } 27 color[u] = 3; 28 cout << u << " "; 29 } 30 31 int GraphTravel() 32 { 33 int count = 0; 34 for (int i = 1; i <= n; ++i) { 35 if (color[i] == 1) { 36 count++; 37 dfs(i); 38 } 39 } 40 return count; 41 } 42 43 int main() 44 { 45 int start, end, weight; 46 cout << "输入点的数量:" << endl; 47 cin >> n; 48 for (int i = 1; i <= n; ++i) { 49 color[i] = 1; 50 } 51 cout << "输入边的数量:" << endl; 52 cin >> edges; 53 cout << "依次输入起点、终点和边的权值:" << endl; 54 init(); 55 for (int i = 1; i <= edges; ++i) { 56 cin >> start >> end >> weight; 57 edge[start][end] = weight; 58 edge[end][start] = weight; 59 } 60 int num = GraphTravel(); 61 cout << endl; 62 cout << "图中不同连通块的数目:" << num << endl; 63 return 0; 64 }
(PS:颜色状态那里没有用string了,有种感觉说不上来,总感觉会出问题,所以改成用1,2,3来表示白灰黑了)
好了,现在,我们来用这个dfs判断一下有没有环:
这里,我们要借用并查集里面的思想,我们在访问节点的时候顺便设置一下父节点,例如,从1访问到2,那么2的父亲就是1.
这种情况下,我们需要对dfs函数稍作修改一下:
1 void dfs(int u) 2 { 3 color[u] = 2; 4 for (int j = 1; j <= n; ++j) { 5 if (u != j && edge[u][j] != INT_MAX) { 6 if (color[j] == 1) { 7 father[j] = u; 8 dfs(j); 9 } 10 else if (color[j] == 2 && father[u] != j) { 11 hasCircle = true; 12 } 13 } 14 } 15 color[u] = 3; 16 cout << u << " "; 17 }
你可能会问:为啥一会儿father[j]=u一会儿又father[u]!=j呢?没事,慢慢来!举个例子,我们从1访问到2:那么是不是当前u就是1?然后j就是2?因为1和1之间肯定没办法访问嘛。那么我就肯定需要把2的父亲设为1,也就是father[2] = 1.这个时候,就dfs(2)了,那么,2就变成u了!然后,for循环遍历再次遍历到1时,u=2,j=1.这个时候他俩换过来了!所以这个时候我判断一下father[2]是否等于1,因为这样才是判断有没有环的关键!正因为father[2] = 1所以下面那个else if语句才不会进去,要不然1和2,2和1,不管啥图都会有环的。
感觉这里还是说得不是很清楚,我们来个图说明一下:

我们从1开始遍历,遍历到2,所以father[2] = 1.然后从2遍历到3,这个时候,father[3] = 2.最后,从3遍历到1,显然father[3]不等于1,因为我还没有为3设置父亲嘛。就算设置了,father[3]也不等于1,因为它会等于2!也就是说,father[2]=1,father[3]=2.那么这个时候,因为father[3]不等于1,所以肯定就会进入下面那个else if语句。那么,hasCircle就被设置为true了。感受一下这思路的巧妙!
至于为什么现在的dfs代码比之前的要多了一个判断u不等于j和edge距离,害,憋说了,都是血泪教训!你不这么来一下,一来程序有可能判断自己和自己,二来有可能判断根本不存在边的两个节点,这都会导致代码出错。(脑子里闪过一个想法,是不是这里的if判断又可以优化一下?只是想想,可能可以。但今天太累了,不想想了,聪明的你一定可以想到!)
好吧我还是想了一下,那个u!=j应该可以去掉,不一定是对的,你可以测试一下。
好了,现在再贴一份完整代码,这里就需要先对father数组初始化了,其实也没变多少。
1 #include <iostream> 2 #include <algorithm> 3 #include <stack> 4 #define N 100 5 using namespace std; 6 stack<int>s; 7 int edge[N][N]; 8 int color[N], father[N]; 9 int n, edges; 10 bool hasCircle = false; 11 void init() 12 { 13 for (int i = 1; i <= n; ++i) { 14 for (int j = 1; j <= n; ++j) { 15 edge[i][j] = INT_MAX; 16 } 17 } 18 } 19 20 void dfs(int u) 21 { 22 color[u] = 2; 23 for (int j = 1; j <= n; ++j) { 24 if (u != j && edge[u][j] != INT_MAX) { 25 if (color[j] == 1) { 26 father[j] = u; 27 dfs(j); 28 } 29 else if (color[j] == 2 && father[u] != j) { 30 hasCircle = true; 31 } 32 } 33 } 34 color[u] = 3; 35 cout << u << " "; 36 } 37 38 int GraphTravel() 39 { 40 int count = 0; 41 for (int i = 1; i <= n; ++i) { 42 if (color[i] == 1) { 43 count++; 44 dfs(i); 45 } 46 } 47 return count; 48 } 49 50 int main() 51 { 52 int start, end, weight; 53 cout << "输入点的数量:" << endl; 54 cin >> n; 55 for (int i = 1; i <= n; ++i) { 56 color[i] = 1; 57 father[i] = 0; 58 } 59 cout << "输入边的数量:" << endl; 60 cin >> edges; 61 cout << "依次输入起点、终点和边的权值:" << endl; 62 init(); 63 for (int i = 1; i <= edges; ++i) { 64 cin >> start >> end >> weight; 65 edge[start][end] = weight; 66 edge[end][start] = weight; 67 } 68 int num = GraphTravel(); 69 cout << endl; 70 cout << "图中不同连通块的数目:" << num << endl; 71 if (hasCircle) { 72 cout << "图中有环" << endl; 73 } 74 else { 75 cout << "图中没有环" << endl; 76 } 77 return 0; 78 }

这还是之前那个图,我们再来一个图测试一下:


完美!
可能读者会对最后打印出来节点的顺序有所疑问,其实这是节点“变黑的顺序”,这个顺序很有用!具体有啥用呢?To be continued...

浙公网安备 33010602011771号