Facebook Pixel

2860. Happy Students

MediumArrayEnumerationSorting
Leetcode Link

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 than k
  • The smallest value among non-selected students (nums[k]) must be greater than k
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 but k is not strictly less than nums[i])
  • If nums[i] >= k, this student MUST NOT be selected (otherwise they'd be unhappy since they're selected but k is not strictly greater than nums[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 need nums[k-1] < k
  • The smallest value among non-selected students is nums[k], so we need nums[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:

  1. Sort the array: First, we sort nums in ascending order. This ensures that if we select k students, we're selecting the ones with the smallest nums values (indices 0 to k-1).

  2. Enumerate all possible group sizes: We iterate through each possible number of selected students i from 0 to n (inclusive).

  3. 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 index i-1 is the last selected student. For them to be happy, we need i > nums[i-1]. If nums[i-1] >= i, this grouping is invalid, so we skip it with continue.

    • Check the first non-selected student (if i < n): The student at index i is the first non-selected student. For them to be happy, we need i < nums[i]. If nums[i] <= i, this grouping is invalid, so we skip it with continue.

  4. 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: Check nums[0] >= 1? Yes (1 >= 1) β†’ invalid, skip
  • i = 2: Check nums[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 Evaluator

Example 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:

  1. Select nobody (all students happy being unselected)
  2. 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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More