5 Best Ways to Check Whether All Rooms Can Be Unlocked in Python

πŸ’‘ Problem Formulation: Imagine a scenario in which we have a series of rooms locked and each room may contain keys to other rooms. Given a list of lists, where each sublist contains keys to other rooms, write a program to determine if we can unlock all the rooms. The input might look like [[1],[2],[3],[]], with the desired output being a boolean value indicating whether all rooms can be unlocked or not.

Method 1: Depth-First Search (DFS)

This method involves a classic DFS algorithm, where we start from room 0 and visit each room reachable from it using the keys we find. The function canUnlockAllRooms will return True if we can visit all the rooms, else False. DFS is recursive and utilizes a set to keep track of visited rooms.

Here’s an example:

def canUnlockAllRooms(rooms):
    def dfs(room):
        visited.add(room)
        for key in rooms[room]:
            if key not in visited:
                dfs(key)
    visited = set()
    dfs(0)
    return len(visited) == len(rooms)

# Example usage
print(canUnlockAllRooms([[1],[2],[3],[]]))

The output of this code snippet is True.

This code defines a recursive function dfs that takes a room index and marks it as visited. It then recursively visits all rooms accessible via keys from that room. The main function checks if the number of visited rooms is equal to total rooms, signifying all are accessible.

Method 2: Breadth-First Search (BFS)

Breadth-First Search is an alternative traversal approach where we use a queue to explore keys at each room level by level. This non-recursive technique ensures we visit all accessible rooms in the shortest path order. Here, the canUnlockAllRooms function also utilizes a set to track visited rooms.

Here’s an example:

from collections import deque

def canUnlockAllRooms(rooms):
    visited = set([0])
    queue = deque([0])
    while queue:
        room = queue.popleft()
        for key in rooms[room]:
            if key not in visited:
                visited.add(key)
                queue.append(key)
    return len(visited) == len(rooms)

# Example usage
print(canUnlockAllRooms([[1,3],[3,0,1],[2],[0]]))

The output of this code snippet is False.

The BFS implementation here starts with room 0 and explores each level of accessible rooms through a queue, adding newly discovered rooms to the queue and visited set. The function returns True if all rooms are visited by the time the queue is empty.

Method 3: Iterative Depth-First Search

The iterative version of DFS uses a stack instead of recursion to traverse the room graph. While similar to the recursive approach, it might be preferred in scenarios with large input sizes to avoid stack overflow errors. The function checks if each room is visited by tracking them in a set.

Here’s an example:

def canUnlockAllRooms(rooms):
    visited = set([0])
    stack = [0]
    while stack:
        room = stack.pop()
        for key in rooms[room]:
            if key not in visited:
                visited.add(key)
                stack.append(key)
    return len(visited) == len(rooms)

# Example usage
print(canUnlockAllRooms([[1],[0,2],[3],[]]))

The output of this code snippet is True.

This code utilises a stack for an iterative DFS approach. By popping the last visited room off the stack and iterating through its keys, we can visit all accessible rooms without function call overhead. It offers a robust alternative to recursion.

Method 4: Union Find

This method applies the Union Find (Disjoint Set) data structure to group rooms that are accessible from one another. It combines the sets containing each room when a key is found, and in the end, if all rooms belong to one set, then all rooms can be unlocked.

Here’s an example:

def canUnlockAllRooms(rooms):
    parent = list(range(len(rooms)))
    
    def find(x):
        if parent[x] != x:
            parent[x] = find(parent[x])
        return parent[x]
    
    def union(x, y):
        parent[find(x)] = find(y)
    
    for i, room in enumerate(rooms):
        for key in room:
            union(i, key)
    
    return len(set(find(x) for x in parent)) == 1

# Example usage
print(canUnlockAllRooms([[1],[2],[3],[4],[0]]))

The output of this code snippet is False.

The union-find implementation in this code keeps track of connected components (accessible rooms set). The find function locates the root room, and union merges sets. Finally, if there’s only one set, all rooms are accessible.

Bonus One-Liner Method 5: Recursive Set Comprehension

For a more Pythonic and concise solution, a single line recursive set comprehension method can be employed. This stylish functional approach can solve the problem as effectively as the previous methods but with less code.

Here’s an example:

canUnlockAllRooms = lambda rooms, seen={0}: len(seen) == len(rooms) or canUnlockAllRooms(rooms, seen | set.union(*map(rooms.__getitem__, seen)))

# Example usage
print(canUnlockAllRooms([[1,2],[2,3],[1,3],[]]))

The output of this code snippet is True.

This compact code uses a lambda function and set comprehension. It expands the set of seen rooms by getting keys from all seen rooms and checks if all rooms are seen. This combines the recursive and functional programming paradigms.

Summary/Discussion

  • Method 1: Depth-First Search (DFS). Good for exploring connected components. Recursion stack limit may be a weakness for very large datasets.
  • Method 2: Breadth-First Search (BFS). Ensures shortest path traversal. Non-recursive, hence stable for large graphs. May not be as straightforward to implement as DFS.
  • Method 3: Iterative Depth-First Search. Avoids recursion limits. Useful for massive datasets. However, can be more complex to understand compared to recursive DFS.
  • Method 4: Union Find. Efficient for large numbers of rooms and keys. Runs in almost linear time. However, the concept of disjoint sets can be complex for beginners.
  • Bonus Method 5: Recursive Set Comprehension. Elegant and concise. Combines the power of recursion and set operations in a one-liner. However, readability may be challenging for those not familiar with functional programming.