【问题标题】:Algorithm to find middle of largest free time slot in period?找到周期内最大空闲时间段中间的算法?
【发布时间】:2012-08-01 13:20:10
【问题描述】:

假设我想在 00:00–00:59 期间安排一系列事件。我将它们安排在整分钟(00:01,从不 00:01:30)。

我想在那段时间内将它们尽可能地分开,但我事先不知道在那一小时内我总共会有多少事件。我今天可以安排一个活动,明天再安排两个。

我脑子里有一个明显的算法,我可以想出蛮力的方法来实现它,但我相信有人知道更好的方法。我更喜欢 Ruby 或我可以翻译成 Ruby 的东西,但我会尽我所能。

所以我脑子里能想到的算法:

事件 1 刚刚在 00:00 结束。

事件 2 在 00:30 结束,因为那个时间离现有事件最远。

事件 3 可能在 00:15 或 00:45 结束。所以也许我只选择第一个,00:15。

事件 4 然后在 00:45 结束。

事件 5 在 00:08 左右结束(从 00:07:30 向上取整)。

等等。

所以我们可以查看每对花费的分钟数(例如,00:00–00:15、00:15–00:30、00:30–00:00),选择最大的范围(00:30– 00:00),除以二并取整。

但我相信它可以做得更好。分享!

【问题讨论】:

  • 如果您必须添加新的活动,您能否重新安排已安排的活动?
  • @RomanSaveljev Nupe,一旦安排,就无法重新安排。
  • 为什么事件 2 应该在 0:30 而不是 01:00 (00:59)?还是应该?
  • 罗曼:你说得对。我会将问题编辑为 00:00–00:59 而不是 00:00–01:00。

标签: algorithm scheduling


【解决方案1】:

由于您最多只能安排 60 个事件,所以我认为使用静态表值得一试(与思考算法和测试它相比)。我的意思是对你来说,在时间之内安排事件是非常微不足道的任务。但是要告诉计算机如何做到这一点并不容易。

所以,我建议用静态时间值定义表,在该表中放置下一个事件。可能是这样的:

00:00, 01:00, 00:30, 00:15, 00:45...

【讨论】:

  • 这很好。而且,如果您使用代码生成该静态计划,如果您愿意,该算法仍然可以与您的代码一起保存,但您只使用了缓存的输出。
  • 在回家的路上刚刚意识到您不仅限于参加 60 场活动(假设您需要更多活动来获得更多乐趣)。 61 日及以后将不得不与其他活动共享分钟时段。所以,现在的要求是:每一个新事件都尽可能远离其他事件。这意味着您可以重新开始从表中挑选
  • 是的,我修改了我的解决方案 (gist) 以环绕,因此 60 号的安排与 0 号一样,依此类推。
【解决方案2】:

您可以使用位反转来安排您的活动。只需取事件序列号的二进制表示,反转其位,然后将结果缩放到给定范围(0..59 分钟)。

另一种方法是按 (0000,1000,0100,1100,...) 的顺序生成位反转字。

这允许轻松分发多达 32 个事件。如果需要更多事件,在缩放结果后,您应该检查结果分钟是否已被占用,如果是,则生成并缩放下一个单词。

这是 Ruby 中的示例:

class Scheduler
  def initialize
    @word = 0
  end

  def next_slot
    bit = 32
    while  (((@word ^= bit) & bit) == 0) do
      bit >>= 1;
    end
  end

  def schedule
    (@word * 60) / 64
  end
end


scheduler = Scheduler.new

20.times do
  p scheduler.schedule
  scheduler.next_slot
end

按顺序生成位反转字的方法借鉴自“Matters Computational ”,第1.14.3章。


更新:

由于从 0..63 缩放到 0..59,该算法倾向于在 0、15、30 和 45 之后生成最小的槽。问题是:它总是从这些(最小的)槽开始填充间隔,而从最大的插槽开始填充更自然。因此,算法并不完美。另一个问题是需要检查“已经占用的分钟”。

幸运的是,一个小修复解决了所有这些问题。换个方式

while  (((@word ^= bit) & bit) == 0) do

while  (((@word ^= bit) & bit) != 0) do

并用 63 初始化 @word(或继续用 0 初始化它,但执行一次迭代以获得第一个事件)。此修复将反转字从 63 减少到零,它始终将事件分配到可能的最大槽,并且在前 60 次迭代中不允许“冲突”事件。


其他算法

前面的方法很简单,但它只能保证(在任何时候)最大的空槽不超过最小槽的两倍。由于您希望将事件间隔尽可能远,因此可能首选基于斐波那契数或黄金比例的算法:

  1. 将初始间隔 (0..59) 放入优先级队列(最大堆,优先级 = 间隔大小)。
  2. 要安排一个事件,弹出优先级队列,以黄金比例 (1.618) 分割生成的间隔,使用分割点作为此事件的时间,然后将两个生成的间隔放回优先级队列。

这保证了最大的空槽不超过(大约)最小槽的 1.618 倍。对于较小的槽,近似值会变差,并且尺寸相关为 2:1。

如果不方便在调度更改之间保留优先级队列,您可以提前准备一个包含 60 个可能事件的数组,并在每次需要新事件时从该数组中提取下一个值。

【讨论】:

  • 这听起来很有趣,但恐怕我不明白。您能指出一些资源或编写示例代码吗?
  • @HenrikN:添加了示例代码。 (我在 Ruby 方面没有太多经验,所以这段代码很可能并不完美)。
  • 谢谢!这很整洁。稍微修改它以返回与我的示例相同类型的输出:gist.github.com/3229544 正如你所说,它并没有完全均匀地分配它们与一定数量的结果。对于 30 个结果,它似乎也有点偏离(例如,它有 54、56 和 0,但没有 58)。但是很酷。我会尝试真正理解它的作用:)
  • @HenrikN:你不会得到 30 个结果的“58”,因为 30 不是 2 的幂。您将获得 32 个结果。该算法仅在二次幂之后才提供更均匀分布的事件。否则它只保证最大的空槽不超过最小槽的两倍。 (您可以使用基于斐波那契数或黄金比例的其他算法来改进此比例)。但由于其他原因,我的算法有点偏离。查看更新。 – Evgeny Kluev 21 分钟前
【解决方案3】:

由于您无法重新安排活动,并且您不提前知道有多少活动将到达,我怀疑您自己的提议(带有 Roman 使用 01:00 的注释)是最好的。

但是,如果您对最多到达多少事件有任何类型的估计,您可能可以对其进行优化。例如,假设您估计最多 7 个事件,您可以准备 60 / (n - 1) = 10 分钟的时隙,并像这样安排事件:

  • 00:00
  • 01:00
  • 00:30
  • 00:10
  • 00:40
  • 00:20
  • 00:50 // 相隔 10 分钟

请注意,最后几个事件可能不会到达,因此使用 00:50 的可能性很小。

这会更公平然后非基于估计的算法,特别是在最坏的情况下,所有插槽都被使用:

  • 00:00
  • 01:00
  • 00:30
  • 00:15
  • 00:45
  • 00:07
  • 00:37 // 仅相隔 7 分钟

【讨论】:

    【解决方案4】:

    我为我的解决方案编写了一个 Ruby 实现。它有一个边缘情况,任何超过 60 的事件都将在第 0 分钟堆积起来,因为现在每个空闲时间空间的大小都相同,并且它更喜欢第一个。

    我没有具体说明如何处理超过 60 次的事件,我也不是很在意,但我想如果你在乎的话,随机化或循环法可以解决这种极端情况。

    each_cons(2)gets bigrams;其余的可能很简单:

    class Scheduler
      def initialize
        @scheduled_minutes = []
      end
    
      def next_slot
        if @scheduled_minutes.empty?
          slot = 0
        else
          circle = @scheduled_minutes + [@scheduled_minutes.first + 60]
          slot = 0
          largest_known_distance = 0
    
          circle.each_cons(2) do |(from, unto)|
            distance = (from - unto).abs
            if distance > largest_known_distance
              largest_known_distance = distance
              slot = (from + distance/2) % 60
            end
          end
        end
    
        @scheduled_minutes << slot
        @scheduled_minutes.sort!
        slot
      end
    
      def schedule
        @scheduled_minutes
      end
    end
    
    
    scheduler = Scheduler.new
    
    20.times do
      scheduler.next_slot
      p scheduler.schedule
    end
    

    【讨论】:

      猜你喜欢
      • 2022-06-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-08-06
      • 2018-12-13
      • 2015-09-19
      相关资源
      最近更新 更多