3447. Assign Elements to Groups with Constraints
Problem Description
You have two integer arrays: groups
and elements
.
Each groups[i]
represents the size of the i-th group. Your task is to assign exactly one element from the elements
array to each group following these rules:
-
Assignment Rule: An element at index
j
can be assigned to groupi
only ifgroups[i]
is divisible byelements[j]
. In other words,groups[i] % elements[j] == 0
. -
Priority Rule: If multiple elements can be assigned to a group, choose the element with the smallest index
j
. -
No Match Rule: If no element from the
elements
array can divide a group's size, assign-1
to that group. -
Reusability: The same element can be assigned to multiple groups.
Your goal is to return an array assigned
where assigned[i]
contains the index of the element chosen for group i
, or -1
if no suitable element exists.
For example, if groups = [6, 4]
and elements = [3, 2, 5]
:
- For group 0 with size 6: Both element at index 0 (value 3) and element at index 1 (value 2) can divide 6. We choose index 0 (smallest index).
- For group 1 with size 4: Only element at index 1 (value 2) can divide 4.
- Result:
[0, 1]
Intuition
The naive approach would be to check every group against every element to find valid assignments. However, we can optimize this by thinking about the problem from a different angle.
Instead of checking each group individually, we can precompute which element should be assigned to every possible group size up to the maximum group size. This way, when we need to find the assignment for a specific group, we can simply look it up.
The key insight is that if an element elements[j]
has value x
, it can divide all multiples of x
. So for element x
, the divisible group sizes would be x, 2x, 3x, 4x, ...
up to the maximum group size.
By iterating through elements in order (from smallest index to largest), we can ensure that when multiple elements can divide a group size, the one with the smallest index gets recorded first. We only update the assignment for a group size if it hasn't been assigned yet (still -1
).
For example, if elements = [2, 3]
and maximum group size is 6:
- Element at index 0 (value 2) can handle group sizes: 2, 4, 6
- Element at index 1 (value 3) can handle group sizes: 3, 6
Since we process index 0 first, group size 6 gets assigned to index 0 even though index 1 could also handle it.
This precomputation approach reduces redundant calculations, especially when there are multiple groups with the same size. We compute the assignment for each possible size once, then simply look up the result for each group.
Solution Approach
We implement the precomputation strategy using the following steps:
Step 1: Find Maximum Group Size
First, we find the maximum value in the groups
array, denoted as mx
. This helps us determine the range of group sizes we need to consider.
mx = max(groups)
Step 2: Initialize Assignment Array
We create an array d
of size mx + 1
to store the element index assigned to each possible group size. Initially, all values are set to -1
, indicating no assignment yet.
d = [-1] * (mx + 1)
Step 3: Process Elements and Assign to Group Sizes
We iterate through the elements
array with both index j
and value x
:
for j, x in enumerate(elements):
For each element, we first check two conditions:
- If
x > mx
: The element is larger than any group size, so it can't divide any group - If
d[x] != -1
: This element value has already been assigned by an earlier element with a smaller index
If either condition is true, we skip this element:
if x > mx or d[x] != -1: continue
Otherwise, we iterate through all multiples of x
up to mx
and assign index j
to those positions in array d
that haven't been assigned yet:
for y in range(x, mx + 1, x):
if d[y] == -1:
d[y] = j
The range (x, mx + 1, x)
generates the sequence x, 2x, 3x, ...
up to mx
.
Step 4: Build Final Result
Finally, we construct the answer by looking up each group size in our precomputed array d
:
return [d[x] for x in groups]
For each group size in groups
, we retrieve its assigned element index from d[x]
. If no element was assigned to that size, d[x]
remains -1
.
Time Complexity: O(n + m × (mx/min_element)) where n is the length of groups
, m is the length of elements
, and mx is the maximum group size.
Space Complexity: O(mx) for the assignment array d
.
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 a concrete example to illustrate the solution approach.
Given:
groups = [6, 4, 8, 3]
elements = [3, 2, 5]
Step 1: Find Maximum Group Size
mx = max([6, 4, 8, 3]) = 8
Step 2: Initialize Assignment Array
Create array d
of size 9 (indices 0-8), all initialized to -1:
d = [-1, -1, -1, -1, -1, -1, -1, -1, -1] 0 1 2 3 4 5 6 7 8
Step 3: Process Each Element
Processing element at index 0 (value 3):
- Check: 3 ≤ 8 ✓ and d[3] == -1 ✓
- Mark multiples of 3: positions 3, 6
d = [-1, -1, -1, 0, -1, -1, 0, -1, -1] 0 1 2 3 4 5 6 7 8
Processing element at index 1 (value 2):
- Check: 2 ≤ 8 ✓ and d[2] == -1 ✓
- Mark multiples of 2: positions 2, 4, 6, 8
- Note: position 6 already has 0, so we skip it
d = [-1, -1, 1, 0, 1, -1, 0, -1, 1] 0 1 2 3 4 5 6 7 8
Processing element at index 2 (value 5):
- Check: 5 ≤ 8 ✓ and d[5] == -1 ✓
- Mark multiples of 5: position 5
d = [-1, -1, 1, 0, 1, 2, 0, -1, 1] 0 1 2 3 4 5 6 7 8
Step 4: Build Final Result
Look up each group size in array d
:
- Group size 6: d[6] = 0 (element at index 0 divides 6)
- Group size 4: d[4] = 1 (element at index 1 divides 4)
- Group size 8: d[8] = 1 (element at index 1 divides 8)
- Group size 3: d[3] = 0 (element at index 0 divides 3)
Result: [0, 1, 1, 0]
This demonstrates how the precomputation ensures that each group size gets assigned the element with the smallest index that can divide it, and how the same element index can be reused for multiple groups.
Solution Implementation
1class Solution:
2 def assignElements(self, groups: List[int], elements: List[int]) -> List[int]:
3 # Find the maximum value in groups to determine the range we need to process
4 max_group_value = max(groups)
5
6 # Initialize assignment array with -1 (indicating no assignment yet)
7 # Index represents a potential group value, value represents the assigned element index
8 assignments = [-1] * (max_group_value + 1)
9
10 # Process each element with its index
11 for element_index, element_value in enumerate(elements):
12 # Skip if element value exceeds max group value or if already processed
13 if element_value > max_group_value or assignments[element_value] != -1:
14 continue
15
16 # Assign current element index to all multiples of element_value
17 # This ensures each group value gets the smallest valid element index
18 for multiple in range(element_value, max_group_value + 1, element_value):
19 # Only assign if this position hasn't been assigned yet
20 if assignments[multiple] == -1:
21 assignments[multiple] = element_index
22
23 # Map each group value to its assigned element index
24 return [assignments[group_value] for group_value in groups]
25
1class Solution {
2 public int[] assignElements(int[] groups, int[] elements) {
3 // Find the maximum value in groups array
4 int maxGroup = Arrays.stream(groups).max().getAsInt();
5
6 // Create an array to store the assigned element index for each possible group value
7 // Index represents the group value, value represents the assigned element index
8 int[] assignments = new int[maxGroup + 1];
9 Arrays.fill(assignments, -1); // Initialize all assignments to -1 (unassigned)
10
11 // Process each element to assign it to compatible groups
12 for (int elementIndex = 0; elementIndex < elements.length; ++elementIndex) {
13 int currentElement = elements[elementIndex];
14
15 // Skip if element is larger than max group or already processed
16 if (currentElement > maxGroup || assignments[currentElement] != -1) {
17 continue;
18 }
19
20 // Assign current element to all groups that are multiples of it
21 for (int multiple = currentElement; multiple <= maxGroup; multiple += currentElement) {
22 // Only assign if the group hasn't been assigned yet
23 if (assignments[multiple] == -1) {
24 assignments[multiple] = elementIndex;
25 }
26 }
27 }
28
29 // Build the result array by looking up assignments for each group
30 int groupCount = groups.length;
31 int[] result = new int[groupCount];
32 for (int i = 0; i < groupCount; ++i) {
33 result[i] = assignments[groups[i]];
34 }
35
36 return result;
37 }
38}
39
1class Solution {
2public:
3 vector<int> assignElements(vector<int>& groups, vector<int>& elements) {
4 // Find the maximum value in groups array
5 int maxGroupValue = ranges::max(groups);
6
7 // Initialize assignment array with -1 (indicating no assignment)
8 // Index represents group value, value represents assigned element index
9 vector<int> assignment(maxGroupValue + 1, -1);
10
11 // Iterate through each element
12 for (int elementIndex = 0; elementIndex < elements.size(); ++elementIndex) {
13 int elementValue = elements[elementIndex];
14
15 // Skip if element value exceeds max group value or already processed
16 if (elementValue > maxGroupValue || assignment[elementValue] != -1) {
17 continue;
18 }
19
20 // Assign current element to all groups that are multiples of elementValue
21 for (int multiple = elementValue; multiple <= maxGroupValue; multiple += elementValue) {
22 // Only assign if the group hasn't been assigned yet
23 if (assignment[multiple] == -1) {
24 assignment[multiple] = elementIndex;
25 }
26 }
27 }
28
29 // Build result array by looking up assignments for each group
30 vector<int> result(groups.size());
31 for (int i = 0; i < groups.size(); ++i) {
32 result[i] = assignment[groups[i]];
33 }
34
35 return result;
36 }
37};
38
1/**
2 * Assigns elements to groups based on divisibility relationships.
3 * For each group value, finds the smallest index in elements array where
4 * the element divides the group value.
5 *
6 * @param groups - Array of group values to find divisors for
7 * @param elements - Array of potential divisor elements
8 * @returns Array where each position contains the index of the first element
9 * that divides the corresponding group value, or -1 if none exists
10 */
11function assignElements(groups: number[], elements: number[]): number[] {
12 // Find the maximum value in groups to limit our search range
13 const maxGroupValue: number = Math.max(...groups);
14
15 // Initialize array to store the smallest index of divisor for each value
16 // Index represents the value, content represents the smallest element index that divides it
17 const divisorIndices: number[] = Array(maxGroupValue + 1).fill(-1);
18
19 // Iterate through each element with its index
20 for (let elementIndex = 0; elementIndex < elements.length; ++elementIndex) {
21 const currentElement: number = elements[elementIndex];
22
23 // Skip if element is larger than max group value or if we already found a divisor
24 if (currentElement > maxGroupValue || divisorIndices[currentElement] !== -1) {
25 continue;
26 }
27
28 // Mark all multiples of currentElement up to maxGroupValue
29 // This efficiently finds all values that currentElement can divide
30 for (let multiple = currentElement; multiple <= maxGroupValue; multiple += currentElement) {
31 // Only update if no divisor has been assigned yet (keeps the smallest index)
32 if (divisorIndices[multiple] === -1) {
33 divisorIndices[multiple] = elementIndex;
34 }
35 }
36 }
37
38 // Map each group value to its corresponding divisor index
39 return groups.map((groupValue: number) => divisorIndices[groupValue]);
40}
41
Time and Space Complexity
Time Complexity: O(M × log M + n)
The time complexity consists of two main parts:
- Finding the maximum value in
groups
:O(n)
wheren
is the length ofgroups
- Iterating through
elements
and for each elementx
, iterating through its multiples up toM
: For each elementx
inelements
, we iterate through valuesx, 2x, 3x, ...
up toM
. The number of multiples ofx
up toM
isM/x
. Summing over all possible values from 1 toM
, we getM/1 + M/2 + M/3 + ... + M/M = M × (1 + 1/2 + 1/3 + ... + 1/M) = M × H(M)
, whereH(M)
is the harmonic series which is approximatelyO(log M)
. Since we process at mostm
elements (length ofelements
), and in the worst case we process divisors up toM
, the total isO(M × log M)
- Building the result array:
O(n)
Therefore, the total time complexity is O(M × log M + n)
.
Space Complexity: O(M)
The space complexity is determined by:
- The array
d
of sizeM + 1
:O(M)
- The output array of size
n
:O(n)
(not counted as auxiliary space since it's the required output)
Therefore, the auxiliary space complexity is O(M)
.
Where:
n
is the length of thegroups
arraym
is the length of theelements
arrayM
is the maximum value in thegroups
array
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Overwriting Previously Assigned Values
The Problem: A common mistake is to unconditionally assign element indices to all multiples without checking if a better (smaller index) assignment already exists. For example:
# WRONG: This overwrites existing assignments
for multiple in range(element_value, max_group_value + 1, element_value):
assignments[multiple] = element_index # Overwrites without checking!
Why It Happens: When processing elements, if we don't check whether a position has already been assigned, we might replace a smaller index with a larger one, violating the priority rule.
Example Scenario:
groups = [6]
,elements = [2, 3]
- Element at index 0 (value 2) assigns index 0 to position 6
- Element at index 1 (value 3) would overwrite position 6 with index 1
- Wrong result:
[1]
instead of correct[0]
Solution: Always check if a position is unassigned before updating it:
if assignments[multiple] == -1: assignments[multiple] = element_index
Pitfall 2: Not Handling the Element Value Itself
The Problem: Forgetting to check and potentially assign the element value itself (not just its multiples) can lead to incorrect results when a group size exactly matches an element value.
Why It Happens:
The condition if element_value > max_group_value or assignments[element_value] != -1
might skip processing when assignments[element_value]
is already set, even though we still need to process the element's other multiples.
Example Scenario:
groups = [4, 8]
,elements = [4, 2]
- Element at index 0 (value 4) should assign index 0 to positions 4 and 8
- If we skip entirely when
assignments[4] != -1
, position 8 might not get assigned
Solution:
The current implementation handles this correctly by checking assignments[element_value] != -1
as an optimization - if the element value itself is already assigned by a smaller index, then ALL its multiples must also be assigned by that same smaller index (since any divisor of x is also a divisor of multiples of x).
Pitfall 3: Incorrect Range Boundaries
The Problem: Using incorrect loop boundaries when iterating through multiples:
# WRONG: Off-by-one error
for multiple in range(element_value, max_group_value, element_value): # Missing +1
Why It Happens:
Python's range()
function excludes the end value, so we need max_group_value + 1
to include max_group_value
itself.
Example Scenario:
groups = [10]
,elements = [5]
- Without
+1
, the range would berange(5, 10, 5)
producing only[5]
- Group size 10 wouldn't get assigned even though 5 divides 10
Solution:
Always use range(element_value, max_group_value + 1, element_value)
to ensure the maximum group value is included in the iteration.
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
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
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 assets algo monster recursion jpg You first call Ben and ask
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!