@rkenmi - Paint adjacent boundaries with distinct colors

Paint adjacent boundaries with distinct colors


Did you know that the minimum number of colors needed for this to work is 4?

Paint adjacent boundaries with distinct colors


Back to Top

Updated on June 10, 2017

In my recent A.I. class, I had an assignment where I had to write some code that will paint the countries of Africa with distinct colors. For the colors, I was given a domain of set(['red','blue','white','yellow','green']). Additionally, a Python dictionary is given that contains each country and their adjacent countries.

For example, Egypt is right next to Libya and Sudan. This means that if Egypt is given some color, such as red, then Libya and Sudan must not be red.

Here is the Python code that paints adjacent countries with different colors. I have tried two different inference methods, forward checking and AC-3. I found my forward checking algorithm to only use 4 out of the 5 colors given, where as my AC-3 used all 5 colors. 4 is definitely the minimum -- any less is not possible for this assignment without contradicting the adjacent and distinct color rule.

    countries = {
        "algeria": ["libya", "mali", "mauritania", "morocco", "niger", "tunisia"],
        "angola": ["democratic_republic_of_the_congo", "republic_of_the_congo", "namibia", "zambia"],
        "benin":["togo", "burkina_fazo", "niger", "nigeria"],
        "botswana":["namibia", "south_africa", "zimbabwe", "zambia"],
        "burkina_fazo":["benin", "ivory_coast", "ghana", "mali", "niger", "togo"],
        "burundi":["democratic_republic_of_the_congo", "rwanda", "tanzania"],
        "cameroon":["central_african_republic", "chad", "republic_of_the_congo", "equatorial_guinea", "gabon", "nigeria"],
        "cape_verde":[],
        "central_african_republic":["cameroon", "chad", "democratic_republic_of_the_congo", "republic_of_the_congo", "sudan"],
        "chad":["cameroon", "central_african_republic", "libya", "niger", "nigeria", "sudan"],
        "comoros":[],
        "democratic_republic_of_the_congo":["angola", "burundi", "central_african_republic", "republic_of_the_congo", "rwanda", "sudan", "tanzania", "uganda", "zambia"],
        "republic_of_the_congo":["angola", "cameroon", "central_african_republic", "democratic_republic_of_the_congo", "gabon"],
        "ivory_coast":["burkina_fazo", "ghana", "guinea", "liberia", "mali"],
        "dijbouti":["eritrea", "ethiopia", "somalia"],
        "egypt":["libya", "sudan"],
        "equatorial_guinea":["gabon", "cameroon"],
        "eritrea":["dijbouti", "ethiopia", "sudan"],
        "ethiopia":["eritrea", "dijbouti", "somalia", "sudan", "kenya"],
        "gabon":["equatorial_guinea", "cameroon","republic_of_the_congo"],
        "gambia":["senegal"],
        "ghana":["burkina_fazo","ivory_coast", "togo"],
        "guinea":["ivory_coast","guinea-bissau","liberia","mali","senegal","sierra_leone"],
        "guinea-bissau":["guinea", "senegal"],
        "kenya":["ethiopia", "somalia", "tanzania", "uganda", "sudan"],
        "lesotho":["south_africa"],
        "liberia":["guinea", "ivory_coast", "sierra_leone"],
        "libya":["algeria", "chad", "egypt", "niger", "sudan", "tunisia"],
        "madagascar":[],
        "malawi":["mozambique", "tanzania", "zambia"],
        "mali":["algeria", "niger", "burkina_fazo", "ivory_coast", "guinea", "senegal", "mauritania"],
        "mauritania":["algeria", "mali", "senegal"],
        "mauritius":[],
        "morocco":["algeria"],
        "mozambique":["malawi", "south_africa", "swaziland", "tanzania", "zambia", "zimbabwe"],
        "namibia":["angola", "botswana", "south_africa", "zambia", "zimbabwe"],
        "niger":["algeria", "benin", "burkina_fazo", "chad", "libya", "mali", "nigeria"],
        "nigeria":["benin", "cameroon", "chad", "niger"],
        "reunion":[],
        "rwanda":["burundi", "democratic_republic_of_the_congo", "tanzania", "uganda"],
        "sao_tome_and_principe":[],
        "senegal":["gambia", "guinea", "guinea-bissau", "mali", "mauritania"],
        "seychelles":[],
        "sierra_leone":["guinea", "liberia"],
        "somalia":["dijbouti", "ethiopia", "kenya"],
        "south_africa":["botswana", "lesotho", "mozambique", "namibia", "swaziland", "zimbabwe"],
        "sudan":["central_african_republic", "chad", "democratic_republic_of_the_congo", "egypt", "eritrea", "ethiopia", "kenya", "libya", "uganda"],
        "swaziland":["mozambique","south_africa"],
        "tanzania":["burundi", "democratic_republic_of_the_congo", "kenya", "malawi", "mozambique", "rwanda","uganda","zambia"],
        "togo":["benin", "burkina_fazo", "ghana"],
        "tunisia":["algeria", "libya"],
        "uganda":["democratic_republic_of_the_congo", "kenya", "rwanda", "sudan", "tanzania"],
        "zambia":["angola", "botswana", "democratic_republic_of_the_congo", "malawi", "mozambique", "namibia", "tanzania", "zimbabwe"],
        "zimbabwe":["botswana", "mozambique", "south_africa", "zambia", "namibia"],}


    domain = set(['red','blue','white','yellow','green'])

    def selectUnassignedVariable(assignment, csp): # Minimum Remaining Variable - Choose the variable with the smallest domain

    # count keeps track of the smallest length of domains
        count = 0

    # select_country keeps track of the country with the domain of smallest length
        select_country = False


        for country in assignment:
            if len(assignment[country]) > 1:

            # count should only be 0 at initialization
                if count is 0 or count > len(assignment[country]):
                    count = len(assignment[country])
                    select_country = country

        return select_country

    def orderDomainValues(): # Unordered
        return domain

    """

    def inference (csp, var, value, assignment): # Forward checking
        for adj_c in csp[var]: # for each adjacent country to the current country represented by 'var'
            if value is assignment[adj_c]: # if this color is exactly the only color that this adjacent country has in the domain
                return False
            elif (value in assignment[adj_c]): # otherwise check if this color is a subset of the adjacent countrys domain
                assignment[adj_c].remove(value) # if so, remove the color from the adjacent country's domain
        return True

    """
    def inference (csp, assignment): # AC-3

        """ initialize the queue with all the arcs in csp"""

    # queue = list of arcs (i.e. an arc from Chad to Egypt)
        queue = []

    # tail represents a country
        for tail in csp :

        # curr represents a country adjacent to tail
        # add arcs from tail -> (adjacent countries to tail) to the queue
            for curr in csp[tail]:
                queue.append( ( tail, curr ) )

        """ loop until queue is empty """
        while queue:
            (tail, head) = queue.pop()

        # check if the arc from tail -> head satisfies constraint
            if revise(csp, assignment, tail, head):

            # the arc from tail -> head failed constraint, but was fixed in the revise function

            # add new arcs from (adjacent countries of tail) -> tail to the queue
                for curr in csp[tail]:
                    queue.append((curr, tail))



    def revise(csp, assignment, tail, head): # returns True (arc is revised) or False (arc needs no revision -- it passes all constraints)

    # default return value is False (may change later)
        revised = False

    # This procedure is needed since python collections are mutable
    # tempAssignment is a copy of assignment that is iterated while changes are made to assignment
        tempAssignment = {}
        for country in assignment:
            tempAssignment[country] = assignment[country].copy()

    # Initialize boolean for constraint check
        constraintSatisfied = True


    # x - a color in tail domain
    # y - a color in head domain
        for x in tempAssignment[tail]:
            for y in tempAssignment[head]:
                constraintSatisfied = True

                """ Constraints:
                    # 1. For every color x in tail, there is some color y in head that is legal
                    # 2. The domain is not assigned a color already """
                if x is y and len(tempAssignment[tail]) is not 1:
                    constraintSatisfied = False

            if constraintSatisfied is False:
                assignment[tail].remove(x)
            # return value changed from False to True (this arc is revised)
                revised = True

        return revised

    def backtrackingSearch(csp):

    # initialization, runs only once
        assignment = {}
        for country in csp:
            assignment[country] = set(['red','blue','white','yellow','green'])

        return recursiveBacktracking(assignment,csp)

    def recursiveBacktracking(assignment, csp):

        """ GOAL TEST : each country should be mapped to 1 color only, meaning they are assigned """
        if all(len(assignment[country]) == 1 for country in csp):
            return assignment

        """ choose a country with MRV (Minimum-Remaining Values) """
        var = selectUnassignedVariable(assignment, csp)

        """ choose a color from all available colors. No ordering is used for orderDomainValues. """
        for value in orderDomainValues():

        # if the color is in the countrys domain, proceed
            if value in assignment[var]:

            # assign this color to this country; the countrys domain is just now one element of this color
                assignment[var] = set([value])

                """ run the inference (AC-3) and remove necessary colors from adjacent countries """
                inferences = inference(csp, assignment)

            # recursive loop, find the iteration that passes the goal test and has all countries assigned
                result = recursiveBacktracking(assignment, csp)
                if result is not None:
                    return result

    # this iteration of the recursion loop did not pass the goal test, so we terminate it
        return None


    coloredMap = backtrackingSearch(countries)
    print "\ncoloredMap = \n\n", coloredMap

Update 1: Fixed formatting of code.


Article Tags:
Pythonadjacencybacktrackingac-3colors