35

Background and Problem Description:

I have some code that solves the graph coloring problem (broadly defined as the problem of assigning "colors" to an undirected graph, making sure that no two vertices connected by an edge have the same color). I'm trying to implement a solution using constraint propagation to improve on the efficiency of a standard recursive backtracking algorithm, but am running into the following error:

  File "C:\Users\danisg\Desktop\coloring\Solver.py", 
  line 99, in solve
  for color in self.domains[var]:
  RuntimeError: Set changed size during iteration

Here, for each vertex, I keep a set of possible particular values for that particular vertex:

  self.domains = { var: set(self.colors) for var in self.vars }

After I make an assignment, I propagate this constraint to the neighboring domains, to limit the search space:

  for key in node.neighbors:          # list of keys corresponding to adjacent vertices
      if color in self.domains[key]:  # remove now to prune possible choices
          self.domains[key].remove(color)

This isn't where the actual error is thrown (in my code, I indicate where the problem is in a try-except block), but may be the source of the problem.

My Question:

Do I have the right idea, here, if not the right implementation? More to the point, how can I fix this? Also, is it necessary to keep a separate domains dictionary? Or could we make domain a property of each node in the graph?

My Code:

Here's the solve function where this code is called:

def solve(self):

    uncolored = [var for var in self.vars if self.map[var].color == None]
    if len(uncolored) == 0:
        return True

    var  = min(uncolored, key = lambda x: len(self.domains[var]))
    node = self.map[var]
    old  = { var: set(self.domains[var]) for var in self.vars }

    for color in self.domains[var]:

        if not self._valid(var, color):
            continue


        self.map[var].color = color
        for key in node.neighbors:

            if color in self.domains[key]:
                self.domains[key].remove(color)

        try:
            if self.solve():
                return True
        except:
            print('happening now')


        self.map[var].color = None
        self.domains = old


    return False

My implementation uses a Node object:

class Solver:

    class Node:

        def __init__(self, var, neighbors, color = None, domain = set()):

            self.var       = var
            self.neighbors = neighbors
            self.color     = color
            self.domain    = domain

        def __str__(self):
            return str((self.var, self.color))



    def __init__(self, graph, K):

        self.vars    = sorted( graph.keys(), key = lambda x: len(graph[x]), reverse = True )  # sort by number of links; start with most constrained
        self.colors  = range(K)
        self.map     = { var: self.Node(var, graph[var]) for var in self.vars }
        self.domains = { var: set(self.colors)           for var in self.vars }

Here are two other functions that are used/are helpful:

def validate(self):

    for var in self.vars:
        node = self.map[var]

        for key in node.neighbors:
            if node.color == self.map[key].color:
                return False

    return True

def _valid(self, var, color):

    node = self.map[var]

    for key in node.neighbors:

        if self.map[key].color == None:
            continue

        if self.map[key].color == color:
            return False

    return True

Data and Example for which the Code is Failing:

The example graph I'm using can be found here.

The function for reading the data:

def read_and_make_graph(input_data):

    lines = input_data.split('\n')

    first_line = lines[0].split()
    node_count = int(first_line[0])
    edge_count = int(first_line[1])

    graph = {}
    for i in range(1, edge_count + 1):
        line  = lines[i]
        parts = line.split()
        node, edge = int(parts[0]), int(parts[1])

        if node in graph:
            graph[node].add(edge)

        if edge in graph:
            graph[edge].add(node)

        if node not in graph:
            graph[node] = {edge}

        if edge not in graph:
            graph[edge] = {node}

    return graph

It should be called as follows:

file_location = 'C:\\Users\\danisg\\Desktop\\coloring\\data\\gc_50_3'
input_data_file = open(file_location, 'r')
input_data = ''.join(input_data_file.readlines())
input_data_file.close()

graph  = read_and_make_graph(input_data)
solver = Solver(graph, 6)  # a 6 coloring IS possible

print(solver.solve())      # True if we solved; False if we didn't
rookie
  • 2,783
  • 5
  • 29
  • 43
  • Your stack trace mentions `_solve` and `for color in domain:`, but neither of those are anywhere in the code you posted. Without a runnable test case that demonstrates the problem, it's hard to give advice more specific than "don't `remove` from a set while you're iterating over it" – Kevin Apr 03 '14 at 19:15
  • @Kevin: Those names were used in a lengthier version of the code (I removed excessive code to try to make the simplest instance of the problem). The data I've supplied is the test case that produces the error. Everything should be runnable; sorry the stack trace was inconsistent with the code provided. – rookie Apr 03 '14 at 19:19

2 Answers2

69

I think the problem is here:

for color in self.domains[var]:

    if not self._valid(var, color):
        continue

    self.map[var].color = color
    for key in node.neighbors:

        if color in self.domains[key]:
            self.domains[key].remove(color)  # This is potentially bad.

if key == var when self.domains[key].remove(color) is called, you change the size of the set you're currently iterating over. You can avoid this by using

for color in self.domains[var].copy():

Using copy() will allow you to iterate over a copy of the set, while removing items from the original.

dano
  • 91,354
  • 19
  • 222
  • 219
  • Though this is a good suggestion, this doesn't actually happen in the code supplied (just verified). The mapping for domains is also `{var: possible_colors}`. Can you please explain where `for color in self.domains[var].keys():` should be put? – rookie Apr 03 '14 at 20:12
  • My suggestion was to replace `for color in self.domains[var]:`, which is in the solve() method, with `for color in self.domains[var].keys():`. That _should_ eliminate the backtrace you posted. – dano Apr 03 '14 at 20:15
  • `self.domains[var].keys()` produces the following: `for color in self.domains[var].keys(): AttributeError: 'set' object has no attribute 'keys'` – rookie Apr 03 '14 at 20:19
  • Ah, sorry about that, I was thinking it was a dict. You can use .copy(), instead. – dano Apr 03 '14 at 20:24
  • I've edited my original answer to use .copy() instead of .keys(). – dano Apr 03 '14 at 20:31
  • This works...but why?! I put in some code to check if `var == key`, and the execution never hit this line. Can you say a little more about why this works? Is it the conflict happening somewhere deeper in the recursion? Please let me know if this check is invalid: `for key in node.neighbors: if key == var: print('dano found the error!')` – rookie Apr 03 '14 at 20:40
  • I think it's probably the recursive nature of solve(). Say the first time you enter solve(), var == 1. key may never == 1 in that stack frame, but then you recursively call solve() again later in the for loop. If you end up removing a color from self.domains[1] in that lower stack frame (or any subsequent stack frame while recursing), you've blown up the top stack frame. So once you make it back to the original stack frame, the size of the set it's iterating over has changed, and it throws an exception. – dano Apr 03 '14 at 20:44
  • exactly what I was thinking, but much better explained. Thank you so much!! – rookie Apr 03 '14 at 20:49
  • Can you not do something with iterators instead in Python too in a similar way to what Java allows? Is yield the same in Python perhaps? https://realpython.com/introduction-to-python-generators/ – JGFMK May 16 '21 at 17:10
1

Use set() object.copy() to solve the problem

class Door():
    def __init__(self,id):
        self.id = id

if __name__ == '__main__':
    cache_door = set()
    cache_door.add(Door(1))
    cache_door.add(Door(2))
    cache_door.add(Door(3))
    cache_door.add(Door(4))
    print cache_door
    for door in cache_door.copy():
        if door.id == 1:
            cache_door.remove(door)
    print cache_door
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Oct 14 '22 at 23:23