diff --git a/labs/04-priority-queues-exercises.ipynb b/labs/04-priority-queues-exercises.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..6a121d1b583cdd7373f1bdfcb46afc1f1357ded3 --- /dev/null +++ b/labs/04-priority-queues-exercises.ipynb @@ -0,0 +1,468 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "994d4d0c", + "metadata": {}, + "source": [ + "NAME:" + ] + }, + { + "cell_type": "markdown", + "id": "651a2e17-2fe1-41c7-be26-a7d3b40f9277", + "metadata": {}, + "source": [ + "# UE5 Fundamentals of Algorithms\n", + "# Lab 4: Priority queues" + ] + }, + { + "cell_type": "markdown", + "id": "dd4f535a-2401-45c6-9d4a-ca8f146aa9c9", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "markdown", + "id": "615a1282-53cf-4610-bddd-2a3ba105e5bf", + "metadata": {}, + "source": [ + "<details style=\"border: 1px\">\n", + "<summary> How to use those notebooks</summary>\n", + " \n", + "For each of the following questions:\n", + "- In the `# YOUR CODE HERE` cell, remove `raise NotImplementedError()` to write your code\n", + "- Write an example of use of your code or make sure the given examples and tests pass\n", + "- Add extra tests in the `#Tests` cell\n", + " \n", + "</details>" + ] + }, + { + "cell_type": "markdown", + "id": "b93b7332-5a40-4a85-b505-83835d68fe67", + "metadata": {}, + "source": [ + "## Exercise 1: How to use a priority queue\n", + "\n", + "Your are given a list of elements (`items`) and a matching list of priorities (`priorities`) as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b91e0762-b158-44b1-850e-2987d7206b2b", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "items = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']\n", + "priorities = [2, 1, 3, 3, 0, 1, 1, 2]\n", + "\n", + "for item, priority in zip(items, priorities):\n", + " print((item, priority))" + ] + }, + { + "cell_type": "markdown", + "id": "2a348646-7633-4e79-a4ad-9597893c327d", + "metadata": {}, + "source": [ + "Use the Python [PriorityQueue](https://docs.python.org/3/library/queue.html#queue.PriorityQueue) module to store such values, and then return by _ascending_ order." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18d02388-dca0-4493-a1c6-9d2c1314683d", + "metadata": { + "deletable": false, + "nbgrader": { + "cell_type": "code", + "checksum": "b4b91474b8a893af393ecf41a4177229", + "grade": false, + "grade_id": "cell-7a9ba41922a9d48b", + "locked": false, + "schema_version": 3, + "solution": true, + "task": false + }, + "tags": [] + }, + "outputs": [], + "source": [ + "q = PriorityQueue()\n", + "\n", + "# YOUR CODE HERE\n", + "raise NotImplementedError()" + ] + }, + { + "cell_type": "markdown", + "id": "f44ea802-5fbf-404c-8430-0b931da0bf53", + "metadata": {}, + "source": [ + "Write a similar version that returns the `items` by _descending_ order (we assume `priorities` are always integers)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9bc92f94-97a1-46fa-813c-63fb6abc9f65", + "metadata": { + "deletable": false, + "nbgrader": { + "cell_type": "code", + "checksum": "4296a9b681b44c000d6aec878f352558", + "grade": false, + "grade_id": "cell-55279200308121d0", + "locked": false, + "schema_version": 3, + "solution": true, + "task": false + }, + "tags": [] + }, + "outputs": [], + "source": [ + "q = PriorityQueue()\n", + "\n", + "# YOUR CODE HERE\n", + "raise NotImplementedError()" + ] + }, + { + "cell_type": "markdown", + "id": "c2ab7bd0-a237-4755-9421-a2dee82e77a4", + "metadata": {}, + "source": [ + "## Exercise 2: Implement your own priority queue\n", + "\n", + "Implement your own priority queue class and compare it to the Python module `PriorityQueue` use previously. Use the stack (or the queue) we have seen in the previous lab." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ca96c6ee-54c9-47ba-959e-60c7168256d6", + "metadata": { + "deletable": false, + "nbgrader": { + "cell_type": "code", + "checksum": "97ee0cb3d80986b98414224fb7f81255", + "grade": false, + "grade_id": "cell-256c1b911658a81f", + "locked": false, + "schema_version": 3, + "solution": true, + "task": false + }, + "tags": [] + }, + "outputs": [], + "source": [ + "class MyPriorityQueue():\n", + " # YOUR CODE HERE\n", + " raise NotImplementedError()" + ] + }, + { + "cell_type": "markdown", + "id": "9e2f1c19-1f64-4de1-b9e8-1ed2e167cf40", + "metadata": {}, + "source": [ + "Write comparisons with the `PriorityQueue` module:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e86176bf-fb09-44a1-8732-5becb06921af", + "metadata": { + "deletable": false, + "nbgrader": { + "cell_type": "code", + "checksum": "a7d5a588a04dc75038101a4010a5ad1c", + "grade": false, + "grade_id": "cell-fd82f6f4e92e8816", + "locked": false, + "schema_version": 3, + "solution": true, + "task": false + }, + "tags": [] + }, + "outputs": [], + "source": [ + "p1 = PriorityQueue()\n", + "p2 = MyPriorityQueue()\n", + "\n", + "# YOUR CODE HERE\n", + "raise NotImplementedError()" + ] + }, + { + "cell_type": "markdown", + "id": "9f0d41e9-254a-4121-a209-e751e6122e7c", + "metadata": { + "tags": [] + }, + "source": [ + "## Exercise 3: Tasks scheduler\n", + "\n", + "In this exercise we want to write an algorithm that will schedule some tasks. Think of a tasks as an event with a name (eg `\"Task 1\"`, a `start` time and an `end` time). To keep it simple, we will consider times as `int`. The goal will be to write a scheduler that will decide which task to do, with an important condition: tasks cannot overlap (ie at any given moment, only 1 task can be picked). You'll start by defining a `Task` class and then the `TaskScheduler` class that manages the list of tasks you picked and that do not overlap.\n", + "\n", + "Start by writing the `Task` class and in particular include a way to compare tasks by overriding the [comparison operator](https://docs.python.org/3/library/operator.html):\n", + "\n", + "```python\n", + " def __lt__(self, other):\n", + " return self.start < other.start\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "44aacc95-2aa6-485b-8759-ac292244f290", + "metadata": { + "deletable": false, + "nbgrader": { + "cell_type": "code", + "checksum": "31d7c78de176602c4028bc2873ba2eaa", + "grade": false, + "grade_id": "cell-8eeb5fa664a2abf2", + "locked": false, + "schema_version": 3, + "solution": true, + "task": false + }, + "tags": [] + }, + "outputs": [], + "source": [ + "class Task:\n", + " # YOUR CODE HERE\n", + " raise NotImplementedError()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6430a61e-879d-4c2d-bc92-a6f288786242", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "task1 = Task(\"Task 1\", 1, 4)\n", + "task2 = Task(\"Task 2\", 2, 5)\n", + "assert task1 < task2" + ] + }, + { + "cell_type": "markdown", + "id": "7563c27b-c412-4899-acb3-57c10f2abb8d", + "metadata": {}, + "source": [ + "Now write the `TaskScheduler` class. You can use the `PriorityQueue` or your own implementation of a priority queue. Tip: write 3 methods:\n", + "\n", + "1. add a task\n", + "2. check if overlapping\n", + "3. return the non-overlapping tasks" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6e9d6120-ffb9-4a2f-9fb6-e3c0761ecf75", + "metadata": { + "deletable": false, + "nbgrader": { + "cell_type": "code", + "checksum": "eb1328388c170d781a8878d366569388", + "grade": false, + "grade_id": "cell-e47f1725f66b5081", + "locked": false, + "schema_version": 3, + "solution": true, + "task": false + }, + "tags": [] + }, + "outputs": [], + "source": [ + "from queue import PriorityQueue\n", + "\n", + "class TaskScheduler:\n", + " # YOUR CODE HERE\n", + " raise NotImplementedError()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "558776cd-9f8f-404e-bc7f-803c4fbbae55", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Usage example\n", + "s = TaskScheduler()\n", + "\n", + "t1 = Task(\"Task 1\", 1, 4)\n", + "t2 = Task(\"Task 2\", 2, 5) # overlap\n", + "t3 = Task(\"Task 3\", 5, 6)\n", + "\n", + "s.put(t1)\n", + "s.put(t2) # cannot add (overlap)\n", + "s.put(t3)\n", + "\n", + "for task in s.get():\n", + " print(f\"{task.name}: [{task.start}, {task.end}]\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14c5196d-d45e-4314-9678-60548fe2f811", + "metadata": {}, + "outputs": [], + "source": [ + "# Tests\n", + "s = TaskScheduler()\n", + "assert s.get() == []" + ] + }, + { + "cell_type": "markdown", + "id": "2911284c-7c5b-4c65-a426-dac028d6bd4f", + "metadata": {}, + "source": [ + "## Exercise 4: Merge sorted lists (using heaps)\n", + "\n", + "The `heapq` [(doc)](https://docs.python.org/3/library/heapq.html) module provides an efficient implementation of priority queue using the heap sorting algorithm, that use can use instead of the `PriorityQueue` class. Here is an example on how to use the heap. The operations complexity are are:\n", + "- `heapify` $O(n)$ as it find the smallest value (only) to return next\n", + "- `heappop` $O(log(n))$ as it finds again the smallest value (only) to return next, but in an efficient way" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "da1fbbad-7bb6-4f89-a446-bdb308a545ac", + "metadata": {}, + "outputs": [], + "source": [ + "import heapq\n", + "nums = [10, 20, 5, 7, 9, 15, 3]\n", + "heapq.heapify(nums)\n", + "sorted_list = [heapq.heappop(nums) for _ in range(len(nums))]\n", + "sorted_list" + ] + }, + { + "cell_type": "markdown", + "id": "327aa702-5699-42ba-96e1-b75079d31050", + "metadata": {}, + "source": [ + "Write a function `merge_sorted_lists(lists)` that takes a list of sorted lists, and uses `heapq` to merge them into a single sorted list as a result.\n", + "\n", + "1. Use `heapq` to store the first element of each list.\n", + "2. Store each element in the heap as a tuple `(value, list_index, element_index)`:\n", + " - `value` is the element value,\n", + " - `list_index` is the index of the list it comes from,\n", + " - `element_index` is the position of the element in its original list.\n", + "3. At each step, extract the smallest element from the heap and add it to the result list.\n", + "4. Insert the next element from the same list into the heap until all lists are empty." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4b20f11a-47b8-419a-b94a-902943048c76", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "import heapq\n", + "\n", + "def merge(lists):\n", + " min_heap = []\n", + " merged_list = []\n", + "\n", + " for i, lst in enumerate(lists): # each first element in heap\n", + " if lst:\n", + " heapq.heappush(min_heap, (lst[0], i, 0)) # (value, list_index, element_index)\n", + "\n", + " while min_heap:\n", + " val, list_index, element_index = heapq.heappop(min_heap)\n", + " merged_list.append(val)\n", + "\n", + " if element_index + 1 < len(lists[list_index]):\n", + " next_val = lists[list_index][element_index + 1]\n", + " heapq.heappush(min_heap, (next_val, list_index, element_index + 1))\n", + "\n", + " return merged_list" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1b7e1364-1335-4daf-9100-ef933f97b86d", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Usage example\n", + "lists = [\n", + " [1, 4, 5],\n", + " [1, 3, 4],\n", + " [2, 6]\n", + "]\n", + "\n", + "merge(lists) # [1, 1, 2, 3, 4, 4, 5, 6]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "44600161-32a0-4b84-b152-65a15ba103ae", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Tests\n", + "assert merge([[]]) == []\n", + "assert merge([[1, 2, 3]]) == [1, 2, 3]" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}