# SPDX-License-Identifier: GPL-2.0-or-later __author__ = "Nutti " __status__ = "production" __version__ = "6.6" __date__ = "22 Apr 2022" class Node: def __init__(self, key, value=None): self.key = key self.value = value self.edges = [] def degree(self): return len(self.edges) def connected_nodes(self): return [e.other(self) for e in self.edges] class Edge: def __init__(self, node_1, node_2): self.node_1 = node_1 self.node_2 = node_2 def other(self, node): if self.node_1 == node and self.node_2 == node: raise RuntimeError("Loop edge in {} is not supported." .format(node.key)) if node not in (self.node_1, self.node_2): raise RuntimeError("Node {} does not belong to this edge." .format(node.key)) if self.node_1 == node: return self.node_2 return self.node_1 class Graph: def __init__(self): self.edges = [] self.nodes = {} def add_node(self, node): if node.key in self.nodes: raise RuntimeError("Node '{}' is already registered." .format(node.key)) self.nodes[node.key] = node def add_edge(self, node_1, node_2): if node_1.key not in self.nodes: raise RuntimeError("Node '{}' is not registered." .format(node_1.key)) if node_2.key not in self.nodes: raise RuntimeError("Node '{}' is not registered." .format(node_2.key)) edge = Edge(node_1, node_2) self.edges.append(edge) node_1.edges.append(edge) node_2.edges.append(edge) def get_node(self, key): return self.nodes[key] def dump_graph(graph): print("=== Node ===") for _, node in graph.nodes.items(): print("Key: {}, Value {}".format(node.key, node.value)) print("=== Edge ===") for edge in graph.edges: print("{} - {}".format(edge.node_1.key, edge.node_2.key)) # VF2 algorithm # Ref: https://stackoverflow.com/questions/8176298/ # vf2-algorithm-steps-with-example # Ref: https://github.com/satemochi/saaaaah/blob/master/geometric_misc/ # isomorph/vf2/vf2.py def graph_is_isomorphic(graph_1, graph_2): def is_iso(pairs, matching_node, new_node): # Algorithm: # 1. The degree is same (It's faster). # 2. The connected node is same. if matching_node.degree() != new_node.degree(): return False matching_connected = [c.key for c in matching_node.connected_nodes()] new_connected = [c.key for c in new_node.connected_nodes()] for p in pairs: n1 = p[0] n2 = p[1] if n1 in matching_connected and n2 not in new_connected: return False if n1 not in matching_connected and n2 in new_connected: return False return True def dfs(graph_1, graph_2): def generate_pair(g1, g2, pairs): remove_1 = [p[0] for p in pairs] remove_2 = [p[1] for p in pairs] keys_1 = sorted(list(set(g1.nodes.keys()) - set(remove_1))) keys_2 = sorted(list(set(g2.nodes.keys()) - set(remove_2))) for k1 in keys_1: for k2 in keys_2: yield (k1, k2) pairs = [] stack = [generate_pair(graph_1, graph_2, pairs)] while stack: try: k1, k2 = next(stack[-1]) n1 = graph_1.get_node(k1) n2 = graph_2.get_node(k2) if is_iso(pairs, n1, n2): pairs.append([k1, k2]) stack.append(generate_pair(graph_1, graph_2, pairs)) if len(pairs) == len(graph_1.nodes): return True, pairs except StopIteration: stack.pop() diff = len(pairs) - len(stack) for _ in range(diff): pairs.pop() return False, [] # First, check simple condition. if len(graph_1.nodes) != len(graph_2.nodes): return False, {} if len(graph_1.edges) != len(graph_2.edges): return False, {} is_isomorphic, pairs = dfs(graph_1, graph_2) node_pairs = {} for pair in pairs: n1 = pair[0] n2 = pair[1] node_1 = graph_1.get_node(n1) node_2 = graph_2.get_node(n2) node_pairs[node_1] = node_2 return is_isomorphic, node_pairs