具有评分的平面四色图着色的有效算法

时间:2018-02-07 11:13:27

标签: algorithm graph

我正在制作一个游戏,你必须使用4色定理为图像着色。没有相邻区域可以是相同的颜色。有四种颜色,红绿蓝黄色,每个红色区域有10个点,绿色有6个,蓝色有3个,黄色有1个。

我希望算法能够计算出任何给定图像的最大分数。我有从图像中提取平面图的代码,它为每个区域提供了它的邻居列表。

到目前为止,我已经完成了一项强力实施,它会检查所有可能的颜色,但是对于n个区域,这种颜色会增加到4 ** n。我可以采取的一种方法是尽可能地尝试优化此搜索。

有没有更快的方法?我知道2种颜色有线性时间算法但是出于游戏设计的原因,我通常不会生成可以用2种颜色着色的图像。

谢谢:)

编辑:因为这里的sascha请求是一些示例python dicts,它们的键是区域id,列表是该区域的邻居列表

easy = {2:[4],4:[2,3,14,13],3:[4],14:[4],13:[4]}

最高分:46(我认为)

(我的python bruteforce 0.1s)

中= {2:[4,5,6],4:[2,3],3:[4,18],5:[2,6],6:[5,2,13,18 ],13:[6,20,21,22],18:[6,3,20,22],20:[18,13],22:[18,13],21:[13]}

最高分:77

(我的python bruteforce 7.2s)

hard = {2:[5,6,9],5:[2,4],4:[5,23],6:[2,7,10],3:[8,16], 8:[3,7,12],7:[6,8,10,11],9:[2,10],10:[6,9,7,13,14,15,17,18], 11:[7,12,13],12:[8,11,15,16,19],13:[10,11,15],14:[10,15],15:[10,13,12 ,14,17,19],16:[3,12,25,27],17:[10,15,18],18:[10,17,19,20],19:[15,18,12 ,27:,20:[18,22,24,26,27,25],22:[20],23:[4,24,26],24:[23,20],25:[16,20 ],26:[23,20],27:[19,20,16]}

(我的python bruteforce未知)

EDIT2:

所以我完成了游戏,如果你有兴趣,可以查看here.

为了游戏,我意识到我只需要一个高分而不是绝对的最高分(这就是问题所要求的)。因此,我实施了贪婪的着色,并且每次运行10,000次,并且获得最佳得分结果。在少于30个区域的所有小板上,这产生与蛮力方法相同的结果,但是时间复杂度对于更大的板更好地扩展。所以它可能找不到绝对最好的解决方案,总能找到一个非常好的解决方案。

非常感谢@SaiBot和@sascha的帮助:)

2 个答案:

答案 0 :(得分:2)

这是我对这个问题的尝试。我无法提出更好的时间复杂性,但优化了蛮力。

我逐个处理节点,只允许着色,使得没有两个邻居节点具有相同的颜色。

我为每个中间(非完整)着色添加了上限估计。为此我假设每个非彩色节点将以最高得分的颜色着色(仅允许与已着色的邻居不同的颜色)。因此,在上限计算中,尚未着色的两个相邻节点都可以着色"红色"。通过这种估计,我构建了一个分支定界算法,当上限估计值仍低于当前最大值时终止当前搜索路径。

小图表的运行时间小于 1 ms ,对于中等图表, 15 ms ,对于大图表 3.2秒 >。三幅图的结果分别为46,77和194。

import time
import copy

def upperBoundScore(graph, dfsGraphOder, dfsIndex, coloring, scoring, currentScore):
    maxAdditionalScore = 0;
    for i in range(dfsIndex, len(dfsGraphOder)):
        neighbourColors = {coloring[node] for node in graph[dfsGraphOder[i]]}
        possibleColors = {1, 2, 3, 4} - neighbourColors
        if len(possibleColors) < 1:  # if for one node no color is available stop
            return -1
        maxAdditionalScore += scoring[list(possibleColors)[0]]
    return currentScore+maxAdditionalScore

def colorRemainingGraph(graph, dfsGraphOder, dfsIndex, coloring, scoring, currentScore):
    global maxScore
    global bestColoring
    # whole graph colored
    if dfsIndex == len(dfsGraphOder):
        if currentScore > maxScore:
            maxScore = currentScore
            bestColoring = copy.deepcopy(coloring)
    # only proceed if current coloring can get better then best coloring
    elif upperBoundScore(graph, dfsGraphOder, dfsIndex, coloring, scoring, currentScore) > maxScore:
        neighbourColors ={coloring[node] for node in graph[dfsGraphOder[dfsIndex]]}
        possibleColors = list({1, 2, 3, 4} - neighbourColors)
        for c in possibleColors:
            coloring[dfsGraphOder[dfsIndex]] = c
            currentScore += scoring[c]
            colorRemainingGraph(graph, dfsGraphOder, dfsIndex+1, coloring, scoring, currentScore)
            currentScore -= scoring[c]
            coloring[dfsGraphOder[dfsIndex]] = 0

#graph = {2: [4], 4: [2, 3, 14, 13], 3: [4], 14: [4], 13: [4]}
#graph = {2: [4, 5, 6], 4: [2, 3], 3: [4, 18], 5: [2, 6], 6: [5, 2, 13, 18], 13: [6, 20, 21, 22], 18: [6, 3, 20, 22], 20: [18, 13], 22: [18, 13], 21: [13]}
graph = {2: [5, 6, 9], 5: [2, 4], 4: [5, 23], 6: [2, 7, 10], 3: [8, 16], 8: [3, 7, 12], 7: [6, 8, 10, 11], 9: [2, 10], 10: [6, 9, 7, 13, 14, 15, 17, 18], 11: [7, 12, 13], 12: [8, 11, 15, 16, 19], 13: [10, 11, 15], 14: [10, 15], 15: [10, 13, 12, 14, 17, 19], 16: [3, 12, 25, 27], 17: [10, 15, 18], 18: [10, 17, 19, 20], 19: [15, 18, 12, 27], 20: [18, 22, 24, 26, 27, 25], 22: [20], 23: [4, 24, 26], 24: [23, 20], 25: [16, 20], 26: [23, 20], 27: [19, 20, 16]}

# 0 = uncolored, 1 = red, 2 = green, 3 = blue, 4 = Yellow
scoring = {1:10, 2:6, 3:3, 4:1}
coloring = {node: 0 for node in graph.keys()}
nodeOrder = list(graph.keys())
maxScore = 0
bestColoring = {}

start = time.time()
colorRemainingGraph(graph, nodeOrder, 0, coloring, scoring, 0)
end = time.time()
print("Runtime: "+ str(end - start))
print("Max Score: "+str(maxScore))
print(bestColoring)

对于大图,得到的颜色为(1 =红色,2 =绿色,3 =蓝色,4 =黄色):

{2:1,3:1,4:1,5:2,6:2,7:1,8:2,9:2,10:3,11:2,12:1,13: 1,14:1,15:4,16:2,17:2,18:1,19:2,20:2,22:1,23:2,24:1,25:1,26:1, 27:1}

要验证算法输出的着色是否正确,可以使用以下代码,该代码检查任何两个邻居节点是否具有相同的颜色。

def checkSolution(graph, coloring):
    validColoring=1
    for node in graph:
        for neighbour in graph[node]:
            if coloring[node] == coloring[neighbour]:
                print("wrong coloring found "+ str(node) + " and " + str(neighbour) + " have the same color")
                validColoring = 0
    if validColoring:
        print("Coloring is valid")

答案 1 :(得分:2)

这里有一些使用python的简化整数编程方法。

基本思想是使用现代高质量混合整数编程软件的惊人功能,而无需自己实现算法。我们只需要定义模型(也许可以调整一些东西)!

请记住,(混合)整数编程通常是NP难的,我们假设那些启发式算法在这里解决了我们的问题!

代码可能看起来有些难看,因为使用的建模工具非常低级。模型本身的结构非常简单。

代码(python3; numpy,scipy,cylp + CoinOR Cbc求解器)

这里是类似原型的代码,它缺少最终解决方案的提取。由于这只是一个演示(你没有标记语言),这只是表明它是一种可行的方法。

from cylp.cy import CyClpSimplex
import itertools
import numpy as np
import scipy.sparse as sp
from timeit import default_timer as time

""" Instances """
# hard = {2: [4], 4: [2, 3, 14, 13], 3: [4], 14: [4], 13: [4]}
# hard = {2: [4, 5, 6], 4: [2, 3], 3: [4, 18], 5: [2, 6], 6: [5, 2, 13, 18], 13: [6, 20, 21, 22], 18: [6, 3, 20, 22], 20: [18, 13], 22: [18, 13], 21: [13]}
hard = {2: [5, 6, 9],
        5: [2, 4],
        4: [5, 23],
        6: [2, 7, 10],
        3: [8, 16],
        8: [3, 7, 12],
        7: [6, 8, 10, 11],
        9: [2, 10],
        10: [6, 9, 7, 13, 14, 15, 17, 18],
        11: [7, 12, 13],
        12: [8, 11, 15, 16, 19],
        13: [10, 11, 15],
        14: [10, 15],
        15: [10, 13, 12, 14, 17, 19],
        16: [3, 12, 25, 27],
        17: [10, 15, 18],
        18: [10, 17, 19, 20],
        19: [15, 18, 12, 27],
        20: [18, 22, 24, 26, 27, 25],
        22: [20],
        23: [4, 24, 26],
        24: [23, 20],
        25: [16, 20],
        26: [23, 20],
        27: [19, 20, 16]}

""" Preprocessing -> neighbor conflicts
    (remove dupes after sorting <-> symmetry

    Remark: for difficult use-cases one could try to think about special
    characteristics of the graph, like (not necessarily for this problem)
    chordal -> calc all max-cliques in P(olynomial-time) => pretty good convex-hull

    Here: just forbid conflicting-pairs (in each color-dimension).
"""

START_T = time()

conflicts = []
for key, vals in hard.items():
    for val in vals:
        conflicts.append((key, val))
conflicts_np = np.array(conflicts)
conflicts_np = np.sort(conflicts, axis=1)
conflicts_np = np.unique(conflicts_np, axis=0)

""" Preprocessing -> map IDs to gapless range [0-N)
"""
unique = np.unique(conflicts)
old2new = {}
new2old = {}
counter = itertools.count()
N = unique.shape[0]

for i in unique:
    new_id = next(counter)
    old2new[i] = new_id
    new2old[new_id] = i

conflicts_np = np.vectorize(old2new.get)(conflicts_np)

""" Sparse conflict matrix """
conflict_matrix = sp.coo_matrix((np.ones(conflicts_np.shape[0]*2),
                                (np.tile(np.arange(conflicts_np.shape[0]), 2),
                                 conflicts_np.ravel(order='F'))), shape=(conflicts_np.shape[0], N*4))
I, J, V = sp.find(conflict_matrix)

""" Integer Programming """

model = CyClpSimplex()
# 4 colors -> 4 binary vars per element in N
x = model.addVariable('x', N*4, isInt=True)
# scoring: linear-objective
model.objective = -np.hstack((np.full(N, 10), np.full(N, 6), np.full(N, 3), np.full(N, 1)))
# sub-opt way of forcing binary-constraints (from ints)
# (this awkward usage is due to problem with cylp in the past)
model += sp.eye(N*4) * x >= np.zeros(N*4)
model += sp.eye(N*4) * x <= np.ones(N*4)

# conflicts in each color-dimensions
# sub-opt numpy/scipy usage
for ind, i in enumerate(range(4)):
    if ind == 0:
        model += conflict_matrix * x <= 1
    else:
        shifted_conflicts = sp.coo_matrix((V,(I,J+(ind*N))), shape=(conflict_matrix.shape[0], N*4))
        model += shifted_conflicts * x <= 1

# force exactly one color per element
# sub-opt numpy/scipy usage
template = np.zeros(N*4)
template[0] = 1
template[N] = 1
template[2*N] = 1
template[3*N] = 1
all_color_dims = [sp.csc_matrix(np.roll(template, i).reshape(1,-1)) for i in range(N)]
model += sp.vstack(all_color_dims) *x == 1

cbcModel = model.getCbcModel() # Clp -> Cbc model / LP -> MIP
start_time = time()
status = cbcModel.solve()
end_time = time()

print("  CoinOR CBC used {:.{prec}f} secs".format(end_time - start_time, prec=3))
print("  Complete process used {:.{prec}f} secs".format(end_time - START_T, prec=3))

输出

Welcome to the CBC MILP Solver 
Version: 2.9.9 
Build Date: Jan 15 2018 

command line - ICbcModel -solve -quit (default strategy 1)
Continuous objective value is -200 - 0.00 seconds
Cgl0003I 0 fixed, 0 tightened bounds, 20 strengthened rows, 0 substitutions
Cgl0003I 0 fixed, 0 tightened bounds, 24 strengthened rows, 0 substitutions
Cgl0003I 0 fixed, 0 tightened bounds, 16 strengthened rows, 0 substitutions
Cgl0004I processed model has 153 rows, 100 columns (100 integer (100 of which binary)) and 380 elements
Cutoff increment increased from 1e-05 to 0.9999
Cbc0038I Initial state - 0 integers unsatisfied sum - 0
Cbc0038I Solution found of -194
Cbc0038I Before mini branch and bound, 100 integers at bound fixed and 0 continuous
Cbc0038I Mini branch and bound did not improve solution (0.01 seconds)
Cbc0038I After 0.01 seconds - Feasibility pump exiting with objective of -194 - took 0.00 seconds
Cbc0012I Integer solution of -194 found by feasibility pump after 0 iterations and 0 nodes (0.01 seconds)
Cbc0001I Search completed - best objective -194, took 0 iterations and 0 nodes (0.01 seconds)
Cbc0035I Maximum depth 0, 0 variables fixed on reduced cost
Cuts at root node changed objective from -194 to -194
Probing was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
Gomory was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
Knapsack was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
Clique was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
MixedIntegerRounding2 was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
FlowCover was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
TwoMirCuts was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)

Result - Optimal solution found

Objective value:                -194.00000000
Enumerated nodes:               0
Total iterations:               0
Time (CPU seconds):             0.01
Time (Wallclock seconds):       0.01

Total time (CPU seconds):       0.01   (Wallclock seconds):       0.01

  CoinOR CBC used 0.013 secs
  Complete process used 0.042 secs

结果

你的&#34;大&#34; 实例在 0.042秒(子选择代码的完整时间)和 0.013 内解决secs用在核心求解器中。当然这仅仅是一个例子,解释这不是那么科学!

结果与 SaiBot 有趣的定制解决方案(以及较小的示例)相同!

(一些早期的代码有一个得分错误,这让我要求Saibot仔细检查他的解决方案,我现在可以重现!)

转移

MIP解算器应该可用于大多数架构和环境,甚至可能在移动设备上(具有一些潜在的非平凡构建过程)。这些建模/使用取决于建模系统和周围软件。

相关问题