调度算法,找到设定长度的所有非重叠间隔

时间:2017-02-27 21:02:48

标签: python algorithm optimization

我需要为我的管理应用程序实现一个算法,该算法将告诉我何时以及可以为哪个用户分配任务。

我实施了一个强力解决方案,似乎有效,但我想知道是否有更有效的方法来做到这一点。为简单起见,我重写了算法以对数字列表进行操作(而不是数据库查询等)。下面我将尝试解释我的思维方式。

假设我们有3个用户可以分配给该任务。

user_a_busy = [[1,2], [2,4], [5,6]]
user_b_busy = [[4,7], [7,8]]
user_c_busy = [[0,1], [1,5]]

列表中的每个元素表示用户在白天不可用的时段。因此,用户A在凌晨1点到凌晨2点,凌晨2点和凌晨4点之间忙碌,依此类推。为了能够迭代用户并识别它们,我以字典的形式表示上述列表。

users_to_check = {'A':user_a_busy, 'B':user_b_busy, 'C':user_c_busy}

现在假设我们有一项任务需要1小时才能完成,我们希望在1小时的间隔内检查午夜到上午10点之间的时间段(因此任务只能在整个小时内开始)。以下是以列表形式检查的每个期间的表示。

task_intervals_to_check = [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7], [7, 8], [8, 9], [9, 10]]

这是一个检查两个区间是否重叠的函数:

def intervals_overlap(service, busy):
    if service[1] > busy[0] and service[0] < busy[1]:
        return True
    return False

所以现在这里是循环,导致可用小时字典和可分配给任务的用户:

result = defaultdict(list)
for interval in task_intervals_to_check:
    for user, user_busy in users_to_check.iteritems():
        overlaps = False
        for busy_period in user_busy:
            if intervals_overlap(interval, busy_period):
                overlaps = True
                break
        if not overlaps:
            result[interval[0]].append(user)

对于长度为1小时的任务,结果为:

  

{0:['A','B'],1:['B'],2:['B'],3:['B'],4:['A'],5: ['C'],6:   ['A','C'],7:['A','C'],8:['A','C','B'],9:['A','C',' B']}

对于长度为2小时的任务,结果是:

  

{0:['B'],1:['B'],2:['B'],5:['C'],6:['A','C'],7: ['A','C'],   8:['A','C','B']}

这是预期的结果。下面的图表帮助我找到了正确的结果:

enter image description here 所以现在我的问题是,有没有办法优化这个解决方案?这是可接受的解决方案吗?

3 个答案:

答案 0 :(得分:1)

您可以尝试摆脱最外层的循环。假设您有句点的开头和结尾来检查ps, pe(示例中的0和10)以及task_duration中的任务持续时间(示例中为1或2)。假设所有内容都以完整小时为单位,busy_intervals按时间排序。

result = defaultdict(list)
for user, user_busy in users_to_check.iteritems():
    for l_busy_per,r_busy_per in zip([[0, ps]] + user_busy, user_busy + [[pe, 0]]):
        avail_start = l_busy_per[1]
        avail_end = r_busy_per[0]
        for hit in range(avail_start, avail_end+1-task_duration):
            result[hit].append(user)

答案 1 :(得分:0)

我想在问题的表示上添加一些内容。我认为只有开始时间的表示既充足又自然。如果用户a忙于0-1,2-4和5-6,我会推荐这样的表示:

a_busy = (0, 2, 3, 5)

这意味着用户a在a_busy中每次忙于一个单位时间。此外,分配的时间段也更自然地表示出来。

task_times = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

然后我们甚至可以使用基本集合理论为每个用户推导出一个解决方案。设user_busy为开始时间的集合,在给定长度的情况下无法分配用户。此外,让slot_to_fill成为时隙的开始时间,在给定长度的情况下,期望由用户填充。 然后,slots_to_fill和user_busy的差异是用户的最佳分配。以下是length = 2和您的用户a:

的示例
user_busy = set([0, 1, 2, 3, 4, 5]) # Set where user cannot be assigned
slots_to_fill = set([0, 1, 2, 3, 4, 5, 6, 7, 8]) # Set where users shall be assigned
x = slots_to_fill - user_busy
print(x) # {6, 7, 8}

此解决方案最困难的方面是根据您的数据构建集合。在这个问题的自然表示中,解决方案是微不足道的,可以分解为基于每个用户完成:

from itertools import chain

user_busy = [[1,2], [2,4], [5,6]]
task_intervals_to_check = [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7], [7, 8], [8, 9], [9, 10]]
length = 2

# Convert original data to tuples of starting times
busy_start_time = tuple(chain.from_iterable(range(i, j) for i, j in user_busy))
slots_to_fill = tuple(chain.from_iterable(range(i, j) for i, j in task_intervals_to_check))

def assign(fillslots, not_avail, length):
    return filter(lambda x: all(x+i not in not_avail for i in range(length)) and x+length-1 <= max(fillslots), fillslots)

times = assign(slots_to_fill, busy_start_time, length)
print(list(times))

这将返回可以分配用户的开始时间列表,这些列表比列表更方便处理。可以通过将分配间隔的长度添加到开始时间来计算结束时间。

最后,我不认为在运行时优化方面有很多好处,因为这个问题在计算上相当便宜。 如果要优化解决方案质量,首先必须定义目标。例如,这可能是这样的:在填充所有时段时最小化分配总数。尽管如此,这不会增加问题的难度。 与用户相关的约束会使问题变得更加困难,例如: G。用户A和用户B不得在两小时内分配,只有在分配了用户B的情况下才能分配用户C.

答案 2 :(得分:0)

因此,我认为随着您的扩展,您最终将实现更高级的公式,并且现在已经进行了简单的集成。我怀疑,你最好把时间表作为一个矩阵。

我的解决方案是用Ruby制作的 - 但这个概念适用于其他语言。

这将允许您找到单独的空闲时间块,但是选择2-4小时您可以获得类似这样的内容,以便一目了然地呈现:

[ 1 , 1 , 1 ], 
[ 1 , 1 , 1 ], 
[ 1 , 1 , 1 ], 

以后可以派上用场进行更高级的搜索和实现算法。对于这个简单的解决方案,我将用以下内容进行演示。

calendar = [ # 0 is free, 1 is busy
  [ 1 , 1 , 1 ], #12AM to
  [ 1 , 1 , 1 ], #1AM to
  [ 1 , 1 , 1 ], #2AM to
  [ 1 , 1 , 1 ], #3AM to
  [ 1 , 1 , 1 ], #4AM to
  [ 1 , 1 , 0 ], #5AM to
  [ 1 , 1 , 0 ], #6AM to
  [ 1 , 1 , 0 ], #7AM to
  [ 1 , 1 , 0 ], #8AM to
  [ 0 , 1 , 1 ], #9AM to
  [ 0 , 1 , 1 ], #10AM to
  [ 1 , 1 , 1 ], #11AM to
  [ 1 , 1 , 1 ], #12PM to
  [ 1 , 0 , 1 ], #1PM to
  [ 1 , 0 , 1 ], #2PM to
  [ 1 , 0 , 1 ], #3PM to
  [ 1 , 1 , 0 ], #4PM to
  [ 1 , 1 , 0 ], #5PM to
  [ 1 , 1 , 1 ], #6PM to
  [ 1 , 1 , 1 ], #7PM to
  [ 1 , 1 , 1 ], #8PM to
  [ 1 , 1 , 1 ], #9PM to
  [ 1 , 1 , 1 ], #10PM to
  [ 1 , 1 , 1 ], #11PM to
  ["A","B","C"] #Users
  ]


def find_available_slot(length, calendar)
  [].tap do |results|
    calendar.transpose.collect do |schedule|
      times = schedule[0...-1]
      blocks = sort_it(times.each_index.select {|i| times[i] == 0 }).select { |x| x.count >= length }
      results << [blocks, schedule.last] unless blocks.empty?
    end
    results
  end
end

def sort_it(arr)
  tmp, main = [], []
  arr.each_with_index do |x, i|
    if arr[i-1]
      if arr[i-1] + 1 == x
        tmp << x
      else
        main << tmp unless tmp.empty?
        tmp = [x]
      end
    else
      tmp << x
    end
  end
  main << tmp
  main
end

find_available_slot(2, calendar)

对于我的示例计划,查找可用2小时的块,它会返回以下结果:

=> [[[[9, 10]], "A"], [[[13, 14, 15]], "B"], [[[5, 6, 7, 8], [16, 17]], "C"]]

因此结果返回一个嵌套数组,并且数组的每个元素都是这些用户的块(如果有的话)。因此result [0]将是第一个用户可用的结果[0] [0]将是块,结果[0] [1]将告诉您哪个用户。

这种调度矩阵是非常强大的长期,我建议您使用2d这样的任何实现。

我做了一个简短的谷歌搜索,你可以在这里阅读更多内容:

Appointment scheduling algorithm (N people with N free-busy slots, constraint-satisfaction)

FInd overlapping appointments in O(n) time?

http://www.geeksforgeeks.org/given-n-appointments-find-conflicting-appointments/