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: 表示一门课与其他课冲突的情况:
    • 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

至此,最小冲突法的介绍及一些简单应用就结束了,更为复杂的客服排排班等,也可用此方法解决。

posted @ 2020-01-02 15:36  GC_AIDM  阅读(892)  评论(0编辑  收藏  举报