在特定时刻与大多数观众一起找到活动的最快方式?

时间:2016-12-08 14:16:37

标签: algorithm sorting

问题:

有一个特殊的日子,当< = 1'' 000事件被举行。

有< = 1' 000&000; 39%的观众观看这些事件(不一定是相同的数字)。

特殊日子分为< = 100� 000时刻(每个活动持续一整天)。

每个观众在特殊日子里正在观看一个事件。

从第一刻到第二刻,他们只观看一大块事件。

I / O:

首先,我们给出了观众人数N和时刻数。

然后,我们给了N行。第i行包含三个数字:

M1,M2,E - 告诉我们第i个观众将从时刻M1到M2(包括)时刻观看事件E.

然后,我们得到问题。每个问题都是一个数字Q.它询问当前Q中最多的事件是什么。(如果有多个事件,则返回最低数字的事件)

考虑到内存限制(128MB),回答这些问题的快速(est)算法是什么?

2 个答案:

答案 0 :(得分:0)

我能想到的最快(现在)是按事件排序N行。这具有复杂度O(N log(N))。要回答一个问题,对已排序的数组行进行单次扫描就足够了。这具有复杂度O(N)。

内存要求是(假设​​4字节整数):

  • 阵列为12 N(也可能是另外6 N的排序)
  • N用于存储结果(可以有多个“获胜者”,想象一下所有人都在同一时间间隔访问不同的事件)。

答案 1 :(得分:0)

以下解决方案具有构建数据结构的复杂度O(n log n)。每个查询都可以在O(1)中得到满足。

创建一个类Viewer来表示您的输入行。那就是:

class Viewer
    int StartMoment;
    int EndMoment;
    int EventId;

还要创建一个类Event,其中包含事件编号及其计数:

class Event
    int Id;
    int Count;

创建{Id}键入的Event个实例字典。还要创建Event的优先级队列(最大堆),按Count排序,降序和Id。请注意,优先级队列包含对字典中项目的引用。此外,您的优先级队列必须有效地实现 reduce-key 删除(即删除任意项)。像配对堆这样的东西对此有好处。它可以使用二进制堆,但很难实现。

字典就在那里,您可以轻松查找优先级队列中的内容。这使得它非常快,O(1)可以在队列中定位,并且可以非常快速地进行删除和重新排序。调用字典EventsDict。优先级队列为EventsQueue。两者最初都是空的。

创建另一个优先级队列,这是Viewer个实例之一,由EndMoment排序。称之为CurrentViewers。它也是空的。

最后,创建一个moment-count Event引用数组。这是下面算法的输出。称之为Moments

将查看器行读入Viewer实例列表。称之为Viewers。按StartMomentEventId排序。

现在开始按顺序浏览Viewer行。

int currentMoment;
for each viewer
{
    Event currentEvent;
    if (viewer.StartMoment > currentMoment)
    {
        // The most viewed event at the current moment is at the top of the CurrentEvents heap
        Moments[currentMoment] = EventsQueue.Peek();

        currentMoment = viewer.StartMoment;
        // remove any viewers that are no longer active, and update the events queue accordingly
        while (CurrentViewers.Peek().EndMoment < currentMoment)
        {
            oldViewer = CurrentViewers.Dequeue();
            currentEvent = EventsDict[oldViewer.EventId];
            EventsQueue.Remove(currentEvent);
            --EventsQueue.Count;
            // if the count goes to 0, remove it from the active list
            // Otherwise re-insert it into the queue
            if (currentEvent.Count == 0)
            {
                EventsDict.Remove(currentEvent.Id);
            }
            else
            {
                EventsQueue.Enqueue(currentEvent);
            }
        }
    }
    if (EventsDict.ContainsKey(viewer.EventId))
    {
        // updates the count and re-inserts the item in the queue
        currentEvent = EventsDict[viewer.EventId];
        EventsQueue.Remove(currentEvent);
        ++currentEvent.Count;
        EventsQueue.Enqueue(currentEvent);
    }
    else
    {
        currentEvent = new Event(viewer.EventId, 1);
        EventsQueue.Enqueue(currentEvent);
    }
    // Add this viewer to the `CurrentViewers` queue
    CurrentViewers.Enqueue(viewer);
}

工作原理:

这里的想法是,我们从拥有观众的最早时刻开始,我们跟踪正在观看哪些事件。在读取每个查看器记录时,我们更新引用事件的计数并将查看器添加到队列中,以便在到达其EndMoment时将其删除。维护EventsQueue,使得当前具有最大数量的查看者的事件始终位于堆的顶部。这就是为什么我们每次计数更改时删除并重新添加事件的原因。 EventsDict仅用于跟踪当前活动的事件。

所以我们假设我们有这五位观众:

eventId  startMoment  endMoment
  7          2           7
  1          3           5
  1          3           4
  7          3           5
  7          4           6

在循环开始时,currentMoment设置为2,因为它是第一个观看事件的开始时刻。第一次通过循环,事件7被添加到事件队列和currentViewers队列。

下一次循环时,viewer.StartMoment为3,但currentMoment为2.所以我们必须更新输出,然后将currentMoment设置为3.没有任何内容从当前查看器队列,因为没有任何endMoment小于3.然后我们将事件1添加到当前事件队列,并添加到当前查看器。

下一次循环(上面列表中的第三项),我们仍处于第3阶段。我们增加事件1的计数并添加重新插入到事件队列中。该项目已添加到currentViewers。

下一次,仍在第3时刻。增加事件7的计数并将查看器添加到currentViewers。

最后一项,我们现在正处于第4时刻。此时,事件1和7都有两个观众。事件1具有较低的id,因此该事件将被放置在输出中。我们还需要从当前事件队列中删除列表中的第三项(事件1,结束时刻4),并减少事件1的计数。

然后我们将当前时刻设置为4并增加事件7的计数。

看起来我离开了算法的最后一部分,它必须在读取所有查看器事件后清空队列。因此,在上面的循环结束时,您需要添加更多代码:

currentMoment = currentViewers.Peek().EndMoment;
while (currentViewers is not empty)
{
    viewer = currentViewers.Dequeue();
    if (viewer.EndMoment > currentMoment)
    {
        Moments[currentMoment] = EventsQueue.Peek();
        currentMoment = viewer.EndMoment;
    }
    currentEvent = EventsDict[oldViewer.EventId];
    EventsQueue.Remove(currentEvent);
    --EventsQueue.Count;
    // if the count goes to 0, remove it from the active list
    // Otherwise re-insert it into the queue
    if (currentEvent.Count == 0)
    {
        EventsDict.Remove(currentEvent.Id);
    }
    else
    {
        EventsQueue.Enqueue(currentEvent);
    }
}

所有代码都是确保在读取所有查看器后填充Moments数组的尾部。它基本上是代码的重复,当事件视图超出范围时,以及当前时刻发生变化时,它会删除事物。

让我们调用观看者N的数量。对观看者行进行排序是O(N log N)。此外,每个查看器都会添加到CurrentViewers队列一次(O(1)),并删除一次(O(log N))。每次循环都涉及插入或删除一个事件,然后重新插入EventsQueue。对于最坏情况分析,让我们说通过循环的evey时间涉及从EventsQueue中删除项目的O(log n)。

所以我们有O(N log N)作为初始排序。 O(N)将项添加到CurrentViewers队列。 O(N log N)从CurrentViewers中删除项目。 O(N)将项目添加到EventsQueue,O(N log N)将项目从EventsQueue中删除。所以施工是:

3*(N log N) + 2*N ~ O(N log N)

您最终会构建一个每时每刻都观看次数最多的数组。因此,单个查询是一个简单的数组查找,O(1)。

观看者和活动少于1,000,000,时间少于100,000,这应该很容易适合128 MB。如果它只占用了一半,我会感到非常惊讶。