Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion DIRECTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@
* [Test Car Pooling](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/intervals/car_pooling/test_car_pooling.py)
* Count Days
* [Test Count Days Without Meetings](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/intervals/count_days/test_count_days_without_meetings.py)
* Employee Free Time
* [Interval](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/intervals/employee_free_time/interval.py)
* [Test Employee Free Time](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/intervals/employee_free_time/test_employee_free_time.py)
* Full Bloom Flowers
* [Test Full Bloom Flowers](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/intervals/full_bloom_flowers/test_full_bloom_flowers.py)
* Insert Interval
Expand All @@ -129,6 +132,9 @@
* Memoization
* [Fibonacci](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/memoization/fibonacci.py)
* [Petethebaker](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/petethebaker.py)
* Prefix Sum
* Continous Sub Array Sum
* [Test Check Subarray Sum](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/prefix_sum/continous_sub_array_sum/test_check_subarray_sum.py)
* Search
* Binary Search
* Divide Chocolate
Expand Down Expand Up @@ -250,6 +256,8 @@
* [Min Increment For Unique](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/arrays/minincrementsforunique/min_increment_for_unique.py)
* Non Overlapping Intervals
* [Test Non Overlapping Intervals](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/arrays/non_overlapping_intervals/test_non_overlapping_intervals.py)
* Sub Array With Sum
* [Test Sub Array With Sum](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/arrays/sub_array_with_sum/test_sub_array_with_sum.py)
* Subarrays With Fixed Bounds
* [Test Subarrays With Fixed Bounds](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/arrays/subarrays_with_fixed_bounds/test_subarrays_with_fixed_bounds.py)
* Circular Buffer
Expand Down Expand Up @@ -937,7 +945,6 @@
* [Test Lonely Integer](https://github.com/BrianLusina/PythonSnips/blob/master/tests/datastructures/arrays/test_lonely_integer.py)
* [Test Longest Consecutive Sequence](https://github.com/BrianLusina/PythonSnips/blob/master/tests/datastructures/arrays/test_longest_consecutive_sequence.py)
* [Test Max Subarray](https://github.com/BrianLusina/PythonSnips/blob/master/tests/datastructures/arrays/test_max_subarray.py)
* [Test Sub Array With Sum](https://github.com/BrianLusina/PythonSnips/blob/master/tests/datastructures/arrays/test_sub_array_with_sum.py)
* [Test Zig Zag Sequence](https://github.com/BrianLusina/PythonSnips/blob/master/tests/datastructures/arrays/test_zig_zag_sequence.py)
* Linked List
* [Test Reorder List](https://github.com/BrianLusina/PythonSnips/blob/master/tests/datastructures/linked_list/test_reorder_list.py)
Expand Down
68 changes: 68 additions & 0 deletions algorithms/intervals/employee_free_time/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Employee Free Time

You’re given a list containing the schedules of multiple employees. Each person’s schedule is a list of non-overlapping
intervals in sorted order. An interval is specified with the start and end time, both being positive integers. Your task
is to find the list of finite intervals representing the free time for all the employees.

> Note: The common free intervals are calculated between the earliest start time and the latest end time of all meetings
across all employees.

## Constraints

- 1 <= `schedule.length`, `schedule[i].length` <= 50
- 0 <= `interval.start`, `interval.end` <= 10^8, where `interval` is any interval in the list of schedules

## Examples

![Example 1](./images/examples/employee_free_time_example_1.png)
![Example 2](./images/examples/employee_free_time_example_2.png)
![Example 3](./images/examples/employee_free_time_example_3.png)

## Solution

The solution’s core revolves around merging overlapping intervals of employees and identifying the free time gaps
between these merged intervals. Using a min-heap, we arrange the intervals based on when they start, sorting them
according to their start times. When we pop an element from the min-heap, it guarantees that the earliest available
interval is returned for processing. As intervals are popped from the min-heap, the algorithm attempts to merge them.
If the currently popped interval’s start time exceeds the merged interval’s end time, a gap is identified, indicating a
free period. After identifying each gap, the algorithm restarts the merging process to continue identifying additional
periods of free time.

We use the following variables in our solution:

- `latest_end_time`: Stores the end time of the previously processed interval.
- `employee_index`: Stores the employee’s index value.
- `interval_index`: Stores the interval’s index of the employee, i.
- `free_time_slots`: Stores the free time intervals.

The steps of the algorithm are given below :
- We store the start time of each employee’s first interval, along with its index value and a value of 0, in a min-heap.
- We set previous to the start time of the first interval present in a heap.
- Then, we iterate a loop until the heap is empty, and in each iteration, we do the following:
- Pop an element from the min-heap and set `employee_index` and `interval_index` to the second and third values,
respectively, from the popped value.
- Select the interval from the input located at `employee_index`,`interval_index`.
- If the selected interval’s start time is greater than `latest_end_time`, it means that the time from `latest_end_time`
to the selected interval’s start time is free. So, add this interval to the `free_time_slots` array.
- Now, update the `latest_end_time` as `max(latest_end_time, end time of selected interval)`.
- If the current employee has any other interval, push it into the heap.
- After all the iterations, when the heap becomes empty, return the `free_time_slots` array.

![Solution 1](./images/solutions/employee_free_time_heap_solution_1.png)
![Solution 2](./images/solutions/employee_free_time_heap_solution_2.png)
![Solution 3](./images/solutions/employee_free_time_heap_solution_3.png)
![Solution 4](./images/solutions/employee_free_time_heap_solution_4.png)
![Solution 5](./images/solutions/employee_free_time_heap_solution_5.png)
![Solution 6](./images/solutions/employee_free_time_heap_solution_6.png)
![Solution 7](./images/solutions/employee_free_time_heap_solution_7.png)

### Time Complexity

The time complexity of this algorithm is O(mlog(n)), where n is the number of employees and m is the total number of
intervals across all employees. This is because the time complexity of filling the heap is O(nlog(n)) and the time
complexity of processing the heap is O(mlog(n)).

### Space Complexity

We use a heap in the solution, which can have a maximum of n elements. Hence, the space complexity of this solution is
O(n), where n is the number of employees.
122 changes: 122 additions & 0 deletions algorithms/intervals/employee_free_time/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
from typing import List, Tuple
import heapq
from algorithms.intervals.employee_free_time.interval import Interval


def employee_free_time(schedules: List[List[Interval]]) -> List[Interval]:
"""
Finds intervals where employees have free time

Complexity:
Time Complexity: O(NlogN), where N is the total number of intervals across all employees. This is dominated by the sorting step.
Space Complexity: O(N) to store the flattened all_schedules list.

Args:
schedules(list): a list of lists, where each inner list contains `Interval` objects representing an employee's schedule.
Returns:
list: Intervals of employee free time
"""
# We need to combine and merge all employee intervals(schedules) into one unified schedule to be able to check
# for free time. Since using a new list incurs a space cost of O(n), we can modify the input list, however, this
# has side effects if the schedule list is used in other parts of the program. To avoid side effects, we copy over
# the input list into a new list to handle the merging
all_schedules: List[Interval] = []
for schedule in schedules:
for schedule_interval in schedule:
all_schedules.append(schedule_interval)

if not all_schedules:
return []

# sort by start time
all_schedules.sort(key=lambda x: x.start)

# Keep track of the latest meeting's end time. This is initialized the first schedule's end time
latest_end = all_schedules[0].end

# This will keep track of the free time slots
free_time_slots: List[Interval] = []

# Now we need to find the gaps in the combined schedule. Since we have already used the first schedule's end time
# as the latest end we have seen so far, we start at the next interval.
for idx in range(1, len(all_schedules)):
current_schedule = all_schedules[idx]
current_interval_start, current_interval_end = (
current_schedule.start,
current_schedule.end,
)

# If the current interval's start time is greater than the latest end time we have seen so far, it means we have
# found free time
if current_interval_start > latest_end:
# we have a free time slot
free_interval = Interval(start=latest_end, end=current_interval_start)
free_time_slots.append(free_interval)

# update the latest_end to the maximum of the latest end time seen so far
latest_end = max(latest_end, current_interval_end)

return free_time_slots


def employee_free_time_heap(schedules: List[List[Interval]]) -> List[Interval]:
"""
Finds intervals where employees have free time using a min heap

Args:
schedules(list): a list of lists, where each inner list contains `Interval` objects representing an employee's schedule.
Returns:
list: Intervals of employee free time
"""
heap: List[Tuple[int, int, int]] = []
# Iterate for all employees' schedules
# and add start of each schedule's first interval along with
# its index value and a value 0.
for i in range(len(schedules)):
if schedules[i]: # Only add if employee has at least one interval
heap.append((schedules[i][0].start, i, 0))

# Create heap from array elements.
heapq.heapify(heap)

# Handle empty heap
if not heap:
return []

# Take an empty array to store results.
free_time_slots = []

# Set 'latest_end_time' to the start time of first interval in heap.
latest_end_time = schedules[heap[0][1]][heap[0][2]].end

# Iterate till heap is empty
while heap:
# Pop an element from heap and set value of employee_index and interval_index
_, employee_index, interval_index = heapq.heappop(heap)

# Select an interval
schedule_interval = schedules[employee_index][interval_index]

# If selected interval's start value is greater than the
# latest_end_time value, it means that this interval is free.
# So, add this interval (latest_end_time, interval's end value) into result.
if schedule_interval.start > latest_end_time:
free_time_slots.append(Interval(latest_end_time, schedule_interval.start))

# Update the latest_end_time as maximum of latest_end_time and interval's end value.
latest_end_time = max(latest_end_time, schedule_interval.end)

# If there is another interval in current employees' schedule,
# push that into heap.
if interval_index + 1 < len(schedules[employee_index]):
heapq.heappush(
heap,
(
schedules[employee_index][interval_index + 1].start,
employee_index,
interval_index + 1,
),
)

# When the heap is empty, return result.
return free_time_slots
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions algorithms/intervals/employee_free_time/interval.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
class Interval:
def __init__(self, start: int, end: int):
self.start = start
self.end = end
self.closed = True # by default, the interval is closed

def set_closed(self, closed: bool) -> None:
# set the flag for closed/open
self.closed = closed

def __str__(self):
return (
"[" + str(self.start) + ", " + str(self.end) + "]"
if self.closed
else "(" + str(self.start) + ", " + str(self.end) + ")"
)

def __eq__(self, other: "Interval") -> bool:
return self.start == other.start and self.end == other.end
105 changes: 105 additions & 0 deletions algorithms/intervals/employee_free_time/test_employee_free_time.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import unittest
from typing import List
from parameterized import parameterized
from algorithms.intervals.employee_free_time.interval import Interval
from algorithms.intervals.employee_free_time import (
employee_free_time,
employee_free_time_heap,
)

EMPLOYEE_FREE_TIME_TEST_CASES = [
(
[
[Interval(start=1, end=2), Interval(start=5, end=6)],
[Interval(start=1, end=3)],
[Interval(start=4, end=10)],
],
[Interval(start=3, end=4)],
),
(
[
[Interval(start=1, end=3), Interval(start=6, end=7)],
[Interval(start=2, end=4)],
[Interval(start=2, end=5), Interval(start=9, end=12)],
],
[Interval(start=5, end=6), Interval(start=7, end=9)],
),
(
[
[Interval(start=2, end=3), Interval(start=7, end=9)],
[Interval(start=1, end=4), Interval(start=6, end=7)],
],
[Interval(start=4, end=6)],
),
(
[
[Interval(start=3, end=5), Interval(start=8, end=10)],
[Interval(start=4, end=6), Interval(start=9, end=12)],
[Interval(start=5, end=6), Interval(start=8, end=10)],
],
[Interval(start=6, end=8)],
),
(
[
[
Interval(start=1, end=2),
Interval(start=3, end=4),
Interval(start=5, end=6),
Interval(start=7, end=8),
Interval(start=9, end=10),
Interval(start=11, end=12),
],
[
Interval(start=1, end=2),
Interval(start=3, end=4),
Interval(start=5, end=6),
Interval(start=7, end=8),
Interval(start=9, end=10),
Interval(start=11, end=12),
],
[
Interval(start=1, end=2),
Interval(start=3, end=4),
Interval(start=5, end=6),
Interval(start=7, end=8),
Interval(start=9, end=10),
Interval(start=11, end=12),
],
[
Interval(start=1, end=2),
Interval(start=3, end=4),
Interval(start=5, end=6),
Interval(start=7, end=8),
Interval(start=9, end=10),
Interval(start=11, end=12),
],
],
[
Interval(start=2, end=3),
Interval(start=4, end=5),
Interval(start=6, end=7),
Interval(start=8, end=9),
Interval(start=10, end=11),
],
),
]


class EmployeeFreeTimeTestCase(unittest.TestCase):
@parameterized.expand(EMPLOYEE_FREE_TIME_TEST_CASES)
def test_employee_free_time(
self, schedule: List[List[Interval]], expected: List[Interval]
):
actual = employee_free_time(schedule)
self.assertListEqual(expected, actual)

@parameterized.expand(EMPLOYEE_FREE_TIME_TEST_CASES)
def test_employee_free_time_heap(
self, schedule: List[List[Interval]], expected: List[Interval]
):
actual = employee_free_time_heap(schedule)
self.assertListEqual(expected, actual)


if __name__ == "__main__":
unittest.main()
Empty file.
Loading
Loading