Skip to content

← Computing Further 6–8

Can Four Colors Paint Any Map of Nepal?

Topic 10 · Grades 6–8 · Graph coloring

Nothing for coloring by hand; the Python parts from Grade 8 (loops, lists and functions), with dictionaries explained on the page.

What this builds on from class
In this unit 8 sections
  1. Try it
  2. How it works
  3. Program it
  4. What your program shows
  5. Going further still
  6. Challenges
  7. Questions
  8. Where to go next

In an atlas, two districts that share a border never have the same color, so your eye can see where one ends. A mapmaker could give each of Nepal’s 77 districts its own color. But how few colors would do?

Start small, with the seven provinces. Choose a color, then tap a province to paint it.

Try it

Color the seven provinces

If two neighbors have the same color, a thick dark line marks the border they share. Tap a province again to clear it.

Loading the map…
Colored
0 / 7
Colors used
0
Clashes
0

Choose a color, then tap a province to paint it. Tap it again to clear it.

What is the fewest colors that can paint the seven provinces, with no two neighbors alike?

Pick an answer to see why.

Try it

All 77 districts

Now the whole map. The Kathmandu Valley’s tiny districts are drawn again, larger, in the box at the top right.

Try it

Color the 77 districts

Choose a color and tap districts. Try to color all 77 with no dark lines. On a phone, the full-screen button (top right) and turning the phone sideways make the small districts easier to tap.

Loading the map…
Colored
0 / 77
Colors used
0
Clashes
0

Choose a color, then tap a district to paint it. Tap it again to clear it.

Before you finish: how many colors do you think all 77 districts need?

Pick an answer to see why.

Three colors for the Valley?

Try it

Three colors for the Valley?

Paint Kathmandu, Lalitpur, Bhaktapur and Kavre (Kavrepalanchok) with three colors and no clashes.

Loading the map…
Colored
0 / 4
Colors used
0
Clashes
0

These four districts each touch the other three. Choose a color, then tap a district.

Could some map force five? Try drawing five countries that all touch each other: one is always shut out. But a map might force five in a cleverer way. People asked for 124 years whether four colors are always enough, for every map anyone could ever draw. The answer needed a computer.

One district at a time

A computer can color as you did: take the districts in some order, and give each the smallest color number none of its colored neighbors has. That is greedy coloring: it takes what looks best right now, and never goes back.

Try it on the provinces: each tap gives a province the smallest color number it can have.

Try it

Greedy, in your order

Tap the provinces one at a time, in any order. Press Clear to try another order.

Loading the map…
Colored
0 / 7
Colors used
0
Clashes
0

Tap the provinces in any order. Each gets the smallest color number its colored neighbors don’t have.

Now let the computer color all 77 districts, a district at a time, in different orders.

Try it

The computer, one district at a time

East to west starts at Jhapa; A to Z is alphabetical; Shuffle is a new random order each time. (Busiest first comes later.)

Loading the map…
Colored
0 / 77
Colors used
0
Clashes
0

Tap districts in any order, or press one of the orders. Each gets the smallest color number its colored neighbors don’t have.

Press Shuffle ten times. How often does greedy manage with four colors?

Pick an answer to see why.

How it works

A map is a graph

For coloring, only which districts touch which matters. So draw a dot for each district and a line for each pair that shares a border: a graph, as in Grade 10 (dots are vertices, lines edges). Coloring the map is coloring the dots so no line joins two of the same color.

KoshiMadheshBagmatiLumbiniGandakiSudurpashchimKarnali
Figure 10.1 Nepal’s seven provinces as a graph: a dot for each province and a line for each pair that shares a border. The three triangles are three provinces that all touch, which is why the provinces need three colors.

Places that meet only at a point aren’t neighbors (four farms meeting at one corner post can share two colors). So Nepal’s districts make a graph of 77 dots and 188 lines. The shortest shared border, Ramechhap–Sindhupalchok, is under a kilometer, but it counts.

Why greedy gets stuck

When greedy reaches a district whose neighbors show 1, 2, 3 and 4, it must use 5, even if a different choice much earlier would have left a color free.

Greedy colors Makwanpur when its neighbors already have colors 1, 2, 3 and 4. Which is true?

Pick an answer to see why.

Busiest first

A better rule picks, at every step, the district hardest to color right now: the one whose neighbors show the most different colors (if several tie, the first on the list). It is the one most likely to get stuck, so it goes while it still has a choice. Daniel Brélaz published this rule in 1979 as DSatur; here it is busiest first. (His version breaks a tie by taking the district with the most uncolored neighbors; on these maps it gives the same counts.)

Press Busiest first in the last widget: four colors. It starts at Jhapa (the first on the list) and spreads west, always to a district hemmed in by colors, each reached while it still has one free.

Program it

The map as data (Python, from Grade 8)

The programs use the page’s own module, nepal.py, made from the widgets’ map. DISTRICTS lists the 77 names, east to west. NEIGHBORS is a dictionary: a list whose items are looked up by name, not number. NEIGHBORS['Kathmandu'] lists Kathmandu’s neighbors. (Dictionaries come properly in Grade 9.)

from nepal import DISTRICTS, NEIGHBORS print(len(DISTRICTS), 'districts')print('Kathmandu borders', NEIGHBORS['Kathmandu']) lines = 0most = DISTRICTS[0]for d in DISTRICTS:    lines = lines + len(NEIGHBORS[d])    if len(NEIGHBORS[d]) > len(NEIGHBORS[most]):        most = dprint(lines // 2, 'shared borders')print(most, 'has the most neighbors:', len(NEIGHBORS[most])) valley = ['Kathmandu', 'Lalitpur', 'Bhaktapur', 'Kavrepalanchok']for a in valley:    for b in valley:        if a != b and b not in NEIGHBORS[a]:            print(a, 'does not touch', b)print('Checked the Valley four.')

Output

77 districtsKathmandu borders ['Bhaktapur', 'Dhading', 'Kavrepalanchok', 'Lalitpur', 'Makwanpur', 'Nuwakot', 'Sindhupalchok']188 shared bordersUdayapur has the most neighbors: 9Checked the Valley four.

Each border is counted from both sides, so the program halves the count. Sindhuli and Makwanpur have 9 neighbors too: > keeps the first it finds. The Valley check printed nothing: each of the four touches the other three.

Check a coloring

A coloring is a dictionary too: colors['Jhapa'] is Jhapa’s color, 1, 2, 3… This function lists every border with the same color on both sides; d < n (alphabetical order) lists each pair once.

from nepal import DISTRICTS, NEIGHBORS def clashes(colors):    found = []    for d in DISTRICTS:        for n in NEIGHBORS[d]:            if d < n and colors[d] == colors[n]:                found.append(d + ' and ' + n)    return found # colors in turn down the list: 1, 2, 3, 4, 1, 2, 3, 4…colors = {}for i in range(len(DISTRICTS)):    colors[DISTRICTS[i]] = i % 4 + 1print(len(clashes(colors)), 'clashes, for example', clashes(colors)[:2])

Output

40 clashes, for example ['Ilam and Morang', 'Dhankuta and Panchthar']

colors = {} makes an empty dictionary; colors[name] = … adds a name. Taking turns gives 40 clashes: four colors are enough only if chosen with care.

Greedy coloring

For each district in order, greedy collects its colored neighbors’ colors (n in colors asks whether n has one yet), then counts up from 1 to the first color not among them.

from nepal import DISTRICTS, NEIGHBORS def greedy(order):    colors = {}    for d in order:        used = []        for n in NEIGHBORS[d]:            if n in colors:                used.append(colors[n])        c = 1        while c in used:            c = c + 1        colors[d] = c    return colors def color_districts():    return greedy(DISTRICTS) colors = color_districts()print('East to west:', max(colors.values()), 'colors')print('Color 5 went to', [d for d in DISTRICTS if colors[d] == 5])print('A to Z:', max(greedy(sorted(DISTRICTS)).values()), 'colors')

Output

East to west: 5 colorsColor 5 went to ['Makwanpur', 'Doti']A to Z: 6 colors

max(colors.values()) is the number of colors used; sorted(DISTRICTS) is the list in alphabetical order. The widget below paints what your color_districts() gives back.

Try it

Your program’s colors

Edit the program above, then press the button. Try greedy(DISTRICTS[::-1]) (west to east).

Loading the map…
Colored
0 / 77
Colors used
0
Clashes
0

Press Color with my program: the page calls your color_districts() and paints the map with what it gives back.

What your color_districts prints, and any error, appears here.

A thousand orders

Is there an order greedy does well with? random.shuffle(order) mixes a list, and random.seed(1) makes the “random” orders the same every run, so you get this page’s counts.

import randomfrom nepal import DISTRICTS, NEIGHBORS def greedy(order):    colors = {}    for d in order:        used = []        for n in NEIGHBORS[d]:            if n in colors:                used.append(colors[n])        c = 1        while c in used:            c = c + 1        colors[d] = c    return colors random.seed(1)count = [0, 0, 0, 0, 0, 0, 0, 0]       # count[k]: orders that needed k colorsfor i in range(1000):    order = DISTRICTS.copy()    random.shuffle(order)    k = max(greedy(order).values())    count[k] = count[k] + 1for k in range(4, 8):    print(k, 'colors:', count[k], 'orders')

Output

4 colors: 0 orders5 colors: 618 orders6 colors: 379 orders7 colors: 3 orders

Busiest first

Each time round, busiest first counts the different colors around every uncolored district (seen, each color once) and takes the one with the most.

from nepal import DISTRICTS, NEIGHBORS def busiest_first(names, neighbors):    colors = {}    while len(colors) < len(names):        best = ''        best_seen = -1        for d in names:            if d not in colors:                seen = []                for n in neighbors[d]:                    if n in colors and colors[n] not in seen:                        seen.append(colors[n])                if len(seen) > best_seen:                    best = d                    best_seen = len(seen)        used = []        for n in neighbors[best]:            if n in colors:                used.append(colors[n])        c = 1        while c in used:            c = c + 1        colors[best] = c    return colors def color_districts():    return busiest_first(DISTRICTS, NEIGHBORS) colors = color_districts()print('Busiest first:', max(colors.values()), 'colors')print('The first five it colored:', list(colors)[:5])

Output

Busiest first: 4 colorsThe first five it colored: ['Jhapa', 'Ilam', 'Morang', 'Panchthar', 'Dhankuta']

A dictionary keeps the order names were added, so list(colors) is the order busiest first chose. It takes the map as parameters, so it can color any map: you will need that below.

Try it

Your busiest first

The map, colored by your busiest_first.

Loading the map…
Colored
0 / 77
Colors used
0
Clashes
0

Press Color with my program: the page calls your color_districts() and paints the map with what it gives back.

What your color_districts prints, and any error, appears here.

What your program shows

  • Four colors are enough for Nepal, and the Valley shows three are not.
  • Greedy almost never finds them. In a thousand random orders it needed 5 or more every time: 618 times five, 379 six, 3 seven.
  • The order is everything. Every map has an order in which greedy does as well as the best coloring: list every district of color 1, then every district of color 2, and so on. When a color-1 district’s turn comes, only other color-1 districts are colored, none of them its neighbor, so 1 is free; and so on up.

greedy and busiest_first are kept as a module, coloring.py, for the programs below.

from nepal import DISTRICTS, NEIGHBORSfrom coloring import greedy, busiest_first four = busiest_first(DISTRICTS, NEIGHBORS)order = []for c in [1, 2, 3, 4]:    for d in DISTRICTS:        if four[d] == c:            order.append(d)print('Order:', order[:4], '...')again = greedy(order, NEIGHBORS)print('Greedy in this order:', max(again.values()), 'colors')print('Same colors as busiest first?', again == four)

Output

Order: ['Jhapa', 'Panchthar', 'Sankhuwasabha', 'Sunsari'] ...Greedy in this order: 4 colorsSame colors as busiest first? True

So the right order exists for every map, but to write it down you must already have the coloring. Checking a coloring is quick, as your clashes shows; finding one with the fewest colors is hard. Deciding whether three colors are enough for a map is one of computer science’s famous hard problems: nobody knows a method fast for every map. For four colors on a flat map, the answer is always yes.

Going further still

Busiest first can fail too. Here is a made-up map of nine regions that can be colored with four. Run busiest first on it.

from coloring import busiest_first NINE = {    'A': ['B', 'C', 'F', 'I'], 'B': ['A', 'C', 'G', 'H'], 'C': ['A', 'B', 'E', 'G', 'I'],    'D': ['E', 'F', 'G', 'H'], 'E': ['C', 'D', 'F', 'G', 'I'], 'F': ['A', 'D', 'E', 'H', 'I'],    'G': ['B', 'C', 'D', 'E', 'H'], 'H': ['B', 'D', 'F', 'G'], 'I': ['A', 'C', 'E', 'F'],}colors = busiest_first(list(NINE), NINE)print(colors)print(max(colors.values()), 'colors')

Output

{'A': 1, 'B': 2, 'C': 3, 'G': 1, 'E': 2, 'I': 4, 'F': 3, 'D': 4, 'H': 5}5 colors

By the time it reaches H, H’s neighbors have all four colors. Can you color the nine with four by hand?

Coloring is everywhere. In an exam timetable, each subject is a dot, a line joins two subjects some student takes both of, and the colors are the days. Radio stations near each other need different frequencies, colored the same way, and so are the few fast registers a processor keeps numbers in.

Challenges

★ Count the clashes (Python, from Grade 8)

Write count_clashes(colors, neighbors): how many borders have the same color on both sides. neighbors is a dictionary like NEIGHBORS, and colors gives each region a color. Count each border once.

def count_clashes(colors, neighbors):    # colors: {'A': 1, 'B': 2, …}; neighbors: {'A': ['B', …], …}    return 0 print(count_clashes({'A': 1, 'B': 1, 'C': 2}, {'A': ['B', 'C'], 'B': ['A', 'C'], 'C': ['A', 'B']}))

Try it

★ Count the clashes

  1. Three regions that all touch count_clashes({'A': 1, 'B': 1, 'C': 2}, {'A': ['B', 'C'], 'B': ['A', 'C'], 'C': ['A', 'B']}) Should give 1 Yours —
  2. A ring of six, two colors in turn count_clashes({'A': 1, 'B': 2, 'C': 1, 'D': 2, 'E': 1, 'F': 2}, {'A': ['B', 'F'], 'B': ['A', 'C'], 'C': ['B', 'D'], 'D': ['C', 'E'], 'E': ['D', 'F'], 'F': ['A', 'E']}) Should give 0 Yours —
  3. A ring of five, all one color count_clashes({'A': 1, 'B': 1, 'C': 1, 'D': 1, 'E': 1}, {'A': ['B', 'E'], 'B': ['A', 'C'], 'C': ['B', 'D'], 'D': ['C', 'E'], 'E': ['A', 'D']}) Should give 5 Yours —
Tests passed
— / 8
Challenge
★ solved

Write your count_clashes in the program above (Edit), then check it.

A test your function gets wrong, anything it prints, and any error appear here.

Hint
Answer

★★ Greedy, in any order (Python, from Grade 8)

Write greedy_colors(order, neighbors): color the regions greedily in the order given, and give back a list of their colors in that same order. greedy_colors(['A', 'B', 'C'], …) for three regions that all touch gives [1, 2, 3].

def greedy_colors(order, neighbors):    return [] print(greedy_colors(['A', 'B', 'C'], {'A': ['B', 'C'], 'B': ['A', 'C'], 'C': ['A', 'B']}))

Try it

★★ Greedy, in any order

  1. Three regions that all touch greedy_colors(['A', 'B', 'C'], {'A': ['B', 'C'], 'B': ['A', 'C'], 'C': ['A', 'B']}) Should give [1, 2, 3] Yours —
  2. A ring of six, going round greedy_colors(['A', 'B', 'C', 'D', 'E', 'F'], {'A': ['B', 'F'], 'B': ['A', 'C'], 'C': ['B', 'D'], 'D': ['C', 'E'], 'E': ['D', 'F'], 'F': ['A', 'E']}) Should give [1, 2, 1, 2, 1, 2] Yours —
  3. A ring of six, in a bad order greedy_colors(['A', 'D', 'B', 'E', 'C', 'F'], {'A': ['B', 'F'], 'B': ['A', 'C'], 'C': ['B', 'D'], 'D': ['C', 'E'], 'E': ['D', 'F'], 'F': ['A', 'E']}) Should give [1, 1, 2, 2, 3, 3] Yours —
Tests passed
— / 8
Challenge
★★ solved

Write your greedy_colors in the program above (Edit), then check it.

A test your function gets wrong, anything it prints, and any error appear here.

Hint
Answer

★★★ Can it be colored? (Python, from Grade 8)

Write can_color(neighbors, k): True if the regions can be colored with at most k colors and no clashes, False if they can’t. The tests have up to nine regions, including the nine-region map above with k = 4, where busiest first fails.

def can_color(neighbors, k):    return True print(can_color({'A': ['B', 'C'], 'B': ['A', 'C'], 'C': ['A', 'B']}, 2))

Try it

★★★ Can it be colored?

  1. Three that all touch, 2 colors can_color({'A': ['B', 'C'], 'B': ['A', 'C'], 'C': ['A', 'B']}, 2) Should give False Yours —
  2. Three that all touch, 3 colors can_color({'A': ['B', 'C'], 'B': ['A', 'C'], 'C': ['A', 'B']}, 3) Should give True Yours —
  3. A ring of five, 2 colors can_color({'A': ['B', 'E'], 'B': ['A', 'C'], 'C': ['B', 'D'], 'D': ['C', 'E'], 'E': ['A', 'D']}, 2) Should give False Yours —
Tests passed
— / 14
Challenge
★★★ solved

Write your can_color in the program above (Edit), then check it.

A test your function gets wrong, anything it prints, and any error appear here.

Hint
Another hint
Answer

Questions

Why does Nepal’s district map need at least four colors?

Pick an answer to see why.

What does greedy coloring do when a region’s neighbors already have colors 1, 2 and 3?

Pick an answer to see why.

a.

A friend says: “My program colored the districts with six colors, so the map needs six.” What would you tell your friend?

Answer
b.

An exam timetable has six subjects. Some students take both Science and Math, some both Math and Computer, some both Computer and Science, and some both English and Nepali. Social Studies shares no students with the others. What is the fewest number of exam days, and why?

Answer
c.

Why did many mathematicians feel uneasy about Appel and Haken’s 1976 proof, and what has made people more sure of it since?

Answer

Where to go next