今天做了一道很有意思的一道题,这道题虽然难度只是中等,但是里面涉及到的东西却是不少。其中,我在里面学习到了并查集这个东西,虽然不是很深刻,至少有一个印象;还有深搜,一直以来,深搜和广搜都是我的弱项,本文的理解是基于别人的博客:lintcode178. graph valid tree 图是否是树。我们来看看题
题意:
给出 n 个节点,标号分别从 0 到 n - 1 并且给出一个 无向 边的列表 (给出每 条边的两个顶点), 写一个函数去判断这张`无向`图是否是一棵树
样例:
给出n = 5 并且 edges = [[0, 1], [0, 2], [0, 3], [1, 4]], 返回 true. 给出n = 5 并且 edges = [[0, 1], [1, 2], [2, 3], [1, 3], [1, 4]], 返回 false.
注意事项:
你可以假设我们不会给出重复的边在边的列表当中. 无向边 [0, 1] 和 [1, 0] 是同一条边, 因此他们不会同时出现在我们给你的边的列表当中。
1.并查集
首先有两点:
A.如果点的个数减去边数不等于1,此图肯定不为树。
B.如果有一条边的两个点同属于一个集合的话,那么此图肯定不为树
第一点我们非常容易的能够判断出来,但是第二种怎么来判断呢?这就需要我们的并查集(我也是第一次接触到并查集,只是知道它的模型,所以可能说的不是很清楚)。
(1).一维数组记录每一个点的集合
我们首先可以定义一个一维数组,默认全部初始化为-1,其中表示的意思就是:假设数组temp[], temp[0]表示的是0这个点属于的集合,不同的集合我们不同数字来表示,比如,0集合和1集合是不同的,同一个集合的点构成了一棵树。
具体的操作:
当我们遍历到一条边时,判断这条边的两个点是否在temp数组中出现,其中分为三种情况:
A.两个点均未出现,也就是两个点对应的temp数组的值是等于-1,那么,我们将这两个点对应的数组值更为两点中最小的那个点(这里没有限制,最大也行,主要是保证他们俩在同一个集合中)。
B.一个点出现过,一个点未出现,那么我们将未出现的那个点对应的数组值更新为出现的那个点的数组值,这样就保证了两个点属于同一个集合。
C.两个点都出现过,只是属于不同的集合,也就是说,两个点对应的数组值不相同,那么此时我们的操作是将两个集合合并起来,具体的操作是:因为每个集合里面的所有值都是相同,那么我们将值较大的那个集合的值全部更新为另一个集合的值,比如有两个集合分别是:{1,1},{2,2},更新过后就是:{1,1,1,1}。这样能保证两个集合合并起来后,构成一个集合。
D.两个点都出现过,同时还属于同一个集合,那么此图肯定不为树。
代码:
1 public boolean validTree(int n, int[][] edges) { 2 //当点数减去边数不等于1时,肯定不为树 3 if (n - edges.length != 1) { 4 return false; 5 } 6 int temp[] = new int[n]; 7 for (int i = 0; i < edges.length; i++) { 8 int node1 = edges[i][0]; 9 int node2 = edges[i][1]; 10 // 当两个点属于同一个集合时,要么两点没有在任何集合里面,要么在同一个集合里面 11 if (temp[node1] == temp[node2]) { 12 //都没有出现过 13 if (temp[node1] == -1) { 14 temp[node1] = temp[node2] = Math.min(node1, node2); 15 } else { //都出现过,同时还属于同一个集合 16 return false; 17 } 18 } else { 19 // 当两个点都在集合里面,但是是在不同的集合里,更新集合 20 if (temp[node1] != -1 && temp[node2] != -1) { 21 int max = Math.max(temp[node1], temp[node2]); 22 int min = Math.min(temp[node1], temp[node2]); 23 for (int j = 0; j < n; j++) { 24 if (temp[j] == max) { //将值较大的集合的值全部更新min 25 temp[j] = min; 26 } 27 } 28 } else { // 当只有一个点在集合里面 29 if (temp[node1] != -1) { 30 temp[node2] = temp[node1]; 31 } else { 32 temp[node1] = temp[node2]; 33 } 34 } 35 } 36 } 37 return true; 38 }
如图所示:
2.深搜遍历
深搜遍历在这里主要作用是:我们寻找每一个点的父节点,如果两个点的父节点相同的话,那么此图肯定不为树。这里的理解上可能有一些问题,我们看看下面的几种情况:
(1).两个点均是第一次出现。如图:
(2).一个点是出现过的,另一个点是第一次出现。如图(提示一下,下面图的描述错了,C的父亲应该是A):
(3).两个点都有父亲,只是父亲不相同,分为两种情况:
(4).两个点都出现过,并且父亲相同,分为两种情况:
代码:
1 private int dfs(int temp[], int node){ 2 //当这个点不在有父亲了,返回这个点 3 if(temp[node] == -1){ 4 return node; 5 } 6 else //如果这点还有父亲的话,那么就继续遍历这个点的父亲 7 { 8 return dfs(temp, temp[node]); 9 } 10 } 11 public boolean validTree(int n, int[][] edges) { 12 13 if (n - edges.length != 1) { 14 return false; 15 } 16 int []temp = new int[n]; 17 Arrays.fill(temp,-1); 18 for(int i = 0; i < edges.length; i++){ 19 //获取edges[i][0]点的父亲 20 int node1 = dfs(temp, edges[i][0]); 21 //获取edges[i][1]点的父亲 22 int node2 = dfs(temp, edges[i][0]); 23 //如果父亲相同的话,肯定不为树 24 if(node1 == node2){ 25 return false; 26 } 27 //将node2的父亲设置为node1 28 temp[node2] = node1; 29 } 30 return true; 31 }