地图染色算法 | 如何使用至多四种颜色填充地图

2,500 阅读4分钟

在数学界有一个传说,那就是不管多么复杂的地图,只需要使用至多四种颜色,就可以填充地图,且满足相邻的两个区域的颜色不同。当然,这里的相邻是指至少有一条边相邻,只是点相邻就不算相邻。

那么,现在就出现一个问题,如果给了你一个地图,并且标记了这个地图的不同区域,你要怎么找到一种填色方案,且至多只能使用四种颜色呢?

为了方便表述问题,我们使用一个二维数组来表示一个地图,并用不同的数字表示不同的区域,用 -1 表示这个位置不在地图上。比如如下地图(灰色表示非地图区域,比如湖泊之类的):

9b70b6ab-47a7-4eca-be73-b37f52f5c42d.svg

这个地图,一共有 10 块区域,我们需要用红黄蓝绿至多四种颜色,来给这 10 块区域上色,但是,又要求相邻的区域不能用重复的颜色。

首先,我们用代码来表示这个地图,如下:

MAP = [
    [ 1,  1,  1,  1,  2,  2, -1],
    [ 3,  3,  2,  2,  2, -1, -1],
    [ 4,  3,  3,  5,  6,  6,  6],
    [ 4,  5,  5,  5,  6,  7,  6],
    [ 4,  5,  8,  7,  7,  7,  9],
    [ 4,  4,  8, -1,  9,  9,  9],
    [ 8,  8,  8, 10, 10, 10,  9],
    [-1, -1, -1, -1, 10, 10, -1]
]

然后,我们定义一个填色数组,来使用 print 函数来打印出颜色。

COLORS = ['\033[1;30;41m', '\033[1;30;42m', '\033[1;30;43m', '\033[1;30;44m']

接着,我们定义一个字典,来表示不同的区域应该填什么颜色。当然,这个字典现在是空的。

COLOR_MAP = {}

当然,一开始我们没有填色,所以,这个函数跑出来的效果如下:

60ae4a1a-9f41-4930-b1ff-8f6e4309165f.png

接下来,我们可以开始填色了,填色的算法其实很简单,就是不断地尝试所有可能的方案,只要遇到一个可以填完颜色的方案为止。具体步骤如下:

  1. 先计算每个区域相邻的区域有哪些,构建一个邻接区域表。
  2. 递归调用以下过程,直到所有区域都被填上颜色了,算法结束:
    1. 检查当前区域所有可以使用的颜色(即,查看自己邻接的区域都用了哪些颜色,没被使用过的就是自己可以用的)。
    2. 如果当前区域没有颜色可以使用,则返回失败给上一层,上一层则会尝试换一种颜色。
    3. 从当前区域可使用的颜色中,逐个尝试,如果下一层一直迭代到最后填色成功,则整体填色成功,否则,就换个颜色继续尝试。

执行完以上步骤之后,地图填色就成功了。这里贴出参考代码:

def color_map(map):
    def get_val(x, y):
        if x < 0 or x >= width or y < 0 or y >= height:
            return -1
        return map[x][y]

    def add_neighbor(x, y, neighbors):
        val = map[x][y]
        if val == -1:
            return
        if val not in neighbors:
            neighbors[val] = set()
        neighbors[val].add(get_val(x, y + 1))
        neighbors[val].add(get_val(x, y - 1))
        neighbors[val].add(get_val(x - 1, y))
        neighbors[val].add(get_val(x + 1, y))

    # 计算每个区域的邻边区域
    width = len(map)
    height = len(map[0])
    neighbors = {}
    for x in range(0, width):
        for y in range(0, height):
            add_neighbor(x, y, neighbors)
    # 给第一个区域填上第一种颜色
    color_map = {}
    for i in range(0, len(neighbors)):
        color_map[i + 1] = 0
    color_map[1] = 1
    # 从第二个区域开始,尝试所有不同的颜色
    def coloring(level):
        # 所有区域都填上颜色了,结束
        if level > len(neighbors):
            return True
        # 获取当前区域的邻区域都用了什么颜色
        neighbor = neighbors[level]
        colors = set()
        for n in neighbor:
            if n == -1:
                continue
            color = color_map[n]
            if color > 0:
                colors.add(color)
        # 如果没有颜色可以用了,则返回上层尝试别的颜色
        if len(colors) == 4:
            return False
        # 对所有还可以使用的颜色进行尝试
        for i in range(1, 5):
            if i in colors:
                continue
            color_map[level] = i
            if coloring(level + 1):
                return True
            # 换种颜色继续试
            color_map[level] = 0
        # 全都不行(理论上不可能),则返回 False
        return False
    
    # 开始着色
    coloring(2)
    return color_map

最后,执行代码:

if __name__ == '__main__':
    COLOR_MAP = color_map(MAP)
    print_map(MAP)

可以看到,程序最终输出的效果如下:

765d0dd2-8da3-48a1-a5e6-dbcf8b6e5dab.png

从效果中我们可以看到,地图被正确地填上了颜色,并且,相邻的区域用的都是不同的颜色。