Below the Radar
Below the Radar

Reputation: 7635

Applying the 4 color theorem to list of neighbor polygons stocked in a graph array

I want to apply the 4 color theorem ie. each neighbor polygons on a map should have a different color. The theorem state that only 4 colors is needed for any kind of map.

As input I have an array of polygon containing id and color id and a graph array of adjacent polygons. As output I want to associate a color id between 0-3 (or at maximum 0-4) to each polygons ensuring that adjacent polygons have different color id.

I have the following code (from this question) that returns a different color id for each neighboor, but it does not ensure that the minimum number of colors is used.

#array of input polygon tuples (id, color_id) color_id is None at beginning
input_polygons = [(0, None), (1, None), (2, None), (3, None), (4, None), (5, None), (6, None), (7, None), (8, None)]

#graph array of neighbors ids
adjacent_polygons_graph = [[0, 1], [0, 2], [0, 3], [0, 4], [0, 5], [1, 0], [2, 0], [2, 3], [3, 0], [3, 2], [3, 6], [3, 8], [4, 0], [4, 5], [4, 6], [5, 0], [5, 4], [6, 3], [6, 4], [6, 7], [7, 6], [8, 3]]

colored = []
for polygon in input_polygons:
    nbrs = [second for first, second in adjacent_polygons_graph if first == polygon[0]]
    used_colors = []
    for nbr in nbrs:
        used_colors += [second for first, second in colored if first == nbr]
    polygon[1]=[color for color in range(10) if color not in used_colors][0]
    colored.append(polygon)

I would like the script to reduce at maximum the number of colors (4 or 5) using brute force loop or other method.

Upvotes: 4

Views: 2931

Answers (1)

PM 2Ring
PM 2Ring

Reputation: 55489

Here is the graph coloring code extracted from the code in my math.stackexchange answer. That code is admittedly a little complicated because it uses Knuth's Algorithm X for two tasks: solving the tromino packing problem, and then solving the coloring problem for each packing problem solution.

This code finds 24 3-color solutions or 756 4-color for your sample data in less than 1 second. There are actually more solutions, but I arbitrarily set the colors for the first 2 nodes to reduce the total number of solutions, and to speed up the searching algorithm a little.

''' Knuth's Algorithm X for the exact cover problem, 
    using dicts instead of doubly linked circular lists.
    Written by Ali Assaf

    From http://www.cs.mcgill.ca/~aassaf9/python/algorithm_x.html

    and http://www.cs.mcgill.ca/~aassaf9/python/sudoku.txt

    Python 2 / 3 compatible

    Graph colouring version
'''

from __future__ import print_function
from itertools import product

def solve(X, Y, solution):
    if not X:
        yield list(solution)
    else:
        c = min(X, key=lambda c: len(X[c]))
        Xc = list(X[c])

        for r in Xc:
            solution.append(r)
            cols = select(X, Y, r)
            for s in solve(X, Y, solution):
                yield s
            deselect(X, Y, r, cols)
            solution.pop()

def select(X, Y, r):
    cols = []
    for j in Y[r]:
        for i in X[j]:
            for k in Y[i]:
                if k != j:
                    X[k].remove(i)
        cols.append(X.pop(j))
    return cols

def deselect(X, Y, r, cols):
    for j in reversed(Y[r]):
        X[j] = cols.pop()
        for i in X[j]:
            for k in Y[i]:
                if k != j:
                    X[k].add(i)

#Invert subset collection
def exact_cover(X, Y):
    newX = dict((j, set()) for j in X)
    for i, row in Y.items():
        for j in row:
            newX[j].add(i)
    return newX

def colour_map(nodes, edges, ncolours=4):
    colours = range(ncolours)

    #The edges that meet each node
    node_edges = dict((n, set()) for n in nodes)
    for e in edges:
        n0, n1 = e
        node_edges[n0].add(e)
        node_edges[n1].add(e)

    for n in nodes:
        node_edges[n] = list(node_edges[n])

    #Set to cover
    coloured_edges = list(product(colours, edges))
    X = nodes + coloured_edges

    #Subsets to cover X with
    Y = dict()
    #Primary rows
    for n in nodes:
        ne = node_edges[n]
        for c in colours:
            Y[(n, c)] = [n] + [(c, e) for e in ne]

    #Dummy rows
    for i, ce in enumerate(coloured_edges):
        Y[i] = [ce]

    X = exact_cover(X, Y)

    #Set first two nodes
    partial = [(nodes[0], 0), (nodes[1], 1)]
    for s in partial:
        select(X, Y, s)

    for s in solve(X, Y, []):
        s = partial + [u for u in s if not isinstance(u, int)]
        s.sort()
        yield s

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

input_polygons = [
    (0, None), (1, None), (2, None), (3, None), (4, None), 
    (5, None), (6, None), (7, None), (8, None),
]

#graph array of neighbors ids
adjacent_polygons_graph = [
    (0, 1), (0, 2), (0, 3), (0, 4), (0, 5), (1, 0), (2, 0), (2, 3), 
    (3, 0), (3, 2), (3, 6), (3, 8), (4, 0), (4, 5), (4, 6), 
    (5, 0), (5, 4), (6, 3), (6, 4), (6, 7), (7, 6), (8, 3),
]

# Extract the nodes list 
nodes = [t[0] for t in input_polygons]

# Get an iterator of all solutions with 3 colours
all_solutions = colour_map(nodes, adjacent_polygons_graph, ncolours=3)

# Print all solutions
for count, solution in enumerate(all_solutions, start=1):
    print('%2d: %s' % (count, solution))

output

 1: [(0, 0), (1, 1), (2, 1), (3, 2), (4, 2), (5, 1), (6, 1), (7, 0), (8, 1)]
 2: [(0, 0), (1, 1), (2, 1), (3, 2), (4, 2), (5, 1), (6, 1), (7, 0), (8, 0)]
 3: [(0, 0), (1, 1), (2, 1), (3, 2), (4, 2), (5, 1), (6, 1), (7, 2), (8, 1)]
 4: [(0, 0), (1, 1), (2, 1), (3, 2), (4, 2), (5, 1), (6, 1), (7, 2), (8, 0)]
 5: [(0, 0), (1, 1), (2, 1), (3, 2), (4, 2), (5, 1), (6, 0), (7, 2), (8, 0)]
 6: [(0, 0), (1, 1), (2, 1), (3, 2), (4, 2), (5, 1), (6, 0), (7, 1), (8, 0)]
 7: [(0, 0), (1, 1), (2, 1), (3, 2), (4, 2), (5, 1), (6, 0), (7, 2), (8, 1)]
 8: [(0, 0), (1, 1), (2, 1), (3, 2), (4, 2), (5, 1), (6, 0), (7, 1), (8, 1)]
 9: [(0, 0), (1, 1), (2, 1), (3, 2), (4, 1), (5, 2), (6, 0), (7, 1), (8, 1)]
10: [(0, 0), (1, 1), (2, 1), (3, 2), (4, 1), (5, 2), (6, 0), (7, 1), (8, 0)]
11: [(0, 0), (1, 1), (2, 1), (3, 2), (4, 1), (5, 2), (6, 0), (7, 2), (8, 0)]
12: [(0, 0), (1, 1), (2, 1), (3, 2), (4, 1), (5, 2), (6, 0), (7, 2), (8, 1)]
13: [(0, 0), (1, 1), (2, 2), (3, 1), (4, 1), (5, 2), (6, 2), (7, 0), (8, 0)]
14: [(0, 0), (1, 1), (2, 2), (3, 1), (4, 2), (5, 1), (6, 0), (7, 2), (8, 0)]
15: [(0, 0), (1, 1), (2, 2), (3, 1), (4, 1), (5, 2), (6, 0), (7, 2), (8, 0)]
16: [(0, 0), (1, 1), (2, 2), (3, 1), (4, 2), (5, 1), (6, 0), (7, 1), (8, 0)]
17: [(0, 0), (1, 1), (2, 2), (3, 1), (4, 1), (5, 2), (6, 0), (7, 1), (8, 0)]
18: [(0, 0), (1, 1), (2, 2), (3, 1), (4, 1), (5, 2), (6, 2), (7, 1), (8, 0)]
19: [(0, 0), (1, 1), (2, 2), (3, 1), (4, 1), (5, 2), (6, 2), (7, 1), (8, 2)]
20: [(0, 0), (1, 1), (2, 2), (3, 1), (4, 1), (5, 2), (6, 2), (7, 0), (8, 2)]
21: [(0, 0), (1, 1), (2, 2), (3, 1), (4, 2), (5, 1), (6, 0), (7, 2), (8, 2)]
22: [(0, 0), (1, 1), (2, 2), (3, 1), (4, 1), (5, 2), (6, 0), (7, 2), (8, 2)]
23: [(0, 0), (1, 1), (2, 2), (3, 1), (4, 2), (5, 1), (6, 0), (7, 1), (8, 2)]
24: [(0, 0), (1, 1), (2, 2), (3, 1), (4, 1), (5, 2), (6, 0), (7, 1), (8, 2)]

If you just want a single solution, replace the last for loop with

output_polygons = next(all_solutions)
print(output_polygons)

FWIW, here's some code that can be used to create a diagram from the graph data.

# Create a Graphviz DOT file from graph data
colors = {
    None: 'white',
    0: 'red', 1: 'yellow', 
    2: 'green', 3: 'blue'
}

def graph_to_dot(nodes, edges, outfile=sys.stdout):
    outfile.write('strict graph test{\n')
    outfile.write('    node [style=filled];\n')

    for n, v in nodes:
        outfile.write('    {} [fillcolor={}];\n'.format(n, colors[v]))   
    outfile.write('\n')   

    for u, v in edges:
        outfile.write('    {} -- {};\n'.format(u, v))
    outfile.write('}\n')

You can call it like this:

# Produce a DOT file of the colored graph
with open('graph.dot', 'w') as f:
    graph_to_dot(output_polygons, adjacent_polygons_graph, f)

It can also be used to produce a DOT file of the input_polygons.

To generate a PNG image file from the DOT file using the Graphviz neato utility, you can do this (in Bash).

neato -Tpng -ograph.png graph.dot

Here's the DOT file for the first solution above:

strict graph test{
    node [style=filled];
    0 [fillcolor=red];
    1 [fillcolor=yellow];
    2 [fillcolor=yellow];
    3 [fillcolor=green];
    4 [fillcolor=green];
    5 [fillcolor=yellow];
    6 [fillcolor=yellow];
    7 [fillcolor=red];
    8 [fillcolor=yellow];

    0 -- 1;
    0 -- 2;
    0 -- 3;
    0 -- 4;
    0 -- 5;
    1 -- 0;
    2 -- 0;
    2 -- 3;
    3 -- 0;
    3 -- 2;
    3 -- 6;
    3 -- 8;
    4 -- 0;
    4 -- 5;
    4 -- 6;
    5 -- 0;
    5 -- 4;
    6 -- 3;
    6 -- 4;
    6 -- 7;
    7 -- 6;
    8 -- 3;
}

And here's the PNG:

3 color solution

Here's a live version, running on the SageMathCell server.

Upvotes: 5

Related Questions