2077. Paths in Maze That Lead to Same Room
Problem Description
The problem involves finding the confusion score of a maze, which is determined by counting the number of unique cycles of length 3 among the rooms. A cycle of length 3 would look like a trip from one room to a second, then to a third, and back to the first, without repetition of rooms or revisiting the starting room before the cycle is complete. The array corridors
provides the connections between rooms, where each connection enables two-way travel between the connected rooms.
For example, if we have a cycle 1 → 2 → 3 → 1, it counts as one valid cycle. We need to ensure that each trio of rooms forms exactly one cycle for counting purposes. Cycles with more than three rooms or cycles that don't return to the starting room after exactly three steps are not considered in the confusion score.
The goal is to calculate the total number of these length 3 cycles in the entire maze based on the corridors provided.
Intuition
To find all possible cycles of length 3, we first convert the corridors
list into a graph representation where each room has a list of directly connected rooms. A defaultdict(set)
is used to facilitate this as it automatically handles the creation of keys and prevents duplicate entries for connections between rooms.
Once we have the graph, for each room i
, we look at all pairs of its connected rooms (using the combinations
function from the itertools
module). For every pair of connected rooms j
and k
, we check if there's a direct connection from j
to k
. If such a connection exists, we've found a cycle of length 3 (i → j → k → i), so we increment a counter ans
. However, each cycle will be counted three times (once for each room in the cycle as the starting room), so we'll divide the total count by 3 to get the correct number of unique cycles.
By iterating through all rooms and their connections, we comprehensively check every possibility for cycles of length 3 and calculate the confusion score to return it.
Learn more about Graph patterns.
Solution Approach
The Reference Solution Approach employs a graph data structure, a combinations utility, and some simple arithmetic. Here's a step-by-step breakdown of the implementation:
-
Graph Representation: First, we need to represent the maze as a graph where the nodes are rooms, and edges are the corridors between them. We use a
defaultdict(set)
from Python's collections module to make this easy. Aset
is chosen for each room's connections because it prevents duplication of corridors and allows for quick membership testing. -
Building the Graph: With the input
corridors
list, which contains pairs of rooms connected by corridors, we iterate through each pair. For each corridor(a, b)
, we add roomb
to the set of connections for rooma
and vice versa, effectively constructing an undirected graph. -
Searching for Cycles: To find all unique 3-room cycles, we look at each room
i
and consider all combinations of its connected rooms. We use Python'sitertools.combinations()
to generate all unique pairs of connected rooms(j, k)
without repetition. -
Cycle Validation: For each pair of rooms
(j, k)
connected to roomi
, we check ifj
andk
are directly connected to each other - this would complete the cyclei → j → k → i
. If a direct connection exists, we increment a counterans
. This process ensures that we only count cycles once and that they are of length 3. -
Avoiding Double Counting: Since each cycle appears three times (once starting at each room), we divide the total count by 3 at the end to get the number of unique cycles. The floor division operator
//
ensures that the result is an integer.
Here is a snippet of how this is being implemented in code:
g = defaultdict(set)
for a, b in corridors:
g[a].add(b)
g[b].add(a)
ans = 0
for i in range(1, n + 1):
for j, k in combinations(g[i], 2):
if j in g[k]:
ans += 1
return ans // 3
In summary, the solution involves constructing an undirected graph, identifying all possible unique cycles of length 3 by examining each node's connections, validating those cycles, and then ensuring that we count each cycle only once by dividing the count by three. This approach leads to an efficient calculation of the confusion score for any given maze represented by its corridors.
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 consider a small maze with 4 rooms and a set of corridors that connect them:
Corridors: [(1, 2), (1, 3), (2, 3), (2, 4), (3, 4)]
Graph Representation
Following the solution approach, we first convert the list of corridors into a graph representation.
Room 1: connected to Room 2 and Room 3 Room 2: connected to Room 1, Room 3, and Room 4 Room 3: connected to Room 1, Room 2, and Room 4 Room 4: connected to Room 2 and Room 3
Building the Graph
The graph constructed from the corridors looks like this:
g = defaultdict(set)
g[1] = {2, 3}
g[2] = {1, 3, 4}
g[3] = {1, 2, 4}
g[4] = {2, 3}
Searching for Cycles
Next, we consider each room and look for all combinations of connected rooms to find cycles of length 3.
For room 1: The combinations of connected rooms are (2, 3).
For room 2: The combinations of connected rooms are (1, 3), (1, 4), and (3, 4).
For room 3: The combinations of connected rooms are (1, 2), (1, 4), and (2, 4).
For room 4: The combinations of connected rooms are (2, 3).
Cycle Validation
We then check whether the combinations actually form cycles of length 3.
- For room 1, the pair (2, 3) is connected, forming a cycle:
1 → 2 → 3 → 1
. - For room 2, the pair (3, 4) is connected, forming a cycle:
2 → 3 → 4 → 2
. - For room 3, the pair (2, 4) is connected, forming a cycle:
3 → 2 → 4 → 3
. - For room 4, the pair (2, 3) is connected, but this cycle was already counted when considering room 2, so it's not unique.
Avoiding Double Counting
Each of the valid cycles identified will appear three times, once for each room in the cycle. We've found the cycles:
1 → 2 → 3 → 1 2 → 3 → 4 → 2 3 → 2 → 4 → 3
These are counted for rooms 1, 2, and 3 respectively. Since each of these cycles is counted thrice (once from each room’s perspective), we get a total count of 3 cycles. However, there is only one unique cycle for each set.
Final Answer
By dividing the total count by 3, we obtain the final answer:
ans = 3 // 3 = 1
So, the confusion score for the maze with these corridors is 1. There is only one unique cycle of length 3 among the rooms in this maze.
Solution Implementation
1from collections import defaultdict
2from itertools import combinations
3
4class Solution:
5 def numberOfPaths(self, numNodes: int, corridors: List[List[int]]) -> int:
6 # Create a graph as a dictionary with default value type 'set', for adjacency list representation
7 graph = defaultdict(set)
8
9 # Build the graph, where each node has a set of its adjacent nodes
10 for first, second in corridors:
11 graph[first].add(second)
12 graph[second].add(first)
13
14 # Initialize counter to keep track of the number of triangular paths
15 trianglePathsCount = 0
16
17 # Iterate through each node
18 for node in range(1, numNodes + 1):
19 # Iterate through all possible pairs of adjacents nodes
20 for neighbor1, neighbor2 in combinations(graph[node], 2):
21 # Check if this pair of nodes forms a triangle with the current node
22 if neighbor1 in graph[neighbor2]:
23 # If so, increment the counter
24 trianglePathsCount += 1
25
26 # Since each triangle is counted three times, divide the result by 3
27 return trianglePathsCount // 3
28
1class Solution {
2 public int numberOfPaths(int n, int[][] corridors) {
3 // Graph represented as an array of hashsets where each hashset is a list of connected nodes
4 Set<Integer>[] graph = new Set[n + 1];
5
6 // Initialize each node's adjacency list
7 for (int i = 0; i <= n; ++i) {
8 graph[i] = new HashSet<>();
9 }
10
11 // Build the graph from corridor connections
12 for (int[] corridor : corridors) {
13 int nodeA = corridor[0];
14 int nodeB = corridor[1];
15 graph[nodeA].add(nodeB);
16 graph[nodeB].add(nodeA);
17 }
18
19 // Count the number of valid paths
20 int pathCount = 0;
21
22 // Iterate over every node to find potential triangles
23 for (int currentNode = 1; currentNode <= n; ++currentNode) {
24 // Get neighbors of the current node
25 var neighbors = new ArrayList<>(graph[currentNode]);
26 int neighborCount = neighbors.size();
27
28 // Check every pair of neighbors to find a triangle
29 for (int i = 0; i < neighborCount; ++i) {
30 for (int j = i + 1; j < neighborCount; ++j) {
31 int neighborA = neighbors.get(i);
32 int neighborB = neighbors.get(j);
33
34 // If the neighbors are also connected, we found a triangle, increment the count
35 if (graph[neighborB].contains(neighborA)) {
36 pathCount++;
37 }
38 }
39 }
40 }
41
42 // Since each triangle is counted 3 times (once for each vertex), divide by 3 to get the correct count
43 return pathCount / 3;
44 }
45}
46
1class Solution {
2public:
3 int numberOfPaths(int n, vector<vector<int>>& corridors) {
4 // Initialize an adjacency list for the graph where each node
5 // has a set of connected nodes (to represent an undirected graph)
6 vector<unordered_set<int>> graph(n + 1);
7
8 // Populate the graph with corridors data
9 for (const auto& corridor : corridors) {
10 // Extracting endpoints of the corridor
11 int node1 = corridor[0], node2 = corridor[1];
12 // Since this is an undirected graph, add each node to the other's adjacency list
13 graph[node1].insert(node2);
14 graph[node2].insert(node1);
15 }
16
17 // Initialize a variable to store the number of triangular paths found
18 int answer = 0;
19
20 // Iterate over each node in the graph to check for triangular paths
21 for (int current = 1; current <= n; ++current) {
22 // Create a vector to easily iterate over the adjacent nodes
23 vector<int> neighbors;
24 neighbors.assign(graph[current].begin(), graph[current].end());
25
26 // Iterate over pairs of neighbors to check if they are also connected to each other
27 for (int i = 0; i < neighbors.size(); ++i) {
28 for (int j = i + 1; j < neighbors.size(); ++j) {
29 int neighbor1 = neighbors[i], neighbor2 = neighbors[j];
30
31 // If neighbor1 is connected to neighbor2, it forms a triangular path
32 answer += graph[neighbor2].count(neighbor1);
33 }
34 }
35 }
36
37 // Since each triangular path is counted three times (once for each vertex),
38 // we divide by 3 to get the correct number of unique paths.
39 return answer / 3;
40 }
41};
42
1// Interface representing a pair of integers
2interface Pair {
3 first: number;
4 second: number;
5}
6
7// Function to calculate the number of triangular paths
8function numberOfPaths(n: number, corridors: Pair[]): number {
9 // Initialize an adjacency list for the graph
10 const graph: Set<number>[] = new Array(n + 1);
11
12 // Fill the graph array with empty sets for each node
13 for (let i = 0; i <= n; i++) {
14 graph[i] = new Set();
15 }
16
17 // Populate the graph with corridors data
18 for (const corridor of corridors) {
19 // Extracting endpoints of the corridors
20 const node1 = corridor.first;
21 const node2 = corridor.second;
22
23 // Add each node to the other's adjacency list
24 graph[node1].add(node2);
25 graph[node2].add(node1);
26 }
27
28 // Variable to store the number of triangular paths found
29 let answer = 0;
30
31 // Check each node for triangular paths
32 for (let currentNode = 1; currentNode <= n; currentNode++) {
33 // Create an array of neighbors from the current node's adjacency list
34 const neighbors: number[] = Array.from(graph[currentNode]);
35
36 // Iterate over pairs of neighbors to check for direct connections
37 for (let i = 0; i < neighbors.length; i++) {
38 for (let j = i + 1; j < neighbors.length; j++) {
39 const neighbor1 = neighbors[i];
40 const neighbor2 = neighbors[j];
41
42 // If neighbor1 is directly connected to neighbor2, a triangular path is formed
43 if (graph[neighbor2].has(neighbor1)) {
44 answer++;
45 }
46 }
47 }
48 }
49
50 // Every triangle path has been counted 3 times, divide by 3 to normalize
51 return answer / 3;
52}
53
54// Example usage
55// Define some corridors as per the interface Pair
56const corridors: Pair[] = [
57 { first: 1, second: 2 },
58 { first: 1, second: 3 },
59 { first: 2, second: 3 }
60];
61
62// Call our function with n nodes and the corridors array
63const trianglePaths = numberOfPaths(3, corridors);
64console.log(trianglePaths); // Output should be 1 as there is one triangle path (1 - 2 - 3)
65
Time and Space Complexity
Time Complexity:
The given code consists of building a graph and then finding all the triangles in it. Here's a breakdown of the time complexity:
- Building the adjacency graph
g
has a time complexity ofO(E)
, whereE
is the number of edges or corridors because we iterate over each corridor to build the graph. - The outer loop runs
n
times (wheren
is the number of rooms), so it has a time complexity ofO(N)
. - Inside the outer loop, the
combinations
function is called. Each call ofcombinations
can generate up toO(d^2)
combinations, whered
is the degree (number of edges) of nodei
. The degree can vary for each node, and in the worst case, it could ben-1
, which would result inO((n-1)^2)
combinations for that node. - Checking if
j
is ing[k]
isO(1)
with the hash set data structure, and this is done once for each combination generated in the previous step. So this operation can be potentiallyO(n^2)
in the worst case for each node. - Finally, the
ans
is divided by 3 outside of the loops, which is a constant-time operationO(1)
.
Taking all these into account, the total time complexity is O(N + E + N * (n-1)^2)
, which simplifies to O(N * (n-1)^2)
in the worst-case scenario when the graph is dense (since N
dominates E
). In other words, the time complexity can be expressed as O(N^3)
when each node is connected to every other node.
Space Complexity:
The space complexity of the code is mainly due to the storage required for the adjacency graph.
- The adjacency graph
g
will have a space complexity ofO(V + E)
, whereV
is the number of nodes andE
is the number of edges, to store all vertices and their edges. - There is some additional overhead due to the storage of combinations in the inner loop, but this does not increase space complexity as it is temporary and does not depend on the size of the input.
- The space complexity of other variables used (like
ans
,i
,j
,k
) isO(1)
.
The dominant term in the space complexity is the storage for the graph, which gives us O(V + E)
space complexity.
Putting the time and space complexities together, we have:
- Time Complexity:
O(N^3)
- Space Complexity:
O(V + E)
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 graph svg Graph Fundamentals Tree with 0 cycle At this point you should be pretty familiar with trees A tree is a special kind of graph a connected acyclic cycle less graph A graph may contain cycle s and nodes could
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!