Mini-conflict的介绍与简单应用
最近接触到为客户的客服排班的需求,之前根据客户的需求,同事已经完成了自动排班系统,需要我继续支撑的是做一些优化即可。当我接触到这个项目之后,我便联想到以前所学的CSP最小冲突法或许可以解决排班问题。在这里,想要介绍一下这种方法。
CSP最小冲突法
CSP最小冲突法的主要思想是,找到满足约束条件的情况。
主要步骤:
- 初始化一个状态,根据使冲突最小的原则,改变一个变量的取值(如果存在多个最小冲突相等,则根据概率等选择)
- 不断重复此步骤,知道找到满足约束条件的最优解。
- 但是,根据大量前人实验,最小冲突法可能无法找到最优解,陷入僵局,此时,需要重新生成初始解
具体的算法代码,我们可以使用这本书上的 Artificial Intelligence: A Modern Approach,有兴趣的也可以看他的GitHub
import random
def min_conflicts(vars, domains, constraints, neighbors, max_steps=1000):
"""Solve a CSP by stochastic hillclimbing on the number of conflicts."""
# Generate a complete assignment for all vars (probably with conflicts)
current = {}
for var in vars:
val = min_conflicts_value(var, current, domains, constraints, neighbors)
current[var] = val
# Now repeatedly choose a random conflicted variable and change it
for i in range(max_steps):
conflicted = conflicted_vars(current,vars,constraints,neighbors)
if not conflicted:
return (current,i)
var = random.choice(conflicted)
val = min_conflicts_value(var, current, domains, constraints, neighbors)
current[var] = val
return (None,None)
def min_conflicts_value(var, current, domains, constraints, neighbors):
"""Return the value that will give var the least number of conflicts.
If there is a tie, choose at random."""
return argmin_random_tie(domains[var],
lambda val: nconflicts(var, val, current, constraints, neighbors))
def conflicted_vars(current,vars,constraints,neighbors):
"Return a list of variables in current assignment that are in conflict"
return [var for var in vars
if nconflicts(var, current[var], current, constraints, neighbors) > 0]
def nconflicts(var, val, assignment, constraints, neighbors):
"Return the number of conflicts var=val has with other variables."
# Subclasses may implement this more efficiently
def conflict(var2):
val2 = assignment.get(var2, None)
return val2 != None and not constraints(var, val, var2, val2)
return len(list(filter(conflict, neighbors[var])))
def argmin_random_tie(seq, fn):
"""Return an element with lowest fn(seq[i]) score; break ties at random.
Thus, for all s,f: argmin_random_tie(s, f) in argmin_list(s, f)"""
best_score = fn(seq[0]); n = 0
for x in seq:
x_score = fn(x)
if x_score < best_score:
best, best_score = x, x_score; n = 1
elif x_score == best_score:
n += 1
if random.randrange(n) == 0:
best = x
return best
上面是书中作者写好的几个方程,而我们要做的就是利用这几个方程,去解决实际的问题。下面,就让我们先从最简单的八皇后问题看起。
八皇后
说起CSP最小冲突法,就要说很著名的八皇后问题,具体的描述可以点击:八皇后
先定义我们的八个皇后,这里我们可以用0-7八个数字表示
vars = range(8)
再定义一下,八个皇后对应可以移动的区域,这里我们可以用字典型数据表示
domains = {key: range(8) for key in vars}
print(domains)
- {0: range(0, 8),
1: range(0, 8),
2: range(0, 8),
3: range(0, 8),
4: range(0, 8),
5: range(0, 8),
6: range(0, 8),
7: range(0, 8)}
接下来,既然我们要使用最小冲突法,那么就要确定冲突值。
- 根据问题描述,这里我们定义每个皇后跟其他皇后冲突,则,我们可以得到冲突值,这里用neighbors表示这些值
neighbors = {var: [v for v in vars if v != var] for var in vars}
print(neighbors)
- {0: [1, 2, 3, 4, 5, 6, 7],
1: [0, 2, 3, 4, 5, 6, 7],
2: [0, 1, 3, 4, 5, 6, 7],
3: [0, 1, 2, 4, 5, 6, 7],
4: [0, 1, 2, 3, 5, 6, 7],
5: [0, 1, 2, 3, 4, 6, 7],
6: [0, 1, 2, 3, 4, 5, 7],
7: [0, 1, 2, 3, 4, 5, 6]} - 同样使用字典类型,key表示皇后,key对应的值表示此皇后的neighbors,即为此皇后的冲突。
下面,我们需要给一些约束条件,这些即帮助最小冲突法去迭代运算。
- 我们定义约束条件函数会 return ture,如果两个皇后直接满足我们所定义的约束条件。即如果两个皇后不在同一条竖线,对角线上,则返回true
def constraints_ok(col1, row1, col2, row2):
return (row1 != row2 and
col1+row1 != col2+row2 and
col1-row1 != col2-row2 and
col1 != col2)
最后,当我们需要测试这些代码是否正确,那么如何保证对呢,这里我们就需要可视化展示了。
- 在python中,有一个皇冠字符,我们用它来表示皇后
print('\u265b')
- ♛
- 这里我们写一个可视化展示函数
def display(assignment):
for row in range(8):
for col in range(8):
if assignment[col] == row:
print('\u265b', end='')
else:
print('--', end='')
print()
测试一下这个可视化展示函数,可以使用一下代码测试
display({0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7:7})
♛--------------
--♛------------
----♛----------
------♛--------
--------♛------
----------♛----
------------♛--
--------------♛
接下来,就可以愉快的测试我们的函数,算法了
solution, steps = min_conflicts(vars, domains, constraints_ok, neighbors)
print('Solution found in', steps, 'steps')
display(solution)
这里,我放上一种展示结果
----------♛----
♛--------------
--------♛------
--♛------------
--------------♛
----♛----------
------------♛--
------♛--------
至此,我们对八皇后的测试就结束了,当然,这里都是通用的,你也可以说设置N个皇后,只需要改遍var的值范围就可以了。
排课表
简单的八皇后问题,我们已经解决了,那么复杂一点的排课表呢?
问题描述:
- 我们有三个教室:(CSB130, CSB325, CSB425)
- 每节课一个小时,从早上九点到晚上四点(9am,10am,11am,12am,1pm,2pm,3pm,4pm)
- 我们需要把这22节课排出一个可用的课表:
CS160, CS163, CS164,
CS220, CS270, CS253,
CS320, CS314, CS356, CS370,
CS410, CS414, CS420, CS430, CS440, CS445, CS453, CS464,
CS510, CS514, CS535, CS540, CS545 - 约束条件:
- 不能两节课同时出现在一间教室中
- 课程第一个数字相同的课不能出现在同一时间(例如:1开头的课不能跟1开头的其他课排在一个时间)
- 有个例外,163跟164可以同时出现。
首先,定义初始变量
classes = ['CS160', 'CS163', 'CS164',
'CS220', 'CS270', 'CS253',
'CS320', 'CS314', 'CS356', 'CS370',
'CS410', 'CS414', 'CS420', 'CS430', 'CS440', 'CS445', 'CS453', 'CS464',
'CS510', 'CS514', 'CS535', 'CS540', 'CS545']
times = [' 9 am','10 am', '11 am','12 pm',' 1 pm',' 2 pm',' 3 pm', '4 pm']
rooms = ['CSB 130', 'CSB 325', 'CSB 425']
接下来,我们依然要使用上述的算法方程,需要我们自己写的只有三个方程
- schedule:迭代运算,给出最终课表
- constraints_ok:约束条件方程
- display:可视化,展示结果
这里对每个方程里面的参数进行一下简单的说明:
- solution, steps = schedule(classes, times, rooms, max_steps)
- classes: 所有课名字的集合,例如 “CS410” 等。这里建议用把 classes 设置成 list 型。
- times: 所有时间的集合,例如 “10 am” 等。这里建议把 times 设置成 list 型。
- room:所有教室的集合,例如 “CSB325” 等。这里建议把 room 设置为 list 型。
- solution, steps = min_conflicts(classes, domains, constraints_ok, neighbors, max_steps=1000)
- domains:这个与我们在解决皇后问题时,domains 的作用一致。它表述了,每节课对应的教室与时间的可能性。
- 设置为一个字典,其中每节课作为一个key输入,对应的值是一个list,其中list是由若干个tuple组成,每个tuple代表一种可能的教室与时间的组合。
- 例如,
- neighbors: 表示一门课与其他课冲突的情况:
- domains:这个与我们在解决皇后问题时,domains 的作用一致。它表述了,每节课对应的教室与时间的可能性。
- solution: 表示结果,这里solution是个字典型数据,key表示每节课,对应的值是一个tuple,表示其对应的教室与时间的解。
- result = constraints_ok(class_name_1, value_1, class_name_2, value_2)
- class_name_1: 代表第一个课程,例如 'CS410'
- class_name_: 代表第二个课程,例如 'CS420'
- value_1:第一个课程对应的教室与时间,用tuple表示,(CSB325,10am)
- value_2:第二个课程对应的教室与时间,用tuple表示,(CSB325,10am)
- return:如果课程一,跟课程二不冲突,(即value_1跟value_2不冲突)就返回Ture,否则返回False
- display(solution, rooms, times)
- 用于展示课程表。
- 不再细说(可以选择制成 Dataframe,或者直接打印也可以)
下面,展示一下我写的几个方程:
def schedule(classes, times, rooms, max_steps):
domains = {}
neighbors = {}
for classname in classes:
# define neighbors
classbridge = copy.deepcopy(classes)
classbridge.remove(classname)
neighbors[classname] = classbridge
# define assigments
domains[classname]=[]
for time in times:
for roomname in rooms:
domains[classname].append((roomname,time))
solution, steps = min_conflicts(classes, domains, constraints_ok, neighbors, max_steps=1000)
return solution, steps
def constraints_ok(class_name_1, value_1, class_name_2, value_2):
if (class_name_1=='CS163' and class_name_2=='CS164') or (class_name_1=='CS164' and class_name_2=='CS163'):
if value_1[0]!= value_2[0] or value_1[1] != value_2[1]:
return True
else:
return False
if class_name_1[2] == class_name_2[2]:
if value_1[1] != value_2[1]:
return True
else:
if value_1[0]!= value_2[0] or value_1[1] != value_2[1]:
return True
return False
def display(assignments, rooms, times):
data={}
data[' ']=[]
for time in times:
data[' '].append(time)
for roomname in rooms:
data[roomname]=[]
for time in times:
i = 0
for assignment in assignments:
if assignments[assignment][0]==roomname and assignments[assignment][1]==time:
data[roomname].append(assignment)
break
if i == 22:
data[roomname].append(' ')
i+=1
frame = pd.DataFrame(data)
print(frame)
max_steps = 100
solution, steps = schedule(classes, times, rooms, max_steps)
print('Took', steps, 'steps')
display(solution, rooms, times)
运行上面的语句,测试代码,可以看到具体的展示,这里由于是随机初始化,生成的解每次都是不同的,下面展示一种情况:
CSB 130 CSB 325 CSB 425
0 9 am CS535 CS410 CS164
1 10 am CS356 CS420 CS270
2 11 am CS414 CS514
3 12 pm CS314 CS545 CS453
4 1 pm CS510 CS220 CS440
5 2 pm CS540 CS430 CS370
6 3 pm CS464 CS160 CS253
7 4 pm CS163 CS320 CS445
至此,最小冲突法的介绍及一些简单应用就结束了,更为复杂的客服排排班等,也可用此方法解决。