1434. Number of Ways to Wear Different Hats to Each Other
Problem Description
In this problem, you are given n
people and a fixed collection of 40
different types of hats, each type has its unique label ranging from 1
to 40
. A 2D integer array hats
represents the hat preferences for each person, such that hats[i]
contains a list of all the hats preferred by the i-th
person. Your task is to determine the total number of unique ways that these n
people can wear these hats so that no two people are wearing the same type of hat.
This is a classic combinatorial problem with a restriction that ensures the uniqueness of the hat type for each individual. The problem asks you to return this total number of unique distributions modulo 10^9 + 7
, which is a common technique to manage large output values in computational problems, ensuring the result stays within the limits of standard integer data types.
Intuition
At its core, the solution to this problem is rooted in combinatorics and dynamic programming (DP). The intuition behind using dynamic programming comes from two key observations:
-
The number of people
n
is small enough (maximum10
) to consider the problem using state space representation where each state represents a set of people who have already been assigned a hat. This allows us to compress the state into a binary number (for instance, a binary representation where each bit represents whether a person has been assigned a hat or not). -
The solution builds upon subproblems where each subproblem considers one less hat. This helps in constructing the final solution iteratively as DP excels at optimizing problems where the solution can be built from smaller subproblems.
Given the small number of people, we can use bit masks to represent who has a hat already. The dynamic programming array f[i][j]
signifies the number of ways to assign hats up to the i-th
hat, considering the people configuration j
. The bit representation of j
has '1' in the positions of people who have already been assigned hats and '0' otherwise.
The DP approach then incrementally builds up the solution by considering cases where either the i-th
hat is not used or it's assigned to one of the people who like it following the rules.
Each state transition thus involves either:
- Keeping the configuration as is (not assigning the new hat), which doesn't change the state, or
- Including a new assignment (giving the
i-th
hat to personk
), which changes the state by updating the bit mask to reflect that personk
now has a hat.
This way, the final answer is built up by considering all the possibilities, taking into account which hats can be worn by whom, until all hats up to the maximum value in the given lists are processed.
Learn more about Dynamic Programming and Bitmask patterns.
Solution Approach
The implementation of the solution for this problem utilizes dynamic programming (DP) with bit masking and involves a few steps:
-
Initialize the DP Table: A 2-dimensional DP table
f
is created with dimensions(m + 1) x (1 << n)
, wherem
is the maximum hat number preferred by any person andn
is the number of people. Each entryf[i][j]
will store the count of ways to distribute the firsti
types of hats across the people represented by statej
. -
Define the Base Case: We start with the base case
f[0][0] = 1
, which means there is one way to assign zero hats to zero people. -
Map Preferences: We create a mapping
g
from each hat to a list of people who like that hat. The mapping is needed to quickly find which people can be considered when trying to distribute a particular hat. -
Iterate Over Hats: For each hat type from
1
tom
, we iterate to assign this hat to different people. For each statej
that represents which people already have hats, we can have two possibilities:-
Hat not assigned: If the current hat is not assigned to anyone, then the number of ways to distribute hats remains the same as the previous hat, so
f[i][j] = f[i - 1][j]
. -
Hat assigned: If the hat is assigned to one of the potential people who like it, each assignment will result in a new state from
j
toj ⊕ (1 << k)
where⊕
denotes the XOR operation and(1 << k)
denotes a bit mask with only thek-th
bit set (meaning assigning thei-th
hat to thek-th
person). This operation toggles thek-th
bit ofj
, so we accumulate these new ways intof[i][j]
i.e.,f[i][j] = (f[i][j] + f[i - 1][j ⊕ (1 << k)]) % mod
.
-
-
Modulo Operation: Since the number of ways can be large, we take every sum modulo
10^9 + 7
(stored in the variablemod
) to keep the values within the limits of a 32-bit signed integer. -
Return the Result: After processing all hats, the result will be in
f[m][2^n - 1]
, which represents alln
people having different hats.
This DP approach exploits the concept of state transitions while considering all permutations of hat assignments, respecting the individual preferences and ensuring unique ownership of hat types across all the people. The use of bit masking is a clever trick to represent a set of states succinctly when the universe is small, which is typically harder to do when dealing with large sets or where relationships between elements are complex.
The algorithm complexity primarily depends on the number of hats (which is at most 40) and the number of people (the size of the bitmask, which is 2^n
states). Although it looks like a large number, the small limitation of n
makes this algorithm feasible.
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 with a small example.
Suppose we have n = 2
people and m = 3
different types of hats. The hats preferred by the people are represented by the 2D array hats
such that hats[0] = [1, 2]
and hats[1] = [2, 3]
. Our goal is to find the number of unique ways to distribute hats so that both people wear different hats from their preference list.
Step by Step Implementation:
-
Initialize the DP Table: We create a 2-dimensional DP table
f
with dimensions(3 + 1) x (1 << 2)
(since there are at most 3 types of hats and 2 people). The table is initialized with zeros, except for the base casef[0][0] = 1
. -
Define the Base Case: As defined,
f[0][0] = 1
means one way to assign zero hats to zero people. -
Map Preferences: We create a mapping
g
from each hat to the list of people who like that hat. From the preference list, we getg[1] = [0]
,g[2] = [0, 1]
,g[3] = [1]
. -
Iterate Over Hats:
-
For hat 1: Only person 0 likes this hat. We update
f[1][1]
to 1, which indicates that there's one way to assign hat 1 to person 0. -
For hat 2: Both person 0 and person 1 like this hat. We can assign this hat to person 0 if person 0 hasn't been given a hat already, which means updating
f[2][1]
tof[1][0]
. Similarly, we can assign hat 2 to person 1 if person 1 hasn't been given a hat already, updatingf[2][2]
. -
For hat 3: Only person 1 likes this hat. We update
f[3][2]
tof[2][0]
because we can give hat 3 to person 1 if they have no hat yet.
-
-
Modulo Operation: After each assignment, we perform
f[i][j] = (f[i][j] + f[i - 1][j ⊕ (1 << k)]) % mod
to keep numbers within the 32-bit signed integer limit. -
Return the Result: The result is
f[3][3]
, which represents both people having different hats. In this example,f[3][3]
should give us 2, indicating there are two ways to give hats to the people according to their preferences: Person 0 can wear hat 1 and Person 1 can wear hat 3 or Person 0 can wear hat 2 and Person 1 can wear hat 3.
Thus, the example helps us see how the dynamic programming table builds up solutions with different combinations using bit masks to represent states. The table f
is updated by considering each hat and each combination of which people already have hats (states). This results in a final count of the number of unique ways to distribute hats so that no two people are wearing the same type of hat.
Solution Implementation
1from collections import defaultdict
2
3class Solution:
4
5 def numberWays(self, hats: List[List[int]]) -> int:
6 # Create a mapping of which hat numbers are preferred by each person
7 preference_map = defaultdict(list)
8 for person_id, preferred_hats in enumerate(hats):
9 for hat in preferred_hats:
10 preference_map[hat].append(person_id)
11
12 # Define a modulo value for the answer
13 mod = 10**9 + 7
14
15 # Determine the number of people
16 num_people = len(hats)
17
18 # Find the maximum hat number to define the range of hats
19 max_hat_number = max(max(hat_list) for hat_list in hats)
20
21 # Initialize a DP array where f[i][j] is the number of ways where
22 # i is the current hat number, and j is the bitmask representing
23 # the assignment status of hats to people (1 means assigned)
24 dp = [[0] * (1 << num_people) for _ in range(max_hat_number + 1)]
25
26 # Base case: 0 ways with 0 hats assigned
27 dp[0][0] = 1
28
29 # Iterate through all hat numbers
30 for hat in range(1, max_hat_number + 1):
31 # Iterate through all possible combinations of people
32 for mask in range(1 << num_people):
33 # The number of ways to assign hats without the current hat
34 dp[hat][mask] = dp[hat - 1][mask]
35 # Try to assign the current hat to each person who prefers this hat
36 for person in preference_map[hat]:
37 # Check if the current person has not already been assigned a hat
38 if mask & (1 << person):
39 # Add the number of ways to assign hats with
40 # the current person assigned the current hat
41 dp[hat][mask] = (dp[hat][mask] + dp[hat - 1][mask ^ (1 << person)]) % mod
42
43 # Return the total number of ways to assign all hats to all people
44 return dp[max_hat_number][(1 << num_people) - 1]
45
1class Solution {
2 public int numberWays(List<List<Integer>> hats) {
3 // Number of friends
4 int numFriends = hats.size();
5 // Maximum hat number across all friends
6 int maxHatNumber = 0;
7
8 // Determine the highest numbered hat
9 for (List<Integer> friendHats : hats) {
10 for (int hat : friendHats) {
11 maxHatNumber = Math.max(maxHatNumber, hat);
12 }
13 }
14
15 // Create an array to associate each hat with a list of friends who like it
16 List<Integer>[] hatToFriends = new List[maxHatNumber + 1];
17 Arrays.setAll(hatToFriends, k -> new ArrayList<>());
18
19 // Populate hatToFriends lists with the indices of friends
20 for (int i = 0; i < numFriends; ++i) {
21 for (int hat : hats.get(i)) {
22 hatToFriends[hat].add(i);
23 }
24 }
25
26 // A modulus value for the result
27 final int MOD = (int) 1e9 + 7;
28
29 // Dynamic programming table where 'f[i][j]' represents the number of ways to assign
30 // hats to the first 'i' hats such that 'j' encodes which friends have received hats
31 int[][] dpTable = new int[maxHatNumber + 1][1 << numFriends];
32
33 // Base case: there's 1 way to assign 0 hats (none to anyone)
34 dpTable[0][0] = 1;
35
36 // Build the table from the base case
37 for (int i = 1; i <= maxHatNumber; ++i) {
38 for (int j = 0; j < 1 << numFriends; ++j) {
39 // Start with the number of ways without the current hat
40 dpTable[i][j] = dpTable[i - 1][j];
41
42 // Iterate through all friends who like the current hat
43 for (int friendIndex : hatToFriends[i]) {
44 // Check if the friend hasn't been given a hat yet in combination 'j'
45 if ((j >> friendIndex & 1) == 1) {
46 // Add ways to assign hats from previous combination with one less hat,
47 // ensuring that friend 'friendIndex' now has a hat
48 dpTable[i][j] = (dpTable[i][j] + dpTable[i - 1][j ^ (1 << friendIndex)]) % MOD;
49 }
50 }
51 }
52 }
53
54 // Return the number of ways for the last hat and all friends (the full set bit mask)
55 return dpTable[maxHatNumber][(1 << numFriends) - 1];
56 }
57}
58
1class Solution {
2public:
3 int numberWays(vector<vector<int>>& hats) {
4 int numPeople = hats.size(); // Number of people
5 int maxHatID = 0; // Maximum hat ID
6 // Find the maximum hat ID to know the range of hat IDs available
7 for (auto& personHats : hats) {
8 maxHatID = max(maxHatID, *max_element(personHats.begin(), personHats.end()));
9 }
10 // Create a graph where each hat ID points to a list of people who like that hat
11 vector<vector<int>> hatToPeople(maxHatID + 1);
12 for (int i = 0; i < numPeople; ++i) {
13 for (int hat : hats[i]) {
14 hatToPeople[hat].push_back(i);
15 }
16 }
17
18 const int MOD = 1e9 + 7; // Modulo value for avoiding integer overflow
19 // f[i][j] will be the number of ways to assign hats considering first i hats
20 // where j is a bitmask representing which people have already been assigned a hat
21 int dp[maxHatID + 1][1 << numPeople];
22 memset(dp, 0, sizeof(dp));
23 dp[0][0] = 1; // Base case: no hats assigned to anyone
24
25 // Iterate over all hats
26 for (int i = 1; i <= maxHatID; ++i) {
27 // Iterate over all possible assignments of hats to people
28 for (int mask = 0; mask < (1 << numPeople); ++mask) {
29 // The number of ways without assigning the current hat remains the same
30 dp[i][mask] = dp[i - 1][mask];
31 // Iterate over all the people who like the current hat
32 for (int person : hatToPeople[i]) {
33 // If this person has not yet been assigned a hat
34 if (mask & (1 << person)) {
35 // Add the number of ways by assigning the current hat to this person and update it modulo MOD
36 dp[i][mask] = (dp[i][mask] + dp[i - 1][mask ^ (1 << person)]) % MOD;
37 }
38 }
39 }
40 }
41 // Return the number of ways to assign all hats to all people
42 return dp[maxHatID][(1 << numPeople) - 1];
43 }
44};
45
1function numberWays(hats: number[][]): number {
2 // Number of people
3 const numPeople = hats.length;
4 // Maximum hat number
5 const maxHatNumber = Math.max(...hats.flat());
6
7 // Graph representing which people can wear which hats
8 const hatToPeopleGraph: number[][] = Array.from({ length: maxHatNumber + 1 }, () => []);
9
10 // Populate the graph with the information about which people can wear which hats
11 for (let i = 0; i < numPeople; ++i) {
12 for (const hat of hats[i]) {
13 hatToPeopleGraph[hat].push(i);
14 }
15 }
16
17 // DP array to store ways to distribute hats
18 // dp[hatNumber][mask] will represent the number of ways to distribute
19 // the first hatNumber hats among people represented by mask
20 const dp: number[][] = Array.from({ length: maxHatNumber + 1 }, () =>
21 Array.from({ length: 1 << numPeople }, () => 0),
22 );
23
24 // Base case: for 0 hats we have one way to assign - none to anyone
25 dp[0][0] = 1;
26
27 // Modulus for the result to prevent integer overflow
28 const mod = 1e9 + 7;
29
30 // Iterate over all hats
31 for (let hatNumber = 1; hatNumber <= maxHatNumber; ++hatNumber) {
32 // Iterate over all combinations of people
33 for (let mask = 0; mask < 1 << numPeople; ++mask) {
34 // The number of ways to distribute hats without considering the current hat
35 dp[hatNumber][mask] = dp[hatNumber - 1][mask];
36 // Iterate over all people that can wear the current hat
37 for (const person of hatToPeopleGraph[hatNumber]) {
38 // Check if the current person is included in the mask
39 if (((mask >> person) & 1) === 1) {
40 // Add ways from the previous hat state excluding the current person
41 dp[hatNumber][mask] = (dp[hatNumber][mask] + dp[hatNumber - 1][mask ^ (1 << person)]) % mod;
42 }
43 }
44 }
45 }
46
47 // Return the number of ways to assign all hats to all people
48 // All people are represented by the mask (1 << numPeople) - 1, which has all bits set
49 return dp[maxHatNumber][(1 << numPeople) - 1];
50}
51
Time and Space Complexity
The provided code defines a function numberWays
which calculates the number of ways people can wear hats given certain constraints. The analysis of its time and space complexity is as follows:
Time Complexity
The time complexity of the function is O(m * 2^n * n)
. Here's a breakdown of why that's the case:
m
represents the maximum number of different hats, which can go up to40
as per the problem constraints.n
is the number of people, limited to10
in this scenario.2^n
signifies the number of different states or combinations for the assignment of hats to then
people. Since each person can either wear a hat or not, there are2^n
possible states.
For each of the m
hats, the algorithm iterates over all 2^n
combinations, and for each combination, it can potentially iterate over all n
people to update the state (f[i][j]
). As a result, the time complexity amounts to the multiplicative product of these terms.
Space Complexity
The space complexity of the function is O(m * 2^n)
. This is due to the following reasons:
- An
m + 1
by2^n
sized 2D listf
is created to store the states of hat assignments, where each sub-listf[i]
has a length of2^n
to represent all combinations ofn
people, and there arem + 1
such sub-lists (ranging from0
tom
). - The additional data structures use negligible space compared to the size of
f
, so their contribution to space complexity is not considered dominant.
In summary, the algorithm requires a significant amount of space proportional to the number of hat combinations multiplied by the number of different hats.
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
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
Bitmask and Dynamic Programming Bit manipulation is a crucial aspect of computer programming and one of the most powerful tools for bit manipulation is bitmasks Let's first understand what a bit is A bit is a binary digit It's the smallest piece of data in a computer and can be
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