1912. Design Movie Rental System
Problem Description
This LeetCode problem presents a scenario in which you are required to design a system for a movie rental company that manages several shops. The system should be capable of performing actions such as searching for available copies of a movie to rent, booking (renting) a movie, returning (dropping) a movie, and generating a report of the currently rented movies.
For this system, each movie available for rent is recorded as an entry consisting of the shop number, movie number, and rental price. The challenge is to implement these features while making sure that shop and movie data are handled efficiently for rapid searching, renting, dropping, and reporting activities.
Key functionalities to be supported:
-
Search: Find the cheapest 5 shops (or fewer, if less are available) with available copies of a specific movie to rent. The shops should be sorted primarily by ascending price, then by shop number if prices are identical.
-
Rent: Book an available movie from a specific shop.
-
Drop: Return a rented movie to a specific shop.
-
Report: Generate a list of the cheapest 5 rented movies (or fewer, if less are rented) across all shops, sorted primarily by ascending price, then by shop number, and finally by movie number in case of ties.
The main obstacle is to manage the storage and retrieval of movie data to ensure the operations can be executed efficiently and meet the problem's requirements.
Intuition
The intuition behind solving this problem lies in efficiently managing the data regarding shop inventories and movie rentals. This involves carefully selecting data structures that will allow for fast lookups, insertions, and deletions, which are critical for the operations that the system needs to perform.
Here's the rationale for the solution approach:
-
Use a sorted data structure to maintain the availability and price of unrented movies. This enables us to efficiently search for the cheapest available movies and sort them as required. Python's
SortedList
, which is part of thesortedcontainers
library, is an excellent choice for this task because it maintains sorted order upon insertion and deletion, allowing for efficient operations that conform to the problem's specifications. -
Maintain a mapping that relates each unique shop and movie pair to their corresponding rental price. This helps quickly reference the price when a movie is rented or returned.
-
Keep another sorted list to track all rented movies. This list is sorted by price, and further by shop and movie numbers when prices are the same, fulfilling the reporting requirement to list rented movies in the desired order.
Using these data structures and maintaining these relationships allows the MovieRenting System to perform search, rent, drop, and report operations as per the given constraints and return the correct results.
The provided Python solution demonstrates implementing this approach with class methods corresponding to the required functionalities.
Learn more about Heap (Priority Queue) patterns.
Solution Approach
The solution is implemented using the MovieRentingSystem
class, which maintains three primary data structures to manage unrented and rented movies along with their prices:
-
self.unrented
- A dictionary with keys as movie IDs and values asSortedList
objects that hold tuples of (price, shop). This allows keeping the unrented copies of each movie sorted by price (then by shop in case of a tie). TheSortedList
is suitable for this purpose because it provides efficientO(log n)
insertion and removal along with the ability to retrieve the smallest elements, i.e., the cheapest movies. -
self.shopAndMovieToPrice
- A dictionary to track the price of renting each movie from each shop. The keys are tuples of (shop, movie), and values are the prices. This provides an O(1) lookup for the price when a movie is rented or dropped. -
self.rented
- ASortedList
that maintains all the rented movie data as tuples of (price, shop, movie). Since the list is sorted, this again provides efficient operations and supports getting the cheapest rented movies for the report.
The class methods implement the functionalities as follows:
-
__init__
initializes the system with the given number of shops and movie entries. It populates theself.unrented
andself.shopAndMovieToPrice
data structures using theentries
array. -
search
method takes a movie ID as an argument and returns a list of up to 5 shop numbers offering the cheapest unrented copies of the movie, sorted by price. -
rent
method takes shop and movie IDs and performs the rental operation. It looks up the rental price usingself.shopAndMovieToPrice
, removes the movie fromself.unrented
, and adds the rental information toself.rented
. -
drop
method is the inverse operation of therent
method. It reinstates the movie toself.unrented
and removes it fromself.rented
, assuming the movie is dropped off at the same shop from where it was rented. -
report
method returns a list of the cheapest 5 (or fewer, if fewer are available) rented movies. It does so by retrieving the first 5 elements from theself.rented
list, which are kept sorted by price, and then by shop and movie where needed.
The algorithms employed for the data manipulations within these methods take full advantage of the properties of the SortedList
structure (from the sortedcontainers
library), which keeps elements sorted automatically. This ensures efficient performance and correctness of the implemented system.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's illustrate the solution approach using a small example: Imagine we have a movie rental system with two shops (Shop 1 and Shop 2). Each shop has one copy of two movies (Movie A and Movie B) available for rent. Here are the starting rental prices for each movie at each shop:
- Shop 1: Movie A for 2
- Shop 2: Movie A for 5
With the MovieRentingSystem
class, we initialize our system with these movie entries.
Initialization:
entries = [(1, 'A', 4), (1, 'B', 2), (2, 'A', 3), (2, 'B', 5)]
Here's how the system sets up the initial data structures upon running the __init__
method:
self.unrented
becomes a dictionary with keys as movie IDs and values as SortedList objects, like so:- 'A': [(3, 2), (4, 1)] - sorted by price, then by shop number.
- 'B': [(2, 1), (5, 2)]
self.shopAndMovieToPrice
becomes:{(1, 'A'): 4, (1, 'B'): 2, (2, 'A'): 3, (2, 'B'): 5}
self.rented
is initially empty since no movie has been rented yet:SortedList()
.
Search Operation: Let's find the cheapest shops to rent Movie A.
movie_renting_system.search('A')
The search
method looks up 'A' in self.unrented
dictionary and returns the first 5 (or all, if fewer) tuples, sorted by price:
- Output: [(3, 2), (4, 1)] - Shop 2 offers the cheapest price for Movie A at 4.
Rent Operation: Suppose we want to rent Movie A from Shop 2.
movie_renting_system.rent(2, 'A')
The rent
method does the following:
- It removes (3, 2) from
self.unrented['A']
, resulting in: 'A': [(4, 1)] - It adds (3, 2, 'A') to
self.rented
, resulting in: [(3, 2, 'A')]
Drop Operation: After watching, we return Movie A to Shop 2.
movie_renting_system.drop(2, 'A')
The drop
method essentially reverses the rent
operation:
- It removes (3, 2, 'A') from
self.rented
. - It adds (3, 2) back to
self.unrented['A']
, now it is: 'A': [(3, 2), (4, 1)] - automatically sorted.
Report Operation: If we now want to generate a report of the rented movies, we call:
movie_renting_system.report()
The report
method reads from self.rented
, which is currently empty because the rented movie was dropped off. Thus, the output at this point is an empty list.
This example demonstrates the solution's capability to handle various operations efficiently while accurately maintaining and updating the underlying data structures according to the problem specifications.
Solution Implementation
1import collections
2from typing import List
3from sortedcontainers import SortedList
4
5class MovieRentingSystem:
6 def __init__(self, n: int, entries: List[List[int]]):
7 # Initialize a dictionary to hold unrented movies with each movie's price and shop info.
8 self.unrented_movies = collections.defaultdict(SortedList) # {movie: (price, shop)}
9
10 # A dictionary to map each shop and movie combo to its price.
11 self.price_lookup = {} # {(shop, movie): price}
12
13 # Initialize a SortedList to hold rented movies with their price, shop, and movie info.
14 self.rented_movies = SortedList() # (price, shop, movie)
15
16 # Fill dictionaries with initial entries.
17 for shop, movie, price in entries:
18 self.unrented_movies[movie].add((price, shop))
19 self.price_lookup[(shop, movie)] = price
20
21 def search(self, movie: int) -> List[int]:
22 # Return a list of shops that have the specified movie among the 5 cheapest options.
23 return [shop for _, shop in self.unrented_movies[movie][:5]]
24
25 def rent(self, shop: int, movie: int) -> None:
26 # Rent a movie by moving it from the unrented_movie list to the rented_movies list.
27 price = self.price_lookup[(shop, movie)]
28 self.unrented_movies[movie].remove((price, shop))
29 self.rented_movies.add((price, shop, movie))
30
31 def drop(self, shop: int, movie: int) -> None:
32 # Drop off a rented movie by moving it back from the rented_movies list to unrented_movies.
33 price = self.price_lookup[(shop, movie)]
34 self.unrented_movies[movie].add((price, shop))
35 self.rented_movies.remove((price, shop, movie))
36
37 def report(self) -> List[List[int]]:
38 # Report the 5 cheapest rented movies as a list of [shop, movie] pairs.
39 return [[shop, movie] for _, shop, movie in self.rented_movies[:5]]
40
41# End of the MovieRentingSystem class
42
43# The MovieRentingSystem class can be used as follows:
44# obj = MovieRentingSystem(n, entries)
45# available_shops = obj.search(movie)
46# obj.rent(shop, movie)
47# obj.drop(shop, movie)
48# report = obj.report()
49
1import java.util.*;
2
3class MovieRentingSystem {
4 // A map to hold unrented movies with each movie's price and shop info.
5 private TreeMap<Integer, TreeSet<int[]>> unrentedMovies;
6
7 // A map to track the price for each shop and movie combination.
8 private Map<Integer, Integer> priceLookup;
9
10 // A SortedSet to hold rented movies with their price, shop, and movie info.
11 private TreeSet<int[]> rentedMovies;
12
13 public MovieRentingSystem(int n, int[][] entries) {
14 unrentedMovies = new TreeMap<>();
15 priceLookup = new HashMap<>();
16 rentedMovies = new TreeSet<>(
17 (a, b) -> {
18 if (a[0] != b[0]) return a[0] - b[0]; // First sort by price
19 if (a[1] != b[1]) return a[1] - b[1]; // Then by shop
20 return a[2] - b[2]; // Finally by movie
21 }
22 );
23
24 for (int[] entry : entries) {
25 int shop = entry[0];
26 int movie = entry[1];
27 int price = entry[2];
28
29 int[] shopPricePair = new int[]{price, shop};
30 unrentedMovies.computeIfAbsent(movie, x -> new TreeSet<>(
31 (a, b) -> {
32 if (a[0] != b[0]) return a[0] - b[0]; // Sort by price
33 return a[1] - b[1]; // Then by shop
34 }
35 )).add(shopPricePair);
36
37 priceLookup.put(shop * 10001 + movie, price); // Composite key to map uniquely to every shop and movie combination
38 }
39 }
40
41 // Return a list of shops that have the specified movie among the 5 cheapest options.
42 public List<Integer> search(int movie) {
43 List<Integer> shops = new ArrayList<>();
44 if (!unrentedMovies.containsKey(movie)) return shops;
45
46 for (int[] pair : unrentedMovies.get(movie).headSet(new int[]{Integer.MAX_VALUE, Integer.MAX_VALUE}, true)) {
47 shops.add(pair[1]);
48 if (shops.size() == 5) break;
49 }
50
51 return shops;
52 }
53
54 // Rent a movie by removing it from the unrented list and adding it to the rented list.
55 public void rent(int shop, int movie) {
56 int price = priceLookup.get(shop * 10001 + movie);
57 unrentedMovies.get(movie).remove(new int[]{price, shop});
58 rentedMovies.add(new int[]{price, shop, movie});
59 }
60
61 // Drop off a rented movie by moving it back from the rented list to the unrented list.
62 public void drop(int shop, int movie) {
63 int price = priceLookup.get(shop * 10001 + movie);
64 unrentedMovies.computeIfAbsent(movie, x -> new TreeSet<>(
65 Comparator.comparingInt((int[] arr) -> arr[0]).thenComparingInt(arr -> arr[1])
66 )).add(new int[]{price, shop});
67 rentedMovies.remove(new int[]{price, shop, movie});
68 }
69
70 // Report the 5 cheapest rented movies as a list of [shop, movie] pairs.
71 public List<List<Integer>> report() {
72 List<List<Integer>> result = new ArrayList<>();
73 for (int[] info : rentedMovies) {
74 List<Integer> pair = new ArrayList<>();
75 pair.add(info[1]);
76 pair.add(info[2]);
77 result.add(pair);
78 if (result.size() == 5) break;
79 }
80
81 return result;
82 }
83}
84
85// Usage example:
86// MovieRentingSystem obj = new MovieRentingSystem(n, entries);
87// List<Integer> availableShops = obj.search(movie);
88// obj.rent(shop, movie);
89// obj.drop(shop, movie);
90// List<List<Integer>> report = obj.report();
91
1#include <vector>
2#include <unordered_map>
3#include <set>
4
5using namespace std;
6
7// Defining a comparator for the set so that we can keep the entries sorted based on price first, and then shop.
8struct MovieComparator {
9 bool operator() (const tuple<int, int, int>& a, const tuple<int, int, int>& b) const {
10 if (get<0>(a) != get<0>(b))
11 return get<0>(a) < get<0>(b);
12 if (get<1>(a) != get<1>(b))
13 return get<1>(a) < get<1>(b);
14 return get<2>(a) < get<2>(b);
15 }
16};
17
18class MovieRentingSystem {
19private:
20 unordered_map<int, set<pair<int, int>>> unrentedMovies; // {movie: set of (price, shop)}
21 unordered_map<pair<int, int>, int, hash<pair<int,int>>> priceLookup; // {(shop, movie): price}
22 set<tuple<int, int, int>, MovieComparator> rentedMovies; // set of (price, shop, movie)
23
24public:
25 MovieRentingSystem(int n, vector<vector<int>>& entries) {
26 for (const auto& e : entries) {
27 int shop = e[0], movie = e[1], price = e[2];
28 unrentedMovies[movie].insert({price, shop});
29 priceLookup[{shop, movie}] = price;
30 }
31 }
32
33 vector<int> search(int movie) {
34 vector<int> shops;
35 for (const auto& [price, shop] : unrentedMovies[movie]) {
36 if (shops.size() >= 5) break; // Only interested in the 5 cheapest options
37 shops.push_back(shop);
38 }
39 return shops;
40 }
41
42 void rent(int shop, int movie) {
43 int price = priceLookup[{shop, movie}];
44 unrentedMovies[movie].erase({price, shop});
45 rentedMovies.insert(make_tuple(price, shop, movie));
46 }
47
48 void drop(int shop, int movie) {
49 int price = priceLookup[{shop, movie}];
50 unrentedMovies[movie].insert({price, shop});
51 rentedMovies.erase(make_tuple(price, shop, movie));
52 }
53
54 vector<vector<int>> report() {
55 vector<vector<int>> cheapestRentedMovies;
56 for (const auto& [price, shop, movie] : rentedMovies) {
57 if (cheapestRentedMovies.size() >= 5) break; // Report only the 5 cheapest rented movies
58 cheapestRentedMovies.push_back({shop, movie});
59 }
60 return cheapestRentedMovies;
61 }
62};
63
64// Usage example:
65// MovieRentingSystem obj(n, entries);
66// vector<int> availableShops = obj.search(movie);
67// obj.rent(shop, movie);
68// obj.drop(shop, movie);
69// vector<vector<int>> report = obj.report();
70
1import { SortedList } from 'sortedcontainers';
2
3// TypeScript does not have a direct equivalent to Python's `defaultdict`.
4// We create a Map that returns a new SortedList if the key is not found.
5const unrentedMovies: Map<number, SortedList<[number, number]>> = new Map();
6
7// A Map to keep track of the price of renting a movie from a specific shop.
8const priceLookup: Map<string, number> = new Map();
9
10// A SortedList to hold rented movies with their price, shop, and movie info.
11const rentedMovies: SortedList<[number, number, number]> = new SortedList();
12
13interface Entry {
14 shop: number;
15 movie: number;
16 price: number;
17}
18
19// A function to initialize the system with the given list of entries.
20function initializeSystem(entries: Entry[]): void {
21 entries.forEach(entry => {
22 const { shop, movie, price } = entry;
23 if (!unrentedMovies.has(movie)) {
24 unrentedMovies.set(movie, new SortedList());
25 }
26 unrentedMovies.get(movie)?.add([price, shop]);
27 priceLookup.set(`${shop}-${movie}`, price);
28 });
29}
30
31// A function to search for the 5 cheapest copies of a specified movie.
32function search(movie: number): number[] {
33 const availableShops = unrentedMovies.get(movie);
34 return availableShops?.toArray().slice(0, 5).map(([_, shop]) => shop) || [];
35}
36
37// A function to rent a movie, moving it from unrented to rented.
38function rent(shop: number, movie: number): void {
39 const price = priceLookup.get(`${shop}-${movie}`);
40 if (price !== undefined) {
41 unrentedMovies.get(movie)?.remove([price, shop]);
42 rentedMovies.add([price, shop, movie]);
43 }
44}
45
46// A function to drop off a rented movie, moving it back to unrented.
47function drop(shop: number, movie: number): void {
48 const price = priceLookup.get(`${shop}-${movie}`);
49 if (price !== undefined) {
50 rentedMovies.remove([price, shop, movie]);
51 unrentedMovies.get(movie)?.add([price, shop]);
52 }
53}
54
55// A function to report the 5 cheapest rented movies.
56function report(): number[][] {
57 return rentedMovies.toArray().slice(0, 5).map(([_, shop, movie]) => [shop, movie]);
58}
59
60// Function calls for the system would replace class method calls:
61// initializeSystem(entries);
62// const availableShops = search(movie);
63// rent(shop, movie);
64// drop(shop, movie);
65// const reportList = report();
66
Time and Space Complexity
The given Python code defines a class MovieRentingSystem
that keeps track of rented and unrented movies between various shops with their respective prices and implements methods to search, rent, drop, and report movies.
Space Complexity
- The space complexity for initializing the system is
O(E)
whereE
is the number of entries, considering that each entry is stored inself.unrented
andself.shopAndMovieToPrice
.
Time Complexity
-
init method: For each entry, the add operation to
self.unrented
SortedList isO(logS)
where S is the number of different movies in the worst case, and adding toself.shopAndMovieToPrice
isO(1)
making the overall complexityO(E * logS)
for initializing the system. -
search method: Retrieving the top 5 movies is
O(1)
per movie but since we need to retrieve up to 5 it becomesO(5)
or simplyO(1)
regardless of the number of movies because it's a constant-time operation due to the limitation of 5 shops. -
rent method: The remove operation from
self.unrented[movie]
isO(logS)
since it's a portion of SortedList and adding the entry toself.rented
is alsoO(logR)
where R is the number of rented movies at the time. Thus, the time complexity for this method isO(logS + logR)
. -
drop method: Similar to the rent method, we have a remove operation from
self.rented
which isO(logR)
and an add operation toself.unrented[movie]
which isO(logS)
. Hence, the time complexity here is alsoO(logS + logR)
. -
report method: The generation of the report, which involves getting up to the top 5 entries, is
O(1)
since it doesn't depend on the size of theself.rented
but is rather capped at 5.
Considering that S and R can vary independently, we should not simplify O(logS + logR)
to O(log(S + R))
or any such combined form since they represent different aspects of the system's data.
Learn more about how to find time and space complexity quickly using problem constraints.
What's the output of running the following function using input [30, 20, 10, 100, 33, 12]
?
1def fun(arr: List[int]) -> List[int]:
2 import heapq
3 heapq.heapify(arr)
4 res = []
5 for i in range(3):
6 res.append(heapq.heappop(arr))
7 return res
8
1public static int[] fun(int[] arr) {
2 int[] res = new int[3];
3 PriorityQueue<Integer> heap = new PriorityQueue<>();
4 for (int i = 0; i < arr.length; i++) {
5 heap.add(arr[i]);
6 }
7 for (int i = 0; i < 3; i++) {
8 res[i] = heap.poll();
9 }
10 return res;
11}
12
1class HeapItem {
2 constructor(item, priority = item) {
3 this.item = item;
4 this.priority = priority;
5 }
6}
7
8class MinHeap {
9 constructor() {
10 this.heap = [];
11 }
12
13 push(node) {
14 // insert the new node at the end of the heap array
15 this.heap.push(node);
16 // find the correct position for the new node
17 this.bubble_up();
18 }
19
20 bubble_up() {
21 let index = this.heap.length - 1;
22
23 while (index > 0) {
24 const element = this.heap[index];
25 const parentIndex = Math.floor((index - 1) / 2);
26 const parent = this.heap[parentIndex];
27
28 if (parent.priority <= element.priority) break;
29 // if the parent is bigger than the child then swap the parent and child
30 this.heap[index] = parent;
31 this.heap[parentIndex] = element;
32 index = parentIndex;
33 }
34 }
35
36 pop() {
37 const min = this.heap[0];
38 this.heap[0] = this.heap[this.size() - 1];
39 this.heap.pop();
40 this.bubble_down();
41 return min;
42 }
43
44 bubble_down() {
45 let index = 0;
46 let min = index;
47 const n = this.heap.length;
48
49 while (index < n) {
50 const left = 2 * index + 1;
51 const right = left + 1;
52
53 if (left < n && this.heap[left].priority < this.heap[min].priority) {
54 min = left;
55 }
56 if (right < n && this.heap[right].priority < this.heap[min].priority) {
57 min = right;
58 }
59 if (min === index) break;
60 [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61 index = min;
62 }
63 }
64
65 peek() {
66 return this.heap[0];
67 }
68
69 size() {
70 return this.heap.length;
71 }
72}
73
74function fun(arr) {
75 const heap = new MinHeap();
76 for (const x of arr) {
77 heap.push(new HeapItem(x));
78 }
79 const res = [];
80 for (let i = 0; i < 3; i++) {
81 res.push(heap.pop().item);
82 }
83 return res;
84}
85
Recommended Readings
https algomonster s3 us east 2 amazonaws com cover_photos heap svg Priority Queue and Heap What is the relationship between priority queue and heap Priority Queue is an Abstract Data Type and Heap is the concrete data structure we use to implement a priority queue Priority Queue A priority queue
LeetCode Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way we
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https algomonster s3 us east 2 amazonaws com recursion jpg You first
Want a Structured Path to Master System Design Too? Don’t Miss This!