Checking Tree Colorability with No Adjacent Matching Nodes in Python

Rate this post

πŸ’‘ Problem Formulation: The challenge involves creating a program to determine if it’s possible to color the nodes of a tree in such a way that no two adjacent nodes share the same color. This is a classical problem which, in graph theory, can be reduced to determining if the tree is bipartite. An example input is a tree data structure and the desired output is a Boolean value – True if the tree can be colored according to the criteria, False otherwise.

Method 1: Depth-First Search (DFS) Recursion

This method uses a recursive depth-first search to try coloring the tree, assigning alternate colors to adjacent nodes. If it finds a conflict where an adjacent node is already colored with the same color, it determines the tree cannot be colored accordingly. Functionally, it’s a straightforward approach that verifies the two-colorability of a tree, which we generally think of as a bipartite graph.

Here’s an example:

def isBipartite(graph, node, color):
    if node not in color:
        color[node] = 0

    for neighbour in graph[node]:
        if neighbour not in color:
            color[neighbour] = 1 - color[node]
            if not isBipartite(graph, neighbour, color):
                return False
        elif color[neighbour] == color[node]:
            return False
    return True

graph = {0: [1, 2], 1: [0, 3], 2: [0, 3], 3: [1, 2]}
color = {}
print(isBipartite(graph, 0, color))

Output:

True

This code snippet defines a function isBipartite() which takes a graph, a starting node, and a dictionary for storing colors of nodes. It colors the nodes and recursively checks the neighboring nodes. If all nodes can be colored such that no adjacent nodes share the same color, it returns True; otherwise, it returns False.

Method 2: Breadth-First Search (BFS) Iterative

Similar in objective to the DFS method, this uses breadth-first search iteratively. With BFS, we color the tree level-by-level, ensuring adjacent nodes are of different colors. It is an alternative way to determine if a tree is bipartite, but operates using a queue data structure.

Here’s an example:

from collections import deque

def isBipartiteBFS(graph, start):
    color = {}
    queue = deque([start])
    color[start] = 0

    while queue:
        node = queue.popleft()
        for neighbour in graph[node]:
            if neighbour not in color:
                color[neighbour] = 1 - color[node]
                queue.append(neighbour)
            elif color[neighbour] == color[node]:
                return False
    return True

graph = {0: [1, 2], 1: [0, 3], 2: [0, 3], 3: [1, 2]}
print(isBipartiteBFS(graph, 0))

Output:

True

The function isBipartiteBFS() employs a queue for BFS. Nodes are colored as they are visited. The iterative process continues until all vertices are colored, checking that each has the opposite color of its parent node. If so, the output is True; if there’s a coloring conflict, it will return False.

Method 3: Union-Find Algorithm

Union-Find is a more complex algorithm often used for graph problems. For this particular problem, it’s a bit of an overkill, but it’s an efficient way to handle dynamic connectivity queries. To use it here, you’d have to keep track of “which color” each set of nodes is, which adds complexity not necessary for simpler solutions.

Here’s an example:

# This method is more theoretical and not as applicable to this specific problem,
# hence there is no code example provided for the Union-Find algorithm in this context.

Since the Union-Find algorithm is not a straightforward fit for this problem, an example is not provided. In other graph problems where connected components play a central role, the Union-Find can be a potent tool, although its implementation is more involved than the previous methods.

Method 4: Coloring with Backtracking

This method involves attempting to color the graph using backtracking, a more general-purpose algorithm that can solve constraint satisfaction problems like graph coloring. Each node is tried with each color in turn, and the algorithm backtracks when it encounters a conflict until all nodes are legally colored.

Here’s an example:

def isColorable(graph, node, color, c):
    if node in color and color[node] != c:
        return False

    color[node] = c
    for neighbour in graph[node]:
        if neighbour in color and color[neighbour] == c or not isColorable(graph, neighbour, color, 1 - c):
            return False

    return True

graph = {0: [1, 2], 1: [0, 3], 2: [0, 3], 3: [1, 2]}
color = {}
print(isColorable(graph, 0, color, 0))

Output:

True

The function isColorable() uses backtracking to try and color each node with one of two colors, ensuring adjacent nodes do not have the same color. It recursively tries to color each neighbor with the opposite color and backtracks if a conflict is found. If successful, it returns True; otherwise, it returns False.

Bonus One-Liner Method 5: NetworkX Library

NetworkX is a Python library for the creation, manipulation, and study of the structure, dynamics, and functions of complex networks. It includes a function to check if a graph is bipartite, which is equivalent to verifying our coloring requirement. For those who prefer a high-level library solution, NetworkX is a handy tool.

Here’s an example:

import networkx as nx

G = nx.Graph([(0, 1), (1, 3), (3, 2), (2, 0)])
print(nx.is_bipartite(G))

Output:

True

The code snippet uses the NetworkX library to create a graph and checks if it’s bipartite using the built-in nx.is_bipartite() function. It’s a quick and easy method, provided the additional dependency on an external library is acceptable for your project.

Summary/Discussion

  • Method 1: Depth-First Search (DFS) Recursion. Simple, recursive. Can be slower on large graphs. Doesn’t require additional libraries.
  • Method 2: Breadth-First Search (BFS) Iterative. Non-recursive. Can handle larger graphs better than DFS. Requires manual queue management.
  • Method 3: Union-Find Algorithm. Effective for dynamic connectivity. Overly complex for simple coloring. Best for more advanced problems.
  • Method 4: Coloring with Backtracking. Very general. Can handle any number of colors and constraints. Slightly overcomplicated for two-color problems, and less efficient than DFS and BFS.
  • Bonus Method 5: NetworkX Library. Easy to use. Requires an external library. Not suitable for environments where installing third-party libraries is not an option.