765. Couples Holding Hands
Problem Description
You have n
couples (so 2n
people total) sitting in a row of 2n
seats. Each person has a unique ID number, and couples are paired sequentially: couple 0 consists of persons (0, 1)
, couple 1 consists of persons (2, 3)
, couple 2 consists of persons (4, 5)
, and so on, with the last couple being persons (2n - 2, 2n - 1)
.
The goal is to arrange the seating so that every couple sits next to each other (side by side). You're given an array row
where row[i]
represents the ID of the person currently sitting in seat i
.
To achieve this arrangement, you can perform swaps. A swap involves selecting any two people in the row, having them stand up, and switching their seats.
Your task is to find the minimum number of swaps needed to ensure all couples are sitting together.
For example, if row = [0, 2, 1, 3]
, person 0 is sitting in seat 0, person 2 in seat 1, person 1 in seat 2, and person 3 in seat 3. Since couple 0 (persons 0 and 1) are not sitting together, and couple 1 (persons 2 and 3) are not sitting together, we need to swap. One swap of persons in seats 1 and 2 would result in [0, 1, 2, 3]
, where both couples are now sitting together.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: This problem can be modeled as a graph problem. Each couple can be viewed as a node, and if two couples have their members mixed up in seating positions (i.e., they need to swap to get to correct positions), there's an edge between them. The couples form connected components or cycles that need to be resolved.
Is it a tree?
- No: The graph formed by the couples is not necessarily a tree. It can have multiple disconnected components, and each component represents a cycle of couples that need to swap among themselves.
Is the problem related to directed acyclic graphs?
- No: The problem doesn't involve directed edges or acyclic properties. The relationships between couples that need swapping form undirected cycles.
Is the problem related to shortest paths?
- No: We're not finding shortest paths between nodes. We're identifying connected components (cycles of couples).
Does the problem involve connectivity?
- Yes: The core of the problem is about connectivity. We need to identify which couples are connected through a cycle of necessary swaps. Each connected component represents a group of couples that need to swap among themselves.
Disjoint Set Union
- Yes: DSU (Union-Find) is perfect for this problem. We can use it to group couples that are in the same permutation cycle. The solution uses Union-Find to merge couples that are currently sitting in adjacent pairs, forming connected components.
Conclusion: The flowchart leads us to use Disjoint Set Union (Union-Find), which is implemented in the solution. While DFS could also solve connectivity problems, DSU is more efficient here for finding and merging connected components of couples that need to swap positions.
Intuition
The key insight is to think about what happens when couples are sitting in the wrong positions. If couple A has one person sitting with someone from couple B, then couple B must have their other person sitting somewhere else, possibly with someone from couple C, and so on. This creates a cycle of couples that are "entangled" with each other.
Consider this scenario: Person 0 sits with Person 2, Person 1 sits with Person 3. Here, couples 0 and 1 are entangled. To fix this, we need exactly 1 swap - we can swap Person 1 and Person 2, resulting in couples sitting correctly.
Now imagine three couples are entangled: Person 0 sits with Person 2, Person 1 sits with Person 4, and Person 3 sits with Person 5. This forms a cycle of 3 couples, and we need exactly 2 swaps to fix all of them.
The pattern emerges: if k
couples form a cycle where they're all mixed up with each other, we need exactly k-1
swaps to arrange them correctly. Why? Because each swap fixes one couple and reduces the problem size by 1. When we're down to the last couple in the cycle, they're automatically in the right position after all previous swaps.
To identify these cycles, we can use Union-Find. As we scan through the seats pair by pair (seats 0-1, seats 2-3, etc.), we check which couples are currently sitting together. If Person i
from couple A is sitting with Person j
from couple B, we union couples A and B in our Union-Find structure. This builds up the connected components representing our cycles.
If we have n
couples total and they form x
connected components (cycles), then the total number of swaps needed is (size_1 - 1) + (size_2 - 1) + ... + (size_x - 1) = n - x
.
This is why the solution uses Union-Find to group couples and returns n - (number of connected components)
.
Learn more about Greedy, Depth-First Search, Breadth-First Search, Union Find and Graph patterns.
Solution Approach
The implementation uses Union-Find (Disjoint Set Union) to track which couples form connected components (cycles). Here's how the solution works step by step:
1. Initialize Union-Find Structure
n = len(row) >> 1 # Number of couples (2n people means n couples)
p = list(range(n)) # Parent array for Union-Find, initially each couple is its own parent
We create a parent array p
where p[i] = i
initially, meaning each couple is in its own set.
2. Map People to Couples
Each person's couple number can be calculated as person_id >> 1
(equivalent to person_id // 2
):
- Person 0 and 1 β Couple 0
- Person 2 and 3 β Couple 1
- Person 4 and 5 β Couple 2
3. Build Connected Components
for i in range(0, len(row), 2):
a, b = row[i] >> 1, row[i + 1] >> 1
p[find(a)] = find(b)
We iterate through seats in pairs (0-1, 2-3, 4-5, etc.). For each pair of adjacent seats:
- Get the couple numbers of the two people sitting together:
a = row[i] >> 1
andb = row[i + 1] >> 1
- Union these two couples using
p[find(a)] = find(b)
This unions couples that have their members mixed up in the current seating arrangement.
4. Find Operation with Path Compression
def find(x: int) -> int:
if p[x] != x:
p[x] = find(p[x]) # Path compression
return p[x]
The find operation locates the root of a set and applies path compression for efficiency.
5. Count Connected Components and Calculate Result
return n - sum(i == find(i) for i in range(n))
- Count how many couples are roots of their components:
sum(i == find(i) for i in range(n))
- This gives us the number of connected components
x
- Return
n - x
, which is the minimum number of swaps needed
Why This Works:
If k
couples form a cycle (connected component), they need k-1
swaps to all sit correctly. With x
total components having sizes yβ, yβ, ..., yβ
, the total swaps needed is:
(yβ - 1) + (yβ - 1) + ... + (yβ - 1) = (yβ + yβ + ... + yβ) - x = n - x
The Union-Find efficiently groups the entangled couples and allows us to calculate the answer in O(n) time with nearly O(1) operations for union and find (with path compression).
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with row = [0, 5, 4, 3, 2, 1]
(3 couples, 6 people).
Step 1: Identify the couples
- Couple 0: persons (0, 1)
- Couple 1: persons (2, 3)
- Couple 2: persons (4, 5)
Step 2: Check current seating pairs
- Seats 0-1: Person 0 (Couple 0) sits with Person 5 (Couple 2)
- Seats 2-3: Person 4 (Couple 2) sits with Person 3 (Couple 1)
- Seats 4-5: Person 2 (Couple 1) sits with Person 1 (Couple 0)
Notice all three couples are entangled in a cycle!
Step 3: Build Union-Find structure
Initialize: p = [0, 1, 2]
(each couple is its own parent)
Process seat pairs:
-
Seats 0-1: Person 0 (Couple 0) and Person 5 (Couple 2) β Union couples 0 and 2
find(0) = 0
,find(2) = 2
- Set
p[0] = 2
- Now
p = [2, 1, 2]
-
Seats 2-3: Person 4 (Couple 2) and Person 3 (Couple 1) β Union couples 2 and 1
find(2) = 2
,find(1) = 1
- Set
p[2] = 1
- Now
p = [2, 1, 1]
-
Seats 4-5: Person 2 (Couple 1) and Person 1 (Couple 0) β Union couples 1 and 0
find(1) = 1
,find(0) = find(2) = find(1) = 1
- Already in same component, no change needed
Step 4: Count connected components Check which couples are roots:
- Couple 0:
find(0) = 1
, not a root - Couple 1:
find(1) = 1
, is a root β - Couple 2:
find(2) = 1
, not a root
Number of components = 1
Step 5: Calculate result Minimum swaps = n - components = 3 - 1 = 2
Verification with actual swaps:
- Swap persons in seats 1 and 4:
[0, 1, 4, 3, 2, 5]
- Couple 0 is fixed! - Swap persons in seats 2 and 4:
[0, 1, 2, 3, 4, 5]
- All couples are fixed!
Indeed, we needed exactly 2 swaps, confirming our formula works correctly.
Solution Implementation
1class Solution:
2 def minSwapsCouples(self, row: List[int]) -> int:
3 """
4 Find minimum swaps needed to seat couples together.
5 Uses Union-Find to group couples that are sitting in wrong positions.
6
7 Args:
8 row: List of integers representing people (couples are 0-1, 2-3, 4-5, etc.)
9
10 Returns:
11 Minimum number of swaps needed
12 """
13
14 def find_root(node: int) -> int:
15 """
16 Find the root of the set containing the given node.
17 Implements path compression for optimization.
18
19 Args:
20 node: The node to find the root for
21
22 Returns:
23 The root node of the set
24 """
25 if parent[node] != node:
26 # Path compression: make every node point directly to root
27 parent[node] = find_root(parent[node])
28 return parent[node]
29
30 # Calculate number of couples (each couple consists of 2 people)
31 num_couples = len(row) // 2
32
33 # Initialize parent array for Union-Find
34 # Each couple initially forms its own set
35 parent = list(range(num_couples))
36
37 # Process each seat pair (positions 0-1, 2-3, 4-5, etc.)
38 for seat_index in range(0, len(row), 2):
39 # Get couple IDs for the two people sitting together
40 # Person i belongs to couple i//2
41 couple_a = row[seat_index] // 2
42 couple_b = row[seat_index + 1] // 2
43
44 # Union: Connect the two couples in the same component
45 # If they're from different couples, they need to be swapped
46 root_a = find_root(couple_a)
47 root_b = find_root(couple_b)
48 parent[root_a] = root_b
49
50 # Count number of connected components
51 # Each component requires (size - 1) swaps to fix
52 num_components = sum(1 for couple_id in range(num_couples)
53 if couple_id == find_root(couple_id))
54
55 # Total swaps = total couples - number of components
56 return num_couples - num_components
57
1class Solution {
2 // Parent array for Union-Find (Disjoint Set Union) data structure
3 private int[] parent;
4
5 public int minSwapsCouples(int[] row) {
6 // Calculate number of couples (each couple consists of 2 people)
7 int numCouples = row.length / 2;
8
9 // Initialize parent array for Union-Find
10 parent = new int[numCouples];
11 for (int i = 0; i < numCouples; i++) {
12 parent[i] = i;
13 }
14
15 // Union couples that are sitting together
16 // Process every pair of adjacent seats
17 for (int i = 0; i < row.length; i += 2) {
18 // Get couple ID for person at position i and i+1
19 // Couple ID is person_number / 2 (persons 0,1 are couple 0; persons 2,3 are couple 1, etc.)
20 int coupleA = row[i] / 2;
21 int coupleB = row[i + 1] / 2;
22
23 // Union the two couples in the same component
24 parent[find(coupleA)] = find(coupleB);
25 }
26
27 // Count number of connected components
28 // Each connected component represents a cycle of couples that need to be swapped
29 int connectedComponents = 0;
30 for (int i = 0; i < numCouples; i++) {
31 if (i == find(i)) {
32 connectedComponents++;
33 }
34 }
35
36 // Minimum swaps needed = total couples - number of connected components
37 // Each component of size k needs (k-1) swaps to fix
38 return numCouples - connectedComponents;
39 }
40
41 /**
42 * Find operation with path compression for Union-Find
43 * Returns the root parent of element x
44 */
45 private int find(int x) {
46 if (parent[x] != x) {
47 // Path compression: make every node point directly to root
48 parent[x] = find(parent[x]);
49 }
50 return parent[x];
51 }
52}
53
1class Solution {
2public:
3 int minSwapsCouples(vector<int>& row) {
4 // Total number of couples (each couple has 2 people)
5 int numCouples = row.size() / 2;
6
7 // Parent array for Union-Find data structure
8 // Each index represents a couple group
9 int parent[numCouples];
10
11 // Initialize each couple as its own parent (self-loop)
12 iota(parent, parent + numCouples, 0);
13
14 // Find function with path compression for Union-Find
15 // Returns the root parent of the given couple group
16 function<int(int)> find = [&](int coupleId) -> int {
17 if (parent[coupleId] != coupleId) {
18 // Path compression: directly connect to root parent
19 parent[coupleId] = find(parent[coupleId]);
20 }
21 return parent[coupleId];
22 };
23
24 // Process each pair of adjacent seats
25 // Union couples that are sitting together
26 for (int seatIndex = 0; seatIndex < numCouples * 2; seatIndex += 2) {
27 // Get couple IDs by dividing person ID by 2
28 // Person 0,1 belong to couple 0; Person 2,3 belong to couple 1, etc.
29 int coupleA = row[seatIndex] / 2;
30 int coupleB = row[seatIndex + 1] / 2;
31
32 // Union the two couples if they're sitting together
33 parent[find(coupleA)] = find(coupleB);
34 }
35
36 // Count the number of connected components
37 // Start with total couples and subtract the number of root parents
38 int minSwaps = numCouples;
39 for (int coupleId = 0; coupleId < numCouples; ++coupleId) {
40 // If this couple is its own root, it represents a connected component
41 if (coupleId == find(coupleId)) {
42 minSwaps--;
43 }
44 }
45
46 // The minimum swaps needed equals (total couples - number of components)
47 return minSwaps;
48 }
49};
50
1/**
2 * Finds the minimum number of swaps needed to arrange couples sitting together.
3 * Uses Union-Find (Disjoint Set Union) to group couples that are interconnected.
4 *
5 * @param row - Array representing people where couples are (0,1), (2,3), (4,5), etc.
6 * @returns The minimum number of swaps required
7 */
8function minSwapsCouples(row: number[]): number {
9 // Calculate number of couples (each couple consists of 2 people)
10 const numberOfCouples: number = row.length >> 1;
11
12 // Initialize parent array for Union-Find structure
13 // Each couple initially points to itself as parent
14 const parent: number[] = Array(numberOfCouples)
15 .fill(0)
16 .map((_, index) => index);
17
18 /**
19 * Find operation with path compression for Union-Find.
20 * Finds the root parent of a couple group.
21 *
22 * @param coupleId - The couple ID to find root for
23 * @returns The root parent of the couple group
24 */
25 const findRoot = (coupleId: number): number => {
26 // Path compression: make each node point directly to root
27 if (parent[coupleId] !== coupleId) {
28 parent[coupleId] = findRoot(parent[coupleId]);
29 }
30 return parent[coupleId];
31 };
32
33 // Process each pair of adjacent seats
34 for (let seatIndex = 0; seatIndex < numberOfCouples << 1; seatIndex += 2) {
35 // Get couple IDs by dividing person IDs by 2
36 // Person 0,1 belong to couple 0; Person 2,3 belong to couple 1, etc.
37 const firstCoupleId = row[seatIndex] >> 1;
38 const secondCoupleId = row[seatIndex + 1] >> 1;
39
40 // Union operation: connect the two couples in the same component
41 // If they're not from the same couple, they create a cycle requiring swaps
42 parent[findRoot(firstCoupleId)] = findRoot(secondCoupleId);
43 }
44
45 // Count the number of connected components
46 // Each component represents a group of interconnected couples
47 let connectedComponents = numberOfCouples;
48 for (let coupleId = 0; coupleId < numberOfCouples; ++coupleId) {
49 // If a couple is its own root, it's a component leader
50 if (coupleId === findRoot(coupleId)) {
51 --connectedComponents;
52 }
53 }
54
55 // The minimum swaps needed equals total couples minus number of components
56 // Each component of size k requires (k-1) swaps to fix
57 return connectedComponents;
58}
59
Time and Space Complexity
Time Complexity: O(n Γ Ξ±(n))
The algorithm uses a Union-Find (Disjoint Set Union) data structure with path compression. The main operations are:
- Iterating through the array once:
O(n)
wheren
is the length of the row array - For each pair of elements (n/2 pairs total), performing two
find
operations and one union operation - The
find
operation with path compression has an amortized time complexity ofO(Ξ±(n))
, whereΞ±(n)
is the inverse Ackermann function - The final sum comprehension iterates through n/2 elements, each calling
find
once:O(n Γ Ξ±(n))
Since Ξ±(n)
grows extremely slowly and is effectively constant for all practical values of n
(it's less than 5 for all values of n
less than 2^65536), the overall time complexity is O(n Γ Ξ±(n))
.
Space Complexity: O(n)
The space usage includes:
- The parent array
p
of sizen/2
:O(n)
- The recursion stack for the
find
function, which in the worst case (before path compression) could beO(n)
, but with path compression becomesO(Ξ±(n))
- Variables
a
,b
, and loop counters:O(1)
The dominant factor is the parent array, giving us a total space complexity of O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Couple ID Calculation
A common mistake is using modulo operation (%
) instead of integer division to determine couple IDs. Some might incorrectly think that couples are paired as (0,1), (1,2), (2,3), etc., or try to use person_id % 2
to find partners.
Wrong approach:
# Incorrect: This doesn't properly identify couples couple_id = row[i] % 2 # Wrong! partner = row[i] + 1 if row[i] % 2 == 0 else row[i] - 1 # Overcomplicated
Correct approach:
# Each couple ID is simply person_id // 2 (or person_id >> 1) couple_id = row[i] // 2 # Person 0,1 β Couple 0; Person 2,3 β Couple 1; etc.
2. Forgetting Path Compression in Union-Find
Without path compression, the find operation can degrade to O(n) in the worst case, making the overall algorithm inefficient for large inputs.
Inefficient version:
def find(x):
while parent[x] != x:
x = parent[x] # No path compression
return x
Optimized version:
def find(x):
if parent[x] != x:
parent[x] = find(parent[x]) # Path compression
return parent[x]
3. Misunderstanding the Problem's Seat Pairing
Some might think any two consecutive people in the array should be couples, but the problem specifically states that seats are paired as (0,1), (2,3), (4,5), etc. This means we should only check pairs at even indices.
Wrong iteration:
# Incorrect: Checking every consecutive pair
for i in range(len(row) - 1):
couple_a = row[i] // 2
couple_b = row[i + 1] // 2
# This would check pairs like (1,2), (3,4) which aren't seat pairs
Correct iteration:
# Check only seat pairs: (0,1), (2,3), (4,5), etc.
for i in range(0, len(row), 2):
couple_a = row[i] // 2
couple_b = row[i + 1] // 2
4. Incorrect Component Counting Logic
When counting connected components, you must check if each node is its own root after all unions are complete, not during the union process.
Wrong timing:
components = n # Start with n components
for i in range(0, len(row), 2):
if find(couple_a) != find(couple_b):
components -= 1 # Wrong: This doesn't account for all merges correctly
union(couple_a, couple_b)
Correct approach:
# First, perform all unions
for i in range(0, len(row), 2):
parent[find(couple_a)] = find(couple_b)
# Then count components
components = sum(1 for i in range(n) if i == find(i))
5. Not Handling Edge Cases
While the problem guarantees valid input, in practice you might want to validate:
- Empty array or array with odd length
- Invalid person IDs (negative or >= 2n)
- Duplicate person IDs
Robust version with validation:
def minSwapsCouples(self, row: List[int]) -> int:
if not row or len(row) % 2 != 0:
return 0 # or raise an exception
n = len(row) // 2
if len(set(row)) != len(row): # Check for duplicates
raise ValueError("Duplicate person IDs found")
# Continue with main algorithm...
Consider the classic dynamic programming of longest increasing subsequence:
Find the length of the longest subsequence of a given sequence such that all elements of the subsequence are sorted in increasing order.
For example, the length of LIS for [50, 3, 10, 7, 40, 80]
is 4
and LIS is
[3, 7, 40, 80]
.
What is the recurrence relation?
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Want a Structured Path to Master System Design Too? Donβt Miss This!