К содержимому
ИС30

Поиск по сайту

Конспекты, лабы, квизы, ЧаВо и страницы

Войти
Алгоритмы и структуры данных
АиСДЛаба 2

Кучи: планировщик NIGHTBUILD

Шесть задач на sort-me.org: проверка пирамиды, пирамидальная сортировка, очередь с минимумом и максимумом, назначение задач на ядра, менеджер памяти и фибоначчиева куча.

Курьянов Николай Алексеевич
Дедлайн
пятница, 30 октября
до 23:59
Через 14 днейДедлайн мягкий: сдать можно и позже, но лучше не тянуть

Время по таймеру контеста на sort-me.org.

Войти, чтобы записаться на сдачу
Войдите

Задание

Шесть задач про кучи на сюжете ночного сбоя планировщика NIGHTBUILD: от проверки инварианта пирамиды до фибоначчиевой кучи. Решения сдаются на sort-me.org на GNU C++20, у каждой задачи свои баллы, задача F со звёздочкой.

Лимиты

Во всех задачах 1 секунда. Память: 128 МБ в задачах A, B, D и F, 256 МБ в задачах C и E.

Заготовки

У каждой задачи есть заготовка кода по Google C++ Style Guide: массивы, структуры и функции, память выделяется через new[] и освобождается через delete[]. Ввод и вывод уже написаны, остаётся реализовать функции. Кучу в каждой задаче стоит написать самому: ради этого лаба и задумана.

ЗадачаЧто нужноЛимит памяти
A. Core Invariantпроверить, что массив — пирамида128 МБ
B. Wakeup Queueпирамидальная сортировка128 МБ
C. Priority Dispatcherочередь с извлечением минимума и максимума256 МБ
D. CPU Allocatorназначение задач на ядра двумя кучами128 МБ
E. Memory Managerвыделение блоков памяти с таймаутом256 МБ
F. [*] Queue Fabricфибоначчиева куча128 МБ

A. Core Invariant

[02:47] Ночной билд остановлен. [02:48] Планировщик перешёл в аварийный режим. [02:49] Принято решение восстановить ядро вручную.

Сердце планировщика NIGHTBUILD — приоритетная очередь в виде неубывающей пирамиды. Если хотя бы один элемент нарушает иерархию, система не запустится.

Дан массив aa длины nn — состояние очереди в момент сбоя. Планировщик корректен, если для каждого ii (1≤i≤n1 \le i \le n):

  • если 2i≤n2i \le n, то ai≤a2ia_i \le a_{2i};
  • если 2i+1≤n2i + 1 \le n, то ai≤a2i+1a_i \le a_{2i+1}.

Нужно определить, можно ли безопасно перезапустить NIGHTBUILD.

Входные данные

  • Первая строка: целое число nn (1≤n≤1051 \le n \le 10^5).
  • Вторая строка: nn целых чисел aia_i (∣ai∣≤2⋅109|a_i| \le 2 \cdot 10^9) — приоритеты процессов.

Выходные данные

YES, если массив удовлетворяет инварианту пирамиды, иначе NO.

Лимит памяти: 128 МБ.

Пример

stdin
5
1 0 1 2 0
stdout
NO

Примечание. a1=1>a2=0a_1 = 1 > a_2 = 0 — инвариант нарушен уже у корня. Числа до 2⋅1092 \cdot 10^9 по модулю не помещаются в int, нужен 64-битный тип.

Заготовка

Ввод и вывод уже написаны, остаётся реализовать функцию.

main.cpp
#include <cstdint>
#include <iostream>
 
// Возвращает true, если a[1..n] — неубывающая пирамида: каждый элемент
// не больше своих детей a[2i] и a[2i + 1]. Нумерация с 1, a[0] не занят.
bool IsHeap(const int64_t* a, int n) {
  // TODO
  return false;
}
 
int main() {
  std::ios::sync_with_stdio(false);
  std::cin.tie(nullptr);
 
  int n;
  std::cin >> n;
  int64_t* a = new int64_t[n + 1];
  for (int i = 1; i <= n; ++i) {
    std::cin >> a[i];
  }
  std::cout << (IsHeap(a, n) ? "YES" : "NO") << "\n";
  delete[] a;
  return 0;
}

B. Wakeup Queue

[03:12] Планировщик остаётся в аварийном режиме. [03:14] Автоматическая очередь пробуждения не инициализировалась. [03:15] Система ожидает ручного восстановления приоритетов.

После аварии очередь пробуждения сбросилась в неупорядоченное состояние. Дан массив aa из nn целых чисел — приоритеты процессов. Выведите их в порядке неубывания.

Только пирамидальная сортировка

По условию нужно использовать пирамидальную сортировку (heapsort): построить пирамиду за O(n)O(n) и nn раз перенести максимум в конец.

Входные данные

  • Первая строка: целое число nn (1≤n≤1061 \le n \le 10^6).
  • Вторая строка: nn целых чисел aia_i (∣ai∣≤109|a_i| \le 10^9).

Выходные данные

nn чисел — элементы массива в порядке неубывания.

Лимит памяти: 128 МБ.

Пример

stdin
10
1 8 2 1 4 7 3 2 3 6
stdout
1 1 2 2 3 3 4 6 7 8

Заготовка

Ввод и вывод уже написаны, остаётся реализовать функции.

main.cpp
#include <iostream>
 
// Просеивает a[i] вниз в пирамиде из первых n элементов массива.
void SiftDown(int* a, int n, int i) {
  // TODO
}
 
// Сортирует a[0..n-1] по неубыванию пирамидальной сортировкой.
// Готовыми функциями сортировки и кучами из STL пользоваться нельзя.
void HeapSort(int* a, int n) {
  // TODO
}
 
int main() {
  std::ios::sync_with_stdio(false);
  std::cin.tie(nullptr);
 
  int n;
  std::cin >> n;
  int* a = new int[n];
  for (int i = 0; i < n; ++i) {
    std::cin >> a[i];
  }
  HeapSort(a, n);
  for (int i = 0; i < n; ++i) {
    std::cout << a[i] << " ";
  }
  std::cout << "\n";
  delete[] a;
  return 0;
}

C. Priority Dispatcher

[03:42] Планировщик частично восстановлен. [03:45] Очередь диспетчера пуста. [03:47] Необходимо срочно восстановить механизм выборки приоритетов.

Диспетчер выбирает задачи и для немедленного выполнения, и для отложенных операций, поэтому очередь должна поддерживать три команды:

  • Insert(x) — добавить процесс с приоритетом xx;
  • GetMin — извлечь и вывести минимальный приоритет;
  • GetMax — извлечь и вывести максимальный приоритет.

Гарантируется, что извлечения из пустой очереди не будет.

Подсказка

Подойдёт min-max куча или две кучи (на минимум и на максимум), связанные между собой: извлечённый из одной элемент нужно убрать и из другой — по индексам или ленивым удалением.

Входные данные

  • Первая строка: целое число qq (1≤q≤1051 \le q \le 10^5) — количество операций.
  • Далее qq строк с командами. Число в Insert пишется в скобках, без пробела.
  • ∣x∣≤2⋅109|x| \le 2 \cdot 10^9.

Выходные данные

Для каждой команды GetMin и GetMax — извлечённое значение в отдельной строке.

Лимит памяти: 256 МБ.

Пример

stdin
10
Insert(100)
Insert(99)
Insert(1)
Insert(2)
GetMin
GetMax
Insert(1)
GetMin
GetMin
GetMax
stdout
1
100
1
2
99

Заготовка

Ввод и вывод уже написаны, остаётся реализовать функции.

main.cpp
#include <cstdint>
#include <iostream>
#include <string>
 
// Очередь, из которой можно извлекать и минимум, и максимум.
// Устройство — на ваше усмотрение: память под capacity элементов.
struct Dispatcher {
  int capacity;
  // TODO
};
 
void Init(Dispatcher* dispatcher, int capacity) {
  dispatcher->capacity = capacity;
  // TODO
}
 
void Destroy(Dispatcher* dispatcher) {
  // TODO
}
 
void Insert(Dispatcher* dispatcher, int64_t x) {
  // TODO
}
 
// Извлекает и возвращает минимальный элемент. Очередь не пуста.
int64_t ExtractMin(Dispatcher* dispatcher) {
  // TODO
  return 0;
}
 
// Извлекает и возвращает максимальный элемент. Очередь не пуста.
int64_t ExtractMax(Dispatcher* dispatcher) {
  // TODO
  return 0;
}
 
int main() {
  std::ios::sync_with_stdio(false);
  std::cin.tie(nullptr);
 
  int q;
  std::cin >> q;
  Dispatcher dispatcher;
  Init(&dispatcher, q);
  std::string command;
  for (int i = 0; i < q; ++i) {
    std::cin >> command;
    if (command == "GetMin") {
      std::cout << ExtractMin(&dispatcher) << "\n";
    } else if (command == "GetMax") {
      std::cout << ExtractMax(&dispatcher) << "\n";
    } else {
      // Команда вида Insert(x): число между скобками.
      Insert(&dispatcher, std::stoll(command.substr(7)));
    }
  }
  Destroy(&dispatcher);
  return 0;
}

D. CPU Allocator

[04:10] Планировщик проснулся, но распределение ядер отключено. [04:12] Очередь свободных CPU пуста, занятые не освобождаются. [04:14] Требуется восстановить модуль назначения задач на ядра по минимальной стоимости.

У каждого из nn ядер своя ставка aia_i — стоимость минуты работы, все ставки различны.

  • Задача jj приходит в момент tjt_j и длится fjf_j минут.
  • В момент прихода она сразу назначается на свободное ядро с минимальной ставкой.
  • Если свободных ядер нет, задача отбрасывается.
  • Ядро занято на полуинтервале [tj,tj+fj)[t_j, t_j + f_j) и снова свободно ровно в момент tj+fjt_j + f_j.

Суммарная стоимость — сумма (ставка ядра) ×\times (длительность задачи) по всем назначенным задачам. Задачи даны по возрастанию tjt_j, все tjt_j различны.

Подсказка

Нужны две кучи: свободные ядра по ставке и занятые по моменту освобождения. Перед каждой задачей верните в свободные все ядра, которые освободились к моменту tjt_j включительно. Ответ может не влезть в 32 бита.

Входные данные

  • Первая строка: nn и mm (1≤n,m≤3⋅1051 \le n, m \le 3 \cdot 10^5) — количество ядер и задач.
  • Вторая строка: nn различных целых чисел a1,…,ana_1, \ldots, a_n — ставки ядер.
  • Далее mm строк по два целых числа tjt_j и fjf_j (1≤tj≤1091 \le t_j \le 10^9, 1≤fj≤1061 \le f_j \le 10^6).

Выходные данные

Одно целое число — суммарная стоимость по назначенным задачам.

Лимит памяти: 128 МБ.

Пример

stdin
4 7
3 2 6 4
1 3
2 5
3 7
4 10
5 5
6 100
9 2
stdout
105

Примечание. Как распределятся задачи:

ЗадачаСвободныЯдроЗанято доСтоимость
t=1,f=3t=1, f=32, 3, 4, 6246
t=2,f=5t=2, f=53, 4, 63715
t=3,f=7t=3, f=74, 641028
t=4,f=10t=4, f=102, 621420
t=5,f=5t=5, f=5661030
t=6,f=100t=6, f=100———отброшена
t=9,f=2t=9, f=233116

Итого 6+15+28+20+30+6=1056 + 15 + 28 + 20 + 30 + 6 = 105. Ядро 2 освобождается в момент 4 ровно к приходу четвёртой задачи, ядро 3 — в момент 7 и успевает взять последнюю.

Заготовка

Ввод и вывод уже написаны, остаётся реализовать функцию.

main.cpp
#include <cstdint>
#include <iostream>
 
// Возвращает суммарную стоимость: каждая задача j в момент start[j] уходит
// на свободное ядро с минимальной ставкой и занимает его на duration[j]
// минут, а если свободных ядер нет — отбрасывается.
int64_t TotalCost(const int64_t* rates, int n, const int64_t* start,
                  const int64_t* duration, int m) {
  // TODO
  return 0;
}
 
int main() {
  std::ios::sync_with_stdio(false);
  std::cin.tie(nullptr);
 
  int n;
  int m;
  std::cin >> n >> m;
  int64_t* rates = new int64_t[n];
  for (int i = 0; i < n; ++i) {
    std::cin >> rates[i];
  }
  int64_t* start = new int64_t[m];
  int64_t* duration = new int64_t[m];
  for (int j = 0; j < m; ++j) {
    std::cin >> start[j] >> duration[j];
  }
  std::cout << TotalCost(rates, n, start, duration, m) << "\n";
  delete[] rates;
  delete[] start;
  delete[] duration;
  return 0;
}

E. Memory Manager

[04:45] Очередь задач стабилизировалась. [04:46] Мониторинг памяти сообщает об утечках. [04:48] Модуль управления памятью не реагирует на запросы.

Память выделяется блоками с номерами от 1 до NN. Запрос на выделение получает свободный блок с минимальным номером. Занятый блок освобождается сам через фиксированное время LL после последнего обращения.

  • <Time> + — выделить новый блок в момент Time.
  • <Time> . <BlockNo> — обратиться к блоку BlockNo в момент Time: если блок занят, запрос успешен и время освобождения сдвигается на LL вперёд; если свободен — запрос ошибочный.

События даны по возрастанию времени, одновременные обрабатываются в порядке входа. Памяти гарантированно хватает.

Единицы времени

В условии L=10L = 10 минут, а пример сходится, только если Time — это секунды, то есть блок живёт 600 единиц времени. Блок, выделенный в момент 1, в момент 601 уже свободен.

Подсказка

Две кучи: свободные блоки по номеру и занятые по моменту освобождения. При продлении старая запись в куче занятых устаревает — её можно пропускать при извлечении (ленивое удаление), сверяя с актуальным временем освобождения блока.

Входные данные

Каждая строка — один запрос в одном из двух форматов выше. Time от 0 до 65000, номера блоков от 1 до 30000, запросов не больше 80000. Ввод читается до конца файла.

Выходные данные

Для выделения — номер выделенного блока. Для обращения — +, если блок был занят, и -, если свободен.

Лимит памяти: 256 МБ.

Пример

stdin
1 +
1 +
1 +
2 . 2
2 . 3
3 . 30000
601 . 1
601 . 2
602 . 3
602 +
602 +
1202 . 2
stdout
1
2
3
+
+
-
-
+
-
1
3
-

Заготовка

Ввод и вывод уже написаны, остаётся реализовать функции.

main.cpp
#include <iostream>
 
constexpr int kBlocks = 30000;
 
// Состояние памяти: какие блоки заняты и когда освобождаются.
struct MemoryManager {
  // TODO
};
 
void Init(MemoryManager* memory) {
  // TODO
}
 
void Destroy(MemoryManager* memory) {
  // TODO
}
 
// Выделяет свободный блок с минимальным номером в момент time.
int Allocate(MemoryManager* memory, int time) {
  // TODO
  return 0;
}
 
// Обращение к блоку в момент time: true, если блок занят (тогда его время
// освобождения продлевается), false, если он свободен.
bool Access(MemoryManager* memory, int time, int block) {
  // TODO
  return false;
}
 
int main() {
  std::ios::sync_with_stdio(false);
  std::cin.tie(nullptr);
 
  MemoryManager memory;
  Init(&memory);
  int time;
  char type;
  while (std::cin >> time >> type) {
    if (type == '+') {
      std::cout << Allocate(&memory, time) << "\n";
    } else {
      int block;
      std::cin >> block;
      std::cout << (Access(&memory, time, block) ? '+' : '-') << "\n";
    }
  }
  Destroy(&memory);
  return 0;
}

F. [*] Queue Fabric

[05:45] Ядро NIGHTBUILD стабилизировано, но производительность критически низкая. [05:47] Обычные структуры планировщика не справляются с нагрузкой. [05:49] Принято решение активировать экспериментальный модуль Fib-Kernel.

Нужно реализовать фибоначчиеву кучу: она поддерживает быстрое слияние очередей и амортизированное уменьшение ключа. Очереди нумеруются с 0 в порядке создания. Команды, по одной в строке:

  • create — создать новую пустую кучу;
  • insert k x — добавить xx в кучу kk;
  • extract-min k — извлечь и вывести минимум кучи kk, если куча пуста — вывести *;
  • merge k m — создать новую кучу как результат слияния куч kk и mm;
  • decrease-key k x y — в куче kk заменить значение xx на yy; гарантируется, что xx есть в куче kk и x≥yx \ge y.

Числа по модулю не больше 10910^9, команд не больше 10610^6, все номера куч корректны.

Внимательно с merge

В примере после merge 0 1 куча 0 продолжает отдавать свои элементы 3 и 5, хотя в новой куче 2 пятёрку уже уменьшили до 1. Значит, исходные кучи после слияния не пустеют и не меняются вместе с новой. Если поведение на тестах расходится с ожиданиями — проверьте объявления контеста.

Входные данные

Последовательность команд в описанном формате, до конца файла.

Выходные данные

Для каждой команды extract-min — минимум кучи или *, если она пуста.

Лимит памяти: 128 МБ.

Пример

stdin
create
insert 0 1
insert 0 3
insert 0 5
extract-min 0
create
insert 1 2
insert 1 4
insert 1 6
merge 0 1
decrease-key 2 5 1
extract-min 2
extract-min 2
extract-min 0
extract-min 0
extract-min 0
stdout
1
1
2
3
5
*

Примечание. Куча 0 после первого извлечения — {3,5}\{3, 5\}, куча 1 — {2,4,6}\{2, 4, 6\}, куча 2 после слияния и decrease-key — {1,2,3,4,6}\{1, 2, 3, 4, 6\}. Из кучи 2 извлекаются 1 и 2, из кучи 0 — 3, 5, а затем она пуста.

Заготовка

Ввод и вывод уже написаны, остаётся реализовать функции. Для decrease-key по значению понадобится способ быстро найти узел с ключом xx в куче kk.

main.cpp
#include <cstdint>
#include <iostream>
#include <string>
 
// Узел фибоначчиевой кучи.
struct Node {
  int64_t key;
  int degree = 0;
  bool marked = false;
  Node* parent = nullptr;
  Node* child = nullptr;
  Node* left = this;   // Соседи по циклическому списку.
  Node* right = this;
};
 
struct FibHeap {
  Node* min = nullptr;
  int size = 0;
};
 
void Insert(FibHeap* heap, int64_t x) {
  // TODO
}
 
// Извлекает минимум в *result. Возвращает false, если куча пуста.
bool ExtractMin(FibHeap* heap, int64_t* result) {
  // TODO
  return false;
}
 
// Возвращает новую кучу из элементов first и second.
FibHeap* Merge(FibHeap* first, FibHeap* second) {
  // TODO
  return new FibHeap;
}
 
// Заменяет значение x на y <= x.
void DecreaseKey(FibHeap* heap, int64_t x, int64_t y) {
  // TODO
}
 
int main() {
  std::ios::sync_with_stdio(false);
  std::cin.tie(nullptr);
 
  constexpr int kMaxHeaps = 1000000;
  FibHeap** heaps = new FibHeap*[kMaxHeaps];
  int count = 0;
  std::string command;
  while (std::cin >> command) {
    if (command == "create") {
      heaps[count++] = new FibHeap;
    } else if (command == "insert") {
      int k;
      int64_t x;
      std::cin >> k >> x;
      Insert(heaps[k], x);
    } else if (command == "extract-min") {
      int k;
      std::cin >> k;
      int64_t result;
      if (ExtractMin(heaps[k], &result)) {
        std::cout << result << "\n";
      } else {
        std::cout << "*\n";
      }
    } else if (command == "merge") {
      int k;
      int m;
      std::cin >> k >> m;
      heaps[count++] = Merge(heaps[k], heaps[m]);
    } else if (command == "decrease-key") {
      int k;
      int64_t x;
      int64_t y;
      std::cin >> k >> x >> y;
      DecreaseKey(heaps[k], x, y);
    }
  }
  delete[] heaps;
  return 0;
}

Требования

  • Язык решений — GNU C++20.
  • Лимит 1 секунда; память 128 МБ в задачах A, B, D, F и 256 МБ в C и E.
  • Каждая задача оценивается автоматически по тестам на sort-me.org.
  • В задаче B обязательна пирамидальная сортировка.

Как сдавать

Решения отправляются на sort-me.org: откройте задачу, прикрепите файл с кодом и нажмите «Отправить». Дедлайн указан в таймере контеста.

Материалы

Комментарии0

Пока никто ничего не написал.

Войдите, чтобы оставить комментарий