diff --git a/labs-solutions/03-stacks-queues-exercises.ipynb b/labs-solutions/03-stacks-queues-exercises.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..a52a72860e761ee0a8f519d41fe1c0fbcae63883
--- /dev/null
+++ b/labs-solutions/03-stacks-queues-exercises.ipynb
@@ -0,0 +1,810 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "id": "2f1f2dcd-96a9-45ef-90a6-4ad488635679",
+   "metadata": {},
+   "source": [
+    "# UE5 Fundamentals of Algorithms\n",
+    "# Lab 3: Stacks and Queues"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "b9bd540c-dd15-49ac-bfbd-f2e758688a85",
+   "metadata": {
+    "tags": []
+   },
+   "source": [
+    "---"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "a18516d6-24ae-4e5c-afb7-43a4435ea868",
+   "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": "7fdd7e6b-da89-4100-8241-0b0d47caa3ed",
+   "metadata": {
+    "tags": []
+   },
+   "source": [
+    "Course reminders:\n",
+    "\n",
+    "<img src=\"./figures/stacks-queues.png\" style=\"width:500px\">\n",
+    "\n",
+    "- `stacks` follow the Last-In, First-Out (LIFO) principle\n",
+    "- `queues`follows the First-In, First-Out (FIFO) principle\n",
+    "\n",
+    "They have the following operations:\n",
+    "\n",
+    "- `empty()`: Checks for emptiness.\n",
+    "- `full()`: Checks if it's full (if a maximum size was provided during creation).\n",
+    "- `get()`: Returns (and removes) an element.\n",
+    "- `push(element)`: Adds an element.\n",
+    "- `size()`: Returns the size of the list.\n",
+    "- `peek()`: Returns an element (without removing it).\n",
+    "- `clear()`: Empties the list.\n",
+    "- `contains(element)`: Checks if a specified element has been added.\n",
+    "- `index(element)`: Finds the index of a specified element.\n",
+    "- `remove(element)`: Removes a specific element (if present)."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "d389441b-f86b-41c6-b7ec-6dd89eb61af6",
+   "metadata": {},
+   "source": [
+    "## Exercise 1: Implement a Stack\n",
+    "\n",
+    "Use the `Stack` object below and implement the above operations."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 76,
+   "id": "146beb1c-fcfa-43cb-9718-7bb61e6201bc",
+   "metadata": {
+    "nbgrader": {
+     "grade": false,
+     "grade_id": "cell-bbf71a239f64c912",
+     "locked": false,
+     "schema_version": 3,
+     "solution": true,
+     "task": false
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "class Stack:\n",
+    "    def __init__(self, max_size=1000):\n",
+    "    ### BEGIN SOLUTION ###\n",
+    "        self.items = []\n",
+    "        self.max_size = max_size\n",
+    "        \n",
+    "    def push(self, item):\n",
+    "        if self.full():\n",
+    "            raise OverflowError(\"Stack is full\")\n",
+    "        self.items.append(item)\n",
+    "\n",
+    "    def get(self):\n",
+    "        if not self.empty():\n",
+    "            return self.items.pop()\n",
+    "        raise IndexError(\"get from empty stack\")\n",
+    "\n",
+    "    def empty(self):\n",
+    "        return len(self.items) == 0\n",
+    "\n",
+    "    def full(self):\n",
+    "        if self.max_size is None:\n",
+    "            return False\n",
+    "        return len(self.items) >= self.max_size\n",
+    "\n",
+    "    def size(self):\n",
+    "        return len(self.items)\n",
+    "\n",
+    "    def peek(self):\n",
+    "        if not self.empty():\n",
+    "            return self.items[-1]\n",
+    "        raise IndexError(\"peek from empty stack\")\n",
+    "\n",
+    "    def clear(self):\n",
+    "        self.items.clear()\n",
+    "\n",
+    "    def contains(self, element):\n",
+    "        return element in self.items\n",
+    "\n",
+    "    def index(self, element):\n",
+    "        try:\n",
+    "            return self.items.index(element)\n",
+    "        except ValueError:\n",
+    "            return -1\n",
+    "\n",
+    "    def remove(self, element):\n",
+    "        if element in self.items:\n",
+    "            self.items.remove(element)\n",
+    "            return True\n",
+    "        return False \n",
+    "    ### END SOLUTION ###"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 83,
+   "id": "5afc8b1f-2708-4289-8d54-c6bdada4d184",
+   "metadata": {
+    "tags": []
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "True"
+      ]
+     },
+     "execution_count": 83,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "# Example of use\n",
+    "s = Stack()\n",
+    "s.push(10)\n",
+    "s.push(20)\n",
+    "s.get() == 20\n",
+    "s.get() == 10"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "6f37285b-a4e6-4261-9c10-3e98f4ef1f08",
+   "metadata": {},
+   "source": [
+    "Compare with the `queue` [package](https://docs.python.org/3/library/queue.html) from Python \n",
+    "\n",
+    "```python\n",
+    "import queue\n",
+    "q1 = queue.LifoQueue()\n",
+    "```"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 80,
+   "id": "d7ff762d-e812-433b-8f57-56004477278f",
+   "metadata": {
+    "nbgrader": {
+     "grade": false,
+     "grade_id": "cell-60ff2cf650eb21cb",
+     "locked": false,
+     "schema_version": 3,
+     "solution": true,
+     "task": false
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "import queue\n",
+    "# Tests\n",
+    "### BEGIN SOLUTION ###\n",
+    "custom_stack = Stack(max_size=3)\n",
+    "custom_stack.push(10)\n",
+    "custom_stack.push(20)\n",
+    "\n",
+    "lifo_queue = queue.LifoQueue(maxsize=3)\n",
+    "lifo_queue.put(10)\n",
+    "lifo_queue.put(20)\n",
+    "\n",
+    "assert custom_stack.full() == lifo_queue.full()\n",
+    "### END SOLUTION ###"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "03a0653e-65c2-4e79-9e83-31765cf19098",
+   "metadata": {},
+   "source": [
+    "## Exercise 2: Reverse a string\n",
+    "\n",
+    "Use the `Stack` data structure to reverse a string `s` given as input."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 78,
+   "id": "8b77ae34-ef7c-4664-94e0-8928156f2224",
+   "metadata": {
+    "nbgrader": {
+     "grade": false,
+     "grade_id": "cell-5b0828e97507162e",
+     "locked": false,
+     "schema_version": 3,
+     "solution": true,
+     "task": false
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "def reverse_string(s):\n",
+    "    ### BEGIN SOLUTION ###\n",
+    "    stack = Stack()\n",
+    "    reversed_string = \"\"\n",
+    "\n",
+    "    for char in s: # push char in the stack\n",
+    "        stack.push(char)\n",
+    "\n",
+    "    while not stack.empty(): # pop from the stack\n",
+    "        reversed_string += stack.get()\n",
+    "\n",
+    "    return reversed_string\n",
+    "    ### END SOLUTION ###"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 84,
+   "id": "63719c8e-f60c-4544-8e41-cb6380ae4bcf",
+   "metadata": {
+    "tags": []
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'olleH'"
+      ]
+     },
+     "execution_count": 84,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "# Example of use\n",
+    "reverse_string(\"Hello\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 86,
+   "id": "81e93620-0664-4a9d-ba5f-894937c9769e",
+   "metadata": {
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "# Tests\n",
+    "assert reverse_string(\"Hello\") == \"Hello\"[::-1]\n",
+    "assert reverse_string(\"Hello\") == \"\".join(reversed(\"Hello\"))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "81df9b1e-cfe5-4b69-96a5-c8065259cc7d",
+   "metadata": {},
+   "source": [
+    "## Exercise 3: Check if a word is a palindrom\n",
+    "\n",
+    "Use the `Stack` to check if a sequence of characters reads the same forward and backward."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 62,
+   "id": "cf6fbdd5-53c5-45c2-a0c5-a5ed845c4f81",
+   "metadata": {
+    "nbgrader": {
+     "grade": false,
+     "grade_id": "is_palindrome",
+     "locked": false,
+     "schema_version": 3,
+     "solution": true,
+     "task": false
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "def is_palindrome(s):\n",
+    "    ### BEGIN SOLUTION ###\n",
+    "    return s == reverse_string(s)\n",
+    "    ### END SOLUTION ###"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 87,
+   "id": "f1e3d451-9ac1-4551-9cd5-c3024133495c",
+   "metadata": {
+    "tags": []
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "True"
+      ]
+     },
+     "execution_count": 87,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "# Example of use\n",
+    "is_palindrome(\"ABA\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 88,
+   "id": "586bafba-2fbb-4833-b2e3-609db9b28fbf",
+   "metadata": {
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "# Tests\n",
+    "assert is_palindrome(\"ABA\")\n",
+    "assert not is_palindrome(\"ABAB\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "7a6c95d6-266e-4742-bfef-8b1f2dda4164",
+   "metadata": {},
+   "source": [
+    "## Exercise 4: Store unique elements only\n",
+    "\n",
+    "Use the `Stack` to create a new class that inherits it and call it  `StackUnique` to only store unique values (ie if a value has already been added do not add it)."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 91,
+   "id": "cab518b1-4d59-45d2-9e69-ae95458eeac5",
+   "metadata": {
+    "nbgrader": {
+     "grade": false,
+     "grade_id": "cell-0c6214437a6048f9",
+     "locked": false,
+     "schema_version": 3,
+     "solution": true,
+     "task": false
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "class StackUnique(Stack):\n",
+    "    ### BEGIN SOLUTION ###\n",
+    "    def __init__(self):\n",
+    "        super().__init__()\n",
+    "        self.unique_items = set()\n",
+    "\n",
+    "    def push(self, item):\n",
+    "        if item not in self.unique_items:\n",
+    "            super().push(item)\n",
+    "            self.unique_items.add(item)\n",
+    "\n",
+    "    def get(self):\n",
+    "        item = super().get()\n",
+    "        if item is not None:\n",
+    "            self.unique_items.remove(item)\n",
+    "        return item\n",
+    "    ### END SOLUTION ###"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 92,
+   "id": "53ac7eb2-295e-469b-b63b-ffbe876ff620",
+   "metadata": {
+    "tags": []
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "no more value\n"
+     ]
+    }
+   ],
+   "source": [
+    "# Usage example\n",
+    "s = StackUnique()\n",
+    "s.push(1)\n",
+    "s.push(1)\n",
+    "s.get()\n",
+    "s.get() if not s.empty() else print(\"no more value\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 93,
+   "id": "12847b9d-947f-4181-84cc-d2726199269b",
+   "metadata": {
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "# Tests\n",
+    "s = StackUnique()\n",
+    "s.push(1)\n",
+    "s.push(1)\n",
+    "assert s.get() == 1\n",
+    "assert s.empty()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "882884c2-1918-4224-9124-81bc55e43d4e",
+   "metadata": {
+    "tags": []
+   },
+   "source": [
+    "## Exercise 5: Check if correct number and order of brackets \n",
+    "\n",
+    "To verify if a string contains balanced brackets in the correct order, we can use a `Stack` to check if each opening bracket has a matching closing bracket and that they are correctly nested."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 125,
+   "id": "804ea32d-5bf8-42b9-ae52-6318b26f4065",
+   "metadata": {
+    "nbgrader": {
+     "grade": false,
+     "grade_id": "cell-4b9a5ecdee87514e",
+     "locked": false,
+     "schema_version": 3,
+     "solution": true,
+     "task": false
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "def is_balanced(s):\n",
+    "    ### BEGIN SOLUTION ###\n",
+    "    stack = []\n",
+    "\n",
+    "    matching_brackets = {')': '(', '}': '{', ']': '['}\n",
+    "    \n",
+    "    for char in s:\n",
+    "        if char in matching_brackets.values():\n",
+    "            stack.append(char)\n",
+    "        elif char in matching_brackets.keys():\n",
+    "            if not stack or stack.pop() != matching_brackets[char]:\n",
+    "                return False\n",
+    "\n",
+    "    return len(stack) == 0 # check if balanced\n",
+    "    ### END SOLUTION ###"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 126,
+   "id": "d7c86615-db59-4cff-aa16-32c3928fbbae",
+   "metadata": {
+    "tags": []
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "]\n",
+      ")\n"
+     ]
+    },
+    {
+     "data": {
+      "text/plain": [
+       "True"
+      ]
+     },
+     "execution_count": 126,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "# Usage example\n",
+    "is_balanced(\"([])\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 124,
+   "id": "aab38745-f306-4015-bcff-563f3c1b4166",
+   "metadata": {
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "# Tests\n",
+    "assert is_balanced(\"([])\")\n",
+    "assert is_balanced(\"([{}])\")\n",
+    "assert not is_balanced(\"(]\")\n",
+    "assert is_balanced(\"(([]){})\")\n",
+    "assert not is_balanced(\"({[})\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "91792d87-9c2b-4a4f-ad6e-1980bb76b06e",
+   "metadata": {},
+   "source": [
+    "## Exercice 6: Merge overlapping intervals\n",
+    "\n",
+    "Given a list of intervals, where each interval is represented as a list `[start, end]`, your task is to merge all overlapping intervals. Overlapping intervals should be merged into one interval with the start as the minimum of both intervals starts and the end as the maximum of both intervals ends. For example, with input `[[1, 3], [2, 6], [8, 10], [15, 18], [17, 20]]` you may return as output `[[1, 6], [8, 10], [15, 20]]` where 2 pairs intervals have been marged. Here is a way to solve this problem:\n",
+    "\n",
+    "\n",
+    "1. Check if the input list of intervals is empty (if it is, return an empty list)\n",
+    "2. Sort the intervals by their starting times.\n",
+    "3. Create an empty stack that will contain the merged intervals.\n",
+    "4. Iterate through each interval:\n",
+    "   - If the stack is empty or the current interval does not overlap with the last interval in the stack, push the current interval onto the stack.\n",
+    "   - If there is an overlap, pop the last interval from the stack, merge it with the current interval, and push the merged interval back onto the stack.\n",
+    "5. Convert the stack to a list and return the merged intervals."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 121,
+   "id": "a81eb460-4622-44cd-81b9-c89be4bb5b48",
+   "metadata": {
+    "nbgrader": {
+     "grade": false,
+     "grade_id": "cell-1a6e476af258654c",
+     "locked": false,
+     "schema_version": 3,
+     "solution": true,
+     "task": false
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "def merge_intervals(intervals):\n",
+    "    ### BEGIN SOLUTION ###\n",
+    "    if not intervals:\n",
+    "        return []\n",
+    "\n",
+    "    intervals.sort(key=lambda x: x[0])\n",
+    "\n",
+    "    merged_intervals = Stack()\n",
+    "\n",
+    "    for interval in intervals:\n",
+    "        if merged_intervals.empty() or merged_intervals.items[-1][1] < interval[0]:\n",
+    "            merged_intervals.push(interval)\n",
+    "        else:\n",
+    "            last_interval = merged_intervals.get() \n",
+    "            merged_interval = [last_interval[0], max(last_interval[1], interval[1])]\n",
+    "            merged_intervals.push(merged_interval)\n",
+    "\n",
+    "    return merged_intervals.items\n",
+    "    ### END SOLUTION ###"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 115,
+   "id": "8b02e24d-d498-40f3-a0f9-d6e0e8ad0d2e",
+   "metadata": {
+    "tags": []
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Merged Intervals: [[1, 6], [8, 10], [15, 20]]\n"
+     ]
+    }
+   ],
+   "source": [
+    "# Usage example\n",
+    "intervals = [[1, 3], [2, 6], [8, 10], [15, 18], [17, 20]]\n",
+    "merged = merge_intervals(intervals)\n",
+    "print(\"Merged Intervals:\", merged)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 116,
+   "id": "c04dfd85-04c7-43e5-8aa5-b4774e253bf4",
+   "metadata": {
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "# Tests\n",
+    "assert merge_intervals([[1, 3], [2, 6], [8, 10], [15, 18], [17, 20]]) == [[1, 6], [8, 10], [15, 20]]\n",
+    "assert merge_intervals([[1, 2]]) == [[1, 2]]"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "a445d290-b04f-49b5-a8e7-2c6e259daf58",
+   "metadata": {
+    "tags": []
+   },
+   "source": [
+    "## Exercise 7 (BONUS): Evaluate a postfix expression\n",
+    "\n",
+    "Write a code that given the following expression, provides the following evaluation (using arthmetic operations over numerical values).\n",
+    "\n",
+    "Expression: `\"3 4 +\"`\n",
+    "Evaluation: `3 + 4 = 7`\n",
+    "\n",
+    "First step: write a function `apply_operator` that applies an operation (ie + - * /) over two elements."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 41,
+   "id": "4cc7f805-0887-4422-b6b7-3d591d0df1fb",
+   "metadata": {
+    "nbgrader": {
+     "grade": false,
+     "grade_id": "cell-8c5106f02f243455",
+     "locked": false,
+     "schema_version": 3,
+     "solution": true,
+     "task": false
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "def apply_operator(op, b, a):\n",
+    "    if op == '+':\n",
+    "        return a + b\n",
+    "    elif op == '-':\n",
+    "        return a - b\n",
+    "    elif op == '*':\n",
+    "        return a * b\n",
+    "    elif op == '/':\n",
+    "        return a / b"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "e68bdf7c-ca08-4553-9874-8bd9038fd4b5",
+   "metadata": {},
+   "source": [
+    "Solution in pseudo-code:\n",
+    "- Split the input expression in to a list of tokens\n",
+    "- If not an operator\n",
+    "    - Add the value to the stack\n",
+    "- If an operator \n",
+    "    - Make sure there is enough parameters `a` and `b`\n",
+    "    - Pop `a` and `b`\n",
+    "    - Apply `apply_operator` on `a` and `b`\n",
+    "    - Store the result in the stack"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 42,
+   "id": "e792c90d-1b38-47f5-9879-399debc934b9",
+   "metadata": {
+    "nbgrader": {
+     "grade": false,
+     "grade_id": "cell-e9236618b265b34f",
+     "locked": false,
+     "schema_version": 3,
+     "solution": true,
+     "task": false
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "def evaluate_postfix(expression):\n",
+    "### BEGIN SOLUTION ###\n",
+    "    stack = []\n",
+    "    operators = set(['+', '-', '*', '/'])\n",
+    "\n",
+    "    tokens = expression.split()\n",
+    "    \n",
+    "    for token in tokens:\n",
+    "        # check if not operator\n",
+    "        if token not in operators:\n",
+    "            stack.append(float(token))\n",
+    "        else:\n",
+    "        # when we have an operator\n",
+    "            if len(stack) < 2:\n",
+    "                raise ValueError(\"Invalid expression\")\n",
+    "            b = stack.pop() # make sure b is first\n",
+    "            a = stack.pop() # then a\n",
+    "            result = apply_operator(token, b, a)\n",
+    "            stack.append(result)\n",
+    "\n",
+    "    if len(stack) != 1:\n",
+    "        raise ValueError(\"Invalid expression\")\n",
+    "\n",
+    "    return stack[0]\n",
+    "### END SOLUTION ###"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 43,
+   "id": "ea6e4840-1b7e-4265-b37d-e8c45ea6b3ed",
+   "metadata": {
+    "tags": []
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "14.0\n"
+     ]
+    }
+   ],
+   "source": [
+    "postfix_expression = \"3 4 + 2 *\"\n",
+    "print(evaluate_postfix(postfix_expression))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 136,
+   "id": "0dc4dff8-089b-46a6-a08d-f53ee2fe72c3",
+   "metadata": {
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "assert evaluate_postfix(\"1 1 /\") == 1\n",
+    "assert evaluate_postfix(\"0 1 /\") == 0\n",
+    "assert evaluate_postfix(\"3 4 + 2 *\") == 14\n",
+    "assert evaluate_postfix(\"4 2 3 5 * + *\") == 68 # (4 * (2 + (3 * 5))\n",
+    "assert evaluate_postfix(\"8 4 / 6 2 * +\") == 14 # ((8 / 4) + (6 * 2))"
+   ]
+  }
+ ],
+ "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
+}
diff --git a/labs-solutions/04-priority-queues-exercises.ipynb b/labs-solutions/04-priority-queues-exercises.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..f5e52a2e2f850c3d9931182b9a250661111497ff
--- /dev/null
+++ b/labs-solutions/04-priority-queues-exercises.ipynb
@@ -0,0 +1,618 @@
+{
+ "cells": [
+  {
+   "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": 147,
+   "id": "b91e0762-b158-44b1-850e-2987d7206b2b",
+   "metadata": {
+    "tags": []
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "('a', 2)\n",
+      "('b', 1)\n",
+      "('c', 3)\n",
+      "('d', 3)\n",
+      "('e', 0)\n",
+      "('f', 1)\n",
+      "('g', 1)\n",
+      "('h', 2)\n"
+     ]
+    }
+   ],
+   "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": 149,
+   "id": "18d02388-dca0-4493-a1c6-9d2c1314683d",
+   "metadata": {
+    "nbgrader": {
+     "grade": false,
+     "grade_id": "cell-7a9ba41922a9d48b",
+     "locked": false,
+     "schema_version": 3,
+     "solution": true,
+     "task": false
+    },
+    "tags": []
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "(0, 'e')\n",
+      "(1, 'b')\n",
+      "(1, 'f')\n",
+      "(1, 'g')\n",
+      "(2, 'a')\n",
+      "(2, 'h')\n",
+      "(3, 'c')\n",
+      "(3, 'd')\n"
+     ]
+    }
+   ],
+   "source": [
+    "from queue import PriorityQueue\n",
+    "q = PriorityQueue()\n",
+    "\n",
+    "### BEGIN SOLUTION ###\n",
+    "for item, priority in zip(items, priorities):\n",
+    "    q.put((priority, item))\n",
+    "\n",
+    "while not q.empty():\n",
+    "    next_item = q.get()\n",
+    "    print(next_item)\n",
+    "### END SOLUTION ###"
+   ]
+  },
+  {
+   "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": 137,
+   "id": "9bc92f94-97a1-46fa-813c-63fb6abc9f65",
+   "metadata": {
+    "nbgrader": {
+     "grade": false,
+     "grade_id": "cell-55279200308121d0",
+     "locked": false,
+     "schema_version": 3,
+     "solution": true,
+     "task": false
+    },
+    "tags": []
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "(3, 'c')\n",
+      "(3, 'd')\n",
+      "(2, 'a')\n",
+      "(2, 'h')\n",
+      "(1, 'b')\n",
+      "(1, 'f')\n",
+      "(1, 'g')\n",
+      "(0, 'e')\n"
+     ]
+    }
+   ],
+   "source": [
+    "q = PriorityQueue()\n",
+    "\n",
+    "### BEGIN SOLUTION ###\n",
+    "for item, priority in zip(items, priorities):\n",
+    "    q.put((-priority, item))\n",
+    "\n",
+    "while not q.empty():\n",
+    "    next_item = q.get()\n",
+    "    print((-next_item[0], next_item[1]))\n",
+    "### END SOLUTION ###"
+   ]
+  },
+  {
+   "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` used previously. To write the class, you may re-use the stack (or the queue) classes we have seen previously."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 138,
+   "id": "ca96c6ee-54c9-47ba-959e-60c7168256d6",
+   "metadata": {
+    "nbgrader": {
+     "grade": false,
+     "grade_id": "cell-256c1b911658a81f",
+     "locked": false,
+     "schema_version": 3,
+     "solution": true,
+     "task": false
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "class MyPriorityQueue():\n",
+    "    ### BEGIN SOLUTION ###\n",
+    "    def __init__(self, items = []):\n",
+    "        self.items = []\n",
+    "        for v in items:\n",
+    "            self.ajoute(v)\n",
+    "\n",
+    "    def put(self, v):\n",
+    "        index = 0\n",
+    "        while index < len(self.items) and self.items[index] < v:\n",
+    "            index += 1\n",
+    "        self.items.insert(index, v)\n",
+    "        return v\n",
+    "\n",
+    "    def remove(self):\n",
+    "        v = self.items.pop(0)\n",
+    "        return v\n",
+    "\n",
+    "    def get(self):\n",
+    "        if not self.empty():\n",
+    "            return self.items.pop(0)\n",
+    "\n",
+    "    def display(self):\n",
+    "        for v in self.items:\n",
+    "            print(v)\n",
+    "\n",
+    "    def size(self): \n",
+    "        return len(self.items)\n",
+    "    \n",
+    "    def empty(self):\n",
+    "        return self.size() == 0\n",
+    "    ### END SOLUTION ###"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "9e2f1c19-1f64-4de1-b9e8-1ed2e167cf40",
+   "metadata": {},
+   "source": [
+    "Write code that compare the results with the `PriorityQueue` module:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 139,
+   "id": "e86176bf-fb09-44a1-8732-5becb06921af",
+   "metadata": {
+    "nbgrader": {
+     "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",
+    "### BEGIN SOLUTION ###\n",
+    "for v in [(3, 'e'), (2, 'c'), (6, 'b')]:\n",
+    "    p1.put(v)\n",
+    "    p2.put(v)\n",
+    "\n",
+    "while not p1.empty() and not p2.empty():\n",
+    "    assert p1.get() == p2.get()\n",
+    "### END SOLUTION ###"
+   ]
+  },
+  {
+   "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 task 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 from the ones that are added, 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 with its properties, and include a way to compare tasks by `start` value 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": 141,
+   "id": "44aacc95-2aa6-485b-8759-ac292244f290",
+   "metadata": {
+    "nbgrader": {
+     "grade": false,
+     "grade_id": "cell-8eeb5fa664a2abf2",
+     "locked": false,
+     "schema_version": 3,
+     "solution": true,
+     "task": false
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "class Task:\n",
+    "    ### BEGIN SOLUTION ###\n",
+    "    def __init__(self, name, start, end):\n",
+    "        self.name = name\n",
+    "        self.start = start\n",
+    "        self.end = end\n",
+    "        \n",
+    "    def __lt__(self, other):\n",
+    "        return self.start < other.start\n",
+    "    ### END SOLUTION ###"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 142,
+   "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 that:\n",
+    "\n",
+    "1. add a task and use the **start time as priority**\n",
+    "2. check if overlapping\n",
+    "3. return the list of non-overlapping tasks"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 143,
+   "id": "6e9d6120-ffb9-4a2f-9fb6-e3c0761ecf75",
+   "metadata": {
+    "nbgrader": {
+     "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",
+    "    ### BEGIN SOLUTION ###\n",
+    "    def __init__(self):\n",
+    "        self.tasks = PriorityQueue()\n",
+    "\n",
+    "    def put(self, task):\n",
+    "        tasks_list = list(self.tasks.queue)\n",
+    "        for scheduled_task in tasks_list:\n",
+    "            if self.check(task, scheduled_task[1]): \n",
+    "                print(f\"Cannot schedule {task.name} (overlaps with {scheduled_task[1].name})\")\n",
+    "                return\n",
+    "\n",
+    "        self.tasks.put((task.start, task))\n",
+    "\n",
+    "    def check(self, task1, task2):\n",
+    "        return task1.start < task2.end and task1.end > task2.start\n",
+    "\n",
+    "    def get(self):\n",
+    "        scheduled = []\n",
+    "        while not self.tasks.empty():\n",
+    "            _, task = self.tasks.get()\n",
+    "            scheduled.append(task)\n",
+    "        return scheduled\n",
+    "    ### END SOLUTION ###"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 144,
+   "id": "558776cd-9f8f-404e-bc7f-803c4fbbae55",
+   "metadata": {
+    "tags": []
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Cannot schedule Task 2 (overlaps with Task 1)\n",
+      "Task 1: [1, 4]\n",
+      "Task 3: [5, 6]\n"
+     ]
+    }
+   ],
+   "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": 145,
+   "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 can be used instead of the `PriorityQueue` class. The operations complexity are are:\n",
+    "- `heapify` (equivalent to put) $O(n)$ as it finds the smallest value (only) to return next\n",
+    "- `heappop` (equivalent to get) $O(log(n))$ as it finds again the smallest value (only) to return next, but in an efficient way"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 146,
+   "id": "da1fbbad-7bb6-4f89-a446-bdb308a545ac",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "[3, 5, 7, 9, 10, 15, 20]"
+      ]
+     },
+     "execution_count": 146,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "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. You may follow those steps:\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": 134,
+   "id": "4b20f11a-47b8-419a-b94a-902943048c76",
+   "metadata": {
+    "nbgrader": {
+     "grade": false,
+     "grade_id": "cell-593558331d679bb2",
+     "locked": false,
+     "schema_version": 3,
+     "solution": true,
+     "task": false
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "import heapq\n",
+    "\n",
+    "def merge(lists):\n",
+    "    ### BEGIN SOLUTION ###\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\n",
+    "    ### END SOLUTION ###"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 127,
+   "id": "1b7e1364-1335-4daf-9100-ef933f97b86d",
+   "metadata": {
+    "tags": []
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "[1, 1, 2, 3, 4, 4, 5, 6]"
+      ]
+     },
+     "execution_count": 127,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "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": 128,
+   "id": "44600161-32a0-4b84-b152-65a15ba103ae",
+   "metadata": {
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "# Tests\n",
+    "assert merge([[]]) == []\n",
+    "assert merge([[1, 2, 3]]) == [1, 2, 3]"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "b318e23f-4ab5-455c-9607-90e75245df21",
+   "metadata": {
+    "tags": []
+   },
+   "source": [
+    "## Exercise 5 (BONUS) \n",
+    "\n",
+    "- Re-implement previous exercises that involve a priority queue using heaps\n",
+    "- Compare performance in execution time"
+   ]
+  }
+ ],
+ "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
+}