增加了子集匹配维度的区间树?

时间:2017-06-16 12:56:13

标签: algorithm language-agnostic interval-tree

这是一个关于某个复杂问题的算法问题。基础是:

基于可用插槽保留插槽的调度系统。插槽有一定的标准,我们称之为标签。如果可用的插槽标签集是保留插槽的超集,则预留与这些标签的可用插槽匹配。

作为具体示例,请采用以下方案:

11:00  12:00  13:00
+--------+
| A, B   |
+--------+
       +--------+
       | C, D   |
       +--------+

在11:00到12:30之间,可以预订标签AB,从12:00到13:30 C和{{1可用,并且从大约12:00到12:30有重叠。

D

此处已对11:00 12:00 13:00 +--------+ | A, B | +--------+ +--------+ | C, D | +--------+ xxxxxx x A x xxxxxx 进行了预订,因此在{11}和12:00-ish之间无法对AA进行任何其他预订。

简而言之,这就是一个想法。可用插槽没有特定限制:

  • 可用的插槽可以包含任意数量的标签
  • 任意数量的插槽可以随时重叠
  • 插槽具有任意长度
  • 预订可以包含任意数量的标签

系统中唯一需要遵守的规则是:

  • 添加预订时,至少有一个剩余可用空档必须与预订中的所有标记匹配

澄清一下:如果同时有两个可用的广告位,例如标记为B,则可以在此时针对A进行两次预订,但不能再进行。{/ p >

我使用interval tree的修改后的实现;作为快速概述:

  • 将所有可用的插槽添加到间隔树中(保留重复/重叠)
  • 迭代所有保留的插槽,并且:
    • 从树中查询所有与预约时间相匹配的可用插槽
    • 匹配预订标签的第一个标签被切片并且切片从树中删除

当该过程完成后,剩下的剩余可用插槽片段,我可以查询是否可以在特定时间内进行新预订并添加。

数据结构如下所示:

A

标签本身就是键值对象,它们的列表是"标签集"。如果它有帮助,可以将它们序列化;到目前为止,我使用的是Python { type: 'available', begin: 1497857244, end: 1497858244, tags: [{ foo: 'bar' }, { baz: 42 }] } { type: 'reserved', begin: 1497857345, end: 1497857210, tags: [{ foo: 'bar' }] } 类型,这使得比较变得非常简单。插槽开始/结束时间是树中的UNIX时间戳。我并没有特别嫁给这些特定的数据结构,如果它有用,可以重构它们。

我面临的问题是,这不会无错误地工作;每隔一段时间,预订就会潜入系统,与其他预订相冲突,我还无法弄清楚这是怎么发生的。当标签以复杂的方式重叠时,它也不是很聪明,其中需要计算最佳分布,因此所有预留都可以尽可能地适合可用的时隙;实际上,目前在重叠场景中如何将预留与可用时段匹配是不确定的。

我想知道的是:间隔树大多数都是出于此目的,但我目前的系统添加标签集匹配作为一个额外的维度是笨重和螺栓连接; 是否有可以优雅地处理此问题的数据结构或算法?

必须支持的行动:

  1. 向系统查询与某些标记集匹配的可用插槽(考虑可能降低可用性但本身不属于所述标记集的预留;例如,在上面的示例中查询set的可用性)。
  2. 确保没有预订可以添加到没有匹配可用插槽的系统中。

3 个答案:

答案 0 :(得分:8)

使用constraint programming可以解决您的问题。在python中,这可以使用python-constraint库来实现。

首先,我们需要一种方法来检查两个插槽是否彼此一致。如果两个槽共享一个标签并且它们的rimeframe重叠,则这个函数返回true。在python中,这可以使用以下函数

来实现
def checkNoOverlap(slot1, slot2):
    shareTags = False
    for tag in slot1['tags']:
        if tag in slot2['tags']:
            shareTags = True
            break    
    if not shareTags: return True
    return not (slot2['begin'] <= slot1['begin'] <= slot2['end'] or 
                slot2['begin'] <= slot1['end'] <= slot2['end'])

我不确定你是否希望标签完全相同(如{foo:bar}等于{foo:bar})或只是键(如{foo:bar}等于{foo:qux}),但你可以在上面的函数中改变它。

一致性检查

我们可以将python-constraint模块用于您请求的两种功能。

第二个功能是最简单的。为了实现这一点,我们可以使用函数isConsistent(set),它将提供的数据结构中的插槽列表作为输入。然后,该函数将所有插槽提供给python-constraint,并检查插槽列表是否一致(没有2个插槽不应重叠,重叠)并返回一致性。

def isConsistent(set):
        #initialize python-constraint context
        problem = Problem()
        #add all slots the context as variables with a singleton domain
        for i in range(len(set)):
            problem.addVariable(i, [set[i]])        
        #add a constraint for each possible pair of slots
        for i in range(len(set)):
            for j in range(len(set)):
                #we don't want slots to be checked against themselves
                if i == j:
                    continue
                #this constraint uses the checkNoOverlap function
                problem.addConstraint(lambda a,b: checkNoOverlap(a, b), (i, j))
        # getSolutions returns all the possible combinations of domain elements
        # because all domains are singleton, this either returns a list with length 1 (consistent) or 0 (inconsistent)
        return not len(problem.getSolutions()) == 0

只要用户想要添加预订位置,就可以调用此功能。输入槽可以添加到现有槽的列表中,并且可以检查一致性。如果它是一致的,则重新保留新的插槽。否则,新的插槽会重叠,应该被拒绝。

查找可用的插槽

这个问题有点棘手。我们可以使用与上面相同的功能,并进行一些重大更改。我们现在想要将所有可能的插槽添加到现有插槽中,而不是将新插槽与现有插槽一起添加。然后,我们可以检查所有可能的插槽与保留插槽的一致性,并向约束系统询问一致的组合。

因为如果我们没有对它施加任何限制,可能的插槽数将是无限的,我们首先需要为程序声明一些参数:

MIN = 149780000 #available time slots can never start earlier then this time
MAX = 149790000 #available time slots can never start later then this time
GRANULARITY = 1*60 #possible time slots are always at least one minut different from each other

我们现在可以继续主要功能了。它看起来很像一致性检查,但我们现在添加一个变量来发现所有可用的插槽,而不是用户的新插槽。

def availableSlots(tags, set):
    #same as above
    problem = Problem()
    for i in range(len(set)):
        problem.addVariable(i, [set[i]])
    #add an extra variable for the available slot is added, with a domain of all possible slots
    problem.addVariable(len(set), generatePossibleSlots(MIN, MAX, GRANULARITY, tags))
    for i in range(len(set) +1):
        for j in range(len(set) +1):
            if i == j:
                continue
            problem.addConstraint(lambda a, b: checkNoOverlap(a, b), (i, j))
    #extract the available time slots from the solution for clean output
    return filterAvailableSlots(problem.getSolutions())

我使用一些辅助函数来保持代码更清晰。它们包含在这里。

def filterAvailableSlots(possibleCombinations):
    result = []
    for slots in possibleCombinations:
        for key, slot in slots.items():
            if slot['type'] == 'available':
                result.append(slot)

    return result

def generatePossibleSlots(min, max, granularity, tags):
    possibilities = []
    for i in range(min, max - 1, granularity):
        for j in range(i + 1, max, granularity):
            possibleSlot = {
                              'type': 'available',
                              'begin': i,
                              'end': j,
                              'tags': tags
            }
            possibilities.append(possibleSlot)
    return tuple(possibilities)

现在,您可以将函数getAvailableSlots(tags,set)与您想要可用插槽的标签和一组已预留的插槽一起使用。请注意,此函数确实返回所有一致的可能插槽,因此不需要查找最大长度或其他最优化的插槽。

希望这有帮助! (我按照你在pycharm中描述的那样开始工作)

答案 1 :(得分:4)

这是一个解决方案,我将在下面包含所有代码。

1。创建一个槽表和一个预订表

example tables

2。创建预订矩阵x插槽

根据是否可以使用预留槽组合

填充真值或假值

example boolean combinations matrix

3。找出允许最多Reservation-Slot Combinations

的最佳映射

注意:我当前的解决方案在非常大的数组中扩展性很差,因为它涉及循环遍历列表大小=插槽数的所有可能的排列。我发布了another question,看看是否有人能找到更好的方法。但是,此解决方案是准确的,可以进行优化

enter image description here

Python代码源

第1部分

from IPython.display import display
import pandas as pd
import datetime

available_data = [
    ['SlotA', datetime.time(11, 0, 0), datetime.time(12, 30, 0), set(list('ABD'))],
    ['SlotB',datetime.time(12, 0, 0), datetime.time(13, 30, 0), set(list('C'))],
    ['SlotC',datetime.time(12, 0, 0), datetime.time(13, 30, 0), set(list('ABCD'))],
    ['SlotD',datetime.time(12, 0, 0), datetime.time(13, 30, 0), set(list('AD'))],
]

reservation_data = [
    ['ReservationA', datetime.time(11, 15, 0), datetime.time(12, 15, 0), set(list('AD'))],
    ['ReservationB', datetime.time(11, 15, 0), datetime.time(12, 15, 0), set(list('A'))],
    ['ReservationC', datetime.time(12, 0, 0), datetime.time(12, 15, 0), set(list('C'))],
    ['ReservationD', datetime.time(12, 0, 0), datetime.time(12, 15, 0), set(list('C'))],
    ['ReservationE', datetime.time(12, 0, 0), datetime.time(12, 15, 0), set(list('D'))]
]

reservations = pd.DataFrame(data=reservation_data, columns=['reservations', 'begin', 'end', 'tags']).set_index('reservations')
slots = pd.DataFrame(data=available_data, columns=['slots', 'begin', 'end', 'tags']).set_index('slots')

display(slots)
display(reservations)

第2部分

def is_possible_combination(r):
    return (r['begin'] >= slots['begin']) & (r['end'] <= slots['end']) & (r['tags'] <= slots['tags'])

solution_matrix = reservations.apply(is_possible_combination, axis=1).astype(int)
display(solution_matrix)

第3部分

import numpy as np
from itertools import permutations

# add dummy columns to make the matrix square if it is not
sqr_matrix = solution_matrix
if sqr_matrix.shape[0] > sqr_matrix.shape[1]:
    # uhoh, there are more reservations than slots... this can't be good
    for i in range(sqr_matrix.shape[0] - sqr_matrix.shape[1]):
        sqr_matrix.loc[:,'FakeSlot' + str(i)] = [1] * sqr_matrix.shape[0]
elif sqr_matrix.shape[0] < sqr_matrix.shape[1]:
    # there are more slots than customers, why doesn't anyone like us?
    for i in range(sqr_matrix.shape[0] - sqr_matrix.shape[1]):
        sqr_matrix.loc['FakeCustomer' + str(i)] = [1] * sqr_matrix.shape[1]

# we only want the values now
A = solution_matrix.values.astype(int)

# make an identity matrix (the perfect map)
imatrix = np.diag([1]*A.shape[0])

# randomly swap columns on the identity matrix until they match. 
n = A.shape[0]

# this will hold the map that works the best
best_map_so_far = np.zeros([1,1])

for column_order in permutations(range(n)):
    # this is an identity matrix with the columns swapped according to the permutation
    imatrix = np.zeros(A.shape)
    for row, column in enumerate(column_order):
        imatrix[row,column] = 1

    # is this map better than the previous best?
    if sum(sum(imatrix * A)) > sum(sum(best_map_so_far)):
        best_map_so_far = imatrix

    # could it be? a perfect map??
    if sum(sum(imatrix * A)) == n:
        break

if sum(sum(imatrix * A)) != n:
    print('a perfect map was not found')

output = pd.DataFrame(A*imatrix, columns=solution_matrix.columns, index=solution_matrix.index, dtype=int)
display(output)

答案 2 :(得分:0)

Arnetinker建议的方法既有帮助,但最终还不够。我想出了一种能够很好地解决它的混合方法。

主要问题是它是一个三维问题,难以立即解决所有方面的问题。它不仅仅是匹配时间重叠或标签重叠,而是将时间片与标签重叠相匹配。基于时间和甚至标签将插槽与其他插槽匹配非常简单,但是在另一时间将已经匹配的可用性插槽与另一个预留匹配相当复杂。这意味着,一种可用性可以在不同时间覆盖两个预留:

+---------+
| A, B    |
+---------+
xxxxx xxxxx
x A x x A x
xxxxx xxxxx

尝试将其纳入基于约束的编程需要非常复杂的约束关系,这是难以管理的。我的解决方案是简化问题......

删除一个维度

它不是一次解决所有维度,而是极大地简化了问题,大大消除了时间维度。我通过使用现有的区间树并根据需要对其进行切片来完成此操作:

def __init__(self, slots):
    self.tree = IntervalTree(slots)

def timeslot_is_available(self, start: datetime, end: datetime, attributes: set):
    candidate = Slot(start.timestamp(), end.timestamp(), dict(type=SlotType.RESERVED, attributes=attributes))
    slots = list(self.tree[start.timestamp():end.timestamp()])
    return self.model_is_consistent(slots + [candidate])

要查询特定插槽是否可用,我只采用在该特定时间(self.tree[..:..])相关的插槽,这会将计算的复杂性降低到本地化子集:

  |      |             +-+ = availability
+-|------|-+           xxx = reservation
  |  +---|------+
xx|x  xxx|x
  |  xxxx|
  |      |

然后我确认该狭窄片段内的一致性:

@staticmethod
def model_is_consistent(slots):
    def can_handle(r):
        return lambda a: r.attributes <= a.attributes and a.contains_interval(r)

    av = [s for s in slots if s.type == SlotType.AVAILABLE]
    rs = [s for s in slots if s.type == SlotType.RESERVED]

    p = Problem()
    p.addConstraint(AllDifferentConstraint())
    p.addVariables(range(len(rs)), av)

    for i, r in enumerate(rs):
        p.addConstraint(can_handle(r), (i,))

    return p.getSolution() is not None

(我在这里省略了一些优化和其他代码。)

这部分是Arne和Tinker建议的混合方法。它使用基于约束的编程来找到匹配的槽,使用tinker建议的矩阵算法。基本上:如果有任何解决此问题的方法,其中所有预留都可以分配给不同的可用时隙,则此时间片处于一致状态。由于我传递了所需的预留位置,如果模型仍然一致,包括该插槽,这意味着保留该插槽是安全的。

如果在这个狭窄的窗口中有两个短的预留可分配给相同的可用性,这仍然是有问题的,但是这种可能性很低,结果只是可用性查询的假阴性;误报会更有问题。

查找可用的插槽

查找所有可用的插槽是一个更复杂的问题,因此需要进行一些简化。首先,它只能查询模型的特定标签集的可用性(那里没有&#34;给我所有全局可用的插槽&#34;)其次,它只能被查询具有特定粒度(期望的时隙长度)。这非常适合我的特定用例,我只需要为用户提供他们可以保留的插槽列表,例如 9:15-9:30,9:30-9:45等。。这通过重用上面的代码使算法变得非常简单:

def free_slots(self, start: datetime, end: datetime, attributes: set, granularity: timedelta):
    slots = []
    while start < end:
        slot_end = start + granularity
        if self.timeslot_is_available(start, slot_end, attributes):
            slots.append((start, slot_end))
        start += granularity

    return slots

换句话说,它只是在给定的时间间隔内遍历所有可能的时隙,并逐字检查该时隙是否可用。它有点蛮力解决方案,但效果非常好。