0

Given a graph the root node of which is defined by a Node object:

class Node:

    def __init__(self, val = 0, neighbors = None):
        self.val = val
        self.neighbors = neighbors if neighbors is not None else []
    
    def __eq__(self, other) -> bool:
        
        if isinstance(other, self.__class__):
            return self.__repr__() == other.__repr__()
    
    def __repr__(self):
        return f"Node(val: {self.val}, neighbors: {self.neighbors})"
    
    def __str__(self):
        return self.__repr__()

The Graph class is defined as below, which uses the Node class above to construct itself from an adjacency list

class Graph:
    
    def __init__(self, adj_list=[]):
        
        self.root = adj_list or self.make_graph(adj_list)
        
        
    def __repr__(self):
        return str(self.root)
    
    def __str__(self):
        return self.__repr__()
    
    def __eq__(self, other):
    
        if isinstance(other, self.__class__):
            return other.root == self.root
        
        return False
    
    
    def make_graph(self, adj_list) -> Node:

        # Ref: https://stackoverflow.com/a/72499884/16378872
        nodes = [Node(i + 1) for i in range(len(adj_list))]
        
        for i, neighbors in enumerate(adj_list):
            nodes[i].neighbors = [nodes[j-1] for j in neighbors]
            
        return nodes[0]

For example the adjacency list [[2,4],[1,3],[2,4],[1,3]] is transformed to a Graph as below

graph = Graph([[2,4],[1,3],[2,4],[1,3]])

print(graph)

Node(val: 1, neighbors: [Node(val: 2, neighbors: [Node(val: 1, neighbors: [...]), Node(val: 3, neighbors: [Node(val: 2, neighbors: [...]), Node(val: 4, neighbors: [Node(val: 1, neighbors: [...]), Node(val: 3, neighbors: [...])])])]), Node(val: 4, neighbors: [Node(val: 1, neighbors: [...]), Node(val: 3, neighbors: [Node(val: 2, neighbors: [Node(val: 1, neighbors: [...]), Node(val: 3, neighbors: [...])]), Node(val: 4, neighbors: [...])])])])

Now if I have 2 graphs:

graph1 = Graph([[2,4],[1,3],[2,4],[1,3]])
graph2 = Graph([[2,4],[1,3],[2,4],[1,3]])

print(graph1 == graph2)

True

I can check their equality by comparing the return values of Node.__repr__() of both these graph objects graph1 and graph2 which essentially is done via the __eq__() special method of Graph that is comparing the equality of the root nodes of both the graphs, hence using the __repr__() special method of Node as explained above.

The __repr__ method truncates the deeply nested neighbors in the output as [...], however there could be some nodes deep inside that do not have equal values of Node.val, hence making the comparison result unreliable by this method.

My concern is, is there a better and more reliable way to do this equality test instead of just comparing the __repr__() of both the graph's root nodes?.

SigKill
  • 87
  • 5
  • If you change the order of edges in the list, the result of the same graph will be `False`. – Mechanic Pig Jun 06 '22 at 10:29
  • @mkrieger1, the need is to compare the Graph generated using the adjacency list. Comparing the list is way easier and I wish I could get away with that :) – SigKill Jun 06 '22 at 10:37
  • @MechanicPig, yes that is correct. However, it is guaranteed that the source lists that are being compared will always have the edges in the same order. However a solution that is resistant to the different orders of the edges in the adjacency list will be ideal. – SigKill Jun 06 '22 at 10:40
  • 1
    *"the need is to compare the Graph generated using the adjacency list"*: but your generated graph has the adjacency list as an attribute! If you don't want to compare by adjacency list, then you should not store the adjacency list as attribute either. – trincot Jun 06 '22 at 11:29
  • @trincot In the class `Graph` I was populating a class attribute `root` as type `Node` that is generated using the adjacency list, which was then used for all comparisons. In the final code I may as well remove the adjacency list attribute. I have edited the question to remove it anyways. – SigKill Jun 06 '22 at 12:04

1 Answers1

2

You can implement a depth-first traversal and compare nodes by value and degree. Mark nodes as visited to avoid they are traversed a second time:

    def __eq__(self, other) -> bool:
        visited = set()
        
        def dfs(a, b):
            if a.val != b.val or len(a.neighbors) != len(b.neighbors):
                return False
            if a.val in visited:
                return True
            visited.add(a.val)
            return all(dfs(*pair) for pair in zip(a.neighbors, b.neighbors))
            
        return isinstance(other, self.__class__) and dfs(self, other)

This code assumes that a node's value uniquely identifies the node within the same graph.

This also assumes that the graph is connected, otherwise components that are disconnected from the root will not be compared.

trincot
  • 317,000
  • 35
  • 244
  • 286