Timus 1037. Memory management

Timus 1037. Memory management 要求你实现一个内存管理器。


1037. Memory management

Time Limit: 2.0 second
Memory Limit: 16 MB

Background

Don't you know that at school pupils’ programming contest a new computer language has been developed. We call it D++. Generally speaking it doesn't matter if you know about it or not. But to run programs written in D++ we need a new operating system. It should be rather powerful and complex. It should work fast and have a lot of possibilities. But all this should be done in a future.

And now you are to… No. You should not devise the name for the operating system. You are to write the first module for this new OS. And of course it's the memory management module. Let's discuss how it is expected to work.

Problem

Our operating system is to allocate memory in pieces that we’ll call “blocks”. The blocks are to be numbered by integers from 1 up to N. When operating system needs more memory it makes a request to the memory management module. To process this request the memory management module should find free memory block with the least number. You may assume that there are enough blocks to process all requests.

Now we should define the meaning of words “free block”. At the moment of first request to the memory management module all blocks are considered to be free. Also a block becomes free when there were no requests to it during T minutes.

You may wonder about a notion “request to allocated blocks”. What does it mean, “request to allocated block”? The answer is simple: at any time the memory management module may be requested to access a given block. To process this request the memory management module should check if the requested block is really allocated. If it is, the request is considered to be successful and the block remains allocated for T minutes more. Otherwise the request is failed.

That's all about the algorithms of the memory management block. You are to implement them for N = 30 000 and T = 10 minutes.

Input

Each line of input contains a request for memory block allocation or memory block access. Memory allocation request has a form:

<Time> +

where <Time> is a nonnegative integer number not greater than 65 000. Time is given in seconds. Memory block access request has a form:

<Time> . <BlockNo>

where <Time> meets conditions mentioned above for the memory allocation request and <BlockNo> is an integer value in range from 1 to N. There will be no more than 80000 requests.

Output

For each line of input you should print exactly one line with a result of request processing. For memory allocation request you are to write an only integer — a number of allocated block. As it was mentioned above you may assume that every request can be satisfied, there will be no more than N simultaneously allocated blocks. For memory block access request you should print the only character:

  • '+' if request is successful (i.e. block is really allocated);
  • '-' if request fails (i.e. block with number given is free, so it can't be accessed).

Requests are arranged by their times in an increasing order. Requests with equal times should be processed as they appear in input.

Sample

input output
1 +
1 +
1 +
2 . 2
2 . 3
3 . 30000
601 . 1
601 . 2
602 . 3
602 +
602 +
1202 . 2
1
2
3
+
+
-
-
+
-
1
3
-

Problem Author: Alexander Klepinin
Problem Source: Ural State University Internal Contest October'2000 Students Session


题意

一门新的程序设计语言 D++ 被开发出来,用于在校学生参加的程序设计竞赛。但是需要一个新的操作系统才能运行 D++ 语言写的程序。现在你需要为新的操作系统写第一个模块:内存管理。

该操作系统以“块”为单位分配内存,“块”从 1 到 N 进行编号。当操作系统需要内存时就向内存管理模块发送一个请求。内存管理模块收到该请求后将分配编号最小的“自由块”。

任何已分配的“内存块”如果在 T 分钟之内没有收到访问请求,将被释放,成为“自由块”。

任何时刻操作系统都可以请求访问一个给定编号的“内存块”。如果该“内存块”尚未分配,该请求将失败。否则,该请求成功,并且将该“内存块”保持 T 分钟不被释放。

输入的每一行包含一个内存分配请求或者内存访问请求。内存分配请求的如下所示:

<Time> +

内存访问请求如下所示:

<Time> . <BlockNo>

这里 <Time> 是一个不大于 65,000 的非负整数,表示请求的时间。<BlockNo> 是一个 1 到 N 之间的整数,表示所要访问的内存块的编号。

对应每一行输入都有一行输出。对于内存分配请求,输出分配的“内存块”编号。对于内存访问请求,如果成功,输出“+”号,否则,输出“-”号。

解题思路

在程序的开始将建立一个存储已经分配的“内存块”的优先队列。所以,需要一个数据结构来表示“内存块”,如下所示:

struct Block
{
  public int Id { get; private set; }
  public int Time { get; set; }
  public Block(int id, int time) : this() { Id = id; Time = time; }
}

这里 Id 表示“内存块”的编号,Time 表示该“内存块”到期时间(过了这个时间该“内存块”将被释放)。

由于到期时间早的“内存块”将先出队,所以应该是“小者优先”,而不是通常的“大者优先”。因此,需要一个逆序的“比较器”,如下所示:

sealed class TimeComparer : IComparer<Block>
{
  public int Compare(Block x, Block y)
  {
    return (x.Time == y.Time) ? 0 : ((x.Time < y.Time) ? 1 : -1);
  }
}

接着,将建立一个存储“自由块”编号的优先队列。由于该内存管理模块将分配编号最小的“自由块”,因此,也需要一个逆序的“比较器”,如下所示:

sealed class IdComparer : IComparer<int>
{
  public int Compare(int x, int y)
  {
    return (x == y) ? 0 : ((x < y) ? 1 : -1);
  }
}

现在,轮到主程序上场了:

using System;
using System.Collections.Generic;
using Skyiv.Util;

namespace Skyiv.Ben.Timus
{
  // http://acm.timus.ru/problem.aspx?space=1&num=1037
  sealed class T1037
  {
    static void Main()
    {
      const int T = 10 * 60, N = 30000;
      var used = new KeyedPriorityQueue(new TimeComparer());
      var free = new PriorityQueue<int>(new IdComparer());
      for (var i = 1; i <= N; i++) free.Push(i);
      for (string s; (s = Console.ReadLine()) != null; )
      {
        var ss = s.Split();
        int id, time = int.Parse(ss[0]);
        for (Block v; used.Count > 0 && (v = used.Top()).Time <= time; used.Pop()) free.Push(v.Id);
        if (ss.Length == 2)
        {
          used.Push(new Block(id = free.Pop(), time + T));
          Console.WriteLine(id);
        }
        else if (ss.Length == 3)
        {
          var hited = used.ContainsKey(id = int.Parse(ss[2]));
          if (hited) used.Update(new Block(id, time + T));
          Console.WriteLine(hited ? "+" : "-");
        }
      }
    }
  }
}

这段代码是非常简单的。在 Main 方法的主循环中按行读取输入,获取请求的时间(time = int.Parse(ss[0])), 接着用一个循环将 used 队列中所有已经到期的“内存块”出队(used.Pop()),并将其编号(Id)加入到 free 队列中(free.Push(v.Id))。

接着,如果是请求分配内存(ss.Length == 2),则从 free 队列中出队,并加入到 used 队列中去(used.Push(new Block(id = free.Pop(), time + T))),然后输出该“内存块”的编号即可(Console.WriteLine(id))。

如果是内存访问请求(ss.Length == 3),则检查要访问的“内存块”是否在 used 队列中(hited = used.ContainsKey(id = int.Parse(ss[2]))),如果是的话,就更新其到期时间(used.Update(new Block(id, time + T)))。最后则输出结果(Console.WriteLine(hited ? "+" : "-"))。

现在,轮到存储已经分配的“内存块”的优先队列 KeyedPriorityQueue 上场了:

class KeyedPriorityQueue
{
  Dictionary<int, int> keys;
  IComparer<Block> comparer;
  Block[] heap;

  public int Count { get; private set; }

  public KeyedPriorityQueue(IComparer<Block> comparer)
  {
    this.keys = new Dictionary<int, int>();
    this.comparer = (comparer == null) ? Comparer<Block>.Default : comparer;
    this.heap = new Block[16];
  }

  public bool ContainsKey(int id)
  {
    return keys.ContainsKey(id);
  }

  public void Update(Block v)
  {
    if (!ContainsKey(v.Id)) throw new ArgumentOutOfRangeException("v", v, "更新优先队列时无此健值");
    var cmp = comparer.Compare(v, heap[keys[v.Id]]);
    heap[keys[v.Id]].Time = v.Time;
    if (cmp < 0) SiftDown(keys[v.Id]);
    else if (cmp > 0) SiftUp(keys[v.Id]);
  }

  public void Push(Block v)
  {
    if (Count >= heap.Length) Array.Resize(ref heap, Count * 2);
    heap[keys[v.Id] = Count++] = v;
    SiftUp(keys[v.Id]);
  }

  public Block Pop()
  {
    var v = Top();
    keys.Remove(v.Id);
    heap[0] = heap[--Count];
    if (Count > 0) SiftDown(keys[heap[0].Id] = 0);
    return v;
  }

  public Block Top()
  {
    if (Count > 0) return heap[0];
    throw new InvalidOperationException("优先队列为空");
  }

  void SiftUp(int n)
  {
    var v = heap[n];
    for (var n2 = n / 2; n > 0 && comparer.Compare(v, heap[n2]) > 0; n = n2, n2 /= 2)
      heap[keys[heap[n2].Id] = n] = heap[n2];
    heap[keys[v.Id] = n] = v;
  }

  void SiftDown(int n)
  {
    var v = heap[n];
    for (var n2 = n * 2; n2 < Count; n = n2, n2 *= 2)
    {
      if (n2 + 1 < Count && comparer.Compare(heap[n2 + 1], heap[n2]) > 0) n2++;
      if (comparer.Compare(v, heap[n2]) >= 0) break;
      heap[keys[heap[n2].Id] = n] = heap[n2];
    }
    heap[keys[v.Id] = n] = v;
  }
}

如上所示,KeyedPriorityQueue 类和我在上一篇随笔“用 C# 实现优先队列”中实现的 PriorityQueue<T> 类非常相像。这两者的算法是一样的,都是用“堆(heap)”来实现优先队列。

KeyedPriorityQueue 用一个字典(Dictionary<int, int> keys)来存储“内存块”的编号(Id)及其在堆中的索引,以便判断给定的“内存块”是否在队列中(bool ContainsKey(int id)),并且根据需要更新其到期时间(Update(Block v))。

其他方法,PushPopTopSiftUpSiftDown,除了加入对字典 keys 的处理以外,和 PriorityQueue<T> 类是一样的。

进一步的讨论

这个程序中使用 KeyedPriorityQueue 来存储已分配的“内存块”,使用 PriorityQueue<T> 来存储尚未分配的“自由块”。这两个优先队列的算法是一样的,可以想办法合并。这将在下一篇随笔中讨论。

这道题中假设了问题的规模满足以下限制条件:

  1. “内存块”的总数目 N 不超过 30,000
  2. 时间 Time 不超过 65,000
  3. 请求的总数不超过 80,000

我们在程序中只使用第一个限制条件,即在 Main 方法的第四行中将所有的“内存块”都加入到存储“自由块”的优先队列 free 中:

for (var i = 1; i <= N; i++) free.Push(i);

实际上,这个限制条件也很容易绕过的。我们可以修改存储“自由块”的优先队列,如下所示:

sealed class MyPriorityQueue
{
  int id = 1;
  PriorityQueue<int> queue;

  public MyPriorityQueue(IComparer<int> comparer)
  {
    queue = new PriorityQueue<int>(comparer);
  }

  public void Push(int v)
  {
    queue.Push(v);
  }

  public int Pop()
  {
    return (queue.Count == 0) ? id++ : queue.Pop();
  }
}

然后,将 Main 方法的第三行中的 PriorityQueue<int> 替换为 MyPriorityQueue,并删除第四行和第一行中的 N = 30000 即可。

因为在 MyPriorityQueue 类的 Pop 方法中,一旦发现该优先队列为空,就返回字段 id 的值,并使 id  的值增一。这个 id 就是我们将要分配的最小“自由块”的编号。

这样,我们的内存管理模块就可以无视题目的限制条件,在问题的规模超过这些限制条件时仍然能够正常工作。


返回目录

你可能感兴趣的:(memory)