Programming Exercises Documentation Help

June 2024, Week 4: June 17th - June 23rd

June 17 -> 633. Sum of Square Numbers

Given a non-negative integer c, decide whether there are two integers a and b such that a^2 + b^2 = c.

Example 1:

  • Input: c = 5

  • Output: true

  • Explanation: 1 * 1 + 2 * 2 = 5

Example 2:

  • Input: c = 3

  • Output: false

Constraints:

  • 0 <= c <= 2^31 - 1

Approach 1: Two-Pointer Technique

def judgeSquareSum1(c: int) -> bool: """ Determines if a given non-negative integer 'c' can be expressed as the sum of squares of two integers 'a' and 'b'. The function uses a two-pointer technique starting from 0 and the square root of 'c'. It iteratively checks the sum of squares of the two pointers, 'start_index' and 'end_index'. If the sum is less than 'c', it increases 'start_index' to get a larger sum. If the sum is greater than 'c', it decreases 'end_index' to get a smaller sum. If the sum is equal to 'c', the function returns True as it has found the pair of numbers. If no such pair is found after the loop, it returns False. This approach works because if there exist two numbers 'a' and 'b' such that a^2 + b^2 = c, then 'a' and 'b' must each be less than or equal to sqrt(c). The time complexity of this function is O(√c) because, in the worst case, the while loop iterates up to the square root of 'c' times. The space complexity is O(1) as it uses a constant amount of extra space. """ start_index = 0 end_index = int(math.sqrt(c)) while start_index <= end_index: # a * a instead of a ** 2 because it's faster squares_sum = start_index * start_index + end_index * end_index if squares_sum < c: start_index += 1 elif squares_sum > c: end_index -= 1 else: return True return False

Understanding the Core Idea

The core idea of this solution is to find if the given integer c can be decomposed into the sum of two perfect squares using a two-pointer approach. Here's the breakdown of the concept:

  • Two-Pointer Approach:

    • The function initializes two pointers: start_index at 0 and end_index at the square root of c. These pointers represent potential values of 'a' and 'b'.

    • In each iteration, it calculates the squares_sum of the squares of these pointers.

    • If squares_sum equals c, it means we've found a pair of integers (a, b) whose squares add up to c.

    • If squares_sum is less than c, we increment start_index to increase the sum (since a^2 is the smaller term).

    • If squares_sum is greater than c, we decrement end_index to decrease the sum (since b^2 is the larger term).

  • Mathematical Basis:

    • The algorithm is based on the fact that if a number c can be expressed as the sum of two squares, the numbers a and b must each be less than or equal to the square root of c. This ensures we only check relevant values.

Code Walkthrough

  1. Initialization:

    • start_index is set to 0, representing the smallest possible square.

    • end_index is set to the integer part of the square root of c, representing the largest possible square within the range.

  2. Main Loop (Two-Pointer Search):

    • The while loop runs as long as start_index is less than or equal to end_index.

    • In each iteration:

      • squares_sum is calculated as start_index * start_index + end_index * end_index.

      • Decision Point (Conditional Statements):

        • If squares_sum == c, the function returns True (pair found).

        • If squares_sum < c, we increase start_index by 1.

        • If squares_sum > c, we decrease end_index by 1.

  3. Result Calculation/Return:

    • If the loop completes without finding a match, the function returns False (no pair exists).

Complexity Analysis

Time Complexity:

  • where is the input integer. In the worst case, the while loop will run up to the square root of times.

Space Complexity:

  • (Constant): The algorithm uses only a fixed number of variables (start_index, end_index, squares_sum), and this number doesn't grow with the input size.

Example

Input: c = 98

Step-by-Step Walkthrough:

  1. Initialization:

    • start_index is set to 0.

    • end_index is set to the integer part of the square root of 98, which is 9.

  2. Main Loop (Two-Pointer Search):

    • While (Iteration 1):

      • squares_sum =

      • Since 81 < 98, we increment start_index to 1.

    • While (Iteration 2):

      • squares_sum =

      • Since 82 < 98, we increment start_index to 2.

    • While (Iteration 3):

      • squares_sum =

      • Since 85 < 98, we increment start_index to 3.

    • While (Iteration 4):

      • squares_sum =

      • Since 90 < 98, we increment start_index to 4.

    • While (Iteration 5):

      • squares_sum =

      • Since 97 < 98, we increment start_index to 5.

    • While (Iteration 6):

      • squares_sum =

      • Since 106 > 98, we decrement end_index to 8.

    • While (Iteration 7):

      • squares_sum =

      • Since 89 < 98, we increment start_index to 6.

    • While (Iteration 8):

      • squares_sum =

      • Since 100 > 98, we decrement end_index to 7.

    • While (Iteration 9):

      • squares_sum =

      • Since 85 < 98, we increment start_index to 7.

    • While (Iteration 10):

      • squares_sum =

      • Since 98 == 98, we have found a valid pair (7, 7), and the function returns True.

  3. Loop Termination: The loop terminates when squares_sum == c, indicating a valid pair of integers has been found.

  4. Iteration Summary:

    start_index

    end_index

    squares_sum

    Comparison

    Action

    0

    9

    81

    81 < 98

    start_index += 1

    1

    9

    82

    82 < 98

    start_index += 1

    2

    9

    85

    85 < 98

    start_index += 1

    3

    9

    90

    90 < 98

    start_index += 1

    4

    9

    97

    97 < 98

    start_index += 1

    5

    9

    106

    106 > 98

    end_index -= 1

    5

    8

    89

    89 < 98

    start_index += 1

    6

    8

    100

    100 > 98

    end_index -= 1

    6

    7

    85

    85 < 98

    start_index += 1

    7

    7

    98

    98 == 98

    Return True

  5. Result Calculation/Final Steps:

    • The function directly returns True as soon as it finds the valid pair (7, 7). It does not need to calculate or return any additional values.

    • The pair (7, 7) satisfies the condition , confirming that 98 can be expressed as the sum of two squares.

Approach 2: Number Theory (Fermat's Theorem)

def judgeSquareSum2(c: int) -> bool: """ Determines if a given non-negative integer 'c' can be expressed as the sum of squares of two integers 'a' and 'b'. The function uses properties from number theory, particularly Fermat's theorem on sums of two squares. According to the theorem, a non-negative integer can be expressed as a sum of two squares if and only if every prime factor of the form (4k + 3) has an even exponent in the factorization of 'c'. The function iterates through possible prime factors up to the square root of 'c'. For each factor, it counts the number of times it divides 'c'. If a prime factor of the form (4k + 3) divides 'c' an odd number of times, the function returns False. Additionally, after factoring out all smaller primes, if the remaining part of 'c' is a prime of the form (4k + 3), the function also returns False. If no such prime factors are found, the function returns True. The time complexity of this solution is O(√c log c) because it iterates up to the square root of 'c' and performs division operations for each prime factor (log c). The space complexity is O(1) as it uses a constant amount of extra space. """ index = 2 while index * index <= c: divisors_count = 0 if c % index == 0: while c % index == 0: divisors_count += 1 c //= index if divisors_count % 2 and index % 4 == 3: return False index += 1 return c % 4 != 3

Understanding the Core Idea

The core idea of this solution is to leverage Fermat's Theorem on sums of two squares to determine if a given number can be expressed as such. Fermat's theorem states:

In simpler terms, a number can be expressed as the sum of two squares unless it has a prime factor of the form (such as 3, 7, 11, etc.) that appears an odd number of times in its prime factorization.

This is because a prime of the form cannot be expressed as the sum of two squares (as opposed to primes of the form which can always be expressed as the sum of two squares). When a prime of the form appears with an even exponent in the prime factorization of , its contribution to the overall sum of squares can be paired off and effectively neutralized. However, if such a prime appears with an odd exponent, it cannot be paired off, and its presence fundamentally prevents from being expressed as the sum of two squares. This is because the square of any number is congruent to 0 or 1 (mod 4), but never 3.

Another important concept is the Brahmagupta-Fibonacci identity, which states that the product of two sums of two squares is itself a sum of two squares. This property allows us to combine the squares of two numbers to form a new sum of squares:

This identity is crucial in the context of Fermat's theorem and the decomposition of numbers into sums of squares. When a prime factor of the form appears with an even exponent, it can be paired off to form a new sum of squares: , and this can be combined with other sums of squares to form a larger sum of squares. However, if appears with an odd exponent, it cannot be paired off. The number cannot be expressed as a sum of squares due to the presence of the lone term (which is congruent to 3 mod 4).

  • Prime Factorization: The solution systematically checks for prime factors of up to its square root.

  • Counting Divisibility: For each prime factor, it counts how many times it divides evenly (tracked by divisors_count).

  • Checking Fermat's Condition: If a prime factor of the form has an odd divisors_count, the number cannot be a sum of squares (returns False).

  • Base Case: After iterating through smaller primes, the remaining value of is either 1 or a prime. It checks if this remaining prime is of the form . If it is, it returns False; otherwise, it returns True.

Code Walkthrough

  1. Initialization:

    • index is initialized to 2, the smallest prime number.

  2. Main Loop (Checking Prime Factors):

    • The while loop iterates as long as the square of index (potential prime factor) is less than or equal to c.

    • Inner Loop (Counting Divisors):

      • If c is divisible by index, a nested while loop repeatedly divides c by index to count its occurrences.

    • Decision Points (Fermat's Condition):

      • After the inner loop, it checks if divisors_count is odd (indicating an odd power) and if index is of the form 4k + 3. If both are true, the function returns False.

  3. Base Case (After Main Loop):

    • If the main loop completes without returning, c is either 1 or a prime.

    • It checks if the remaining c is of the form 4k + 3. If so, it returns False; otherwise, True.

Complexity Analysis

Time Complexity:

  • . The main loop runs up to times. For each iteration, the inner loop might run up to times in the worst case (for a prime that divides c many times).

Space Complexity:

  • (Constant): The algorithm uses a fixed number of variables (index, divisors_count, c), regardless of the input size.

Example

Input: c = 98

Step-by-Step Walkthrough:

  1. Initialization:

    • index is initialized to 2, the smallest prime number.

  2. Main Loop (Checking Prime Factors):

    • While (Iteration 1):

      • index = 2

      • c is divisible by index (98 % 2 == 0).

      • Inner loop counts the divisors: divisors_count = 1, c = 49 (after division)

      • divisors_count is 1 and index (2) is not of the form 4k+3, so the loop continues.

      • index is incremented to 3.

    • While (Iteration 2):

      • index = 3

      • c is not divisible by index (49 % 3 != 0), so the loop continues.

      • index is incremented to 4.

    • While (Iteration 3):

      • index = 4

      • c is not divisible by index (49 % 4 != 0), so the loop continues.

      • index is incremented to 5.

    • While (Iteration 4):

      • index = 5

      • c is not divisible by index (49 % 5 != 0), so the loop continues.

      • index is incremented to 6.

    • While (Iteration 5):

      • index = 6

      • c is not divisible by index (49 % 6 != 0), so the loop continues.

      • index is incremented to 7.

    • While (Iteration 6):

      • index = 7

      • c is divisible by index (49 % 7 == 0).

      • Inner loop counts the divisors:

        • divisors_count = 1, c = 7

        • divisors_count = 2, c = 1

      • index (7) is of the form 4k+3, but since divisors_count is even (2), the loop continues.

      • index is incremented to 8.

  3. Loop Termination: The loop terminates after iteration 6 because index * index (64) is not less than or equal to the current value of c (1).

  4. Iteration Summary/Visual Aids:

    c

    index

    divisors_count

    index % 4 == 3

    Result

    108

    2

    1

    False

    Continue

    49

    3

    0

    False

    Continue

    49

    4

    0

    False

    Continue

    49

    5

    0

    False

    Continue

    49

    6

    0

    False

    Continue

    49

    7

    2

    True

    Continue (Even exponent)

  5. Result Calculation/Final Steps:

    • After the loop, c is 1. Since 1 is not of the form 4k + 3, the function returns True, indicating that 98 can be expressed as the sum of two squares .

  6. Visualizing the Pairing of Squares:

    • Since the factorization of 98 is , the prime factor 7 (of the form ) appears with an even exponent (2), allowing it to be paired off and expressed as a sum of squares.

    • This can be visualized using the Brahmagupta-Fibonacci identity to combine the squares of 7 into a new sum of squares:

      • We know that , and we can express as . With the factorization, we get the product of these two sums of squares:

        • .

      • Using the identity, we can combine these to get .

June 18 -> 826. Most Profit Assigning Work

You have n jobs and m workers. You are given three arrays: difficulty, profit, and worker where:

  • difficulty[i] and profit[i] are the difficulty and the profit of the ith job, and

  • worker[j] is the ability of jth worker (i.e., the jth worker can only complete a job with difficulty at most worker[j]).

Every worker can be assigned at most one job, but one job can be completed multiple times.

  • For example, if three workers attempt the same job that pays 1 dollar, then the total profit will be 3 dollars. If a worker cannot complete any job, their profit is $0.

Return the maximum profit we can achieve after assigning the workers to the jobs.

Example 1:

  • Input: difficulty = [2,4,6,8,10], profit = [10,20,30,40,50], worker = [4,5,6,7]

  • Output: 100

  • Explanation:

    • Workers are assigned jobs of difficulty [4,4,6,6], and they get a profit of [20,20,30,30] separately.

Example 2:

  • Input: difficulty = [85,47,57], profit = [24,66,99], worker = [40,25,25]

  • Output: 0

Constraints:

  • n == difficulty.length

  • n == profit.length

  • m == worker.length

  • 1 <= n, m <= 10^4

  • 1 <= difficulty[i], profit[i], worker[i] <= 10^5

Approach 1: Memoization Name

def maxProfitAssignment1(difficulty: List[int], profit: List[int], worker: List[int]) -> int: """ Calculates the maximum total profit that workers can achieve based on their abilities and the given jobs' difficulties and profits. The function first determines the maximum ability of all workers, then initializes a list to store the maximum profit for each level of difficulty up to this maximum ability. By iterating through each job's difficulty and profit, it updates this list to ensure it captures the highest profit available for each difficulty level up to the maximum ability. The function then adjusts this list so that for any given difficulty, it reflects the highest-profit achievable up to that difficulty level, because a job with a lower difficulty may have a higher profit. Finally, it sums up the highest possible profit for each worker based on their respective abilities. The time complexity of this solution is O(n + m + max_ability), where `n` is the number of jobs, `m` is the number of workers, and `max_ability` is the maximum ability of any worker. This is because it iterates through the difficulties and profits once (O(n)), the range of abilities (O(max_ability)), and the workers once (O(m)). The space complexity is O(max_ability) due to the list storing maximum profits per difficulty level. """ max_ability = max(worker) max_profit_per_diff = [0] * (max_ability + 1) # Build the maximum profit for each difficulty level up to max_ability. for diff, prof in zip(difficulty, profit): if diff <= max_ability: max_profit_per_diff[diff] = max(max_profit_per_diff[diff], prof) # Accumulate the maximum profit so far for each difficulty level. for index in range(1, max_ability + 1): max_profit_per_diff[index] = max(max_profit_per_diff[index], max_profit_per_diff[index - 1]) # Compute and return the total maximum profit for all workers. return sum(max_profit_per_diff[ability] for ability in worker)

Understanding the Core Idea

The core idea of this solution is to leverage a precomputed list of maximum profits achievable for each difficulty level up to the maximum ability of the workers. This allows a quick lookup of the best possible profit for any worker's ability, ensuring efficient assignment of workers to jobs.

  • Precomputation of Maximum Profits: The solution first builds a list to store the maximum-profit achievable for each difficulty level up to the maximum worker ability.

  • Accumulation of Profits: It ensures that for any difficulty level, the profit reflects the highest possible profit up to that level. This way, a worker can always find the best job they can perform.

  • Efficient Assignment: By using the precomputed list, the solution quickly sums up the maximum possible profit for each worker based on their ability.

Code Walkthrough

  1. Initialization:

    • max_ability = max(worker): Finds the maximum ability among all workers. This is the highest difficulty level we need to consider.

    • max_profit_per_diff = [0] * (max_ability + 1): Creates a list to store the maximum-profit achievable for each difficulty level from 0 to max_ability. It's initialized with zeros.

  2. Build Maximum Profit per Difficulty:

    • for diff, prof in zip(difficulty, profit): Iterates through each job's difficulty (diff) and profit (prof).

    • if diff <= max_ability: Checks if the current job's difficulty is within the range of worker abilities we need to consider.

    • max_profit_per_diff[diff] = max(max_profit_per_diff[diff], prof): Updates the maximum profit for the current job's difficulty level if the current job offers a higher profit.

  3. Accumulate Maximum Profit:

    • for index in range(1, max_ability + 1): Iterates through each difficulty level from 1 to max_ability.

    • max_profit_per_diff[index] = max(max_profit_per_diff[index], max_profit_per_diff[index - 1]): Updates the maximum profit for the current difficulty level to be the maximum of either its current value or the maximum profit of the previous difficulty level. This ensures that even if a job has a lower difficulty, it can still be chosen if it offers a higher profit.

  4. Result Calculation/Return:

    • sum(max_profit_per_diff[ability] for ability in worker): Iterates through each worker's ability and sums up their maximum achievable profits based on the precomputed max_profit_per_diff list.

Complexity Analysis

Time Complexity:

  • , where:

    • is the length of difficulty and profit (number of jobs)

    • is the length of worker (number of workers)

    • is the maximum value in worker

This is because the algorithm iterates over difficulty and profit once , over the range of possible abilities , and over worker once .

Space Complexity:

This is because the algorithm uses a list max_profit_per_diff to store maximum profits per difficulty level up to the maximum ability. The size of this list is directly proportional to max_ability.

Example

Input:

difficulty = [5, 12, 2, 6, 15, 7, 9] profit = [10, 30, 20, 25, 50, 35, 40] worker = [10, 5, 7, 12, 8]

Step-by-Step Walkthrough:

  1. Initialization:

    • The function starts by finding the maximum ability of the workers: max_ability = max(worker), which is 12.

    • An array max_profit_per_diff of size max_ability + 1 is initialized to store the maximum profit for each difficulty level up to max_ability: max_profit_per_diff = [0] * (max_ability + 1).

  2. Main Loop (Building Max Profit Array):

    • Iteration 1:

      • Current Job: (difficulty = 5, profit = 10)

      • Since 5 <= 12, update max_profit_per_diff[5]: 0 -> 10.

    • Iteration 2:

      • Current Job: (difficulty = 12, profit = 30)

      • Since 12 <= 12, update max_profit_per_diff[12]: 0 -> 30.

    • Iteration 3:

      • Current Job: (difficulty = 2, profit = 20)

      • Since 2 <= 12, update max_profit_per_diff[2]: 0 -> 20.

    • Iteration 4:

      • Current Job: (difficulty = 6, profit = 25)

      • Since 6 <= 12, update max_profit_per_diff[6]: 0 -> 25.

    • Iteration 5:

      • Current Job: (difficulty = 15, profit = 50)

      • Since 15 > 12, skip this job.

    • Iteration 6:

      • Current Job: (difficulty = 7, profit = 35)

      • Since 7 <= 12, update max_profit_per_diff[7]: 0 -> 35.

    • Iteration 7:

      • Current Job: (difficulty = 9, profit = 40)

      • Since 9 <= 12, update max_profit_per_diff[9]: 0 -> 40.

    Iteration Summary:

    max_profit_per_diff: [0, 0, 20, 0, 0, 10, 25, 35, 0, 40, 0, 0, 30]
  3. Second Loop (Propagating Maximum Profits):

    • Iteration 1/12:

      • Here we update the maximum profit for each difficulty level based on the previous level's maximum profit.

      • Update max_profit_per_diff[1]: 0 -> 0.

    • Iteration 2/12:

      • Since the previous maximum profit is 0, the maximum profit for 2 remains 20.

      • Update max_profit_per_diff[2]: 20 -> 20.

    • Iteration 3/12:

      • Since the previous maximum profit is 20 and this level's profit is 0, the maximum profit for 3 gets updated to 20.

      • Update max_profit_per_diff[3]: 0 -> 20.

    • Iteration 4/12 - 12/12:

      • The loop continues, updating the maximum profit for each difficulty level based on the previous level's maximum profit.

    • The final updated max_profit_per_diff array is:

      max_profit_per_diff: [0, 0, 20, 20, 20, 20, 25, 35, 35, 40, 40, 40, 40]
  4. Result Calculation/Final Steps:

    • Worker 1:

      • Ability: 10

      • Maximum Profit: 40

      • Current Total Profit: 0 + 40 = 40

    • Worker 2:

      • Ability: 5

      • Maximum Profit: 20

      • Current Total Profit: 40 + 20 = 60

    • Worker 3:

      • Ability: 7

      • Maximum Profit: 35

      • Current Total Profit: 60 + 35 = 95

    • Worker 4:

      • Ability: 12

      • Maximum Profit: 40

      • Current Total Profit: 95 + 40 = 135

    • Worker 5:

      • Ability: 8

      • Maximum Profit: 35

      • Current Total Profit: 135 + 35 = 170

    • The function returns the total maximum profit of 170.

Approach 2: Binary Search

def maxProfitAssignment2(difficulty: List[int], profit: List[int], worker: List[int]) -> int: """ Calculates the maximum total profit that workers can achieve based on their abilities and the given jobs' difficulties and profits. This function first sorts the jobs by difficulty while pairing each job with its corresponding profit. It then processes these sorted jobs to create a list where each job entry holds the highest profit available up to that difficulty level. This transformation ensures that for any worker's ability, we can quickly find the best possible profit they can achieve using binary search. Since we are dealing with a tuple of (difficulty, profit), the binary search is performed on the difficulty values, and the other value (profit) is used with 'float('inf')' as the upper bound for the binary search. This way, we ensure that `bisect_right` will find the index where a worker's ability can be inserted in the sorted job list with the highest profit available up to that difficulty. We subtract 1 to get the index of the highest-paying job the worker can perform. By summing up the maximum achievable profits for all workers, the function computes the total maximum profit. The time complexity of this solution is O((n + m) log n), where `n` is the number of jobs, and `m` is the number of workers. This is because sorting the jobs takes O(n log n) and each worker's job search takes O(log n) due to binary search, repeated `m` times; hence, the total time complexity is O(n log n + m log n) = O((n + m) log n). The space complexity is O(n) for storing the processed job list. """ jobs = sorted(zip(difficulty, profit)) # Transform the job list to ensure each job entry reflects the # highest profit up to that difficulty level max_profit_so_far = 0 for index, job in enumerate(jobs): max_profit_so_far = max(max_profit_so_far, job[1]) jobs[index] = (job[0], max_profit_so_far) total_profit = 0 for ability in worker: # Use binary search to find the highest-profit job that the # worker can do index = bisect_right(jobs, (ability, float('inf'))) if index > 0: total_profit += jobs[index - 1][1] return total_profit

Understanding the Core Idea

The central concept of this solution is to leverage sorting and binary search to efficiently match workers to the most profitable jobs they can perform. The solution transforms the job list to reflect the highest profit available up to each difficulty level, enabling quick lookups.

  • Sorting Jobs by Difficulty: The jobs are sorted by difficulty to facilitate binary search and cumulative-profit calculations.

  • Cumulative-Profit Calculation: A transformation is applied to the sorted jobs to ensure each job entry reflects the highest profit up to that difficulty level.

  • Binary Search for Worker Assignment: For each worker, a binary search is used to quickly find the highest-paying job they can perform.

Code Walkthrough

  1. Initialization:

    • jobs = sorted(zip(difficulty, profit)): Creates a list of tuples where each tuple contains the difficulty and profit of a job. It then sorts this list in ascending order based on the difficulty.

  2. Job Transformation:

    • max_profit_so_far = 0: Initializes a variable to keep track of the maximum profit seen so far.

    • for index, job in enumerate(jobs): Iterates over the sorted jobs.

      • max_profit_so_far = max(max_profit_so_far, job[1]): Updates max_profit_so_far if the current job has a higher profit.

      • jobs[index] = (job[0], max_profit_so_far): Replaces the profit in the current job tuple with max_profit_so_far. This transformation ensures that for each job, the profit stored in the tuple represents the maximum profit achievable up to that job's difficulty level.

  3. Profit Calculation:

    • total_profit = 0: Initializes the total profit earned by all workers.

    • for ability in worker: Iterates over the abilities of each worker.

      • index = bisect_right(jobs, (ability, float('inf'))): Performs a binary search to find the rightmost index in the jobs list where the tuple (ability, infinity) would be inserted while maintaining the sorted order. This essentially finds the index of the first job with a difficulty greater than the worker's ability.

      • if index > 0: Checks if the worker is qualified for at least one job.

        • total_profit += jobs[index - 1][1]: If the worker is qualified, add the profit of the highest-paying job they can do (located at index - 1) to the total profit.

  4. Result Calculation/Return:

    • Returns the calculated total_profit.

Complexity Analysis

Time Complexity:

  • , where:

    • is the number of jobs

    • is the number of workers

This is because the algorithm:

  • Sorts the jobs:

  • Transforms the job list:

  • Performs binary search for each worker:

Space Complexity:

This is because the algorithm uses additional space for the jobs list which has the same length as the input lists difficulty and profit.

Example

Input:

difficulty = [5, 12, 2, 6, 15, 7, 9] profit = [10, 30, 20, 25, 50, 35, 40] worker = [10, 5, 7, 12, 8]

Step-by-Step Walkthrough:

  1. Initialization:

    • The jobs list is created by pairing corresponding difficulty and profit values and then sorting them in ascending order based on difficulty: [(2, 20), (5, 10), (6, 25), (7, 35), (9, 40), (12, 30), (15, 50)].

    • A variable max_profit_so_far is initialized to 0. It will track the highest profit encountered as the jobs are processed.

  2. Main Loop (Transforming Jobs with Max Profit):

    • The loop iterates through each job in the sorted jobs list.

    • Iteration 1:

      • Job: (2, 20)

      • max_profit_so_far is updated to 20 (max of 0 and 20).

      • The job in jobs is replaced with (2, 20).

    • Iteration 2:

      • Job: (5, 10)

      • max_profit_so_far remains 20.

      • The job in jobs is replaced with (5, 20).

    • Iterations 3-7: Similar logic is applied to the remaining jobs. max_profit_so_far gets updated as needed, ensuring that the profit associated with each job represents the maximum profit achievable up to that difficulty level.

  3. Iteration Summary (Transformed Jobs):

    • The provided table illustrates how the jobs list is transformed during the main loop. The "Max Profit So Far" column shows how this value is tracked and used to update the profit component of each job.

    Iteration

    Original Job

    Max Profit So Far

    Updated Job

    1

    (2, 20)

    20

    (2, 20)

    2

    (5, 10)

    20

    (5, 20)

    3

    (6, 25)

    25

    (6, 25)

    4

    (7, 35)

    35

    (7, 35)

    5

    (9, 40)

    40

    (9, 40)

    6

    (12, 30)

    40

    (12, 40)

    7

    (15, 50)

    50

    (15, 50)

  4. Calculating Total Profit (Worker Loop):

    • The loop iterates through the worker list.

    • Iteration 1 (Worker with ability 10):

      • Binary search (bisect_right) is used to find the rightmost job in jobs with a difficulty less than or equal to the worker's ability (10). This leads to index 5.

      • Since the index is greater than 0, the profit of the job at index 4 is added to total_profit (40).

    • Iteration 2 (Worker with ability 5):

      • Binary search finds index 2.

      • The profit of the job at index 1 (20) is added to total_profit. The total profit is now 60.

    • Iteration 3 (Worker with ability 7):

      • Binary search finds index 4.

      • The profit of the job at index 3 (35) is added to total_profit. The total profit is now 95.

    • **Iteration 4

      • Binary search finds index 6.

      • The profit of the job at index 5 (40) is added to total_profit. The total profit is now 135.

    • **Iteration 5

      • Binary search finds index 4.

      • The profit of the job at index 3 (35) is added to total_profit. The total profit is now 170.

  5. Final Result:

    • The function returns the calculated total_profit, which is 170 in this example. This represents the maximum achievable profit by assigning the workers to the available jobs.

Approach 3: Two-Pointer Technique

def maxProfitAssignment3(difficulty: List[int], profit: List[int], worker:List[int]) -> int: """ Calculates the maximum total profit that workers can achieve based on their abilities and the given jobs' difficulties and profits. This function sorts the jobs by difficulty and pairs them with their respective profits. It also sorts the workers based on their abilities. As it iterates through the sorted workers, it keeps track of the maximum profit available for any job that a worker can perform up to their ability. By accumulating this maximum profit for each worker, it computes the total maximum profit that can be achieved. The time complexity of this solution is O(n log n + m log m), where `n` is the number of jobs and `m` is the number of workers as the jobs and workers are sorted (taking O(n log n) and O(m log m) time, respectively). The space complexity is O(n) for storing the sorted list of jobs. """ jobs = sorted(zip(difficulty, profit)) total_profit = 0 max_profit_so_far, index = 0, 0 for ability in sorted(worker): # Update the maximum profit for jobs within the current # worker's ability while index < len(jobs) and jobs[index][0] <= ability: max_profit_so_far = max(max_profit_so_far, jobs[index][1]) index += 1 total_profit += max_profit_so_far return total_profit

Understanding the Core Idea

The core idea of this solution is to use sorting and a two-pointer technique to efficiently assign the most profitable jobs to workers based on their abilities. The solution processes jobs and workers in a sorted order, ensuring that each worker is matched with the best possible job they can perform.

  • Sorting Jobs and Workers: Both the jobs and workers are sorted to facilitate efficient traversal and profit calculation.

  • Two-Pointer Technique: The solution uses a two-pointer technique to iterate through the sorted job list and keep track of the maximum profit available up to the current worker's ability. This technique ensures that the solution only processes each job and worker once, contributing to its efficiency.

  • Accumulating Profits: By maintaining a running maximum of job profits, the solution ensures that each worker is assigned the best possible job they can perform, maximizing total profit.

Code Walkthrough

  1. Initialization:

    • jobs = sorted(zip(difficulty, profit)): Creates a list of tuples where each tuple contains the difficulty and profit of a job. It then sorts this list in ascending order based on the difficulty.

    • total_profit = 0: Initializes the total profit earned by all workers.

    • max_profit_so_far = 0: Initializes a variable to keep track of the maximum profit seen so far for any job within the current worker's ability.

    • index = 0: Initializes an index to keep track of the current job being considered.

  2. Iterative Profit Calculation:

    • for ability in sorted(worker): Iterates over the abilities of each worker in ascending order.

      • while index < len(jobs) and jobs[index][0] <= ability: This loop continues as long as there are jobs left to consider, and the current job's difficulty is less than or equal to the worker's ability.

        • max_profit_so_far = max(max_profit_so_far, jobs[index][1]): Updates max_profit_so_far if the current job has a higher profit than any previously seen job within the worker's ability range.

        • index += 1: Moves on to the next job.

      • total_profit += max_profit_so_far: Adds the maximum profit achievable by the current worker to the total_profit.

  3. Result Calculation/Return:

    • Returns the calculated total_profit.

Complexity Analysis

Time Complexity:

  • , where:

    • is the number of jobs

    • is the number of workers

This is because the algorithm:

  • Sorts the jobs:

  • Sorts the workers:

  • Iterates over jobs and workers: While the nested loops might seem like , note that the index for jobs only increases and never goes back. So, in total, we iterate over each job and each worker only once, resulting in .

Since the sorting operations dominate, the overall time complexity is .

Space Complexity:

This is because the algorithm uses additional space for the jobs list which has the same length as the input lists difficulty and profit ( elements). The space complexity is dominated by the storage of the sorted job list, hence .

Example

Input:

difficulty = [5, 12, 2, 6, 15, 7, 9] profit = [10, 30, 20, 25, 50, 35, 40] worker = [10, 5, 7, 12, 8]

Step-by-Step Walkthrough:

  1. Initialization:

    • The code combines difficulty and profit into pairs and sorts them by difficulty: jobs = [(2, 20), (5, 10), (6, 25), (7, 35), (9, 40), (12, 30), (15, 50)].

    • The worker array is also sorted: [5, 7, 8, 10, 12].

    • total_profit is initialized to 0.

    • max_profit_so_far (to track the highest profit seen so far) and index (to track the current position in jobs) are both set to 0.

  2. Main Loop (Calculate Total Profit):

    • The loop iterates over the sorted worker array: worker = [5, 7, 8, 10, 12].

    • Iteration 1 (Worker with ability 5):

      • Current max_profit_so_far: 0

      • The while loop finds the jobs that the worker can do (difficulty <= 5).

        • Job (2, 20) is within the worker's ability. Update max_profit_so_far to 20.

        • Job (5, 10) is within the worker's ability. Keep max_profit_so_far at 20.

        • Job (6, 25) is not within the worker's ability (difficulty > 5). Exit the loop.

      • The worker's profit (20) is added to total_profit, making it 20.

    • Iteration 2 (Worker with ability 7):

      • Current max_profit_so_far: 20

      • The while loop finds the jobs that the worker can do (up to difficulty 7).

        • Job (6, 25) is within the worker's ability. Update max_profit_so_far to 25.

        • Job (7, 35) is within the worker's ability. Update max_profit_so_far to 35.

        • Job (9, 40) is not within the worker's ability (difficulty > 7). Exit the loop

      • The worker's profit (35) is added to total_profit, making it 55 (20 + 35).

    • Iteration 3 (Worker with ability 8):

      • Current max_profit_so_far: 35

      • The while loop finds the jobs that the worker can do (up to difficulty 8).

        • Since the worker's ability is 8 and the next job's difficulty is 9, the loop doesn't execute.

      • The worker's profit (35) is added to total_profit, making it 90 (55 + 35).

    • Iteration 4 (Worker with ability 10):

      • Current max_profit_so_far: 35

      • The while loop finds the jobs that the worker can do (up to difficulty 10).

        • Job (9, 40) is within the worker's ability. Update max_profit_so_far to 40.

        • Job (12, 30) is not within the worker's ability (difficulty > 10). Exit the loop.

      • The worker's profit (40) is added to total_profit, making it 130 (90 + 40).

    • Iteration 5 (Worker with ability 12):

      • Current max_profit_so_far: 40

      • The while loop finds the jobs that the worker can do (up to difficulty 12).

        • Job (12, 30) is within the worker's ability. Keep max_profit_so_far at 40.

        • Job (15, 50) is not within the worker's ability (difficulty > 12). Exit the loop.

      • The worker's profit (40) is added to total_profit, making it 170 (130 + 40).

  3. Iteration Summary:

    • The table demonstrates how max_profit_so_far and total_profit change with each iteration of the loop.

    Iteration

    Worker Ability

    Max Profit So Far

    Total Profit

    1

    5

    20

    20

    2

    7

    35

    55

    3

    8

    35

    90

    4

    10

    40

    130

    5

    12

    40

    170

  4. Result Calculation/Final Steps:

    • After the loop completes, the final value of total_profit (170) is returned, representing the maximum-profit achievable.

June 19 -> 1482. Minimum Number of Days to Make m Bouquets

You are given an integer array bloom_day, an integer m and an integer k.

You want to make m bouquets. To make a bouquet, you need to use k adjacent flowers from the garden.

The garden consists of n flowers, the ith flower will bloom in the bloom_day[i] and then can be used in exactly one bouquet.

Return the minimum number of days you need to wait to be able to make m bouquets from the garden. If it is impossible to make m bouquets return -1.

Example 1:

  • Input: bloom_day = [1,10,3,10,2], m = 3, k = 1

  • Output: 3

  • Explanation:

    • Let us see what happened in the first three days. x means flower bloomed and _ means flower did not bloom in the garden.

    • We need three bouquets each should contain one flower.

    • After day 1: [x, _, _, _, _] // we can only make one bouquet.

    • After day 2: [x, _, _, _, x] // we can only make two bouquets.

    • After day 3: [x, _, x, _, x] // we can make three bouquets. The answer is 3.

Example 2:

  • Input: bloom_day = [1,10,3,10,2], m = 3, k = 2

  • Output:-1

  • Explanation:

    • We need three bouquets each has two flowers, that means we need six flowers. We only have five flowers, so it is impossible to get the needed bouquets, and we return -1.

Example 3:

  • Input: bloom_day = [7,7,7,7,12,7,7], m = 2, k = 3

  • Output: 12

  • Explanation:

    • We need two bouquets each should have three flowers.

    • Here is the garden after the 7 and 12 days:

    • After day 7: [x, x, x, x, _, x, x]

    • We can make one bouquet of the first three flowers that bloomed. We cannot make another bouquet from the last three flowers that bloomed because they are not adjacent.

    • After day 12: [x, x, x, x, x, x, x]

    • It is clear that we can make two bouquets in different ways.

Constraints:

  • bloom_day.length == n

  • 1 <= n <= 10^5

  • 1 <= bloom_day[i] <= 10^9

  • 1 <= m <= 10^6

  • 1 <= k <= n

Approach 1.1: Binary Search

def minDays1_1(bloom_day: List[int], m: int, k: int) -> int: """ Determines the minimum number of days required to make `m` bouquets using `k` adjacent flowers from the garden, given the bloom days of the flowers `bloom_day`. The function performs a binary search over the range of bloom days to find the lowest day value at which it's possible to make the required number of bouquets. It first checks if it's possible to make the bouquets given the constraint, and then performs binary search on the range of bloom days (min(bloom_day) to max(bloom_day)) to find the earliest day that satisfies the condition. The can_make_bouquets` helper function checks if it's possible to make the bouquets by a given day by iterating over the bloom_day list and counting the number of adjacent flowers that have bloomed by that day. The binary search reduces the search space logarithmically, and the helper function ensures that the feasibility of making bouquets is checked efficiently. The time complexity of this solution is O(n log D), where `n` is the length of the `bloom_day` list and `D` is the range of bloom days (max(bloom_day) - min(bloom_day)). This is because the binary search runs in O(log D) time, and each check within the search takes O(n) time. The space complexity is O(1) since only a few extra variables are used. """ if k * m > len(bloom_day): return -1 def can_make_bouquets(day: int) -> bool: """Helper function to check if `m` bouquets can be made by a given day.""" bouquet_count = 0 flowers = 0 for bloom in bloom_day: if bloom <= day: flowers += 1 if flowers == k: bouquet_count += 1 if bouquet_count >= m: return True flowers = 0 else: flowers = 0 return bouquet_count >= m left_index, right_index = min(bloom_day), max(bloom_day) while left_index < right_index: mid_index = (left_index + right_index) // 2 if can_make_bouquets(mid_index): right_index = mid_index else: left_index = mid_index + 1 return left_index

Understanding the Core Idea

The solution leverages binary search to efficiently find the minimum number of days required to make m bouquets. It exploits the monotonic property of the problem: if it's possible to make m bouquets after waiting x days, it's also possible after waiting x + 1 days.

  • Binary Search on Days: The search space is the range of possible days (from the minimum to the maximum bloom day). The solution repeatedly checks the midpoint of this range.

  • Bouquet Formation Check: For each candidate day, the code simulates bouquet formation using adjacent flowers that have bloomed by that day.

  • Search Space Reduction: If enough bouquets can be made, the search space is narrowed to earlier days. Otherwise, it's narrowed to later days.

Code Walkthrough

  1. Base Case Check: The code first checks if it's even possible to make m bouquets with k adjacent flowers each, given the total number of flowers. If not, it returns -1.

  2. can_make_bouquets(day) Function:

    • This function simulates the garden on a given day.

    • It iterates through the bloom days of each flower.

    • If a flower has bloomed by the given day, it's added to a potential bouquet (flowers counter incremented).

    • Once k adjacent flowers are found, a bouquet is formed (bouquet_count incremented).

    • If m or more bouquets are formed, the function immediately returns True.

    • If a flower hasn't bloomed, the current potential bouquet is reset (flowers set to 0).

    • Finally, the function returns whether m or more bouquets were formed.

  3. Binary Search Initialization:

    • left_index starts at the earliest bloom day (minimum of bloom_day).

    • right_index starts at the latest bloom day (maximum of bloom_day).

  4. Binary Search Loop:

    • The loop continues as long as there is a range of days to explore (left_index < right_index).

    • mid_index is calculated as the middle of the current range.

    • The can_make_bouquets function checks if m bouquets can be formed by mid_index day.

    • If yes, the answer might be found earlier (or at mid_index), so right_index is set to mid_index.

    • If no, the answer must be later, so left_index is set to mid_index + 1.

  5. Result:

    • When the loop ends, left_index holds the minimum day when m bouquets can be formed. This value is returned as the final result.

Complexity Analysis

Time Complexity:

  • , where n is the length of bloom_day and D is the difference between the maximum and minimum bloom days.

    • The binary search takes time to find the minimum number of days.

    • In each iteration, the can_make_bouquets function takes time to simulate the garden.

Space Complexity:

  • , the solution uses a constant amount of extra space, regardless of the input size. This is because only a few variables are used to track the indices and counters.

Example

Input: bloom_day = [3, 2, 4, 9, 10, 4, 3, 4], m = 3, k = 2

Step-by-Step Walkthrough:

  1. Initialization:

    • The function first checks if it's possible to make three bouquets of size 2, given 8 flowers. Since 3 * 2 = 6 and 6 <= 8, it's possible, and the function proceeds.

    • The binary search range is initialized:

      • left_index = 2 (the minimum value in bloom_day)

      • right_index = 10 (the maximum value in bloom_day)

  2. Main Loop (Binary Search):

    • Iteration 1:

      • mid_index = (2 + 10) // 2 = 6

      • can_make_bouquets(6) is called:

        • The function simulates forming bouquets with flowers that bloom by day 6.

        • It finds only two bouquets can be made. (See iteration summary table below)

          Flower

          Bloom Day

          Bloom Day <= 6

          Action

          Flowers

          Bouquets (2 size)

          1

          3

          Yes

          Increment Flowers

          1

          0

          2

          2

          Yes

          Make Bouquet and Reset Flowers

          2

          1

          3

          4

          Yes

          Increment Flowers

          1

          1

          4

          9

          No

          Reset Flowers

          0

          1

          5

          10

          No

          Reset Flowers

          0

          1

          6

          4

          Yes

          Increment Flowers

          1

          1

          7

          3

          Yes

          Make Bouquet and Reset Flowers

          2

          2

          8

          4

          Yes

          Increment Flowers

          1

          2

        • Since this is less than m = 3, it returns False.

      • The binary search range is updated:

        • left_index = 7 (since we need to look at later bloom days)

    • Iteration 2:

      • mid_index = (7 + 10) // 2 = 8

      • can_make_bouquets(8) is called:

        • The function simulates forming bouquets with flowers that bloom by day 8.

        • It again finds only two bouquets can be made. (See the iteration summary table below)

          Flower

          Bloom Day

          Bloom Day <= 8

          Action

          Flowers

          Bouquets (2 size)

          1

          3

          Yes

          Increment Flowers

          1

          0

          2

          2

          Yes

          Make Bouquet and Reset Flowers

          2

          1

          3

          4

          Yes

          Increment Flowers

          1

          1

          4

          9

          No

          Reset Flowers

          0

          1

          5

          10

          No

          Reset Flowers

          0

          1

          6

          4

          Yes

          Increment Flowers

          1

          1

          7

          3

          Yes

          Make Bouquet and Reset Flowers

          2

          2

          8

          4

          Yes

          Increment Flowers

          1

          2

        • Since this is less than m = 3, it returns False.

      • The binary search range is updated:

        • left_index = 9

    • Iteration 3:

      • mid_index = (9 + 10) // 2 = 9

      • can_make_bouquets(9) is called:

        • The function simulates forming bouquets with flowers that bloom by day 9.

        • This time, it successfully makes three bouquets. (See the iteration summary table below)

          Flower

          Bloom Day

          Bloom Day <= 9

          Action

          Flowers

          Bouquets (2 size)

          1

          3

          Yes

          Increment Flowers

          1

          0

          2

          2

          Yes

          Make Bouquet and Reset Flowers

          2

          1

          3

          4

          Yes

          Increment Flowers

          1

          1

          4

          9

          Yes

          Make Bouquet and Reset Flowers

          2

          2

          5

          10

          No

          Reset Flowers

          0

          2

          6

          4

          Yes

          Increment Flowers

          1

          2

          7

          3

          Yes

          Make Bouquet and Reset Flowers

          2

          3

        • It returns True.

      • The binary search range is updated:

        • right_index = 9

  3. Loop Termination:

    • The loop terminates because left_index and right_index both become 9.

  4. Binary Search Iteration Summary:

    Iteration

    Left Index

    Right Index

    Mid Index

    Can Make Bouquets?

    Action

    1

    2

    10

    6

    No

    left = mid + 1 (7)

    2

    7

    10

    8

    No

    left = mid + 1 (9)

    3

    9

    10

    9

    Yes

    right = mid (9)

  5. Result Calculation/Final Steps:

    • The final value of left_index is 9, indicating that you need to wait 9 days to make three bouquets.

    • The function returns 9 as the result.

Approach 1.2: Binary Search on Sorted Set

def minDays1_2(bloom_day: List[int], m: int, k: int) -> int: """ Determines the minimum number of days required to make `m` bouquets using `k` adjacent flowers from the garden, given the bloom days of the flowers `bloom_day`. The function leverages a binary search on a sorted list of unique bloom days to find the minimum day at which the required number of bouquets could be made. While it has a similar structure to the `minDays1_1` function, it optimizes the search space by considering only the unique bloom days, sorted in advance to potentially reduce the number of comparisons needed. The `can_make_bouquets` function is also slightly optimized in this version, and instead of resetting the flower count immediately after finding k flowers, it continues to count potential bouquets from the remaining flowers. As the previous version, the binary search reduces the search space logarithmically, and the helper function ensures that the feasibility of making bouquets is checked efficiently. The time complexity of this solution is O(n log n), where `n is the length of the bloom_day list. This is because transforming the list to a set takes O(n) time, and sorting the unique bloom days takes O(U log U) time, where `U` is the number of unique bloom days. The binary search runs in O(log U) time, and each check within the search takes O(n) time. Since U <= n, the overall time complexity simplifies from O(n + U log U + n log U) to O(n log n). The space complexity is O(U), which is at most O(n) in the worst case when all bloom days are unique. """ if k * m > len(bloom_day): return -1 def can_make_bouquets(day: int) -> bool: """Helper function to check if `m` bouquets can be made by a given day.""" bouquet_count = 0 flowers = 0 for bloom in bloom_day: if bloom <= day: flowers += 1 else: # Calculate bouquets from accumulated flowers before resetting bouquet_count += flowers // k if bouquet_count >= m: return True flowers = 0 bouquet_count += flowers // k return bouquet_count >= m unique_bloom_days = sorted(set(bloom_day)) left_index, right_index = 0, len(unique_bloom_days) - 1 while left_index < right_index: mid_index = (left_index + right_index) // 2 if can_make_bouquets(unique_bloom_days[mid_index]): right_index = mid_index else: left_index = mid_index + 1 return unique_bloom_days[left_index]

Understanding the Core Idea

This solution builds upon the core idea of binary search like the previous one, but with an optimization focused on the search space. Instead of searching within the full range of bloom days (which could be large), it leverages the fact that only the unique bloom days matter.

  • Binary Search on Unique Bloom Days: The search space is reduced to the sorted set of unique bloom days. This potentially drastically reduces the number of iterations in the binary search.

  • Bouquet Formation Check: The can_make_bouquets function remains largely the same, but it now implicitly handles the gaps between bloom days by counting how many bouquets can be formed from the consecutive bloomed flowers.

  • Optimized Search: The binary search operates on the indices of the sorted unique bloom days, making the comparisons more efficient.

Code Walkthrough

  1. Base Case Check: Identical to the previous solution, this checks if making m bouquets is even possible.

  2. can_make_bouquets(day) Function:

    • The function's logic is slightly modified from the previous version.

    • Instead of resetting flowers to 0 whenever a bloom day is exceeded, it now adds the current flowers count ( divided by k) to bouquet_count. This efficiently accounts for all bouquets formed up to the current point.

    • The rest of the logic remains the same: check if enough bouquets are made and return the result.

  3. Unique Bloom Days Preparation:

    • unique_bloom_days is created by:

      • Converting the bloom_day list to a set to get unique days.

      • Sorting the set to have a defined order for binary search.

  4. Binary Search Initialization and Loop:

    • left_index and right_index are initialized for the indices of unique_bloom_days.

    • The loop and its logic remain almost the same as before, but now it operates on the indices of unique_bloom_days.

  5. Result:

    • After the loop, unique_bloom_days[left_index] gives the minimum bloom day required.

Complexity Analysis

Time Complexity:

  • , where n is the length of bloom_day. This is an improvement over the previous in cases where there are many repeated bloom days.

    • Creating the set takes time, and sorting it takes , where u is the number of unique bloom days.

    • The binary search now takes iterations.

    • Each iteration of can_make_bouquets still takes time.

    • In total, we have .

Since , the overall time complexity simplifies to .

Space Complexity:

  • in the worst case, to store the set of unique bloom days. This is potentially much better than the previous if there are many repeated values. If u is small compared to n, it can be considered .

Example

Input: bloom_day = [3, 2, 4, 9, 10, 4, 3, 4], m = 3, k = 2

Step-by-Step Walkthrough:

  1. Initialization:

    • The function first checks if it's possible to make three bouquets of size 2, given 8 flowers. Since and , it's possible, and the function proceeds.

    • The unique bloom days are extracted and sorted: unique_bloom_days = [2, 3, 4, 9, 10]

    • The binary search range is initialized to the indices of unique_bloom_days:

      • left_index = 0 (the first index)

      • right_index = 4 (the last index)

  2. Main Loop (Binary Search):

    • Iteration 1:

      • mid_index = (0 + 4) // 2 = 2

      • can_make_bouquets(unique_bloom_days[2]), which is can_make_bouquets(4), is called:

        • The function simulates forming bouquets with flowers that bloom by day 4.

        • It finds that only two bouquets can be made. (See iteration summary table below)

          Flower

          Bloom Day

          Bloom Day <= 4

          Action

          Flowers

          Bouquets (2 size)

          1

          3

          Yes

          Increment Flowers

          1

          0

          2

          2

          Yes

          Increment Flowers

          2

          0

          3

          4

          Yes

          Increment Flowers

          3

          0

          4

          9

          No

          Reset Flowers/Calculate Bouquets

          0

          1

          5

          10

          No

          Reset Flowers/Calculate Bouquets

          0

          1

          6

          4

          Yes

          Increment Flowers

          1

          1

          7

          3

          Yes

          Increment Flowers

          2

          1

          8

          4

          Yes

          Final Bouquet Calculation

          3

          2

        • Since this is less than m = 3, it returns False.

      • The binary search range is updated:

        • left_index = 3 (since we need to look at later bloom days)

    • Iteration 2:

      • mid_index = (3 + 4) // 2 = 3

      • can_make_bouquets(unique_bloom_days[3]), which is can_make_bouquets(9), is called:

        • The function simulates forming bouquets with flowers that bloom by day 9.

        • This time, it successfully makes three bouquets. (See iteration summary table below)

        Flower

        Bloom Day

        Bloom Day <= 9

        Action

        Flowers

        Bouquets (2 size)

        1

        3

        Yes

        Increment Flowers

        1

        0

        2

        2

        Yes

        Increment Flowers

        2

        0

        3

        4

        Yes

        Increment Flowers

        3

        0

        4

        9

        Yes

        Increment Flowers

        4

        0

        5

        10

        No

        Reset Flowers/Calculate Bouquets

        0

        2

        6

        4

        Yes

        Increment Flowers

        1

        2

        7

        3

        Yes

        Increment Flowers

        2

        2

        8

        4

        Yes

        Final Bouquet Calculation

        3

        3

        • It returns True.

      • The binary search range is updated:

        • right_index = 3

  3. Loop Termination:

    • The loop terminates because left_index and right_index both become 3.

  4. Binary Search Iteration Summary:

    Iteration

    Left Index

    Right Index

    Mid Index

    Can Make Bouquets?

    Action

    1

    0

    4

    2

    No

    left = mid + 1 (3)

    2

    3

    4

    3

    Yes

    right = mid (3)

  5. Result Calculation/Final Steps:

    • The final value of left_index is 3.

    • The function returns unique_bloom_days[3], which is 9, indicating that you need to wait 9 days to make three bouquets.

June 20 -> 1552. Magnetic Force Between Two Balls

In the universe Earth C-137, Rick discovered a special form of magnetic force between two balls if they are put in his new invented basket. Rick has n empty baskets, the ith basket is at position[i], Morty has m balls and needs to distribute the balls into the baskets such that the minimum magnetic force between any two balls is maximum.

Rick stated that magnetic force between two different balls at positions x and y is |x - y|.

Given the integer array position and the integer m. Return the required force.

Example 1:

june20-2024-ex1.png
  • Input: position = [1,2,3,4,7], m = 3

  • Output: 3

  • Explanation: Distributing the three balls into baskets 1, 4 and 7 will make the magnetic force between ball pairs [3, 3, 6]. The minimum magnetic force is 3. We cannot achieve a larger minimum magnetic force than 3.

Example 2:

  • Input: position = [5,4,3,2,1,1000000000], m = 2

  • Output: 999999999

  • Explanation: We can use baskets 1 and 1000000000.

Constraints:

  • n == position.length

  • 2 <= n <= 10^5

  • 1 <= position[i] <= 10^9

  • All integers in position are distinct.

  • 2 <= m <= position.length

Approach 1: Binary Search

def maxDistance1(position: List[int], m: int) -> int: """ Determines the maximum possible minimum magnetic force between any two balls placed in baskets. This function uses a binary search approach to find the optimal minimum distance between balls. It first sorts the position list, then employs a helper function can_place_balls` to check if it's possible to place all `m` balls with a given minimum distance. The binary search efficiently narrows down the range of possible distances. The search space is defined by the following bounds: - Lower Bound (`1`): The minimum possible force is 1, when balls are placed in adjacent positions. - Upper Bound (`(max(position) - min(position)) // (m - 1)`): To maximize the minimum distance, we aim to spread the balls as far apart as possible. This upper bound represents the theoretical maximum average distance achievable with `m` balls and `m-1` gaps. The time complexity of this solution is O(n log(n * range)), where `n` is the length of the position list and range is `(max(position) - min(position)) / (m - 1)`. This is because we sort the position list in O(n log n) time and perform binary search with the `can_place_balls` check in O(n log(range)) time, and when combined, it results in O(n log(n) + n log(range)) = O(n log(n * range)). The space complexity is O(n) due to the sorting operation. """ def can_place_balls(min_distance: int) -> bool: """ Helper function that checks if 'm' balls can be placed with at least 'min_distance' between them. """ remaining_balls = m - 1 next_valid_position = position[0] + min_distance for pos in position[1:]: if pos >= next_valid_position: remaining_balls -= 1 next_valid_position = pos + min_distance if remaining_balls == 0: return True return False if remaining_balls > 0 else True position.sort() start_index, end_index = 1, (position[-1] - position[0]) // (m - 1) while start_index < end_index: mid_index = 1 + (start_index + end_index) // 2 if can_place_balls(mid_index): start_index = mid_index else: end_index = mid_index - 1 return start_index

Understanding the Core Idea

The central concept of this solution is to leverage binary search to efficiently find the maximum possible minimum distance between any two balls. This approach works because the problem has a monotonic property: if we can place m balls with a minimum distance d, we can also place them with any distance smaller than d.

  • Binary Search on Answer: Instead of checking all possible distances, we use binary search to narrow down the range of potential answers quickly.

  • Greedy Ball Placement: For each potential minimum distance, we use a greedy approach to place balls as far apart as possible.

  • Monotonicity: The ability to place balls at a given minimum distance implies we can place them at any smaller distance, allowing for binary search.

Code Walkthrough

  1. Helper Function (can_place_balls):

    • Initializes remaining_balls to m - 1, representing the number of balls left to place after the first.

    • Sets next_valid_position to the first position plus the min_distance.

    • Iterates through the remaining positions:

      • If the current position (pos) is greater than or equal to next_valid_position, a ball is placed, remaining_balls is decremented, and next_valid_position is updated.

      • If all balls are placed (remaining_balls becomes 0), it returns True.

    • Returns False if not all balls could be placed, True otherwise.

  2. Initialization:

    • Sorts the position array in ascending order.

    • Initializes the search space with start_index = 1 (minimum possible force) and end_index = (position[-1] - position[0]) // (m - 1) (maximum possible average distance rounded down).

  3. Main Loop (Binary Search):

    • While start_index < end_index:

      • Calculates the mid_index with a slight bias towards the right to ensure progress.

      • Calls can_place_balls with mid_index as the minimum distance.

      • If can_place_balls returns True:

        • Updates start_index to mid_index, as a larger minimum distance might be possible.

      • Otherwise:

        • Updates end_index to mid_index - 1, as the minimum distance must be smaller.

  4. Result Calculation/Return:

    • After the loop ends, start_index will have converged to the maximum possible minimum distance, which is returned.

Complexity Analysis

Time Complexity:

  • , where is the number of positions and range is .

  • This comes from two main operations:

    1. Sorting the positions:

    2. Binary search with ball placement check: iterations, each doing an check.

  • Combined, this gives

Space Complexity:

  • , where is the number of positions.

  • This is due to the space required for sorting the position array, as Python's sort() method uses TimSort, which can require additional space in the worst case.

  • The rest of the algorithm uses only a constant amount of extra space.

Example

Input: position = [64, 16, 128, 8, 2, 32, 1, 4], m = 4

Step-by-Step Walkthrough:

  1. Initialization:

    • The position list is sorted: [1, 2, 4, 8, 16, 32, 64, 128]

    • start_index is set to 1 (minimum possible distance)

    • end_index is set to (128 - 1) // (4 - 1) = 42 (maximum possible average distance)

  2. Binary Search Loop:

    • While 1 < 42 (Iteration 1):

      • mid_index = 1 + (1 + 42) // 2 = 22

      • can_place_balls(22) returns True

        • Balls are placed at positions 1, 32, 64, 128

      • start_index is updated to 22

    • While 22 < 42 (Iteration 2):

      • mid_index = 1 + (22 + 42) // 2 = 33

      • can_place_balls(33) returns False

        • Only 3 balls can be placed (at 1, 64, 128)

      • end_index is updated to 32

    • Iteration 3:

      • mid_index = 1 + (22 + 32) // 2 = 28

      • can_place_balls(28) returns True

        • Balls are placed at positions 1, 32, 64, 128

      • start_index is updated to 28

    • Iteration 4:

      • mid_index = 1 + (28 + 32) // 2 = 31

      • can_place_balls(31) returns True

        • Balls are placed at positions 1, 32, 64, 128

      • start_index is updated to 31

    • Iteration 5:

      • mid_index = 1 + (31 + 32) // 2 = 32

      • can_place_balls(32) returns False

        • Only 3 balls can be placed (at 1, 64, 128)

      • end_index is updated to 31

  3. Loop Termination:

    • The loop terminates when start_index (31) is no longer less than end_index (31)

  4. Visual Aid: Iteration Summary Table

    Iteration

    Start Index

    End Index

    Mid Index

    Can Place

    1

    1

    42

    22

    True

    2

    22

    42

    33

    False

    3

    22

    32

    28

    True

    4

    28

    32

    31

    True

    5

    31

    32

    32

    False

  5. Result Calculation/Final Steps:

    • The algorithm returns start_index, which is 31

    • This means the maximum possible minimum distance between any two balls is 31

The final result indicates that we can place four balls in the baskets such that the minimum distance between any two balls is 31, and this is the maximum possible minimum distance we can achieve. We can place them at positions 1, 32, 64, and 128. The distance between each position is 31, 32, and 64, respectively; hence, the minimum distance is 31.

June 21 -> 1052. Grumpy Bookstore Owner

There is a bookstore owner that has a store open for n minutes. Every minute, some number of customers enters the store. You are given an integer array customers of length n where customers[i] is the number of the customer that enters the store at the start of the ith minute and all those customers leave after the end of that minute.

In some minutes, the bookstore owner is grumpy. You are given a binary array grumpy where grumpy[i] is 1 if the bookstore owner is grumpy during the ith minute, and is 0 otherwise.

When the bookstore owner is grumpy, the customers of that minute are not satisfied, otherwise, they are satisfied.

The bookstore owner knows a secret technique to keep themselves not grumpy for minutes consecutive minutes, but can only use it once.

Return the maximum number of customers that can be satisfied throughout the day.

Example 1:

  • Input: customers = [1,0,1,2,1,1,7,5], grumpy = [0,1,0,1,0,1,0,1], minutes = 3

  • Output: 16

  • Explanation: The bookstore owner keeps themselves not grumpy for the last 3 minutes. The maximum number of customers that can be satisfied = 1 + 1 + 1 + 1 + 7 + 5 = 16.

Example 2:

  • Input: customers = [1], grumpy = [0], minutes = 1

  • Output: 1

Constraints:

  • n == customers.length == grumpy.length

  • 1 <= minutes <= n <= 2 * 10^4

  • 0 <= customers[i] <= 1000

  • grumpy[i] is either 0 or 1.

Approach 1: Sliding Window

def maxSatisfied1(customers: List[int], grumpy: List[int], minutes: int) -> int: """ Calculates the maximum number of satisfied customers in a bookstore given a limited period where the owner can avoid being grumpy. The function first calculates the number of customers that are normally satisfied when the owner is not grumpy. It then uses a sliding window technique to find the period during which the owner can suppress their grumpiness to maximize the number of additionally satisfied customers. This is achieved by iterating through the customers and grumpy lists, keeping track of the maximum number of potentially satisfied customers over any given period of 'minutes' length. There is a slight optimization by combining the normal and additional satisfaction calculations into the same loop. The time complexity of this solution is O(n), where `n` is the length of the customers list, because it processes each element of the list a constant number of times. The space complexity is O(1), since it uses a fixed amount of additional space regardless of the input size. """ satisfied_customers = 0 potential_customers = 0 # Calculate initial normal and potential satisfaction for the first # 'minutes' window for customer, grump in zip(customers[:minutes], grumpy[:minutes]): if grump: potential_customers += customer else: satisfied_customers += customer max_potential_customers = potential_customers # Sliding window to find the maximum additional satisfaction over any # 'minutes' period (plus normal satisfaction) for minute in range(minutes, len(customers)): if grumpy[minute]: potential_customers += customers[minute] else: satisfied_customers += customers[minute] # Remove unsatisfied customers that are outside the current window if grumpy[minute - minutes]: potential_customers -= customers[minute - minutes] max_potential_customers = max(max_potential_customers, potential_customers) return satisfied_customers + max_potential_customers

Understanding the Core Idea

The central concept of this solution is to leverage a sliding window technique to maximize customer satisfaction given the constraint of a limited period where the bookstore owner can suppress their grumpiness. The solution exploits the pattern in the data by separating normally satisfied customers from potentially satisfied customers during the owner's non-grumpy period.

  • Two-part Satisfaction Calculation: The solution divides customer satisfaction into two parts: those naturally satisfied when the owner isn't grumpy, and additional customers that could be satisfied during the "non-grumpy" window.

  • Sliding Window for Optimization: A sliding window of size minutes is used to find the optimal period for the owner to use their "non-grumpy" technique, maximizing additional customer satisfaction.

  • Single-pass Approach: The solution cleverly combines the calculation of normally satisfied customers and the sliding window technique into a single pass through the data, optimizing for efficiency.

Code Walkthrough

  1. Initialization:

    satisfied_customers = 0 potential_customers = 0

    These variables track normally satisfied customers and potential additional satisfied customers in the current window.

  2. Initial Window Calculation:

    for customer, grump in zip(customers[:minutes], grumpy[:minutes]): if grump: potential_customers += customer else: satisfied_customers += customer

    This loop calculates initial satisfaction for the first minutes window, separating normally satisfied from potential additional customers.

  3. Maximum Potential Tracking:

    max_potential_customers = potential_customers

    Initializes the maximum potential additional satisfied customers with the first window's value.

  4. Sliding Window Implementation:

    for minute in range(minutes, len(customers)):

    This loop slides the window through the remaining data, one customer at a time.

  5. Window Update:

    if grumpy[minute]: potential_customers += customers[minute] else: satisfied_customers += customers[minute]

    Updates satisfaction counts for the new customer entering the window.

  6. Window Tail Removal:

    if grumpy[minute - minutes]: potential_customers -= customers[minute - minutes]

    Removes the contribution of the customer leaving the window if they were part of the potential satisfaction count.

  7. Maximum Update:

    max_potential_customers = max(max_potential_customers, potential_customers)

    Updates the maximum potential additional satisfied customers if the current window is better.

  8. Result Calculation/Return:

    return satisfied_customers + max_potential_customers

    Combines normally satisfied customers with the maximum additional satisfied customers from the optimal "non-grumpy" window.

Complexity Analysis

Time Complexity:

  • ), where is the number of minutes (length of the customers list). This is because the algorithm makes a single pass through the data, performing constant-time operations at each step.

Space Complexity:

  • , as the algorithm uses a fixed amount of additional space (a few variables) regardless of the input size. It doesn't create any data structures that scale with the input.

Example

Input:

customers = [1, 0, 1, 2, 1, 1, 7, 5] grumpy = [0, 1, 0, 1, 0, 1, 0, 1] minutes = 3

Step-by-Step Walkthrough:

  1. Initialization:

    • satisfied_customers = 0

    • potential_customers = 0

  2. Main Loop (Initial Window Calculation):

    Since minutes = 3, the initial window covers the first three minutes:

    • Iteration 1 (minute 0):

      • customer = 1, grump = 0

      • Owner is not grumpy, so add to satisfied_customers

      • satisfied_customers = 1, potential_customers = 0

    • Iteration 2 (minute 1):

      • customer = 0, grump = 1

      • Owner is grumpy, but no customers to add to potential_customers

      • satisfied_customers = 1, potential_customers = 0

    • Iteration 3 (minute 2):

      • customer = 1, grump = 0

      • Owner is not grumpy, so add to satisfied_customers

      • satisfied_customers = 2, potential_customers = 0

    • Initial Window Summary:

      [1, 0, 1, 2, 1, 1, 7, 5] Customers [0, 1, 0, 1, 0, 1, 0, 1] Grumpy [S P S] Window S: Satisfied = 2, P: Potential = 0, Max Potential = 0
  3. Sliding Window Loop:

    • Iteration 4 (minute 3):

      • customer = 2, grump = 1

      • Owner is grumpy, add customers to potential_customers

        • potential_customers = 0 + 2 = 2

      • Current window: [1, 3]

      • Update max_potential_customers = 2

        [1, 0, 1, 2, 1, 1, 7, 5] Customers [0, 1, 0, 1, 0, 1, 0, 1] Grumpy [P S P] Window S: Satisfied = 2, P: Potential = 2, Max Potential = 2
    • Iteration 5 (minute 4):

      • customer = 1, grump = 0

      • Owner is not grumpy, add customer to satisfied_customers

        • satisfied_customers = 2 + 1 = 3

      • Current window: [2, 4]

      • No change to potential_customers

        [1, 0, 1, 2, 1, 1, 7, 5] Customers [0, 1, 0, 1, 0, 1, 0, 1] Grumpy [S P S] Window S: Satisfied = 3, P: Potential = 2, Max Potential = 2
    • Iteration 6 (minute 5):

      • customer = 1, grump = 1

      • Owner is grumpy, add customers to potential_customers

        • potential_customers = 2 + 1 = 3

      • Current window: [3, 5]

      • Update max_potential_customers = 3

        [1, 0, 1, 2, 1, 1, 7, 5] Customers [0, 1, 0, 1, 0, 1, 0, 1] Grumpy [P S P] Window S: Satisfied = 3, P: Potential = 3, Max Potential = 3
    • Iteration 7 (minute 6):

      • customer = 7, grump = 0

      • Owner is not grumpy, add to satisfied_customers

      • satisfied_customers = 10

      • Current window: [4, 6]

        • Remove 2 from potential_customers (from minute 3)

        • potential_customers = 3 - 2 = 1

        • The maximum potential customers remain at 3

        [1, 0, 1, 2, 1, 1, 7, 5] Customers [0, 1, 0, 1, 0, 1, 0, 1] Grumpy [S P S] Window S: Satisfied = 10, P: Potential = 1, Max Potential = 3
    • Iteration 8 (minute 7):

      • customer = 5, grump = 1

      • Owner is grumpy, add customers to potential_customers

      • potential_customers = 5 + 1 = 6

      • Current window: [5, 7]

      • Update max_potential_customers = 6

        [1, 0, 1, 2, 1, 1, 7, 5] Customers [0, 1, 0, 1, 0, 1, 0, 1] Grumpy [P S P] Window S: Satisfied = 10, P: Potential = 6, Max Potential = 6
  4. Visual Aid:

    Minute

    Window

    Customers

    Grumpy

    Satisfied Customers

    Potential Customers

    Max Potential

    0

    -

    1

    0

    1

    0

    0

    1

    -

    0

    1

    1

    0

    0

    2

    [0, 2]

    1

    0

    2

    0

    0

    3

    [1, 3]

    2

    1

    2

    2

    2

    4

    [2, 4]

    1

    0

    3

    2

    2

    5

    [3, 5]

    1

    1

    3

    3

    3

    6

    [4, 6]

    7

    0

    10

    1

    3

    7

    [5, 7]

    5

    1

    10

    6

    6

  5. Result Calculation:

    • Final satisfied_customers = 10

    • Final max_potential_customers = 6

    • Total satisfied customers = satisfied_customers + max_potential_customers = 10 + 6 = 16

The algorithm returns 16 as the maximum number of customers that can be satisfied throughout the day.

June 22 -> 1248. Count Number of Nice Subarrays

Given an array of integers nums and an integer k. A continuous subarray is called nice if there are k odd numbers on it.

Return the number of nice subarrays.

Example 1:

  • Input: nums = [1,1,2,1,1], k = 3

  • Output: 2

  • Explanation: The only subarrays with three odd numbers are [1,1,2,1] and [1,2,1,1].

Example 2:

  • Input: nums = [2,4,6], k = 1

  • Output: 0

  • Explanation: There are no odd numbers in the array.

Example 3:

  • Input: nums = [2,2,2,1,2,2,1,2,2,2], k = 2

  • Output: 16

Constraints:

  • 1 <= nums.length <= 50000

  • 1 <= nums[i] <= 10^5

  • 1 <= k <= nums.length

Approach 1: Sliding Window and Counting

def numberOfSubarrays1(nums: List[int], k: int) -> int: """ Counts the number of continuous subarrays within `nums` that contain exactly `k` odd numbers. This function uses a sliding window approach combined with a counting technique to efficiently find nice subarrays. It maintains a window that expands when odd numbers are encountered and contracts when the odd count exceeds k. The key insight is that once a nice subarray is found, any extension of it that doesn't include new odd numbers is also nice. This allows for efficient counting without explicitly listing all subarrays. The time complexity is O(n), where n is the length of nums. Each element is processed at most twice: once in the outer loop and potentially once in the inner while loop. The space complexity is O(1) as it uses only a constant amount of extra space regardless of the input size. """ total_nice_subarrays = 0 current_nice_subarrays = 0 odd_count = 0 start_index = 0 for num in nums: if num % 2 == 1: odd_count += 1 current_nice_subarrays = 0 # When k odd numbers are found, count nice subarrays and contract the # window while odd_count == k: current_nice_subarrays += 1 odd_count -= nums[start_index] % 2 start_index += 1 # Add the count of nice subarrays ending at the current position total_nice_subarrays += current_nice_subarrays return total_nice_subarrays

Understanding the Core Idea

The central concept of this solution is to use a sliding window technique combined with a counting approach to efficiently find nice subarrays. The solution exploits the fact that once a nice subarray is found, any extension of it that doesn't include new odd numbers is also nice.

  • Sliding Window: The solution maintains a window that expands when odd numbers are encountered and contracts when the odd count exceeds k.

  • Incremental Counting: Instead of recounting for each subarray, it incrementally updates the count of nice subarrays.

Code Walkthrough

  1. Initialization:

    total_nice_subarrays = 0 current_nice_subarrays = 0 odd_count = 0 start_index = 0

    The function initializes several variables:

    • total_nice_subarrays to store the final count of nice subarrays.

    • current_nice_subarrays to track the number of valid subarrays ending at the current position.

    • odd_count to count the number of odd numbers in the current window.

    • start_index to mark the beginning of the current window.

  2. Iterate Through Array:

    for num in nums:

    This loop processes each number in the input array.

  3. Handle Odd Numbers:

    if num % 2 == 1: odd_count += 1 current_nice_subarrays = 0

    When an odd number is encountered, the odd count is incremented and the current nice subarray count is reset.

  4. Window Adjustment and Counting:

    while odd_count == k: current_nice_subarrays += 1 odd_count -= nums[start_index] % 2 start_index += 1

    When k odd numbers are found, this loop counts nice subarrays by moving the start of the window. It increments the count for each valid start position and stops when an odd number is removed from the window.

  5. Accumulate Nice Subarray Count:

    total_nice_subarrays += current_nice_subarrays

    The count of nice subarrays ending at the current position is added to the total.

  6. Return Result:

    return total_nice_subarrays

    The function returns the total count of nice subarrays found.

Complexity Analysis

Time Complexity:

  • , where is the length of the input array nums. This is because each element in the array is processed at most twice: once in the outer for loop and potentially once in the inner while loop.

Space Complexity:

  • , as the algorithm uses only a constant amount of extra space regardless of the input size. It maintains a few variables (total_nice_subarrays, current_nice_subarrays, odd_count, start_index) that do not grow with the input size.

Example

Input:

nums = [2, 1, 2, 1, 2, 1, 2, 2] k = 2

Step-by-Step Walkthrough:

  1. Initialization:

    • total_nice_subarrays = 0: Keeps track of all nice subarrays found

    • current_nice_subarrays = 0: Counts nice subarrays ending at the current position

    • odd_count = 0: Tracks the number of odd integers in the current window

    • start_index = 0: Marks the start of the current window

  2. Main Loop (Iterating through nums):

    • Iteration 1 (num = 2):

      • 2 is even, so no changes to odd_count or current_nice_subarrays

      • No nice subarrays found yet

    • Iteration 2 (num = 1):

      • 1 is odd, so odd_count increases to 1

      • current_nice_subarrays resets to 0 (new odd number encountered)

      • Still no nice subarrays (need two odd numbers)

    • Iteration 3 (num = 2):

      • 2 is even, no changes to counters

      • No nice subarrays yet

    • Iteration 4 (num = 1):

      • 1 is odd, odd_count increases to 2

      • current_nice_subarrays resets to 0

      • odd_count == k, so we enter the while loop:

        1. current_nice_subarrays increases to 1

        2. Check if nums[0] (2) is odd - it's not, so odd_count remains 2

        3. start_index increases to 1

        4. Still odd_count == k, so loop continues

        5. current_nice_subarrays increases to 2

        6. nums[1] (1) is odd, so odd_count decreases to 1

        7. start_index increases to 2

      • total_nice_subarrays becomes 2

    • Iteration 5 (num = 2):

      • 2 is even, no changes to odd_count

      • total_nice_subarrays increases to 4 (2 + 2)

    • Iteration 6 (num = 1):

      • 1 is odd, odd_count increases to 2

      • current_nice_subarrays resets to 0

      • odd_count == k, enter while loop:

        1. current_nice_subarrays increases to 1

        2. nums[2] (2) is not odd, odd_count stays 2

        3. start_index increases to 3

        4. Still odd_count == k, continue loop

        5. current_nice_subarrays increases to 2

        6. nums[3] (1) is odd, odd_count decreases to 1

        7. start_index increases to 4

      • total_nice_subarrays becomes 6 (4 + 2)

    • Iteration 7 (num = 2):

      • 2 is even, no changes to odd_count

      • total_nice_subarrays increases to 8 (6 + 2)

    • Iteration 8 (num = 2):

      • 2 is even, no changes to odd_count

      • total_nice_subarrays increases to 10 (8 + 2)

  3. Loop Termination:

    • The loop ends after processing all elements in nums

    • Final state: total_nice_subarrays = 10, odd_count = 1, start_index = 4

  4. Result Calculation:

    • The final result is the value of total_nice_subarrays, which is 10

    • This represents the total number of subarrays containing exactly k = 2 odd numbers

Approach 2: Two-Pass and Counting

def numberOfSubarrays2(nums: List[int], k: int) -> int: """ Counts the number of continuous subarrays within `nums` that contain exactly `k` odd numbers. This function uses a two-pass approach to efficiently count nice subarrays. In the first pass, it counts the number of even integers between each pair of odd integers, storing these counts in the 'even_counts' list. The second pass uses these counts to calculate the total number of nice subarrays. This method leverages the fact that for each sequence of k odd numbers, the number of nice subarrays is the product of the number of even numbers before the first odd number and after the last odd number in the sequence. The time complexity is O(n), where n is the length of nums, as it makes two passes through the input list. The space complexity is O(1) as it uses only a constant amount of extra space regardless of the input size. """ even_counts = [] current_even_count = 1 for num in nums: if num % 2 == 0: current_even_count += 1 else: even_counts.append(current_even_count) current_even_count = 1 even_counts.append(current_even_count) total_nice_subarrays = 0 # Calculate nice subarrays by multiplying counts of even numbers # before and after each sequence of k odd numbers for left_even_count, right_even_count in zip(even_counts, even_counts[k:]): total_nice_subarrays += left_even_count * right_even_count return total_nice_subarrays

Understanding the Core Idea

The central concept of this solution is to leverage a two-pass approach to efficiently count nice subarrays. It exploits the structure of the array by focusing on the distribution of odd numbers and the even numbers between them.

  • Even Number Counting: The solution counts the number of even integers between each pair of odd integers, including the ends of the array.

  • Combinatorial Calculation: It uses these counts to calculate the total number of nice subarrays without explicitly listing them.

Code Walkthrough

  1. Initialization:

    even_counts = [] current_even_count, odd_count = 1, 0

    even_counts will store the count of even numbers between odd numbers, and current_even_count is initialized to 1 to account for the start of the array.

  2. Count Even Numbers and Identify Odd Numbers:

    for num in nums: if num % 2 == 0: current_even_count += 1 else: even_counts.append(current_even_count) current_even_count = 1 even_counts.append(current_even_count)

    This loop counts even numbers between odd numbers. When an odd number is encountered, the current count of even numbers is appended to even_counts, and the count is reset. The final even_counts.append(current_even_count) accounts for even numbers at the end of the array.

  3. Calculate Nice Subarrays:

    total_nice_subarrays = 0 for left_even_count, right_even_count in zip(even_counts, even_counts[k:]): total_nice_subarrays += left_even_count * right_even_count

    This loop calculates the number of nice subarrays. It pairs the count of even numbers before a sequence of k odd numbers with the count after, multiplying them to get the number of nice subarrays for that sequence.

  4. Return Result:

    return total_nice_subarrays

    The function returns the total count of nice subarrays found.

Complexity Analysis

Time Complexity:

  • , where n is the length of the input array nums. This is because the algorithm makes two passes through the data: one to count even numbers between odd numbers, and another to calculate the number of nice subarrays.

Space Complexity:

  • , as the algorithm uses only a constant amount of extra space regardless of the input size. It maintains a few variables (even_counts, current_even_count, odd_count) that do not grow with the input size.

Example

Input:

nums = [2, 1, 2, 1, 2, 1, 2, 2] k = 2

Step-by-Step Walkthrough:

  1. Initialization:

    • even_counts = []: List to store counts of consecutive even numbers

    • current_even_count = 1: Initialize count of the current sequence of even numbers

  2. First Pass: Counting Even Numbers

    • Iteration 1 (num = 2):

      • 2 is even, so current_even_count increases to 2

      • even_counts remains empty: []

    • Iteration 2 (num = 1):

      • 1 is odd, so we append current_even_count (2) to even_counts

      • Reset current_even_count to 1

      • even_counts becomes [2]

    • Iteration 3 (num = 2):

      • 2 is even, so current_even_count increases to 2

      • even_counts remains [2]

    • Iteration 4 (num = 1):

      • 1 is odd, so we append current_even_count (2) to even_counts

      • Reset current_even_count to 1

      • even_counts becomes [2, 2]

    • Iteration 5 (num = 2):

      • 2 is even, so current_even_count increases to 2

      • even_counts remains [2, 2]

    • Iteration 6 (num = 1):

      • 1 is odd, so we append current_even_count (2) to even_counts

      • Reset current_even_count to 1

      • even_counts becomes [2, 2, 2]

    • Iteration 7 (num = 2):

      • 2 is even, so current_even_count increases to 2

      • even_counts remains [2, 2, 2]

    • Iteration 8 (num = 2):

      • 2 is even, so current_even_count increases to 3

      • even_counts remains [2, 2, 2]

  3. Loop Termination:

    • After processing all numbers, append final current_even_count (3) to even_counts

    • Final state of even_counts: [2, 2, 2, 3]

  4. Second Pass: Calculating Nice Subarrays

    • Iteration 1:

      • left_even_count = 2 (from even_counts[0])

      • right_even_count = 2 (from even_counts[2])

      • Calculate nice subarrays: 2 * 2 = 4

      • total_nice_subarrays becomes 4

    • Iteration 2:

      • left_even_count = 2 (from even_counts[1])

      • right_even_count = 3 (from even_counts[3])

      • Calculate nice subarrays: 2 * 3 = 6

      • total_nice_subarrays increases to 10 (4 + 6)

  5. Visual Aid:

    Iteration Summary Table:

    Subarray

    Left Even Count

    Right Even Count

    Nice Subarrays

    Total Nice Subarrays

    1

    2

    2

    4

    4

    2

    2

    3

    6

    10

  6. Result Calculation/Final Steps:

    • The final result is the value of total_nice_subarrays, which is 10

    • This represents the total number of subarrays containing exactly two odd numbers

June 23 -> 1438. Longest Continuous Subarray With Absolute Diff Less Than or Equal to Limit

Given an array of integers nums and an integer limit, return the size of the longest non-empty subarray such that the absolute difference between any two elements of this subarray is less than or equal to limit.

Example 1:

  • Input: nums = [8,2,4,7], limit = 4

  • Output: 2

  • Explanation: All subarrays are:

    • [8] with maximum absolute diff |8-8| = 0 <= 4.

    • [8,2] with maximum absolute diff |8-2| = 6 > 4.

    • [8,2,4] with maximum absolute diff |8-2| = 6 > 4.

    • [8,2,4,7] with maximum absolute diff |8-2| = 6 > 4.

    • [2] with maximum absolute diff |2-2| = 0 <= 4.

    • [2,4] with maximum absolute diff |2-4| = 2 <= 4.

    • [2,4,7] with maximum absolute diff |2-7| = 5 > 4.

    • [4] with maximum absolute diff |4-4| = 0 <= 4.

    • [4,7] with maximum absolute diff |4-7| = 3 <= 4.

    • [7] with maximum absolute diff |7-7| = 0 <= 4.

    • Therefore, the size of the longest subarray is 2.

Example 2:

  • Input: nums = [10,1,2,4,7,2], limit = 5

  • Output: 4

  • Explanation: The subarray [2,4,7,2] is the longest since the maximum absolute diff is |2-7| = 5 <= 5.

Example 3:

  • Input: nums = [4,2,2,2,4,4,2,2], limit = 0

  • Output: 3

Constraints:

  • 1 <= nums.length <= 10^5

  • 1 <= nums[i] <= 10^9

  • 0 <= limit <= 10^9

Approach 1: Sliding Window with Sorted List

def longestSubarray1(nums: List[int], limit: int) -> int: """ Finds the length of the longest subarray where the absolute difference between any two elements is at most 'limit'. This function uses a sliding window approach combined with a sorted list to track the elements in the current window. It iterates through the array once, maintaining a sorted representation of the current window. When a new element is added, it's inserted into its correct position in the sorted list. If the difference between the maximum and minimum elements (which are always at the ends of the sorted list) exceeds the limit, the window is shrunk from the left by removing the leftmost element from the sorted list. The time complexity of this solution is O(n^2) in the worst case, where `n` is the length of the input array. This is because for each element (O(n)), we might need to perform an insertion into a sorted list (O(n) using bisect.insort). However, in practice, it can be much faster if the subarrays tend to be short. The space complexity is O(n), as in the worst case, the sorted list might contain all elements of the input array. """ left_index = 0 sorted_nums = [] for num in nums: # Insert the new element in sorted position bisect.insort(sorted_nums, num) if sorted_nums[-1] - sorted_nums[0] > limit: # If the limit is exceeded, remove the leftmost element from the # window sorted_nums.pop(bisect.bisect(sorted_nums, nums[left_index]) - 1) left_index += 1 return len(nums) - left_index

Understanding the Core Idea

The central concept of this solution is to leverage a sliding window approach combined with a sorted list to efficiently find the longest subarray that satisfies the given condition. This technique allows us to maintain a dynamic view of the current subarray while keeping track of its minimum and maximum elements.

  • Sliding Window: The solution uses two pointers (left_index and the current iteration index) to define a window that expands and contracts as we iterate through the array.

  • Sorted List: A sorted list is maintained to represent the elements in the current window, allowing for quick access to the minimum and maximum elements.

  • Dynamic Window Adjustment: As new elements are added, the window expands. If the condition is violated, the window contracts from the left side.

Code Walkthrough

  1. Initialization:

    left_index = 0 sorted_nums = []

    The left_index variable keeps track of the left boundary of our sliding window. sorted_nums is an empty list that will store the elements of the current window in sorted order.

  2. Iterating Through the Array:

    for num in nums:

    This loop processes each element of the input array nums sequentially.

  3. Inserting New Element:

    bisect.insort(sorted_nums, num)

    The current element num is inserted into sorted_nums at its correct position, maintaining the sorted order. bisect.insort is used for efficient insertion in a sorted list.

  4. Checking and Adjusting Window:

    if sorted_nums[-1] - sorted_nums[0] > limit: sorted_nums.pop(bisect.bisect(sorted_nums, nums[left_index]) - 1) left_index += 1

    This block checks if the difference between the maximum (last element) and minimum (first element) of the sorted list exceeds the limit. If so, it removes the leftmost element of the window from sorted_nums and moves the left boundary of the window.

  5. Result Calculation/Return:

    return len(nums) - left_index

    The function returns the length of the longest valid subarray, which is the total length of the array minus the left index of the sliding window.

Complexity Analysis

Time Complexity:

  • , where is the length of the input array nums. This is because for each element in the array , we perform an insertion into a sorted list using bisect.insort ( in the worst case). However, in practice, it can be much faster if the subarrays tend to be short.

Space Complexity:

  • , where is the length of the input array nums. This is because in the worst case, the sorted_nums list might contain all elements of the input array.

Example

Input:

nums = [10, 1, 2, 4, 7, 2] limit = 5

Step-by-Step Walkthrough:

  1. Initialization:

    • Set left_index = 0

    • Initialize empty sorted_nums list

  2. Main Loop (Iterate through nums):

    • Iteration 1 (nums[0] = 10):

      • Insert 10 into sorted_nums: sorted_nums = [10]

      • No window adjustment needed (10 - 10 = 0 <= 5)

      • Current subarray length: 1

    • Iteration 2 (nums[1] = 1):

      • Insert 1 into sorted_nums: sorted_nums = [1, 10]

      • Window adjustment: 10 - 1 = 9 > 5

        • Remove 10 from sorted_nums: sorted_nums = [1]

        • Update left_index to 1

      • Current subarray length: 1

    • Iteration 3 (nums[2] = 2):

      • Insert 2 into sorted_nums: sorted_nums = [1, 2]

      • No window adjustment needed (2 - 1 = 1 <= 5)

      • Current subarray length: 2

    • Iteration 4 (nums[3] = 4):

      • Insert 4 into sorted_nums: sorted_nums = [1, 2, 4]

      • No window adjustment needed (4 - 1 = 3 <= 5)

      • Current subarray length: 3

    • Iteration 5 (nums[4] = 7):

      • Insert 7 into sorted_nums: sorted_nums = [1, 2, 4, 7]

      • Window adjustment: 7 - 1 = 6 > 5

        • Remove 1 from sorted_nums: sorted_nums = [2, 4, 7]

        • Update left_index to 2

      • Current subarray length: 3

    • Iteration 6 (nums[5] = 2):

      • Insert 2 into sorted_nums: sorted_nums = [2, 2, 4, 7]

      • No window adjustment needed (7 - 2 = 5 <= 5)

      • Current subarray length: 4

  3. Loop Termination:

    • The loop ends after processing all elements in nums

    • Final state: left_index = 2

  4. Visual Aid: Iteration Summary Table

    Iteration

    Current Number

    Left Index

    Sorted Nums

    Current Length

    1

    10

    0

    [10]

    1

    2

    1

    1

    [1]

    1

    3

    2

    1

    [1, 2]

    2

    4

    4

    1

    [1, 2, 4]

    3

    5

    7

    2

    [2, 4, 7]

    3

    6

    2

    2

    [2, 2, 4, 7]

    4

  5. Result Calculation/Final Steps:

    • The algorithm calculates the result as len(nums) - left_index

    • Result = 6 - 2 = 4

The longest subarray where the absolute difference between any two elements is at most 5 has a length of 4, which corresponds to the subarray [2, 4, 7, 2].

Approach 2: Sliding Window with Heaps

def longestSubarray2(nums: List[int], limit: int) -> int: """ Finds the length of the longest subarray where the absolute difference between any two elements is at most 'limit'. This function uses a sliding window approach combined with two heaps (min and max) to efficiently track the minimum and maximum elements in the current window. As we iterate through the array, we expand the window to the right. If the difference between the max and min elements exceeds the limit, we shrink the window from the left until the condition is satisfied again. The heaps allow us to quickly retrieve and update the min and max elements as the window changes. The time complexity of this solution is O(n log n), where n is the length of the input array. This is because we perform n iterations, and in each iteration, we might need to perform heap operations (push and pop) which take O(log n) time. The space complexity is O(n) as in the worst case, all elements might be stored in the heaps. """ max_heap = [] min_heap = [] left_index = 0 max_length = 0 for right_index, num in enumerate(nums): # Push the current element into the heaps heapq.heappush(max_heap, (-num, right_index)) # Negative for max heap heapq.heappush(min_heap, (num, right_index)) # Shrink the window if the difference exceeds the limit while -max_heap[0][0] - min_heap[0][0] > limit: left_index = min(max_heap[0][1], min_heap[0][1]) + 1 # Remove elements outside the current window while max_heap[0][1] < left_index: heapq.heappop(max_heap) while min_heap[0][1] < left_index: heapq.heappop(min_heap) max_length = max(max_length, right_index - left_index + 1) return max_length

Understanding the Core Idea

The central concept of this solution is to leverage a sliding window approach combined with two heaps (min-heap and max-heap) to efficiently find the longest subarray that satisfies the given condition. This technique allows us to maintain a dynamic view of the current subarray while keeping track of its minimum and maximum elements in a more efficient manner.

  • Sliding Window: The solution uses two pointers (left_index and right_index) to define a window that expands and contracts as we iterate through the array.

  • Dual Heap Structure: Two heaps (min-heap and max-heap) are used to efficiently track the minimum and maximum elements in the current window.

  • Dynamic Window Adjustment: As new elements are added, the window expands. If the condition is violated, the window contracts from the left side until the condition is satisfied again.

Code Walkthrough

  1. Initialization:

    max_heap = [] min_heap = [] left_index = 0 max_length = 0

    Two empty heaps are initialized: max_heap for maximum elements and min_heap for minimum elements. left_index tracks the left boundary of the sliding window, and max_length stores the length of the longest valid subarray found so far.

  2. Iterating Through the Array:

    for right_index, num in enumerate(nums):

    This loop processes each element of the input array nums sequentially, using right_index as the current position.

  3. Adding Elements to Heaps:

    heapq.heappush(max_heap, (-num, right_index)) heapq.heappush(min_heap, (num, right_index))

    The current element num is added to both heaps. Note that for max_heap, we use the negative of num to create a max-heap using Python's min-heap implementation.

  4. Adjusting Window:

    while -max_heap[0][0] - min_heap[0][0] > limit: left_index = min(max_heap[0][1], min_heap[0][1]) + 1 while max_heap[0][1] < left_index: heapq.heappop(max_heap) while min_heap[0][1] < left_index: heapq.heappop(min_heap)

    This block checks if the difference between the maximum and minimum elements exceeds the limit. If so, it moves the left boundary of the window and removes elements from the heaps that are now outside the window.

  5. Updating Maximum Length:

    max_length = max(max_length, right_index - left_index + 1)

    After each iteration, update the max_length if the current window is longer than the previous maximum.

  6. Result Calculation/Return:

    return max_length

    The function returns the length of the longest valid subarray.

Complexity Analysis

Time Complexity:

  • , where is the length of the input array nums. This is because for each element in the array , we perform heap operations (push and pop) which take time. In the worst case, we might need to perform these operations for each element.

Space Complexity:

  • , where is the length of the input array nums. This is because in the worst case, all elements might be stored in the heaps simultaneously.

Example

Input:

nums = [10, 1, 2, 4, 7, 2] limit = 5

Step-by-Step Walkthrough:

  1. Initialization:

    • Initialize empty max_heap and min_heap

    • Set left_index = 0 and max_length = 0

  2. Main Loop (Iterate through nums):

    • Iteration 1 (nums[0] = 10):

      • Push (10, 0) to max_heap as (-10, 0) and to min_heap as (10, 0)

        • The heaps become:

          • max_heap = [(-10, 0)]

          • min_heap = [(10, 0)]

      • No window adjustment needed (10 - 10 = 0 <= 5)

      • Update max_length to 1

    • Iteration 2 (nums[1] = 1):

      • Push (1, 1) to both heaps

        • The heaps become:

          • max_heap = [(-10, 0), (-1, 1)]

          • min_heap = [(1, 1), (10, 0)]

      • Window adjustment: 10 - 1 = 9 > 5

        • Update left_index to 1

        • Remove (-10, 0) from max_heap: max_heap = [(-1, 1)]

      • max_length remains 1

    • Iteration 3 (nums[2] = 2):

      • Push (2, 2) to both heaps

        • The heaps become:

          • max_heap = [(-1, 1), (-2, 2)]

          • min_heap = [(1, 1), (2, 2), (10, 0)]

      • No window adjustment needed (2 - 1 = 1 <= 5)

      • Update max_length to 2

    • Iteration 4 (nums[3] = 4):

      • Push (4, 3) to both heaps

        • The heaps become:

          • max_heap = [(-1, 1), (-2, 2), (-4, 3)]

          • min_heap = [(1, 1), (2, 2), (4, 3), (10, 0)]

      • No window adjustment needed (4 - 1 = 3 <= 5)

      • Update max_length to 3

    • Iteration 5 (nums[4] = 7):

      • Push (7, 4) to both heaps

        • The heaps become:

          • max_heap = [(-1, 1), (-2, 2), (-4, 3), (-7, 4)]

          • min_heap = [(1, 1), (2, 2), (4, 3), (7, 4), (10, 0)]

      • Window adjustment: 7 - 1 = 6 > 5

        • Update left_index to 2

        • Remove (1, 1) from min_heap: min_heap = [(2, 2), (4, 3), (7, 4), (10, 0)]

      • max_length remains 3

    • Iteration 6 (nums[5] = 2):

      • Push (2, 5) to both heaps

        • The heaps become:

          • max_heap = [(-2, 2), (-4, 3), (-7, 4), (-2, 5)]

          • min_heap = [(2, 2), (4, 3), (7, 4), (10, 0)]

      • No window adjustment needed (7 - 2 = 5 <= 5)

      • Update max_length to 4

  3. Loop Termination:

    • The loop ends after processing all elements in nums

    • Final state: left_index = 2, max_length = 4

  4. Visual Aid: Iteration Summary Table

    Iteration

    Current Number

    Left Index

    Current Length

    Max Length

    Max Heap (top 3)

    Min Heap (top 3)

    1

    10

    0

    1

    1

    [(10, 0)]

    [(10, 0)]

    2

    1

    1

    1

    1

    [(1, 1)]

    [(1, 1), (10, 0)]

    3

    2

    1

    2

    2

    [(2, 2), (1, 1)]

    [(1, 1), (10, 0), (2, 2)]

    4

    4

    1

    3

    3

    [(4, 3), (1, 1), (2, 2)]

    [(1, 1), (4, 3), (2, 2)]

    5

    7

    2

    3

    3

    [(7, 4), (4, 3), (2, 2)]

    [(2, 2), (4, 3), (7, 4)]

    6

    2

    2

    4

    4

    [(7, 4), (4, 3), (2, 2)]

    [(2, 2), (2, 5), (7, 4)]

  5. Result Calculation/Final Steps:

    • The algorithm maintains the max_length variable throughout the iterations

    • After processing all elements, max_length holds the length of the longest valid subarray

    • The final result is max_length = 4, corresponding to the subarray [2, 4, 7, 2]

Approach 3: Sliding Window with Monotonic Queues

def longestSubarray3(nums: List[int], limit: int) -> int: """ Finds the length of the longest subarray where the absolute difference between any two elements is at most 'limit'. This function uses a sliding window approach with two monotonic queues (deques) to efficiently track the maximum and minimum elements in the current window. As we iterate through the array, we maintain a monotonically decreasing queue for maximum values and a monotonically increasing queue for minimum values. This allows us to quickly access the maximum and minimum elements of the current window at any time. When the difference between these extremes exceeds the limit, we shrink the window from the left, updating the queues accordingly. The time complexity of this solution is O(n), where n is the length of the input array. This is because each element is pushed and popped at most once from each deque, and all operations on deques are O(1). The space complexity is O(n) in the worst case, as the deques might need to store all elements of the input array. """ max_queue = deque() min_queue = deque() left_index = 0 for right_index, num in enumerate(nums): # Maintain monotonically decreasing queue for maximum values while max_queue and num > max_queue[-1]: max_queue.pop() max_queue.append(num) # Maintain monotonically increasing queue for minimum values while min_queue and num < min_queue[-1]: min_queue.pop() min_queue.append(num) # Shrink the window if the difference exceeds the limit if max_queue[0] - min_queue[0] > limit: if max_queue[0] == nums[left_index]: max_queue.popleft() if min_queue[0] == nums[left_index]: min_queue.popleft() left_index += 1 return len(nums) - left_index

Understanding the Core Idea

The central concept of this solution is to leverage a sliding window approach combined with two monotonic queues (implemented using deques) to efficiently find the longest subarray that satisfies the given condition. This technique allows us to maintain a dynamic view of the current subarray while keeping track of its minimum and maximum elements in a highly efficient manner.

  • Sliding Window: The solution uses two pointers (left_index and right_index) to define a window that expands and contracts as we iterate through the array.

  • Monotonic Queues: Two deques are used: a monotonically decreasing queue for maximum values and a monotonically increasing queue for minimum values.

  • Dynamic Window Adjustment: As new elements are added, the window expands. If the condition is violated, the window contracts from the left side.

Code Walkthrough

  1. Initialization:

    max_queue = deque() min_queue = deque() left_index = 0

    Two empty deques are initialized: max_queue for maximum elements and min_queue for minimum elements. left_index tracks the left boundary of the sliding window.

  2. Iterating Through the Array:

    for right_index, num in enumerate(nums):

    This loop processes each element of the input array nums sequentially, using right_index as the current position.

  3. Maintaining Monotonic Queues:

    while max_queue and num > max_queue[-1]: max_queue.pop() max_queue.append(num) while min_queue and num < min_queue[-1]: min_queue.pop() min_queue.append(num)

    These blocks maintain the monotonic property of the queues. For max_queue, we remove elements smaller than the current number from the right side before adding the current number. For min_queue, we remove elements larger than the current number.

  4. Adjusting Window:

    if max_queue[0] - min_queue[0] > limit: if max_queue[0] == nums[left_index]: max_queue.popleft() if min_queue[0] == nums[left_index]: min_queue.popleft() left_index += 1

    This block checks if the difference between the maximum and minimum elements exceeds the limit. If so, it moves the left boundary of the window and updates the queues if necessary.

  5. Result Calculation/Return:

    return len(nums) - left_index

    The function returns the length of the longest valid subarray, which is the total length of the array minus the final position of the left index.

Complexity Analysis

Time Complexity:

  • , where is the length of the input array nums. This is because each element is processed once and all operations on the deques (append, pop, pop-left) are . Although there are nested loops, each element is pushed and popped at most once from each deque, ensuring linear time complexity.

Space Complexity:

  • , where is the length of the input array nums. This is because in the worst case, the deques might need to store all elements of the input array simultaneously.

Example

Input:

nums = [10, 1, 2, 4, 7, 2] limit = 5

Step-by-Step Walkthrough:

  1. Initialization:

    • Initialize empty max_queue and min_queue as deques

    • Set left_index = 0

  2. Main Loop (Iterate through nums):

    • Iteration 1 (nums[0] = 10):

      • Update max_queue: Append 10, max_queue = [10]

      • Update min_queue: Append 10, min_queue = [10]

      • No window adjustment needed (10 - 10 = 0 <= 5)

      • Current window length: 1

    • Iteration 2 (nums[1] = 1):

      • Update max_queue: Append 1, max_queue = [10, 1]

      • Update min_queue: Remove 10, append 1, min_queue = [1]

      • Window adjustment: 10 - 1 = 9 > 5

        • Remove 10 from max_queue: max_queue = [1]

        • Update left_index to 1

      • Current window length: 1

    • Iteration 3 (nums[2] = 2):

      • Update max_queue: Remove 1, append 2, max_queue = [2]

      • Update min_queue: Append 2, min_queue = [1, 2]

      • No window adjustment needed (2 - 1 = 1 <= 5)

      • Current window length: 2

    • Iteration 4 (nums[3] = 4):

      • Update max_queue: Remove 2, append 4, max_queue = [4]

      • Update min_queue: Append 4, min_queue = [1, 2, 4]

      • No window adjustment needed (4 - 1 = 3 <= 5)

      • Current window length: 3

    • Iteration 5 (nums[4] = 7):

      • Update max_queue: Remove 4, append 7, max_queue = [7]

      • Update min_queue: Append 7, min_queue = [1, 2, 4, 7]

      • Window adjustment: 7 - 1 = 6 > 5

        • Remove 1 from min_queue: min_queue = [2, 4, 7]

        • Update left_index to 2

      • Current window length: 3

    • Iteration 6 (nums[5] = 2):

      • Update max_queue: Append 2, max_queue = [7, 2]

      • Update min_queue: Remove 7 and 4, append 2, min_queue = [2, 2]

      • No window adjustment needed (7 - 2 = 5 <= 5)

      • Current window length: 4

  3. Loop Termination:

    • The loop ends after processing all elements in nums

    • Final state: left_index = 2

  4. Visual Aid: Iteration Summary Table

    Iteration

    Current Number

    Left Index

    Current Length

    Max Queue

    Min Queue

    1

    10

    0

    1

    [10]

    [10]

    2

    1

    1

    1

    [1]

    [1]

    3

    2

    1

    2

    [2]

    [1, 2]

    4

    4

    1

    3

    [4]

    [1, 2, 4]

    5

    7

    2

    3

    [7]

    [2, 4, 7]

    6

    2

    2

    4

    [7, 2]

    [2, 2]

  5. Result Calculation/Final Steps:

    • The algorithm calculates the result as len(nums) - left_index

    • Result = 6 - 2 = 4

Last modified: 01 July 2024