1259. Handshakes That Don't Cross
Problem Description
You have an even number of people standing in a circle, represented by numPeople
. Each person needs to shake hands with exactly one other person, resulting in numPeople / 2
total handshakes.
The key constraint is that when people shake hands, their arms cannot cross. Imagine drawing lines between pairs of people who shake hands - these lines must not intersect with each other.
Your task is to find the total number of valid ways to arrange these handshakes such that no two handshakes cross each other. Since the answer can be very large, return the result modulo 10^9 + 7
.
For example, if you have 4 people in a circle:
- One valid arrangement: person 1 shakes hands with person 2, and person 3 shakes hands with person 4
- Another valid arrangement: person 1 shakes hands with person 4, and person 2 shakes hands with person 3 (their arms would go around the outside and inside respectively, not crossing)
- Invalid arrangement: person 1 shakes hands with person 3, and person 2 shakes hands with person 4 (these would cross)
The problem essentially asks you to count all possible non-crossing perfect matchings in a circle of people.
Intuition
When we think about arranging non-crossing handshakes in a circle, let's focus on one specific person - say person 1. This person must shake hands with someone, and this choice has a profound impact on the problem structure.
If person 1 shakes hands with person k
, this handshake divides the circle into two separate groups:
- People between person 1 and person
k
(on one side) - People between person
k
and person 1 (on the other side)
The crucial insight is that because handshakes cannot cross, people in one group can only shake hands with others in the same group. If someone from the left group tried to shake hands with someone from the right group, their handshake would have to cross the handshake between person 1 and person k
.
This observation leads us to a recursive structure. For person 1 to shake hands with person k
, there must be an even number of people on each side (since everyone needs a partner). If there are l
people on the left side and r
people on the right side, then:
- The
l
people on the left can arrange their handshakes indfs(l)
ways - The
r
people on the right can arrange their handshakes indfs(r)
ways - These arrangements are independent, so we multiply them:
dfs(l) * dfs(r)
To find the total number of arrangements for i
people, we sum up all possible choices of who person 1 shakes hands with. Person 1 can only shake hands with person 2, 4, 6, ... (to ensure even numbers of people on both sides), which is why we iterate l
from 0 to i-1
in steps of 2.
This recursive pattern is actually computing the Catalan numbers, a famous sequence that appears in many counting problems involving non-crossing structures.
Learn more about Math and Dynamic Programming patterns.
Solution Approach
The solution implements a dynamic programming approach using memoization to avoid redundant calculations.
Function Design:
We define a recursive function dfs(i)
that calculates the number of valid handshake arrangements for i
people. The base case is when i < 2
, where there's only one way (no handshakes), so we return 1
.
Recursive Logic:
For i
people, we iterate through all possible positions where person 1 can shake hands. Since person 1 needs to shake hands with someone at an even distance to ensure both sides have even numbers of people, we use:
for l in range(0, i, 2):
r = i - l - 2
Here:
l
represents the number of people on the left sider = i - l - 2
represents the number of people on the right side (subtracting 2 for person 1 and their handshake partner)- We iterate
l
from 0 toi-1
in steps of 2 to maintain even counts
Calculation: For each split, the total arrangements equals the product of arrangements on both sides:
ans += dfs(l) * dfs(r)
We accumulate all possible splits and apply modulo 10^9 + 7
to handle large numbers:
ans %= mod
Memoization:
The @cache
decorator automatically memoizes the function results. This is crucial because the same subproblems appear multiple times in different recursive branches. For example, dfs(4)
might be calculated when solving dfs(6)
, dfs(8)
, etc. Without memoization, the time complexity would be exponential.
Time Complexity: O(n²)
where n is numPeople
, as we compute each state once and each state requires O(n)
work.
Space Complexity: O(n)
for the recursion stack and memoization cache.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with numPeople = 6
(6 people in a circle).
Step 1: Initial Call
We call dfs(6)
to find arrangements for 6 people.
Step 2: Person 1's Handshake Options Person 1 can shake hands with persons 2, 4, or 6 (only even positions to maintain even groups).
Option A: Person 1 shakes hands with Person 2
- Left side: 0 people (between 1 and 2)
- Right side: 4 people (persons 3, 4, 5, 6)
- Contribution:
dfs(0) * dfs(4) = 1 * dfs(4)
To calculate dfs(4)
:
- Person 3 shakes with Person 4:
dfs(0) * dfs(2) = 1 * 1 = 1
- Person 3 shakes with Person 6:
dfs(2) * dfs(0) = 1 * 1 = 1
- Total:
dfs(4) = 2
So Option A contributes: 1 * 2 = 2
Option B: Person 1 shakes hands with Person 4
- Left side: 2 people (persons 2, 3)
- Right side: 2 people (persons 5, 6)
- Contribution:
dfs(2) * dfs(2) = 1 * 1 = 1
Option C: Person 1 shakes hands with Person 6
- Left side: 4 people (persons 2, 3, 4, 5)
- Right side: 0 people
- Contribution:
dfs(4) * dfs(0) = 2 * 1 = 2
Step 3: Final Result
Total arrangements: 2 + 1 + 2 = 5
The 5 valid arrangements are:
- (1-2), (3-4), (5-6)
- (1-2), (3-6), (4-5)
- (1-4), (2-3), (5-6)
- (1-6), (2-3), (4-5)
- (1-6), (2-5), (3-4)
Each arrangement ensures no handshakes cross when drawn in the circle.
Solution Implementation
1class Solution:
2 def numberOfWays(self, numPeople: int) -> int:
3 """
4 Calculate the number of ways people can shake hands without crossing.
5
6 Args:
7 numPeople: Even number of people standing in a circle
8
9 Returns:
10 Number of valid handshake arrangements modulo 10^9 + 7
11 """
12 from functools import cache
13
14 MOD = 10**9 + 7
15
16 @cache
17 def count_ways(num_people: int) -> int:
18 """
19 Dynamic programming function to count valid handshake arrangements.
20
21 Args:
22 num_people: Number of people to arrange handshakes for
23
24 Returns:
25 Number of valid arrangements for num_people
26 """
27 # Base case: 0 or 2 people have exactly 1 way to shake hands
28 if num_people < 2:
29 return 1
30
31 total_ways = 0
32
33 # Try pairing person 0 with every other person at odd positions
34 # This ensures we always have even number of people on each side
35 for left_count in range(0, num_people, 2):
36 # Calculate number of people on the right side after pairing
37 right_count = num_people - left_count - 2
38
39 # Multiply ways to arrange left side with ways to arrange right side
40 # Use Catalan number property: C(n) = sum(C(i) * C(n-1-i))
41 total_ways += count_ways(left_count) * count_ways(right_count)
42 total_ways %= MOD
43
44 return total_ways
45
46 return count_ways(numPeople)
47
1class Solution {
2 // Memoization array to store computed results for dynamic programming
3 private int[] memo;
4
5 // Modulo value for preventing integer overflow
6 private final int MOD = (int) 1e9 + 7;
7
8 /**
9 * Calculates the number of ways people can form a circle and shake hands
10 * such that no two handshakes cross each other.
11 *
12 * @param numPeople The number of people in the circle (must be even)
13 * @return The number of valid handshake configurations
14 */
15 public int numberOfWays(int numPeople) {
16 // Initialize memoization array with size numPeople + 1
17 memo = new int[numPeople + 1];
18
19 // Start recursive calculation
20 return calculateWays(numPeople);
21 }
22
23 /**
24 * Recursive helper function with memoization to calculate the number of ways
25 * for a given number of people to shake hands without crossing.
26 *
27 * @param remainingPeople The number of people to consider
28 * @return The number of valid configurations for remainingPeople
29 */
30 private int calculateWays(int remainingPeople) {
31 // Base case: 0 people or odd number results in 1 way (empty configuration)
32 if (remainingPeople < 2) {
33 return 1;
34 }
35
36 // Return memoized result if already calculated
37 if (memo[remainingPeople] != 0) {
38 return memo[remainingPeople];
39 }
40
41 // Try pairing person 0 with every other odd-indexed person
42 // This divides the circle into two independent subproblems
43 for (int leftGroupSize = 0; leftGroupSize < remainingPeople; leftGroupSize += 2) {
44 // Calculate size of right group after removing paired persons
45 int rightGroupSize = remainingPeople - leftGroupSize - 2;
46
47 // Multiply ways for left and right groups, then add to total
48 // Use long multiplication to prevent overflow before applying modulo
49 long currentWays = (1L * calculateWays(leftGroupSize) * calculateWays(rightGroupSize)) % MOD;
50 memo[remainingPeople] = (int) ((memo[remainingPeople] + currentWays) % MOD);
51 }
52
53 return memo[remainingPeople];
54 }
55}
56
1class Solution {
2public:
3 int numberOfWays(int numPeople) {
4 const int MOD = 1e9 + 7;
5
6 // dp[i] stores the number of ways to arrange i people in a circle
7 // where they shake hands without crossing
8 int dp[numPeople + 1];
9 memset(dp, 0, sizeof(dp));
10
11 // Recursive function with memoization
12 // Returns the number of valid handshake arrangements for n people
13 function<int(int)> calculateWays = [&](int n) {
14 // Base case: 0 or 2 people have exactly 1 way
15 if (n < 2) {
16 return 1;
17 }
18
19 // Return memoized result if already computed
20 if (dp[n]) {
21 return dp[n];
22 }
23
24 // Try pairing person 1 with every other person (must be even positions)
25 // This divides the circle into two independent subproblems
26 for (int leftCount = 0; leftCount < n; leftCount += 2) {
27 int rightCount = n - leftCount - 2; // Subtract 2 for the paired persons
28
29 // Multiply the number of ways for left and right groups
30 // Use long long to prevent overflow during multiplication
31 dp[n] = (dp[n] + 1LL * calculateWays(leftCount) * calculateWays(rightCount) % MOD) % MOD;
32 }
33
34 return dp[n];
35 };
36
37 return calculateWays(numPeople);
38 }
39};
40
1/**
2 * Calculates the number of ways to arrange people in a circle where they shake hands
3 * without any handshakes crossing each other.
4 * This is equivalent to calculating the Catalan number for n/2.
5 *
6 * @param numPeople - The number of people (must be even)
7 * @returns The number of valid handshake arrangements modulo 10^9 + 7
8 */
9function numberOfWays(numPeople: number): number {
10 const MOD: number = 1000000007;
11
12 // Memoization array to store computed results for each subproblem
13 const memo: number[] = Array(numPeople + 1).fill(0);
14
15 /**
16 * Recursive function with memoization to calculate number of valid arrangements
17 *
18 * @param remainingPeople - Number of people left to arrange
19 * @returns Number of valid arrangements for the given number of people
20 */
21 const calculateWays = (remainingPeople: number): number => {
22 // Base case: 0 or 1 pair of people has exactly 1 way to arrange
23 if (remainingPeople < 2) {
24 return 1;
25 }
26
27 // Return memoized result if already computed
28 if (memo[remainingPeople] !== 0) {
29 return memo[remainingPeople];
30 }
31
32 // Try fixing person 0's handshake partner and divide into two subproblems
33 // Person 0 can shake hands with person 1, 3, 5, ..., remainingPeople-1
34 for (let leftGroupSize = 0; leftGroupSize < remainingPeople; leftGroupSize += 2) {
35 const rightGroupSize: number = remainingPeople - leftGroupSize - 2;
36
37 // Calculate ways for left and right groups independently
38 // Use BigInt to prevent overflow during multiplication
39 const leftWays: bigint = BigInt(calculateWays(leftGroupSize));
40 const rightWays: bigint = BigInt(calculateWays(rightGroupSize));
41 const totalWays: bigint = (leftWays * rightWays) % BigInt(MOD);
42
43 // Add to current result and keep within modulo
44 memo[remainingPeople] += Number(totalWays);
45 memo[remainingPeople] %= MOD;
46 }
47
48 return memo[remainingPeople];
49 };
50
51 return calculateWays(numPeople);
52}
53
Time and Space Complexity
The time complexity is O(n^2)
where n
is the value of numPeople
.
The function uses memoization with @cache
decorator. For each value i
from 0 to numPeople
, we compute dfs(i)
exactly once due to caching. When computing dfs(i)
, we iterate through all possible left subtree sizes l
from 0 to i-2
in steps of 2, which gives us i/2
iterations. For each iteration, we perform constant time operations (multiplication and modulo).
The total number of operations across all cached calls is:
dfs(0)
: 0 iterationsdfs(2)
: 1 iterationdfs(4)
: 2 iterations- ...
dfs(n)
:n/2
iterations
This sums to 0 + 1 + 2 + ... + n/2 = O(n^2/4) = O(n^2)
.
The space complexity is O(n)
where n
is the value of numPeople
.
The space is used for:
- The memoization cache which stores results for values from 0 to
numPeople
, requiringO(n)
space - The recursion call stack which can go up to depth
O(n)
in the worst case
Therefore, the total space complexity is O(n) + O(n) = O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Partitioning Logic
A common mistake is incorrectly calculating how to partition the circle when person 1 shakes hands with another person. Many developers mistakenly think they can pair person 1 with any other person, but this can lead to odd numbers of people on one or both sides.
Wrong approach:
for partner in range(2, num_people + 1):
left = partner - 2
right = num_people - partner
# This can result in odd numbers on either side
Correct approach:
for left_count in range(0, num_people, 2):
right_count = num_people - left_count - 2
# Ensures both sides have even numbers
2. Forgetting to Apply Modulo Operation
When dealing with large numbers, forgetting to apply the modulo operation at each step can cause integer overflow in languages with fixed integer sizes, or incorrect results due to precision issues.
Wrong approach:
total_ways = 0
for left_count in range(0, num_people, 2):
right_count = num_people - left_count - 2
total_ways += count_ways(left_count) * count_ways(right_count)
return total_ways % MOD # Only applying modulo at the end
Correct approach:
total_ways = 0
for left_count in range(0, num_people, 2):
right_count = num_people - left_count - 2
total_ways += count_ways(left_count) * count_ways(right_count)
total_ways %= MOD # Apply modulo after each addition
3. Not Using Memoization
Without memoization, the recursive solution has exponential time complexity because the same subproblems are solved multiple times. This will cause time limit exceeded for larger inputs.
Wrong approach:
def count_ways(num_people):
if num_people < 2:
return 1
total_ways = 0
for left_count in range(0, num_people, 2):
right_count = num_people - left_count - 2
total_ways += count_ways(left_count) * count_ways(right_count)
return total_ways % MOD
Correct approach:
Use @cache
decorator or implement manual memoization:
@cache
def count_ways(num_people):
# ... rest of the implementation
Or with manual memoization:
memo = {}
def count_ways(num_people):
if num_people in memo:
return memo[num_people]
# ... calculate result
memo[num_people] = result
return result
4. Incorrect Base Case
Some might incorrectly handle the base case, especially for 2 people, thinking it needs special handling.
Wrong approach:
if num_people == 0: return 0 # Wrong: 0 people means 1 valid arrangement (empty) if num_people == 2: return 1 # This is actually correct but redundant
Correct approach:
if num_people < 2: # Handles both 0 and negative cases return 1
The beauty of this solution is that it naturally handles the case of 2 people through the recursive formula without special casing.
How does quick sort divide the problem into subproblems?
Recommended Readings
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Coding Interview 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
Want a Structured Path to Master System Design Too? Don’t Miss This!