判断无向图是否存在环

如果存在回路,则必存在一个子图,是一个环路。环路中所有顶点的度>=2
n 算法:
     第一步:删除所有度<=1的顶点及相关的边,并将另外与这些边相关的其它顶点的度减一。
     第二步:将度数变为1的顶点排入队列,并从该队列中取出一个顶点重复步骤一。
     如果最后还有未删除顶点,则存在环,否则没有环。
n 算法分析:
            由于有m条边,n个顶点。如果m>=n,则根据图论知识可直接判断存在环路。
    (证明:如果没有环路,则该图必然是k棵树 k>=1。根据树的性质,边的数m = n-kk>=1,所以:m<n
            如果m<n 则按照上面的算法每删除一个度为0的顶点操作一次(最多n次),或每删除一个度为1的顶点(同时删一条边)操作一次(最多m次)。这两种操作的总数不会超过m+n。由于m<n,所以算法复杂度为O(n)
图论中有个定理,如果边数m >= 顶点数n,则必定存在回路
否则,m<n时,就要写程序判断了。

如果有环路,则存在一个子图,构成一条回路,回路上的每个节点的度数>=2,所以,凡是度数<2的
顶点和该回路不搭界,可以迭代地去掉他们。

算法如下:
V = vertex set
E = edge set
设原图为G
初始化V = {G中所有度数小于2的顶点}
定义E(V) 为 与V中的顶点相关的边

while V is not empty
v = Next(V)
delete v and E(v) from G
update G;//改变和v点相连的顶点的度数
将所有去掉v后度数变成<2的顶点加入V
end while

if G if empty
没有回路
else
有回路
具体实现时,V可以是队列

这个算法的复杂度是O(n+m),n是顶点个数,m是边个数,m<n,所以复杂度是O(n)

 

 

<think> 我们正在处理无向图检测。注意无向图与有向检测上的不同:无向图中,由于边是无向的,我们需要避免将父结点(回溯边)误认为是。因此,在无向图的DFS中,当我们从一个结点u访问它的邻接结点v时,如果v已经被访问过且v不是u的父结点(即不是从v回溯到u的那条边),那么我们就检测到了一个。 具体步骤: 1. 初始化所有结点为未访问状态。 2. 从任意一个未访问的结点开始进行DFS遍历。 3. 在DFS函数中: - 将当前结点标记为已访问。 - 遍历当前结点的所有邻接结点: - 如果邻接结点未被访问,则将其作为子结点递归访问,并传递当前结点作为父结点(用于在回溯边时识别)。 - 如果邻接结点已被访问,且不是当前结点的父结点(即不是从父结点过来的那条边),则说明存在。 4. 如果遍历完所有结点都没有发现,则无。 注意:无向图的表示可以用邻接矩阵或邻接表。这里我们使用邻接矩阵,但也可以使用邻接表(在性能上更优,特别是稀疏)。 实现要点: - 需要记录每个结点的父结点(在递归调用时传递),以避免将父结点误认为。 - 在DFS递归函数中,对于当前结点u,访问其邻接结点v: if (visited[v] == 0) { 递归调用DFS(v, u) // u是v的父结点 } else if (v != parent) { 发现 } 但是注意:无向图中每条边会被访问两次(因为是无向的),所以我们在遍历时要注意避免重复处理同一条边。使用父结点的概念就是为了避免将父结点的边当作。 另外,由于可能不连通,因此需要检查所有未访问的结点。 代码结构: 函数1:DFS递归函数 参数:当前结点u,父结点parent 函数2:主判函数 遍历所有结点,对未访问的结点调用DFS 我们使用邻接矩阵表示,全局变量: int graph[MAX_VERTICES][MAX_VERTICES]; // 邻接矩阵 int visited[MAX_VERTICES]; // 访问状态数组 int num_vertices; // 顶点数 注意:在无向图中,邻接矩阵是对称的。 示例代码: ```c #include <stdio.h> #include <stdbool.h> #define MAX_VERTICES 100 int graph[MAX_VERTICES][MAX_VERTICES]; int visited[MAX_VERTICES]; int num_vertices; // DFS遍历,当前结点u,父结点parent bool dfs(int u, int parent) { visited[u] = 1; // 标记为已访问 for (int v = 0; v < num_vertices; v++) { // 如果存在边(u,v) if (graph[u][v]) { if (!visited[v]) { // 递归访问v,并将u作为v的父结点 if (dfs(v, u)) { return true; } } // 如果v已经被访问过,并且v不是u的父结点(注意:父结点是通过递归传递的) else if (v != parent) { // 存在 return true; } } } return false; } // 判断无向图是否存在 bool has_cycle() { // 初始化visited数组为0(未访问) for (int i = 0; i < num_vertices; i++) { visited[i] = 0; } // 遍历每一个结点 for (int i = 0; i < num_vertices; i++) { if (!visited[i]) { // 从结点i开始DFS,初始时父结点为-1(表示没有父结点) if (dfs(i, -1)) { return true; } } } return false; } int main() { // 输入的顶点数 printf("请输入的顶点数: "); scanf("%d", &num_vertices); // 输入邻接矩阵(无向图是对称矩阵) printf("请输入的邻接矩阵(无向图,请输入对称矩阵):\n"); for (int i = 0; i < num_vertices; i++) { for (int j = 0; j < num_vertices; j++) { scanf("%d", &graph[i][j]); } } // 判断是否 if (has_cycle()) { printf("存在\n"); } else { printf("中不存在\n"); } return 0; } ``` 注意:在无向图中,邻接矩阵应该是对称的,即graph[i][j]=graph[j][i]。但我们的算法并不依赖于对称性,因为我们是按有向边来检查的。不过,如果输入不是对称的,那么实际上就不是无向图了。 关于双向边:在无向图中,一条边(u,v)在邻接矩阵中会同时记录graph[u][v]和graph[v][u]。在DFS遍历时,当我们从u访问v后,在v的邻接点中又会看到u,这时通过传递父结点v(此时u的父结点是v)就可以避免将(u,v)这条边误判为。 但是,如果存在(即graph[i][i]=1),那么当访问到自时,会出现:当前结点u,邻接点v=u(即自),这时visited[u]已经被访问,且v(即u)不等于parent(因为自没有经由父结点,而是直接指向自己),所以会被判定为。这是正确的,因为自就是一个。 另外,如果存在平行边(多重边),那么我们的邻接矩阵中graph[u][v]可能大于1(但通常我们只记录1表示有边)。如果按照上面的邻接矩阵(0/1),那么平行边会被视为一条边。但如果输入允许多重边,那么我们需要考虑。不过,在无向图检测中,平行边(即两个结点之间有两条边)就构成了。因此,如果我们在邻接矩阵中允许大于1的值,那么当graph[u][v]>1时,我们同样视为存在边。但是,在遍历时,我们只需要判断是否为0(无连接)或非0(有连接)。因此,如果输入邻接矩阵时,平行边会导致graph[u][v]大于1,那么我们在条件`if (graph[u][v])`时仍然会进入。但是,当我们第一次从u访问v后,v被标记为已访问,然后当我们再次从v访问u时(因为是无向,所以v也会连接u),这时我们会发现u已经被访问,且u不是v的父结点(注意:第一次从u到v,那么v的父结点是u;然后第二次从v到u,此时u是v的父结点,所以不会判?)——实际上,这里有一个问题:平行边在邻接矩阵中只用一个元素表示,所以实际上邻接矩阵不能直接表示多重边(因为一个位置只能有一个值)。因此,如果有多重边,我们需要用邻接表来存储,并且在邻接表中记录多条边。在邻接矩阵中,我们通常用0和1表示,所以多重边会被忽略(即只能表示一条边)。因此,如果要考虑多重边,我们需要改变的存储方式。 不过,题目要求使用邻接矩阵,所以我们按照邻接矩阵实现。邻接矩阵无法表示多重边(除非使用整数权重表示边的数量,但我们的算法只关心是否连通,所以即使有多重边,我们在邻接矩阵中将其置为1,则平行边不会被视为两条边)。因此,在邻接矩阵中,平行边不会构成(因为只被视为一条边)。但是,如果实际存在平行边,那么它就是一个(两个结点之间有两条边,形成一个二元)。所以,如果输入可能存在平行边,我们应该使用邻接表,并在邻接表中存储多条边。然后,在DFS遍历时,对于同一个邻接点出现多次的情况,我们需要考虑。但是,在无向图中,从u到v有多条边,那么当我们访问u时,第一次访问v后,v被标记为已访问,然后再次访问v(通过另一条平行边)时,就会触发条件:v已被访问且不是父结点(因为父结点只有一个,就是第一次访问时的父结点),所以会判断存在。因此,在邻接表中,我们可以通过记录多条边来正确检测平行边构成的。 总结:上述代码适用于邻接矩阵存储的无向图(无多重边或自可以正确判断)。如果有自,会被正确识别为;平行边在邻接矩阵中无法表示,所以无法检测(因为邻接矩阵只存0或1)。如果存在平行边,应该使用邻接表存储。 以上代码即可实现无向图检测。 下面生成相关问题: </think> ### 使用C语言通过DFS判断无向图是否存在的实现方法 在无向图判断存在需要使用DFS并记录父子关系,避免将回溯边误判为。以下是实现步骤和代码: #### 算法步骤 1. **初始化**:标记所有节点为未访问状态 2. **DFS遍历**:对于每个未访问节点执行DFS: - 标记当前节点为"访问中" - 遍历当前节点的所有邻接节点: - 若邻接节点未访问,将其作为子节点递归 - 若邻接节点已访问**且不是父节点**,则存在 3. **标记完成**:回溯时标记节点为"已访问" #### C语言实现代码(邻接矩阵表示) ```c #include <stdio.h> #include <stdbool.h> #include <string.h> #define MAX_VERTICES 100 int graph[MAX_VERTICES][MAX_VERTICES]; // 邻接矩阵 int visited[MAX_VERTICES]; // 访问状态:0=未访问,1=访问中 int vertices; // 顶点数 // DFS递归函数 bool dfs_visit(int u, int parent) { visited[u] = 1; // 标记为访问中 for (int v = 0; v < vertices; v++) { if (graph[u][v]) { // 存在边(u,v) if (!visited[v]) { // 未被访问过 if (dfs_visit(v, u)) // 递归访问子节点 return true; } // 已访问且不是父节点(回溯边) else if (v != parent) { return true; // 发现 } } } return false; } // 主函数:判断是否存在 bool has_cycle(bool directed) { memset(visited, 0, sizeof(visited)); for (int i = 0; i < vertices; i++) { if (!visited[i]) { if (dfs_visit(i, -1)) // -1表示根节点无父节点 return true; } } return false; } int main() { printf("输入顶点数: "); scanf("%d", &vertices); printf("输入邻接矩阵(%d×%d):\n", vertices, vertices); for (int i = 0; i < vertices; i++) { for (int j = 0; j < vertices; j++) { scanf("%d", &graph[i][j]); } } if (has_cycle(false)) { printf("存在\n"); } else { printf("中不存在\n"); } return 0; } ``` #### 关键点说明 1. **父节点记录**:`parent`参数避免将回溯边误判为 2. **访问状态**:只需"未访问"和"访问中"两种状态 3. **时间复杂度**:$O(V+E)$,其中$V$为顶点数,$E$为边数 4. **自处理**:若存在(如`graph[i][i]=1`),会被正确识别为 #### 邻接表实现(替代方案) ```c struct Node { int vertex; struct Node* next; }; struct Node* adjList[MAX_VERTICES]; // 邻接表 // DFS函数(邻接表版本) bool dfs_visit(int u, int parent) { visited[u] = 1; struct Node* temp = adjList[u]; while (temp) { int v = temp->vertex; if (!visited[v]) { if (dfs_visit(v, u)) return true; } else if (v != parent) { return true; } temp = temp->next; } return false; } ``` 邻接表更适合稀疏,空间复杂度为$O(V+E)$[^1]。 #### 算法正确性证明 当DFS遍历遇到已访问节点时: - 若是父节点:正常回溯边,忽略 - 若非父节点存在另一条路径到达该节点,形成
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值