2860. Happy Students
Problem Description
You have a class with n
students, represented by a 0-indexed array nums
of length n
. The teacher wants to select a group of students such that every student in the class remains happy.
A student at position i
will be happy if one of these two conditions is met:
- The student is selected AND the total number of selected students is strictly greater than
nums[i]
- The student is not selected AND the total number of selected students is strictly less than
nums[i]
Your task is to find the total number of different ways to select a group of students (which could be 0 students, all students, or any number in between) such that all students remain happy.
For example, if nums = [1, 1]
:
- If we select 0 students: Student 0 is not selected and 0 < 1 (happy), Student 1 is not selected and 0 < 1 (happy). This works.
- If we select 1 student: We need 1 >
nums[i]
for selected students, but 1 is not > 1, so no valid selection. - If we select 2 students: Student 0 is selected and 2 > 1 (happy), Student 1 is selected and 2 > 1 (happy). This works.
So there are 2 ways to make everyone happy.
The solution approach sorts the array first, then checks each possible group size k
from 0 to n
. For a group of size k
, the selected students must be the first k
students in the sorted array (those with smallest nums
values). The code validates each possible group size by checking:
- If selecting
k
students, the largest value among selected students (nums[k-1]
) must be less thank
- The smallest value among non-selected students (
nums[k]
) must be greater thank
Intuition
Let's think about what happens when we select exactly k
students. For each student i
:
- If
nums[i] < k
, this student MUST be selected (otherwise they'd be unhappy since they're not selected butk
is not strictly less thannums[i]
) - If
nums[i] >= k
, this student MUST NOT be selected (otherwise they'd be unhappy since they're selected butk
is not strictly greater thannums[i]
)
This reveals a key insight: once we fix the number of selected students k
, there's at most ONE valid way to select them - we must select all students where nums[i] < k
and reject all students where nums[i] >= k
.
But which students should these be? If we need to select exactly k
students where nums[i] < k
for each selected student, we should greedily choose the k
students with the smallest nums
values. This is why sorting helps.
After sorting, if we select the first k
students (indices 0 to k-1), we need:
- For all selected students (indices 0 to k-1):
nums[i] < k
- For all non-selected students (indices k to n-1):
nums[i] >= k
This simplifies our validation:
- The largest value among selected students is
nums[k-1]
, so we neednums[k-1] < k
- The smallest value among non-selected students is
nums[k]
, so we neednums[k] > k
(strictly greater, not equal)
By iterating through all possible values of k
from 0 to n
and checking these conditions, we can count all valid groupings. The sorting ensures that if the boundary conditions are satisfied, all other students will automatically be happy.
Learn more about Sorting patterns.
Solution Approach
The implementation follows a sorting and enumeration strategy:
-
Sort the array: First, we sort
nums
in ascending order. This ensures that if we selectk
students, we're selecting the ones with the smallestnums
values (indices 0 to k-1). -
Enumerate all possible group sizes: We iterate through each possible number of selected students
i
from 0 ton
(inclusive). -
Validate each group size: For each potential group size
i
, we check two boundary conditions:-
Check the last selected student (if
i > 0
): The student at indexi-1
is the last selected student. For them to be happy, we needi > nums[i-1]
. Ifnums[i-1] >= i
, this grouping is invalid, so we skip it withcontinue
. -
Check the first non-selected student (if
i < n
): The student at indexi
is the first non-selected student. For them to be happy, we needi < nums[i]
. Ifnums[i] <= i
, this grouping is invalid, so we skip it withcontinue
.
-
-
Count valid groupings: If both conditions pass (or don't apply for edge cases like selecting 0 or all students), we have found a valid grouping and increment our answer by 1.
The algorithm has a time complexity of O(n log n)
due to sorting, and space complexity of O(1)
if we don't count the sorting space.
Example walkthrough with nums = [1, 1]
:
- After sorting:
[1, 1]
i = 0
: No students selected, no checks needed β valid,ans = 1
i = 1
: Checknums[0] >= 1
? Yes (1 >= 1) β invalid, skipi = 2
: Checknums[1] >= 2
? No (1 < 2) β valid,ans = 2
- Return
ans = 2
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 work through a detailed example with nums = [2, 0, 3, 1]
.
Step 1: Sort the array
After sorting: nums = [0, 1, 2, 3]
Step 2: Try each possible group size k
k = 0 (select 0 students):
- No students selected
- No boundary checks needed (no last selected, no first unselected)
- Valid! Count = 1
k = 1 (select 1 student):
- We would select student at index 0 (value = 0)
- Check last selected (index 0): Is 1 > nums[0]? Is 1 > 0? Yes β
- Check first non-selected (index 1): Is 1 < nums[1]? Is 1 < 1? No β
- Invalid (student at index 1 would be unhappy)
k = 2 (select 2 students):
- We would select students at indices 0, 1 (values = 0, 1)
- Check last selected (index 1): Is 2 > nums[1]? Is 2 > 1? Yes β
- Check first non-selected (index 2): Is 2 < nums[2]? Is 2 < 2? No β
- Invalid (student at index 2 would be unhappy)
k = 3 (select 3 students):
- We would select students at indices 0, 1, 2 (values = 0, 1, 2)
- Check last selected (index 2): Is 3 > nums[2]? Is 3 > 2? Yes β
- Check first non-selected (index 3): Is 3 < nums[3]? Is 3 < 3? No β
- Invalid (student at index 3 would be unhappy)
k = 4 (select all 4 students):
- All students selected
- Check last selected (index 3): Is 4 > nums[3]? Is 4 > 3? Yes β
- No first non-selected to check
- Valid! Count = 2
Final Answer: 2 valid ways
The two valid configurations are:
- Select nobody (all students happy being unselected)
- Select everybody (all students happy being selected)
Solution Implementation
1class Solution:
2 def countWays(self, nums: List[int]) -> int:
3 """
4 Count the number of valid ways to select students.
5
6 A selection of i students is valid if:
7 - All selected students have happiness threshold < i
8 - All unselected students have happiness threshold >= i
9
10 Args:
11 nums: List of integers representing happiness thresholds
12
13 Returns:
14 Number of valid ways to select students
15 """
16 # Sort the array to check conditions efficiently
17 nums.sort()
18 n = len(nums)
19 valid_ways_count = 0
20
21 # Try selecting i students (from 0 to n students)
22 for i in range(n + 1):
23 # Check if selecting i students is valid
24
25 # If we're selecting i students (i > 0), the last selected student
26 # at index i-1 must have happiness < i (invalid if >= i)
27 if i > 0 and nums[i - 1] >= i:
28 continue
29
30 # If we're not selecting all students (i < n), the first unselected
31 # student at index i must have happiness >= i (invalid if < i)
32 if i < n and nums[i] <= i:
33 continue
34
35 # This is a valid way to select students
36 valid_ways_count += 1
37
38 return valid_ways_count
39
1class Solution {
2 /**
3 * Counts the number of valid ways to select students based on happiness criteria.
4 * A selection is valid when:
5 * - The number of selected students is greater than the happiness value of all selected students
6 * - The number of selected students is less than the happiness value of all non-selected students
7 *
8 * @param nums List of integers representing happiness values of students
9 * @return The count of valid ways to select students
10 */
11 public int countWays(List<Integer> nums) {
12 // Sort the happiness values in ascending order
13 Collections.sort(nums);
14
15 // Get the total number of students
16 int n = nums.size();
17
18 // Initialize counter for valid selections
19 int validSelectionCount = 0;
20
21 // Iterate through all possible selection sizes (0 to n students)
22 for (int selectedCount = 0; selectedCount <= n; selectedCount++) {
23 // Check if current selection size is valid:
24 // 1. If selecting 'selectedCount' students (0 to selectedCount-1 after sorting)
25 // 2. The last selected student (at index selectedCount-1) should have happiness < selectedCount
26 // 3. The first non-selected student (at index selectedCount) should have happiness > selectedCount
27
28 boolean isLastSelectedValid = (selectedCount == 0 || nums.get(selectedCount - 1) < selectedCount);
29 boolean isFirstNonSelectedValid = (selectedCount == n || nums.get(selectedCount) > selectedCount);
30
31 if (isLastSelectedValid && isFirstNonSelectedValid) {
32 validSelectionCount++;
33 }
34 }
35
36 return validSelectionCount;
37 }
38}
39
1class Solution {
2public:
3 int countWays(vector<int>& nums) {
4 // Sort the array in ascending order
5 sort(nums.begin(), nums.end());
6
7 int result = 0;
8 int n = nums.size();
9
10 // Iterate through all possible split points (0 to n)
11 // i represents the number of selected students
12 for (int i = 0; i <= n; ++i) {
13 // Check if current split is invalid:
14 // 1. If we select i students (i > 0), the last selected student (at index i-1)
15 // should be happy, meaning their value should be less than i
16 // 2. If there are unselected students (i < n), the first unselected student (at index i)
17 // should be happy, meaning their value should be greater than i
18
19 bool isInvalid = false;
20
21 // Check if the last selected student would be unhappy
22 if (i > 0 && nums[i - 1] >= i) {
23 isInvalid = true;
24 }
25
26 // Check if the first unselected student would be unhappy
27 if (i < n && nums[i] <= i) {
28 isInvalid = true;
29 }
30
31 // If the split is invalid, skip to next iteration
32 if (isInvalid) {
33 continue;
34 }
35
36 // Valid split found, increment counter
37 ++result;
38 }
39
40 return result;
41 }
42};
43
1/**
2 * Counts the number of ways to partition the array into two groups
3 * where all elements in the first group are less than the group size,
4 * and all elements in the second group are greater than or equal to the group size.
5 *
6 * @param nums - The input array of numbers
7 * @returns The count of valid partition points
8 */
9function countWays(nums: number[]): number {
10 // Sort the array in ascending order
11 nums.sort((a: number, b: number) => a - b);
12
13 // Initialize the answer counter
14 let answer: number = 0;
15 const arrayLength: number = nums.length;
16
17 // Iterate through all possible partition points (0 to n inclusive)
18 for (let partitionIndex: number = 0; partitionIndex <= arrayLength; ++partitionIndex) {
19 // Check if the partition is invalid:
20 // 1. If we have elements before the partition point and the last element >= partition size
21 // 2. If we have elements after the partition point and the first element <= partition size
22 if ((partitionIndex > 0 && nums[partitionIndex - 1] >= partitionIndex) ||
23 (partitionIndex < arrayLength && nums[partitionIndex] <= partitionIndex)) {
24 continue;
25 }
26
27 // Valid partition found, increment the counter
28 ++answer;
29 }
30
31 return answer;
32}
33
Time and Space Complexity
The time complexity is O(n Γ log n)
, where n
is the length of the array. This is dominated by the sorting operation nums.sort()
which uses Timsort algorithm with O(n Γ log n)
complexity. After sorting, the code performs a single loop through n + 1
iterations with constant time operations in each iteration, contributing O(n)
to the overall complexity. Since O(n Γ log n) + O(n) = O(n Γ log n)
, the overall time complexity is O(n Γ log n)
.
The space complexity is O(log n)
. The sorting operation nums.sort()
in Python uses Timsort, which requires O(log n)
space for the recursion stack in the worst case. The rest of the code uses only a constant amount of extra space for variables like n
, ans
, and i
, which contributes O(1)
. Therefore, the overall space complexity is O(log n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Forgetting to Sort the Array
One of the most critical mistakes is attempting to solve this problem without sorting the array first. The algorithm relies on the property that if we select k
students, we should select the k
students with the smallest values to maximize our chances of satisfying the happiness conditions.
Why this fails: Without sorting, you might select students with larger nums
values while leaving students with smaller values unselected, making it impossible to satisfy the happiness conditions for certain valid group sizes.
Solution: Always sort the array at the beginning:
nums.sort() # Critical first step
2. Incorrect Boundary Condition Checks
A common error is using the wrong comparison operators when checking the boundary conditions. Specifically:
- Using
>
instead of>=
when checking if a selection is invalid - Using
<
instead of<=
when checking the non-selected students
Incorrect implementation:
# Wrong comparison operators if i > 0 and nums[i - 1] > i: # Should be >= continue if i < n and nums[i] < i: # Should be <= continue
Correct implementation:
# Correct comparison operators if i > 0 and nums[i - 1] >= i: # Selected student must have value < i continue if i < n and nums[i] <= i: # Non-selected student must have value > i continue
3. Off-by-One Error in Loop Range
Forgetting to include n
in the range, which represents selecting all students:
Incorrect:
for i in range(n): # Missing the case where all students are selected
Correct:
for i in range(n + 1): # Include 0 to n students (inclusive)
4. Misunderstanding the Problem Requirements
A subtle but critical misunderstanding is thinking that we can select any arbitrary set of students. The key insight is that after sorting, if we select k
students, we must select the first k
students (those with the smallest values) to have any chance of satisfying all happiness conditions.
Why this matters: If you try to select students with larger values while leaving students with smaller values unselected, the unselected students with small values won't be happy (since the number of selected students would be greater than or equal to their threshold).
Solution: Always select the first k
students after sorting, which the code does implicitly by checking only the boundary students at indices i-1
and i
.
What's the output of running the following function using input 56
?
1KEYBOARD = {
2 '2': 'abc',
3 '3': 'def',
4 '4': 'ghi',
5 '5': 'jkl',
6 '6': 'mno',
7 '7': 'pqrs',
8 '8': 'tuv',
9 '9': 'wxyz',
10}
11
12def letter_combinations_of_phone_number(digits):
13 def dfs(path, res):
14 if len(path) == len(digits):
15 res.append(''.join(path))
16 return
17
18 next_number = digits[len(path)]
19 for letter in KEYBOARD[next_number]:
20 path.append(letter)
21 dfs(path, res)
22 path.pop()
23
24 res = []
25 dfs([], res)
26 return res
27
1private static final Map<Character, char[]> KEYBOARD = Map.of(
2 '2', "abc".toCharArray(),
3 '3', "def".toCharArray(),
4 '4', "ghi".toCharArray(),
5 '5', "jkl".toCharArray(),
6 '6', "mno".toCharArray(),
7 '7', "pqrs".toCharArray(),
8 '8', "tuv".toCharArray(),
9 '9', "wxyz".toCharArray()
10);
11
12public static List<String> letterCombinationsOfPhoneNumber(String digits) {
13 List<String> res = new ArrayList<>();
14 dfs(new StringBuilder(), res, digits.toCharArray());
15 return res;
16}
17
18private static void dfs(StringBuilder path, List<String> res, char[] digits) {
19 if (path.length() == digits.length) {
20 res.add(path.toString());
21 return;
22 }
23 char next_digit = digits[path.length()];
24 for (char letter : KEYBOARD.get(next_digit)) {
25 path.append(letter);
26 dfs(path, res, digits);
27 path.deleteCharAt(path.length() - 1);
28 }
29}
30
1const KEYBOARD = {
2 '2': 'abc',
3 '3': 'def',
4 '4': 'ghi',
5 '5': 'jkl',
6 '6': 'mno',
7 '7': 'pqrs',
8 '8': 'tuv',
9 '9': 'wxyz',
10}
11
12function letter_combinations_of_phone_number(digits) {
13 let res = [];
14 dfs(digits, [], res);
15 return res;
16}
17
18function dfs(digits, path, res) {
19 if (path.length === digits.length) {
20 res.push(path.join(''));
21 return;
22 }
23 let next_number = digits.charAt(path.length);
24 for (let letter of KEYBOARD[next_number]) {
25 path.push(letter);
26 dfs(digits, path, res);
27 path.pop();
28 }
29}
30
Recommended Readings
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
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
Want a Structured Path to Master System Design Too? Donβt Miss This!