贪心算法应用:欧拉路径(Fleury算法)详解

article/2025/7/4 1:22:10

在这里插入图片描述

Java中的贪心算法应用:欧拉路径(Fleury算法)详解

一、欧拉路径与欧拉回路基础

1.1 基本概念

欧拉路径(Eulerian Path)是指在一个图中,经过图中每一条边且每一条边只经过一次的路径。如果这条路径的起点和终点相同,则称为欧拉回路(Eulerian Circuit)。

1.2 存在条件

  • 无向图欧拉回路存在条件

    • 图是连通的
    • 所有顶点的度数都是偶数
  • 无向图欧拉路径存在条件

    • 图是连通的
    • 恰好有两个顶点的度数是奇数(这两个顶点分别是路径的起点和终点)
  • 有向图欧拉回路存在条件

    • 图是强连通的
    • 每个顶点的入度等于出度
  • 有向图欧拉路径存在条件

    • 图是弱连通的
    • 恰好有一个顶点的出度比入度大1(起点)
    • 恰好有一个顶点的入度比出度大1(终点)
    • 其他所有顶点的入度等于出度

二、Fleury算法原理

Fleury算法是一种用于在图中寻找欧拉路径或欧拉回路的贪心算法。它的核心思想是:尽可能不选择桥边(除非没有其他选择)。

2.1 算法步骤

  1. 检查图是否满足欧拉路径/回路的存在条件
  2. 选择起始顶点
    • 对于欧拉回路:可以选择任意顶点
    • 对于欧拉路径:必须选择奇数度数的顶点之一
  3. 递归/迭代选择边
    • 选择一条非桥边(除非没有其他选择)
    • 删除已选择的边
    • 移动到下一个顶点
  4. 重复上述过程直到所有边都被遍历

2.2 为什么是贪心算法

Fleury算法在每个步骤中都做出局部最优选择(选择非桥边),以期最终得到全局最优解(完整的欧拉路径)。这种"每一步都做出当前看来最佳选择"的策略正是贪心算法的核心特征。

三、Java实现Fleury算法

3.1 图的表示

我们使用邻接表来表示图:

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;public class FleuryAlgorithm {private int vertices; // 顶点数private LinkedList<Integer>[] adjacencyList; // 邻接表// 构造函数public FleuryAlgorithm(int vertices) {this.vertices = vertices;adjacencyList = new LinkedList[vertices];for (int i = 0; i < vertices; i++) {adjacencyList[i] = new LinkedList<>();}}// 添加边public void addEdge(int u, int v) {adjacencyList[u].add(v);adjacencyList[v].add(u); // 无向图}// 删除边public void removeEdge(int u, int v) {adjacencyList[u].remove((Integer) v);adjacencyList[v].remove((Integer) u);}
}

3.2 检查图是否连通(DFS方法)

// 深度优先搜索检查连通性
private void dfs(int v, boolean[] visited) {visited[v] = true;for (int neighbor : adjacencyList[v]) {if (!visited[neighbor]) {dfs(neighbor, visited);}}
}// 检查图是否连通
public boolean isConnected() {boolean[] visited = new boolean[vertices];// 找到第一个度数不为0的顶点int startVertex = 0;for (int i = 0; i < vertices; i++) {if (adjacencyList[i].size() != 0) {startVertex = i;break;}}// 如果没有边,认为是连通的if (startVertex == -1) {return true;}// 从该顶点开始DFSdfs(startVertex, visited);// 检查所有度数不为0的顶点是否都被访问过for (int i = 0; i < vertices; i++) {if (adjacencyList[i].size() > 0 && !visited[i]) {return false;}}return true;
}

3.3 检查边是否为桥(DFS计数方法)

// 计算从u出发可到达的顶点数
private int dfsCount(int u, boolean[] visited) {visited[u] = true;int count = 1;for (int v : adjacencyList[u]) {if (!visited[v]) {count += dfsCount(v, visited);}}return count;
}// 检查u-v是否是桥
private boolean isBridge(int u, int v) {// 计算u的邻接顶点数int degree = adjacencyList[u].size();if (degree == 1) {return false; // 只有一条边,不是桥}// 计算删除u-v前从u可到达的顶点数boolean[] visited = new boolean[vertices];int count1 = dfsCount(u, visited);// 临时删除边u-vremoveEdge(u, v);// 计算删除u-v后从u可到达的顶点数visited = new boolean[vertices];int count2 = dfsCount(u, visited);// 恢复边u-vaddEdge(u, v);// 如果count1 > count2,说明u-v是桥return count1 > count2;
}

3.4 Fleury算法核心实现

// 打印欧拉路径/回路
public void printEulerPath() {// 检查图是否连通if (!isConnected()) {System.out.println("图不连通,不存在欧拉路径");return;}// 计算奇数度数的顶点数int oddDegreeCount = 0;int startVertex = 0;for (int i = 0; i < vertices; i++) {if (adjacencyList[i].size() % 2 != 0) {oddDegreeCount++;startVertex = i;}}// 检查是否存在欧拉路径或回路if (oddDegreeCount > 2) {System.out.println("图中没有欧拉路径或回路");return;}// 开始寻找路径System.out.println("欧拉路径/回路为:");printEulerUtil(startVertex);System.out.println();
}// 递归打印欧拉路径
private void printEulerUtil(int u) {// 遍历所有邻接顶点for (int v : adjacencyList[u]) {// 如果边u-v不是桥,或者只剩下这条边,则选择它if (!isBridge(u, v) || adjacencyList[u].size() == 1) {System.out.print(u + "-" + v + " ");// 删除这条边removeEdge(u, v);// 继续从v开始printEulerUtil(v);break;}}
}

3.5 完整测试代码

public static void main(String[] args) {// 示例1:欧拉回路FleuryAlgorithm g1 = new FleuryAlgorithm(4);g1.addEdge(0, 1);g1.addEdge(0, 2);g1.addEdge(1, 2);g1.addEdge(2, 3);g1.addEdge(3, 0);g1.printEulerPath();// 示例2:欧拉路径FleuryAlgorithm g2 = new FleuryAlgorithm(4);g2.addEdge(0, 1);g2.addEdge(0, 2);g2.addEdge(1, 2);g2.addEdge(2, 3);g2.printEulerPath();// 示例3:没有欧拉路径FleuryAlgorithm g3 = new FleuryAlgorithm(3);g3.addEdge(0, 1);g3.addEdge(1, 2);g3.addEdge(2, 0);g3.addEdge(0, 1); // 多重边g3.printEulerPath();
}

四、算法复杂度分析

  1. 时间复杂度

    • 检查连通性(DFS):O(V + E)
    • 检查桥边:每次检查需要O(E)时间
    • 对于每条边,我们可能需要检查它是否是桥,因此总时间复杂度为O(E²)
  2. 空间复杂度

    • 主要取决于图的表示和递归深度
    • 邻接表:O(V + E)
    • 递归栈:最坏情况下O(E)

五、优化与改进

5.1 桥边检测优化

原始的Fleury算法在每一步都需要检测桥边,这导致了较高的时间复杂度。可以使用以下方法优化:

  1. 动态维护桥边信息:使用更高级的数据结构(如动态图算法)来维护桥边信息
  2. Hierholzer算法:另一种更高效的欧拉路径算法,时间复杂度为O(E)

5.2 Hierholzer算法简介

虽然Fleury算法是经典的贪心算法,但Hierholzer算法通常更高效:

public void hierholzerAlgorithm() {// 1. 检查欧拉路径存在条件(同Fleury算法)// 2. 初始化栈和路径Stack<Integer> stack = new Stack<>();List<Integer> path = new ArrayList<>();// 3. 选择起始顶点(同Fleury算法)int startVertex = ...;stack.push(startVertex);// 4. 主循环while (!stack.isEmpty()) {int current = stack.peek();if (adjacencyList[current].size() == 0) {path.add(stack.pop());} else {int next = adjacencyList[current].get(0);stack.push(next);removeEdge(current, next);}}// 5. 输出路径Collections.reverse(path);System.out.println(path);
}

六、实际应用场景

欧拉路径和Fleury算法在实际中有多种应用:

  1. 邮路问题:邮递员如何走过所有街道且不重复
  2. DNA序列组装:将短DNA片段组装成完整序列
  3. 网络路由:设计网络数据包的路由路径
  4. 电路板钻孔:在电路板上钻孔的最优路径规划
  5. 笔画问题:判断图形是否可以一笔画成

七、常见问题与解决方案

7.1 如何处理多重图?

多重图(有重复边的图)需要特殊处理:

  • 邻接表中存储边时,可以使用带计数的结构
  • 删除边时减少计数而不是完全移除

7.2 如何处理极大图?

对于极大图,递归实现可能导致栈溢出:

  • 改用迭代实现
  • 增加栈大小(-Xss JVM参数)
  • 使用更高效的算法如Hierholzer

7.3 如何验证找到的路径确实是欧拉路径?

验证方法:

  1. 检查路径长度是否等于边数
  2. 检查每条边是否只出现一次
  3. 检查路径是否连续(相邻顶点间有边)

八、扩展与变种

  1. 中国邮路问题:允许重复经过某些边,求最短路径
  2. 有向图欧拉路径:调整算法处理有向边
  3. 加权图欧拉路径:在有权图中寻找最优欧拉路径
  4. 混合图欧拉路径:同时包含有向边和无向边的图

九、总结

Fleury算法是贪心算法在图论中的一个经典应用,它通过在每个步骤中尽可能选择非桥边来构建欧拉路径。虽然它的时间复杂度不是最优的,但其思路清晰,易于理解,是学习贪心算法和欧拉路径的绝佳范例。

Java实现时需要注意图的表示、连通性检查、桥边检测等关键点。对于实际应用,可以考虑使用更高效的算法如Hierholzer,但理解Fleury算法对于掌握贪心算法的应用场景和局限性非常有帮助。

欧拉路径问题及其解决方案在计算机科学和实际应用中都有着广泛的应用,掌握这些算法对于解决现实世界中的路径优化问题具有重要意义。

更多资源:

https://www.kdocs.cn/l/cvk0eoGYucWA

本文发表于【纪元A梦】!


http://www.hkcw.cn/article/VeTajsLApS.shtml

相关文章

【CF】Day73——Codeforces Round 887 (Div. 2) B (思维 + 模拟)

B. Fibonaccharsis 题目&#xff1a; 思路&#xff1a; 比C题有意思&#xff0c;但也么意思 对于这一题我们可以考虑最小的情况&#xff0c;即 f0 0&#xff0c;f1 1 时&#xff0c;然后看看什么时候会超过 n 的最大值&#xff0c;我们可以发现 k > 30 时就炸了&#xff…

工作流引擎-16-开源审批流项目之 整合Flowable官方的Rest包

工作流引擎系列 工作流引擎-00-流程引擎概览 工作流引擎-01-Activiti 是领先的轻量级、以 Java 为中心的开源 BPMN 引擎&#xff0c;支持现实世界的流程自动化需求 工作流引擎-02-BPM OA ERP 区别和联系 工作流引擎-03-聊一聊流程引擎 工作流引擎-04-流程引擎 activiti 优…

Windows+VSCode搭建小智(xiaozhi)开发环境

作为一名DIY达人&#xff0c;肯定不会错过最近很火的“小智AI聊天机器人”&#xff0c;网上教程非常丰富&#xff0c;初级玩家可以直接在乐鑫官方下载ESP-IDF安装包并经过简单的菜单式配置后&#xff0c;即可进行代码编译和烧录&#xff08;详见&#xff1a;Docs&#xff09;。…

《仿盒马》app开发技术分享-- 购物车业务逻辑完善(端云一体)

开发准备 之前我们已经实现了购物车相关的内容&#xff0c;实现了购物车数据列表的展示&#xff0c;但是我们结算订单之后我们的购物车列表并没有刷新&#xff0c;而且底部的状态栏并没有明显的数据展示来提醒用户&#xff0c;而且当我们在商品详情页添加新商品&#xff0c;底…

谷歌CEO皮查伊眼中的“下一代平台“与未来图景

目录 前言 一、从"能力秀"到"平台重构"&#xff1a;AI的"第二乐章" 二、"万物皆可创"&#xff1a;AI如何引爆每个人的创造力&#xff1f; 三、AI走出屏幕&#xff1a;XR眼镜与机器人的未来交响曲 四、Web不死&#xff0c;只是换了…

DDC Learning Record(2)

这些是 “UV 生成策略”&#xff0c;决定了 Houdini 如何分析模型空间&#xff0c;并分配 UV 坐标。它们只影响 UV 坐标的生成方式&#xff0c;不影响后续贴图的读取行为。 face得到的结果是&#xff1a; 每个面都被映射到了整张贴图&#xff08;[0,1]&#xff09;&#xff0c;…

MySQL数据库从0到1

目录 数据库概述 基本命令 查询命令 函数 表的操作 增删改数据和表结构 约束 事务 索引 视图 触发器 存储过程和函数 三范式 数据库概述 SQL语句的分类&#xff1a; DQL&#xff1a;查询语句&#xff0c;凡是select语句都是DQL。 DML&#xff1a;insert,delete,up…

STM32CubeDAC及DMA配置

STM32CubeDAC及DMA配置 一&#xff0c;问题1二&#xff0c;解决11&#xff0c;宏观思路CubeMX配置2&#xff0c;HAL_TIM_Base_Start(&htim6) 的作用1&#xff0c;作用1&#xff1a;使能TIM6的时钟并让它开始计数2&#xff0c;作用2&#xff1a;当 TIM6 溢出时&#xff0c;会…

c++ 赋值函数和拷贝构造函数的调用时机

测试代码&#xff1a; void testcopyAndFuzhi() {class Dog {private:string name;public:Dog(string myname) : name(myname) {}Dog(Dog& otherDog) {std::cout << "调用拷贝构造函数." << endl;this->name otherDog.name;}Dog& operator …

Python列表、字典、元组、集合

列表 list 基本格式&#xff1a;列表名 [元素1,元素2,元素3] 所有元素放在[ ]之中&#xff0c;元素之间用逗号","隔开&#xff0c;元素可以是不同的类型 列表的类型也是列表 li [1,"hello",2] print(type(li)) 列表是可迭代对象&#xff0c;可以用fo…

sctpscan:用于发现 SCTP 网络扫描器!全参数详细教程!Kali Linux教程!

简介 用于发现和安全的 SCTP 网络扫描器 sctpscan 是一款 SCTP 协议端口扫描工具&#xff0c;主要用于识别目标主机上开放的 SCTP&#xff08;Stream Control Transmission Protocol&#xff09;服务端口。 sctpscan 的主要功能是&#xff1a; 探测目标主机 SCTP 协议是否开…

力扣题解654:最大二叉树

一、题目内容 题目要求根据一个不重复的整数数组 nums 构建最大二叉树。最大二叉树的构建规则如下&#xff1a; 创建一个根节点&#xff0c;其值为 nums 中的最大值。递归地在最大值左边的子数组前缀上构建左子树。递归地在最大值右边的子数组后缀上构建右子树。返回由 nums 构…

车载诊断架构 --- DTC消抖参数(Trip Counter DTCConfirmLimit )

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 做到欲望极简,了解自己的真实欲望,不受外在潮流的影响,不盲从,不跟风。把自己的精力全部用在自己。一是去掉多余,凡事找规律,基础是诚信;二是…

75.解决当编辑完用户消息,确认重新生成AI回答时,已有的AI回答还是存在,并且新生成的回答并没有显示到气泡里bug

在上一个bug解决完后&#xff0c;又出来一个bug&#xff0c;我真是服了哈哈哈 新的bug是&#xff1a; 当编辑完用户消息&#xff0c;确认重新生成AI回答时&#xff0c;已有的AI回答还是存在&#xff0c;并且新生成的回答并没有显示到气泡里 出现这个bug的原因还是出现在rege…

C# 用户控件(User Control)详解:创建、使用与最佳实践

在C#应用程序开发中&#xff0c;用户控件&#xff08;User Control&#xff09;是一种强大的工具&#xff0c;它允许开发者将多个标准控件组合成一个可复用的自定义组件。无论是Windows Forms还是WPF&#xff0c;用户控件都能显著提高UI开发的效率&#xff0c;减少重复代码&…

SpringBoot-配置Spring MVC

一、Spring MVC回顾 Spring MVC是一种常用的Java Web框架&#xff0c;它提供了一种基于MVC模式的开发方式&#xff0c;可以方便地实现Web应用程序。在Spring MVC中&#xff0c;WebMvcConfigurer是一种常用的配置方式&#xff0c;可以允许我们自定义Spring MVC的行为&#xff0c…

Python训练营打卡 Day26

知识点回顾&#xff1a; 函数的定义变量作用域&#xff1a;局部变量和全局变量函数的参数类型&#xff1a;位置参数、默认参数、不定参数传递参数的手段&#xff1a;关键词参数传递参数的顺序&#xff1a;同时出现三种参数类型时 ——————————————————————…

【Delphi】接收windows文件夹中文件拖拽

本文根据EmailX45的视频文件&#xff0c;进行了优化改进&#xff0c;原文参见&#xff1a;Delphi: Drag and Drop Files from Explorer into TPanel / TMemo - YouTube 在Windows中&#xff0c;如果将选择的文件拖动到Delphi程序的控件上&#xff0c;有很多实现方法&#xff0c…

电脑的ip地址会自动变怎么办?原因解析和解决方法

在当今互联网时代&#xff0c;IP地址是每台联网设备的"身份证"&#xff0c;但很多用户都遇到过IP地址自动变化的情况。这种现象既可能发生在内网&#xff08;局域网&#xff09;环境中&#xff0c;也可能出现在外网&#xff08;公网&#xff09;连接中。要理解IP地址…

CppCon 2014 学习: C++11 in the Wild

介绍了三个现代 C 的实用工具或功能。下面是对每部分的解释和总结&#xff1a; 1. Auto() 宏 用途&#xff1a;在作用域结束时自动执行清理代码&#xff0c;类似于 RAII 的机制。功能&#xff1a;你可以定义一段代码&#xff0c;在作用域结束时自动执行&#xff08;不需要手动…