Constraint Programming with python-constraint
介绍
在处理约束编程时,我们首先要了解的是,当我们坐下来编写代码时,思维方式与我们通常的思维方式大不相同。
约束编程是声明性编程范例的一个示例,这与我们大多数时候使用的通常的命令式范例相反。
什么是编程范例
范式是指事物的"示例"或"模式"。 编程范例通常被描述为"思维方式"或"编程方式"。 最常见的示例包括过程编程(例如C),面向对象的编程(例如Java)和函数式编程(例如Haskell)。
大多数编程范例都可以归类为命令性或声明性范例组的成员。
命令式和声明式编程之间有什么区别?
简单地说,命令式编程是基于开发人员描述实现目标(某种结果)的解决方案/算法的。 这是通过逐步执行指令的同时通过赋值语句更改程序状态来实现的。 因此,在编写指令的顺序上有很大的不同。
声明式编程则相反-我们不编写有关如何实现目标的步骤,我们不描述目标,而计算机为我们提供了解决方案。 您应该熟悉的一个常见示例是SQL。 您是否告诉计算机如何为您提供所需的结果? 不,您描述了您需要的内容-您需要满足某些条件的某个列,某个表中的值。
安装python-constraint模块
在本文中,我们将使用名为
要安装此模块,请打开终端并运行:
1 | $ pip install python-constraint |
使用python-constraint的基础
这是使用此模块编写的程序的通用框架(注意:我们使用
导入
定义一个变量作为我们的问题
为我们的问题添加变量及其各自的间隔
为我们的问题添加内置/自定义约束
获取解决方案
通过解决方案找到我们需要的解决方案
如前所述,约束编程是声明性编程的一种形式。 语句的顺序无关紧要,只要最后一切都存在即可。 通常用于解决如下问题:
例子A
1 | Find all (x,y) where x ∈ {1,2,3} and 0 <= y < 10, and x + y >= 5 |
如果看这句话,我们可以看到
例如,
看看上面的问题,您可能会想:"那是什么?我可以在不到10分钟的时间内用Python的2个循环和半杯咖啡来做到这一点"。
您是完全正确的,尽管通过此示例,我们可以了解约束编程的外观:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | import constraint problem = constraint.Problem() problem.addVariable('x', [1,2,3]) problem.addVariable('y', range(10)) def our_constraint(x, y): if x + y >= 5: return True problem.addConstraint(our_constraint, ['x','y']) solutions = problem.getSolutions() # Easier way to print and see all solutions # for solution in solutions: # print(solution) # Prettier way to print and see all solutions length = len(solutions) print("(x,y) ∈ {", end="") for index, solution in enumerate(solutions): if index == length - 1: print("({},{})".format(solution['x'], solution['y']), end="") else: print("({},{}),".format(solution['x'], solution['y']), end="") print("}") |
输出:
1 | (x,y) ∈ {(3,9),(3,8),(3,7),(3,6),(3,5),(3,4),(3,3),(3,2),(2,9),(2,8),(2,7),(2,6),(2,5),(2,4),(2,3),(1,9),(1,8),(1,7),(1,6),(1,5),(1,4)} |
让我们逐步介绍该程序。 我们有两个变量,
这两行表示以下内容:
1 | I'm adding a variable x that can only have values [1,2,3], and a variable y that can only have values [0,1,2,..,9] |
接下来,我们定义自定义约束(即
在我们的
定义约束后,我们必须将其添加到问题中。
1 | addConstraint(which_constraint, list_of_variable_order) |
注意:在我们的情况下,写
之后,我们用
注意:例如,如果我们只想获取
1 | problem.addConstraint(constraint.AllDifferentConstraint()) |
您可以在此处找到所有内置约束的列表。 这几乎是您能够执行此类型任务所需的所有知识。
热身的例子
例子B
这是一种有趣的问题约束编程,称为密码算术难题。 在以下形式的算术难题中,每个字符代表一个不同的数字(前导字符不能为0):
1 | TWO + TWO = FOUR |
考虑一下如何使用常规Python解决此问题。 实际上,我鼓励您查找使用命令式编程的问题的解决方案。
它还应该为您提供自行解决示例D所需的全部知识。
请记住," T"和" F"不能为零,因为它们是前导字符,即数字中的第一个数字。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | import constraint problem = constraint.Problem() # We're using .addVariables() this time since we're adding # multiple variables that have the same interval. # Since Strings are arrays of characters we can write #"TF" instead of ['T','F']. problem.addVariables("TF", range(1, 10)) problem.addVariables("WOUR", range(10)) # Telling Python that we need TWO + TWO = FOUR def sum_constraint(t, w, o, f, u, r): if 2*(t*100 + w*10 + o) == f*1000 + o*100 + u*10 + r: return True # Adding our custom constraint. The # order of variables is important! problem.addConstraint(sum_constraint,"TWOFUR") # All the characters must represent different digits, # there's a built-in constraint for that problem.addConstraint(constraint.AllDifferentConstraint()) solutions = problem.getSolutions() print("Number of solutions found: {} ".format(len(solutions))) # .getSolutions() returns a dictionary for s in solutions: print("T = {}, W = {}, O = {}, F = {}, U = {}, R = {}" .format(s['T'], s['W'], s['O'], s['F'], s['U'], s['R'])) |
运行这段代码,我们迎接了可能的解决方案:
1 2 3 4 5 6 7 8 9 | Number of solutions found: 7 T = 7, W = 6, O = 5, F = 1, U = 3, R = 0 T = 7, W = 3, O = 4, F = 1, U = 6, R = 8 T = 8, W = 6, O = 7, F = 1, U = 3, R = 4 T = 8, W = 4, O = 6, F = 1, U = 9, R = 2 T = 8, W = 3, O = 6, F = 1, U = 7, R = 2 T = 9, W = 2, O = 8, F = 1, U = 5, R = 6 T = 9, W = 3, O = 8, F = 1, U = 7, R = 6 |
范例C
1 2 3 | You recently got a job as a cashier. You're trying to convince your friend that it's hard work, there are just SO many ways to give someone their change back! Your"friend" shakes his head, obviously not believing you. He says"It can't be that bad. How many ways can there POSSIBLY be to give someone their change back, for like 60 cents?". Your response is, of course, to sit and quickly write a program that would prove your point. You have a decent amount of pennies (1 cent), nickels (5 cents), dimes (10 cents) and quarters (25 cents), and a lot of kinda suspicious coins worth 3 cents each. Calculate in how many ways you can return change for 60 cents. |
注意:打印结果的顺序不一定与我们添加变量的顺序相同。 也就是说,如果结果为(
因此,我们应该显式打印变量及其值。 这样的结果之一是,我们无法使用内置的
这里的第二个参数是每个变量的"权重"(应该乘以多少次),我们不能保证哪个变量将具有什么权重。
假定权重将按变量添加的顺序分配给变量是一个常见错误,相反,我们在下面的代码中使用此内置约束的三参数形式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | import constraint problem = constraint.Problem() # The maximum amount of each coin type can't be more than 60 # (coin_value*num_of_coints) <= 60 problem.addVariable("1 cent", range(61)) problem.addVariable("3 cent", range(21)) problem.addVariable("5 cent", range(13)) problem.addVariable("10 cent", range(7)) problem.addVariable("20 cent", range(4)) problem.addConstraint( constraint.ExactSumConstraint(60,[1,3,5,10,20]), ["1 cent","3 cent","5 cent","10 cent","20 cent"] ) # Where we explicitly give the order in which the weights should be allocated # We could've used a custom constraint instead, BUT in this case the program will # run slightly slower - this is because built-in functions are optimized and # they find the solution more quickly # def custom_constraint(a, b, c, d, e): # if a + 3*b + 5*c + 10*d + 20*e == 60: # return True # problem.addConstraint(o, ["1 cent","3 cent","5 cent","10 cent","20 cent"]) # A function that prints out the amount of each coin # in every acceptable combination def print_solutions(solutions): for s in sols: print("---") print(""" 1 cent: {0:d} 3 cent: {1:d} 5 cent: {2:d} 10 cent: {3:d} 20 cent: {4:d}""".format(s["1 cent"], s["3 cent"], s["5 cent"], s["10 cent"], s["20 cent"])) # If we wanted to we could check whether the sum was really 60 # print("Total:", s["1 cent"] + s["3 cent"]*3 + s["5 cent"]*5 + s["10 cent"]*10 + s["20 cent"]*20) # print("---") solutions = problem.getSolutions() #print_solutions(solutions) print("Total number of ways: {}".format(len(solutions))) |
运行这段代码将产生:
1 | Total number of ways: 535 |
例子D
1 | CRASH + ERROR + REBOOT = HACKER |
使用约束时,示例B和D几乎相同,只是上下几个变量,更冗长的约束。 关于约束编程,这是一件好事-良好的可伸缩性,至少在花费时间进行编码时如此。 这个难题只有一个解决方案,它是A = 3 B = 7 C = 8 E = 2 H = 6 K = 0 O = 1 R = 5 S = 9 T = 4。
这两种类型的任务(尤其是密码算法)都被更多地用于娱乐和轻松演示约束编程的工作原理,但是在某些情况下约束编程具有实用价值。
我们可以计算出覆盖某个区域的最小广播电台数量,或者找出如何设置交通信号灯以使交通流量最佳。 一般而言-在存在很多可能组合的地方使用约束。
这些示例对于本文的范围而言太复杂了,但是可以证明约束编程可以在现实世界中使用。
更难的例子
例E
1 2 3 4 5 | You wish to pack chocolates for your mother. Luckily you work in a chocolate factory that has a lot of leftover chocolate. You have a few chocolate types at your disposal. Your goal is to bring her the sweetest chocolate possible, that you can pack in your bag and sneak through security, and that wouldn't pass a certain net value for which you'd go to prison if you got caught. Security most likely won't get suspicious if you bring less than 3kg. You can fit 1 dm^3 of chocolate in your bag. You won't go to jail if you steal less than $300 worth of chocolate. |
现在,让我们卷起袖子,开始学习。 如果您理解了前面的示例,应该不会太困难。
首先,我们要弄清楚如果只带那种类型的巧克力,我们可以得到多少,这样我们就可以确定区间的上限。 例如,对于巧克力A,基于重量,我们最多可以带30条;基于价值,我们最多可以带37条;基于体积,我们最多可以带100条。
这些数字中的最小数字是30,这是我们可以带来的巧克力A的最大数量。 相同的步骤为我们提供了其余的最大数量,B-> 44,C-> 75,D-> 100。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | import constraint problem = constraint.Problem() problem.addVariable('A', range(31)) problem.addVariable('B', range(45)) problem.addVariable('C', range(76)) problem.addVariable('D', range(101)) # We have 3kg = 3,000g available def weight_constraint(a, b, c, d): if (a*100 + b*45 + c*10 + d*25) <= 3000: return True # We have 1dm^3 = 1,000cm^3 available def volume_constraint(a, b, c, d): if (a*8*2.5*0.5 + b*6*2*0.5 * c*2*2*0.5 + d*3*3*0.5) <= 1000: return True # We can't exceed $300 def value_constraint(a, b, c, d): if (a*8 + b*6.8 + c*4 + d*3) < 300: return True problem.addConstraint(weight_constraint,"ABCD") problem.addConstraint(volume_constraint,"ABCD") problem.addConstraint(value_constraint,"ABCD") maximum_sweetness = 0 solution_found = {} solutions = problem.getSolutions() for s in solutions: current_sweetness = s['A']*10 + s['B']*8 + s['C']*4.5 + s['D']*3.5 if current_sweetness > maximum_sweetness: maximum_sweetness = current_sweetness solution_found = s print(""" The maximum sweetness we can bring is: {} We'll bring: {} A Chocolates, {} B Chocolates, {} C Chocolates, {} D Chocolates """.format(maximum_sweetness, solution_found['A'], solution_found['B'], solution_found['C'], solution_found['D'])) |
运行这段代码将产生:
1 2 3 4 5 6 | The maximum sweetness we can bring is: 365.0 We'll bring: 27 A Chocolates, 2 B Chocolates, 16 C Chocolates, 2 D Chocolates |
注意:我们可以将每种巧克力类型的所有相关信息存储在字典中,例如
注意:您可能已经注意到,要花一些时间才能计算出此结果,这是约束编程的一个缺点。
例子F
现在,为了获得更多乐趣,我们来制作数独(经典的9x9)求解器。 我们将从JSON文件中读取该难题,并找到该难题的所有解决方案(假设该难题有解决方案)。
如果您忘记了解决数独的规则:
单元格的值可以为1-9
同一行中的所有单元格必须具有不同的值
同一列中的所有单元格必须具有不同的值
3x3正方形(总计9个)中的所有像元必须具有不同的值
该程序的问题之一是-我们如何存储值? 我们不能只是将矩阵作为变量添加到问题中,而让Python神奇地找出我们想要的东西。
我们将使用一个将像变量名一样的数字作为数字的系统(允许),并假装我们有一个矩阵。 索引从(1,1)开始,而不是通常的(0,0)。 使用它,我们将以惯常的方式访问板的元素。
接下来,我们需要简单地告诉Python所有这些单元格的值都可以在1到9之间。
然后我们注意到同一行中的单元格具有相同的第一个索引,例如 (1,x)为第一行。 我们可以轻松地遍历所有行,并说所有单元格必须包含不同的值。 列也是如此。 查看代码时,其他内容更容易理解。
让我们看一个示例JSON文件:
1 2 3 4 5 6 7 8 9 | [[0, 9, 0, 7, 0, 0, 8, 6, 0], [0, 3, 1, 0, 0, 5, 0, 2, 0], [8, 0, 6, 0, 0, 0, 0, 0, 0], [0, 0, 7, 0, 5, 0, 0, 0, 6], [0, 0, 0, 3, 0, 7, 0, 0, 0], [5, 0, 0, 0, 1, 0, 7, 0, 0], [0, 0, 0, 0, 0, 0, 1, 0, 9], [0, 2, 0, 6, 0, 0, 0, 5, 0], [0, 5, 4, 0, 0, 8, 0, 7, 0]] |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | # 1 - - - - - - - - - # 2 - - - - - - - - - # 3 - - - - - - - - - # 4 - - - - - - - - - # 5 - - - - - - - - - # 6 - - - - - - - - - # 7 - - - - - - - - - # 8 - - - - - - - - - # 9 - - - - - - - - - # 1 2 3 4 5 6 7 8 9 import constraint import json problem = constraint.Problem() # We're letting VARIABLES 11 through 99 have an interval of [1..9] for i in range(1, 10): problem.addVariables(range(i * 10 + 1, i * 10 + 10), range(1, 10)) # We're adding the constraint that all values in a row must be different # 11 through 19 must be different, 21 through 29 must be all different,... for i in range(1, 10): problem.addConstraint(constraint.AllDifferentConstraint(), range(i * 10 + 1, i * 10 + 10)) # Also all values in a column must be different # 11,21,31...91 must be different, also 12,22,32...92 must be different,... for i in range(1, 10): problem.addConstraint(constraint.AllDifferentConstraint(), range(10 + i, 100 + i, 10)) # The last rule in a sudoku 9x9 puzzle is that those nine 3x3 squares must have all different values, # we start off by noting that each square"starts" at row indices 1, 4, 7 for i in [1,4,7]: # Then we note that it's the same for columns, the squares start at indices 1, 4, 7 as well # basically one square starts at 11, the other at 14, another at 41, etc for j in [1,4,7]: square = [10*i+j,10*i+j+1,10*i+j+2,10*(i+1)+j,10*(i+1)+j+1,10*(i+1)+j+2,10*(i+2)+j,10*(i+2)+j+1,10*(i+2)+j+2] # As an example, for i = 1 and j = 1 (bottom left square), the cells 11,12,13, # 21,22,23, 31,32,33 have to be all different problem.addConstraint(constraint.AllDifferentConstraint(), square) file_name = input("Enter the name of the .json file containing the sudoku puzzle:") try: f = open(file_name,"r") board = json.load(f) f.close() except IOError: print ("Couldn't open file.") sys.exit() # We're adding a constraint for each number on the board (0 is an"empty" cell), # Since they're already solved, we don't need to solve them for i in range(9): for j in range(9): if board[i][j] != 0: def c(variable_value, value_in_table = board[i][j]): if variable_value == value_in_table: return True # Basically we're making sure that our program doesn't change the values already on the board # By telling it that the values NEED to equal the corresponding ones at the base board problem.addConstraint(c, [((i+1)*10 + (j+1))]) solutions = problem.getSolutions() for s in solutions: print("==================") for i in range(1,10): print("|", end='') for j in range(1,10): if j%3 == 0: print(str(s[i*10+j])+" |", end='') else: print(str(s[i*10+j]), end='') print("") if i%3 == 0 and i!=9: print("------------------") print("==================") if len(solutions) == 0: print("No solutions found.") |
输出(当我们使用示例JSON文件作为输入时):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | ================== |295 | 743 | 861 | |431 | 865 | 927 | |876 | 192 | 345 | ------------------ |387 | 459 | 216 | |612 | 387 | 594 | |549 | 216 | 783 | ------------------ |768 | 524 | 139 | |923 | 671 | 458 | |154 | 938 | 672 | ================== ================== |295 | 743 | 861 | |431 | 865 | 927 | |876 | 192 | 345 | ------------------ |387 | 459 | 216 | |612 | 387 | 594 | |549 | 216 | 738 | ------------------ |763 | 524 | 189 | |928 | 671 | 453 | |154 | 938 | 672 | ================== ================== |295 | 743 | 861 | |431 | 865 | 927 | |876 | 192 | 543 | ------------------ |387 | 459 | 216 | |612 | 387 | 495 | |549 | 216 | 738 | ------------------ |763 | 524 | 189 | |928 | 671 | 354 | |154 | 938 | 672 | ================== |
注意:可以很容易地忽略代码部分,以确保我们不会碰到难题中已经存在的值。
如果我们尝试运行不带该部分的代码,则该程序将尝试并提出所有可想象的数独拼图。 这也可能是一个无休止的循环。
结论和缺点
约束编程既有趣又与众不同,当然也有其缺点。 使用约束编程解决的每个问题都可以用命令式的方式编写,其运行时间相等或更好(在大多数情况下)。
开发人员对问题的理解比对
我最近有一个现实的例子。 我的一个朋友(几个月前才了解Python的存在)需要解决她正在从事的一项物理化学研究项目的问题。
那个朋友需要解决以下问题:
1 2 3 4 5 6 | Generate all combinations (that have a length equal to the number of keys) of values stored in a dictionary (the order of output doesn't matter). The dictionary is {String : List_of_Strings}. In such a way that every combination has exactly one value from the List_of_Strings of a key. You don't know the number of keys in the dictionary in advance, nor do you know how long a List_of_String is, every List_of_String can be of different length. I.e. the dictionary is dynamically generated via user input. Example input: dictionary = {"A" : [1,2],"B" -> [4],"C" -> [5,6,7],"D" -> [8,9]} Example output: (1,4,5,8), (1,4,5,8), (1,4,6,8), (1,4,6,9), (1,4,7,8).... |
尝试思考如何使用命令式编程解决此问题。
我什至无法想到一个当务之急的好的解决方案。 至少在5分钟内,我花了几行代码就解决了约束编程中的问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import constraint # input example generated_dictionary = {'A' : [1,2], 'B' : [4], 'C' : [5,6,7], 'D' : [8,9]} problem = constraint.Problem() for key, value in generated_dictionary.items(): problem.addVariable(key, value) solutions = problem.getSolutions() for solution in solutions: print(solution) |
而已。 我们只是没有添加任何约束,程序为我们生成了所有可接受的组合。 在她的情况下,该程序在运行时的最小差异与它的编写速度和可读性无关紧要。
还有一点要注意的是,
实现了回溯(和递归回溯)功能,以及基于最小冲突理论的问题解决器。 可以将它们作为参数传递给
内置约束列表
当使用可以将乘数列表作为参数的约束(例如
结论
就可读性和易于开发某些程序而言,约束编程是惊人的,但是这样做却以运行时为代价。 对于特定问题,由开发者决定哪个对他/她更重要。