无向图割点、最小生成树和欧拉回路的寻找

看数据结构与算法,顺带实现了所有的数据结构,包括堆和无向图,拿来测试用

自己实现的heap,可以使用python的heapq代替
但是 python 的 heapq使用的是 []结构,直接比较元素的大小进行堆排序 参考heapq.py文件中heappush和_siftdown函数的实现
1 当元素是字典结构时候,需要重新定义元素的比较运算。重写函数_siftdown
2 当元素是新建的类的对象时,重新实现 lt gt le ge eq __ne__比较函数即可。可以参考 tornado/asyncio/events.py TimerHandle

# -*- encoding: utf-8 -*-

from graphviz import Digraph
from heap import heap


class NodeWithoutDirection(object):
    def __init__(self, node_name: str):
        self.name = node_name
        self.edges = set()

    def add_edge(self, edge):
        assert edge not in self.edges, "边已经存在"
        self.edges.add(edge)

    def sub_edge(self, edge):
        assert edge in self.edges, "节点没有该边"
        self.edges = self.edges - {edge}

    def get_edge(self, node):
        for edge in self.edges:
            if edge.nodes == {self, node}:
                return edge
        assert False, "与该节点没有边"


class EdgeWithoutDirection(object):
    def __init__(self, node1: NodeWithoutDirection, node2: NodeWithoutDirection, weight):
        self.nodes = {node1, node2}
        self.weight = weight

    def get_another_node(self, node: NodeWithoutDirection):
        assert node in self.nodes, "节点不属于边"
        # (self.nodes - {node}).pop()
        node1, node2 = self.nodes
        if node == node1:
            return node2
        return node1


def pre_order(start_node: NodeWithoutDirection, node_status: dict, pre_order_list: list):
    """
    先序遍历
    :param start_node: 开始节点
    :param node_status: 所有节点的状态
    :param pre_order_list: 先序遍历节点的顺序
    :return: None
    """
    pre_order_list.append(start_node)
    for edge in start_node.edges:
        another_node = edge.get_another_node(node=start_node)
        if not node_status[another_node]["num"]:
            node_status[start_node]["direction_nodes"].append(another_node)
            node_status[another_node]["num"] = len(pre_order_list) + 1
            node_status[another_node]["father"] = start_node
            pre_order(start_node=another_node, node_status=node_status, pre_order_list=pre_order_list)
        elif start_node not in node_status[another_node]["direction_nodes"] and \
                node_status[another_node]["num"] < node_status[start_node]["num"]:
            node_status[start_node]["anti_direction_nodes"].append(another_node)


class GraphWithoutDirection(object):
    def __init__(self):
        self.nodes = []
        self.node_names = set()
        self.node_name_2_node_index = {}
        self.edges = []

    def add_node(self, node_name: str, near_node_names: list, weights: list = None):
        """
        添加节点
        :param node_name: 新节点名
        :param near_node_names: 新节点相邻的节点名列表
        :param weights: 对应的权重值
        :return: None
        """
        if weights is None:
            weights = [1 for _ in near_node_names]
        assert len(near_node_names) == len(weights), "参数个数不一致"
        node_names = [node_name] + near_node_names
        weights = [0] + weights
        for index, name in enumerate(node_names):
            if name not in self.node_names:
                new_node = NodeWithoutDirection(node_name=name)
                self.node_names.add(name)
                self.nodes.append(new_node)
                self.node_name_2_node_index[name] = len(self.nodes) - 1
            if name != node_name:
                node1 = self.nodes[self.node_name_2_node_index[node_name]]
                node2 = self.nodes[self.node_name_2_node_index[name]]
                new_edge = EdgeWithoutDirection(node1=node1, node2=node2, weight=weights[index])
                self.edges.append(new_edge)
                node1.add_edge(edge=new_edge)

    def show(self, file_name):
        """
        将无向图转化为pdf 好检查结果
        :param file_name: pdf名字
        :return: None
        """
        d = Digraph(filename=file_name, directory="./pdf_data")
        d.clear()
        edges_known = []
        # 将所有节点 画出
        for name in self.node_names:
            d.node(name=name, label=name)
        for node in self.nodes:
            for edge in node.edges:
                node1, node2 = edge.nodes
                if edge.nodes not in edges_known:
                    edges_known.append(edge.nodes)
                    d.edge(tail_name=node1.name, head_name=node2.name, label=str(edge.weight))
        d.view()

    def min_span_tree_by_prim(self):
        """
        最少生成树
        所有已知点 和 未知点的最小权重边加到图中,直到所有的节点都加到图中为止
        :return: None
        """
        new_graph = GraphWithoutDirection()
        node_names_known = {min(self.node_names)}
        while len(node_names_known) != len(self.node_names):
            all_min_weight_edge = None
            for node_name_known in node_names_known:
                node_known = self.nodes[self.node_name_2_node_index[node_name_known]]
                min_weight_edge = None
                for edge in node_known.edges:
                    another_node = (edge.nodes - {node_known}).pop()
                    if another_node.name not in node_names_known and (min_weight_edge is None or min_weight_edge.weight > edge.weight):
                        min_weight_edge = edge
                if min_weight_edge is None:
                    continue
                if all_min_weight_edge is None or all_min_weight_edge.weight > min_weight_edge.weight:
                    all_min_weight_edge = min_weight_edge
            all_min_weight_edge_node_names = {node.name for node in all_min_weight_edge.nodes}
            node_names_known = node_names_known.union(all_min_weight_edge_node_names)
            node_name_1 = all_min_weight_edge_node_names.pop()
            node_name_2 = all_min_weight_edge_node_names.pop()
            weight = all_min_weight_edge.weight
            new_graph.add_node(node_name=node_name_1, near_node_names=[node_name_2], weights=[weight])
            new_graph.add_node(node_name=node_name_2, near_node_names=[node_name_1], weights=[weight])
        new_graph.show(file_name="Prim最小生成树")

    def min_span_tree_by_kruskal(self):
        """
        最小生成树  寻找最小权重的边加到图中
        条件就是 不能形成环,直到所有节点都被连接停止
        未使用 Find/Join 模型
        :return: None
        """
        new_graph = GraphWithoutDirection()
        edge_heap = heap.Heap(len(self.edges))
        edge_heap_list = []
        for edge in self.edges:
            value = edge.weight
            heap_node = heap.HeapNode(value=value, info=edge)
            edge_heap_list.append(heap_node)
        edge_heap.build_heap(edge_heap_list)
        node_names_known = [i for i in range(len(self.node_names))]
        step = 0
        while edge_heap and step < len(self.node_names) - 1:
            edge = edge_heap.pop().info
            node_1, node_2 = edge.nodes
            index_1 = self.node_name_2_node_index[node_1.name]
            index_2 = self.node_name_2_node_index[node_2.name]
            group_id_1, group_id_2 = node_names_known[index_1], node_names_known[index_2]
            if group_id_1 != group_id_2:
                step += 1
                node_name_1 = node_1.name
                node_name_2 = node_2.name
                weight = edge.weight
                new_graph.add_node(node_name=node_name_1, near_node_names=[node_name_2], weights=[weight])
                new_graph.add_node(node_name=node_name_2, near_node_names=[node_name_1], weights=[weight])
                all_index = sorted([group_id_1, group_id_2])
                for index, group_id in enumerate(node_names_known):
                    if group_id == all_index[1]:
                        node_names_known[index] = all_index[0]
        new_graph.show(file_name="Kruskal最小生成树")

    def depth_first_search(self, start_node: NodeWithoutDirection = None, connect_check: bool = True):
        """
        先序遍历和后续遍历 所有节点 计算 Num值和Low值
        :param start_node: 开始节点
        :param connect_check: 是否检查图的连通性(默认是检查的,但是寻找欧拉路径是可以不连通的)
        :return: 返回所有节点的信息 和 截点的列表
        """
        status = {}
        pre_order_node_list = []
        cut_vertex_list = []
        if start_node is None:
            start_node = self.nodes[0]
        for node in self.nodes:
            status[node] = {"num": 0 if node != start_node else 1,
                            "low": len(self.nodes) if node != start_node else 1,
                            "direction_nodes": [],
                            "anti_direction_nodes": [],
                            "father": None
                            }
        pre_order(start_node=start_node, node_status=status, pre_order_list=pre_order_node_list)
        if connect_check:
            assert len(pre_order_node_list) == len(self.nodes), "图不连通"
        for node in pre_order_node_list[::-1]:
            anti_direction_nodes = status[node].get("anti_direction_nodes")
            direction_nodes = status[node].get("direction_nodes")
            if anti_direction_nodes:
                for anti_direction_node in anti_direction_nodes:
                    if status[anti_direction_node].get("num") < status[node]["low"]:
                        status[node]["low"] = status[anti_direction_node].get("num")
            elif not direction_nodes:
                status[node]["low"] = status[node]["num"]
                continue
            if direction_nodes:
                for direction_node in direction_nodes:
                    if status[direction_node]["low"] < status[node]["low"]:
                        status[node]["low"] = status[direction_node]["low"]
            if node == pre_order_node_list[0]:
                cut_vertex_list += [node.name] if len(status[node]["direction_nodes"]) > 1 else []
                continue
            for direction_node in direction_nodes:
                if status[direction_node]["low"] >= status[node]["num"]:
                    cut_vertex_list.append(node.name)
                    break
        return status, cut_vertex_list

    def find_cut_vertex(self):
        """
        借助 深度优先搜索 查询 割点
        :return: 割点列表
        """
        _, cut_vertex_list = self.depth_first_search()
        return cut_vertex_list

    def check_euler_circuit(self):
        """
        检验 欧拉回路是否存在 三种情况
        1. 节点的度 全部为偶数 True
        2. 节点的度 为 奇数的个数 大于 2 False
        3. 节点的度 为 奇数的个数 等于 2 Half True(严格意义上不是欧拉回路但是可以经过所有的边,但是回不到初始的节点)
        :return: bool, None or List
        """
        edges_num = [len(node.edges) % 2 for node in self.nodes]
        odd_edges_nodes = []
        for index, edge_num in enumerate(edges_num):
            if edge_num == 1:
                odd_edges_nodes.append(self.nodes[index])
            if len(odd_edges_nodes) > 2:
                return False, None
        if not odd_edges_nodes:
            return True, None
        if len(odd_edges_nodes) == 2:
            return True, odd_edges_nodes

    def find_euler_circuit(self):
        """
        寻找欧拉回路
        :return: 欧拉回路的节点名
        """
        status, nodes = self.check_euler_circuit()
        if not status:
            return []
        if not nodes:
            start_node = self.nodes[0]
            return self._find_euler_circuit(start=start_node)
        base_path = self._find_path_by_topology(start=nodes[0], end=nodes[1])
        return self._find_euler_circuit(start=nodes[0], euler_circuit=base_path)

    def _find_euler_circuit(self, start: NodeWithoutDirection, euler_circuit: list = None):
        """
        :param start: 从哪个节点开始 寻找欧拉路径。
        如果所有节点的度都是 偶数 那么 开始的节点随意
        但是 仅有两个节点的度是 奇数 那么 开始的节点必须是这两个节点的其中一个
        :param euler_circuit: 基础的欧拉回路
        如果所有节点的度都是 偶数 那么 开始的欧拉回路的列表是空
        但是 仅有两个节点的度是 奇数 那么 开始的欧拉回路是这两个节点的通路路径
        :return: 欧拉回路
        """
        if euler_circuit is None:
            euler_circuit = []
        index = 0
        while index >= 0:
            circle = self._find_path_by_topology(start=start)
            euler_circuit = euler_circuit[:index] + circle + euler_circuit[index + 1:]
            for i in range(index, len(euler_circuit) + 1):
                if i == len(euler_circuit):
                    index = -1
                elif len(euler_circuit[i].edges) >= 2:
                    index = i
                    start = euler_circuit[index]
                    break
        return euler_circuit

    def _find_path_by_topology(self, start: NodeWithoutDirection, end: NodeWithoutDirection = None):
        """
        寻找从开始节点到结束节点的路径
        当结束节点为空时 寻找开始节点到本身的闭环路径
        并将路径所经过的边 在图中删除 防止重复使用
        :param start: 开始节点
        :param end: 结束节点
        :return: 路径
        """
        status, _ = self.depth_first_search(start_node=start, connect_check=False)
        target_node, target_num = None, 1
        for node, info in status.items():
            node_num = info.get("num")
            node_low = info.get("low")
            if end and node == end:
                target_node = node
                break
            if node_num > target_num and node_low == 1:
                target_node = node
                target_num = node_num
        path = []
        last_node = target_node
        while target_node != start:
            path.append(target_node)
            father_node = status[target_node].get("father")
            edge = target_node.get_edge(node=father_node)
            target_node.sub_edge(edge=edge)
            edge = father_node.get_edge(node=target_node)
            father_node.sub_edge(edge=edge)
            target_node = father_node
        path.append(start)
        path = list(reversed(path))
        if end is None:
            path.append(start)
            edge = last_node.get_edge(node=start)
            last_node.sub_edge(edge=edge)
            edge = start.get_edge(node=last_node)
            start.sub_edge(edge=edge)
        return path
# -*- encoding: utf-8 -*-
import random

from graphviz import Digraph


class HeapNode(object):
    def __init__(self, value, info):
        self.value = value
        self.info = info


class Heap(object):
    def __init__(self, cap):
        self.cap = cap
        self.size = 0
        self.heap = [None]

    def show(self, file_name=None):
        d = Digraph(filename=file_name, directory="./pdf_data")
        d.clear()
        node_name = []
        for node in self.heap:
            if node is None:
                node_name.append(None)
                continue
            name = str(id(node))
            d.node(name=name, label=str(node.value))
            node_name.append(name)
        max_father_index = self.size // 2
        for father_index in range(1, max_father_index + 1):
            left_son_index = father_index * 2
            right_son_index = father_index * 2 + 1
            if left_son_index <= self.size:
                d.edge(head_name=node_name[left_son_index], tail_name=node_name[father_index])
            if right_son_index <= self.size:
                d.edge(head_name=node_name[right_son_index], tail_name=node_name[father_index])
        d.view()

    def insert(self, node: HeapNode):
        self.heap.append(None)
        self.size += 1
        index = self.size
        while index > 1:
            father_index = index // 2
            if self.heap[father_index].value > node.value:
                self.heap[index] = self.heap[father_index]
                index = father_index
            else:
                break
        self.heap[index] = node
        return 1

    def pop(self):
        assert self.size > 0, "空堆"
        first_node = self.heap[1]
        last_node = self.heap.pop()
        self.size -= 1
        if first_node == last_node:
            return first_node
        index = 1
        while index <= self.size // 2:
            left_son = self.heap[index * 2]
            father_index = index
            right_son_index = index * 2 + 1
            self.heap[index] = left_son
            if left_son.value < last_node.value:
                index *= 2
            if right_son_index <= self.size and self.heap[right_son_index].value < last_node.value and self.heap[right_son_index].value < self.heap[father_index].value:
                self.heap[father_index] = self.heap[right_son_index]
                index = right_son_index
            if index == father_index:
                break
        self.heap[index] = last_node
        return first_node

    def get_value(self, key):
        index = self.find_node_index(key=key)
        return self.heap[index].value

    def swap_two_node(self, index1, index2):
        self.heap[index1], self.heap[index2] = self.heap[index2], self.heap[index1]

    def keep_father_lt_son(self, father_index):
        """
        下滤 操作
        :param father_index: 父节点下标
        :return: None
        """
        if father_index > self.size // 2:
            return
        left_index = father_index * 2
        right_index = father_index * 2 + 1
        index = father_index
        if self.heap[left_index].value < self.heap[father_index].value:
            index = left_index
        if right_index <= self.size and self.heap[right_index].value < self.heap[father_index].value and self.heap[right_index].value < self.heap[left_index].value:
            index = right_index
        if index == father_index:
            return
        self.swap_two_node(index1=index, index2=father_index)
        self.keep_father_lt_son(father_index=index)

    def build_heap(self, n: list):
        assert len(n) <= self.cap, "堆超限"
        self.heap.extend(n)
        self.size = len(n)
        father_index = self.size // 2
        for index in range(father_index, 0, -1):
            self.keep_father_lt_son(father_index=index)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
几何\ 多边形 多边形切割 浮点函数 几何公式 面积 球面 三角形 三维几何 凸包(graham) 网格(pick) 圆 整数函数 注意 结构\ 并查集 并查集扩展(friend_enemy) 堆(binary) 堆(mapped) 矩形切割 线段树 线段树扩展 线段树应用 子段和 子阵和 其他\ 大数(整数类封装) 分数 矩阵 线性方程组(gauss) 日期 线性相关 数论\ 阶乘最后非零位 模线性方程(组) 质数表 质数随机判定(miller_rabin) 质因数分解 最大公约数欧拉函数 数值计算\ 定积分计算(Romberg) 多项式求根(牛顿法) 周期性方程(追赶法) 图论_NP搜索\ 最大团(n小于64) 最大团 图论_连通性\ 无向图关键边(dfs邻接阵形式) 无向图关键点(dfs邻接阵形式) 无向图块(bfs邻接阵形式) 无向图连通分支(bfs邻接阵形式) 无向图连通分支(dfs邻接阵形式) 有向图强连通分支(bfs邻接阵形式) 有向图强连通分支(dfs邻接阵形式) 有向图最小点基(邻接阵形式) 图论_匹配\ 二分图最大匹配(hungary邻接表形式) 二分图最大匹配(hungary邻接阵形式) 二分图最大匹配(hungary邻接表形式,邻接阵接口) 二分图最大匹配(hungary正向表形式) 二分图最佳匹配(kuhn_munkras邻接阵形式) 一般图最大匹配(邻接表形式) 一般图最大匹配(邻接阵形式) 一般图最大匹配(正向表形式) 一般图匹配(邻接表形式,邻接阵接口) 图论_网络流\ 上下界最大流(邻接阵形式) 上下界最小流(邻接阵形式) 上下界最大流(邻接表形式) 上下界最小流(邻接表形式) 最大流(邻接阵形式) 最大流(邻接表形式) 最大流(邻接表形式,邻接阵接口) 最大流无流量(邻接阵形式) 最小费用最大流(邻接阵形式) 图论_应用\ 欧拉回路(邻接阵形式) 前序表转化 树的优化算法 拓扑排序(邻接阵形式) 最佳边割集 最佳顶点割集 最小边割集 最小顶点割集 最小路径覆盖 图论_最短路径\ 最短路径(单源bellman_ford邻接阵形式) 最短路径(单源dijkstra邻接阵形式) 最短路径(单源dijkstra_bfs邻接表形式) 最短路径(单源dijkstra_bfs正向表形式) 最短路径(单源dijkstra+binary_heap邻接表形式) 最短路径(单源dijkstra+binary_heap正向表形式) 最短路径(单源dijkstra+mapped_heap邻接表形式) 最短路径(单源dijkstra+mapped_heap正向表形式) 最短路径(多源floyd_warshall邻接阵形式) 图论_支撑树\ 最小生成树(kruskal邻接表形式) 最小生成树(kruskal正向表形式) 最小生成树(prim邻接阵形式) 最小生成树(prim+binary_heap邻接表形式) 最小生成树(prim+binary_heap正向表形式) 最小生成树(prim+mapped_heap邻接表形式) 最小生成树(prim+mapped_heap正向表形式) 最小树形图(邻接阵形式) 应用\ joseph模拟 N皇后构造解 布尔母函数 第k元素 幻方构造 模式匹配(kmp) 逆序对数 字符串最小表示 最长公共单调子序列 最长子序列 最大子串匹配 最大子段和 最大子阵和 组合\ 排列组合生成 生成gray码 置换(polya) 字典序全排列 字典序组合 组合公式
构造欧拉回路的整数规划模型如下: 假设有一个无向图 $G=(V,E)$,其中 $V$ 表示节点集合,$E$ 表示边集合。设 $x_{ij}$ 表示从节点 $i$ 到节点 $j$ 的边的数量,$y_i$ 表示节点 $i$ 的度数。则整数规划模型可以表示为: $$ \begin{aligned} &\text{maximize} && 0\\ &\text{subject to} && \sum_{j\in V} x_{ij} - \sum_{j\in V} x_{ji} = 0, \quad \forall i\in V\\ &&& y_i = \sum_{j\in V} x_{ij}, \quad \forall i\in V\\ &&& \sum_{i,j\in V} x_{ij} = |E|\\ &&& x_{ij} \in \{0,1\}, \quad \forall i,j\in V\\ &&& y_i \in \{0,2\}, \quad \forall i\in V\\ \end{aligned} $$ 其中第一个约束条件表示节点 $i$ 的入度和出度相等,第二个约束条件表示节点 $i$ 的度数为其相邻边的数量之和,第三个约束条件表示所有边都必须被遍历,第四个和第五个约束条件是整数规划的限制条件。 Python 可以使用 PuLP 模块来实现整数规划求解: ```python from pulp import * def euler_circuit(edges): # 获取所有的节点 nodes = set() for a, b in edges: nodes.add(a) nodes.add(b) n = len(nodes) # 创建整数规划问题 prob = LpProblem('Euler Circuit', LpMaximize) # 创建变量 x = {} y = {} for i in nodes: for j in nodes: if i != j: x[i, j] = LpVariable(f'x_{i}_{j}', 0, 1, LpInteger) y[i] = LpVariable(f'y_{i}', 0, 2, LpInteger) # 创建目标函数 prob += 0 # 添加约束 for i in nodes: prob += lpSum(x[i, j] for j in nodes if i != j) - lpSum(x[j, i] for j in nodes if i != j) == 0 prob += y[i] == lpSum(x[i, j] for j in nodes if i != j) prob += lpSum(x[i, j] for i in nodes for j in nodes if i != j) == len(edges) # 求解 prob.solve() # 获取结果 circuit = [] for i in nodes: for j in nodes: if i != j and value(x[i, j]) == 1: circuit.append((i, j)) return circuit ``` 其中 `edges` 是一个由边组成的列表,每条边是一个二元组 `(a, b)` 表示从节点 `a` 到节点 `b` 有一条边。函数返回一个欧拉回路,也是一个由边组成的列表。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值