【问题标题】:Algorithm for finding the busiest period?寻找最繁忙时段的算法?
【发布时间】:2011-08-11 17:41:22
【问题描述】:

我有一些这样的数据:

1: 2 - 10
2: 3 - 15
3: 4 - 9
4: 8 - 14
5: 7 - 13
6: 5 - 10
7: 11 - 15

我会尝试一个表示来使其更清楚:

        1     2     3     4     5     6     7     8     9     10     11     12     13     14     15
1             |--------------------------------------X---------|
2                   |--------------------------------X--------------------------------------------|
3                         |--------------------------X---|
4                                                  |-X-------------------------------------|
5                                           |--------X------------------------------|
6                               |--------------------X----------|
7                                                                     |---------------------------|

因此,在示例情况下,如果使用第二种方案,8-9 是关键时期,因为所有点都处于活动状态。在 python 中解决这个问题的快速而好的方法是什么?我正在考虑使用动态编程,但还有其他建议的方法吗?

到目前为止我的方法:

我更多的是从实时的角度思考。所以,每当我得到一个新点时,我都会这样做:假设我已经得到了2-10 并且我得到了3-15 然后我选择开始的最大值和结束的最小值所以这种情况是3-10 并增加这个间隔的计数到 2。然后第三点进入 4-9,选择最大值为 4,最小值为 9,并将值 3-10 更新为 4-9 并将计数更新为 3。现在当 8-14 进来时,我选择这个区间的开始大于4-9,并且这个区间的结束小于4-9。在这种情况下,这是不正确的,所以我将创建一个新的桶 8-14 并将计数设置为 1。这不是整个算法,但应该对我在这里所做的工作有一个高级的了解。我看看能不能画出伪代码。

【问题讨论】:

  • 数据之间有什么相似之处吗? IE,它们是否会发生变化并且永远不会小于 n 或类似的东西?
  • @sjr: 除非我误解那不是他想要做的。在该示例中,它给出了一个时间跨度,并且您想知道在一个时间线的开始和结束之间处于活动状态的所有内容。听起来他想知道没有单一比较来源的多个时间戳中最繁忙的范围。
  • 您能否详细说明您的尝试以及您认为可能需要改进的部分?您的一些基本工作通常会受到 SO 的青睐。
  • @Merlyn Morgan-Graham:+1 代表您的观点 :) 我同意我应该发布我的方法来坚持 SO 的哲学。我已经用我想到但不确定的方法更新了我的问题。我没有准备好伪代码,所以我对采用这种方法有点犹豫。

标签: python algorithm dynamic-programming


【解决方案1】:

如果您想在这里获得线性性能,我已经编写了一个小型 C++ 程序。 我知道它不是 Python,但这里的想法很简单。

我们首先创建一个包含所有点的数组,如果间隔从该索引开始,则增加数组中的项,如果在该索引结束,则减少它。

一旦构造了数组,我们只需迭代并计算我们拥有最大开区间数的位置。

时间复杂度为 O(M + N)

空间复杂度为 O(N)

其中 M 是区间数,N 是区间对中的最大值。

#include <iostream>
#include <vector>

int maxLoad(const std::vector<std::pair<int, int>>& intervals) {
    std::vector<int> points;
    for(const auto& interval : intervals) {
        if(interval.second >= points.size()) points.resize(interval.second + 1);
        ++points[interval.first];
        --points[interval.second];
    }

    int ans = 0;
    int sum = 0;
    for(const auto point : points) {
        sum += point;
        ans = std::max(ans, sum);
    }
    return ans;
}

int main() {
    std::vector<std::pair<int, int>> intervals {
        {2, 10}, {3, 15}, {4, 9}, {8, 14}, {7, 13}, {5, 10}, {11, 15}
    };
    std::cout << maxLoad(intervals) << std::endl;
}

【讨论】:

    【解决方案2】:

    这就是我对基于 bin 的方法的想法,并适用于动态处理添加,基本上是 R.K.说我相信。

    from collections import defaultdict
    from operator import itemgetter
    
    class BusyHour(object):
        def __init__(self):
            self.pairs = defaultdict(int)
        def add_period(self, period):
            start, end = period
            for current_period in range(start, end):
                pair_key = (current_period, current_period + 1) 
                self.pairs[pair_key] += 1
        def get_max(self):
            # sort, defaults to smallest to largest
            # --> items() returns (key, value) pairs
            # --> itemgetter gets the given index of the first argument given to sorted
            return max(self.pairs.items(), key=itemgetter(1))
    
    
    if __name__ == '__main__':
        periods = [(2, 10), (3, 15), (4, 9), (8, 14), (7, 13), (5, 10), (11, 15)]
        bh = BusyHour()
        for period in periods:
            bh.add_period(period)
        print bh.get_max()
    

    更新:仅在调用 get_max 时排序,并使用 defaultdict(int)。

    【讨论】:

    • +1 感谢您抽出宝贵时间。这似乎也可以正常工作。
    • 传奇,没问题!回忆如何使用 sorted()、itemgetter() 和 defaultdict 对我来说是一个很好的练习。直到我这样做才意识到 max() 和 min() 有“关键”参数。
    【解决方案3】:
            1     2     3     4     5     6     7     8     9     10     11     12     13     14     15
    1             |--------------------------------------X---------|
    2                   |--------------------------------X--------------------------------------------|
    3                         |--------------------------X---|
    4                                                  |-X-------------------------------------|
    5                                           |--------X------------------------------|
    6                               |--------------------X----------|
    7                                                                     |---------------------------|
    
                 +1    +1     +1   +1           +1     +1    -1    -2     +1           -1     -1     -2
                  1     2     3     4           5       6    5      3     4             3      2      0
                                                         ^^^^
    

    明白了吗?

    所以你需要改变这个:

    1: 2 - 10
    2: 3 - 15
    3: 4 - 9
    4: 8 - 14
    5: 7 - 13
    6: 5 - 10
    7: 11 - 15
    

    进入:

    [(2,+), (3,+), (4,+), (5,+), (7,+), (8,+), (9,-), (10,-), (10,-), (11,+), (13,-), (14,-), (15,-), (15,-)]
    

    然后您只需迭代,当您看到 + 时向上计数,然后在 - 时倒数。最繁忙的时间间隔将是计数最大的时候。

    所以在代码中:

    intervals = [(2, 10), (3, 15), (4, 9), (8, 14), (7, 13), (5, 10), (11, 15)]
    intqueue = sorted([(x[0], +1) for x in intervals] + [(x[1], -1) for x in intervals])
    rsum = [(0,0)]
    for x in intqueue: 
        rsum.append((x[0], rsum[-1][1] + x[1]))
    busiest_start = max(rsum, key=lambda x: x[1])
    # busiest_end = the next element in rsum after busiest_start 
    
    # instead of using lambda, alternatively you can do:
    #     def second_element(x):
    #         return x[1]
    #     busiest_start = max(rsum, key=second_element)
    # or:
    #     import operator
    #     busiest_start = max(rsum, key=operator.itemgetter(1))
    

    运行时复杂度为(n+n)*log(n+n)+n+nO(n*log(n))

    如果您在程序开始时没有完整的间隔列表,也可以将此想法转换为online algorithm,但可以保证传入的间隔永远不会被安排在过去的时间点。代替排序,您将使用优先级队列,每次间隔到来时,您都推入两个项目,起点和终点,每个项目分别带有 +1 和 -1。然后你弹出并计算并跟踪高峰时段。

    【讨论】:

    • @Lie Ryan:+1 漂亮的实现!下一部分可能与该问题无关,但我很感激您对此的回应。我想我已经理解了整个代码,但是你能解释一下最后一步到底做了什么吗?我知道输出,但 lambda 真正在做什么?如果你能同时给我看一个非 lambda sn-p 就更好了,我会完全理解这个解决方案。接受为答案。谢谢。
    • 它是否处理不连续性?
    • @Legend:lambda 只是函数定义的另一种语法,请参阅this。在max(rsum, key=lambda x: x[1]) 中,这一行只是在数组[(a1,b1), ..., (an,bn)] 中搜索元素(ai,bi),其中第二个元素bi 最大。
    • @sjr:在两个不重叠的间隔子集之间(我假设这就是您所说的“间隙”),计数器应该简单地为 0,因此算法应该仍然有效,尽管我没有测试过了。
    • @j_random_hacker:是的,在 Python 中,元组是按字典顺序比较的,所以 (5, -1) &lt; (5, +1)
    【解决方案4】:

    我认为您也许可以为此使用 set(),如果您确定所有周期至少在一个点相交,它将起作用。

    但是,如果句点不相交,这将不起作用。 你也许可以添加额外的逻辑来解决这个问题,所以我会发布我的想法:

    >>> periods = [(2, 10), (3, 15), (4, 9), (8, 14), (7, 13), (5, 10),]
    >>> intersected = None
    >>> for first, second in periods:
    ...     if not intersected:
    ...         intersected = set(range(first, second + 1))
    ...     else:
    ...         intersected = intersected.intersection(set(range(first, second + 1)))
    ...
    >>> intersected
    set([8, 9])
    

    注意:这不包括 11-15 期间。 正如 R.K. 所提到的,您最好只创建 bin 对

    【讨论】:

    • 有趣。我用我正在考虑的一些方法更新了我的问题。也许我可以将两者结合起来。
    【解决方案5】:

    我首先将点 x 的繁忙度视为 x 左侧的激活次数减去 x 左侧的停用次数。我会按它们发生的时间(在 O(nlog(n)) 时间)对激活和停用进行排序。然后,您可以遍历列表,跟踪活动的数字 (y),通过激活和停用来增加和减少该数字。最繁忙的时期将是 y 最大的点。我想不出比 O(nlog(n)) 更好的解决方案。蛮力将是 O(n^2)。

    【讨论】:

    • R.K 的解决方案对我来说似乎是 O(n),只要他正确地假设我们可以选择一小组离散桶。
    • +1,简单快捷。确保停用对“小于”激活进行排序,以便每当一个段在与另一段开始的位置相同的位置结束时,活动段的计数会先下降再上升。
    • @Michael:R.K 的解决方案不是 O(n),因为每个段可能需要增加每个桶的计数。这是 O(nm) 时间,其中 m 是任何段的最大端点,它需要 O(m) 空间。
    • @Michael Greene:R.K 的解决方案是桶数和参与者数量的 O(m*n),而不是 O(n)。 @j_random_hacker:谢谢,一个很好的建议。
    • O(m*n) 其中 m 是一个常数仍然是 O(n),而我阅读问题 m 的方式将是一个小常数,例如 24。我现在看到这个问题可以包括不断增长的桶,在这种情况下你是对的。
    【解决方案6】:

    不确定我是否理解您的问题。如果您试图找到最常见的“间隔”,您可以按间隔总结它们。这样,对于上面的示例,您就有 12 个存储桶。对于每次使用,您将为该特定用途中使用的每个存储桶添加 1,最后,找到所有存储桶中的最大值。在这里,对于 8-9 区间,这将是 6。

    【讨论】:

    • :+1 谢谢。虽然这可行,但我不确定该方法是否可扩展,但如果我错了,请纠正我。我给出的示例是一个玩具示例,但实际上,这些数字可能很大。如果没有更好的方法来做到这一点,那么是的,我认为你的建议看起来确实是一种潜在的方法。
    猜你喜欢
    • 2020-08-05
    • 1970-01-01
    • 1970-01-01
    • 2011-02-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多