Count of Smaller Numbers After Self

To solve the problem of finding the count of smaller numbers to the right of each element in the given array nums, you can use a Fenwick Tree (Binary Indexed Tree) or other data structures like Balanced BST. Here, I’ll provide a simple and direct approach using Fenwick Tree.

The Fenwick Tree allows you to perform both update and query operations efficiently.

  1. Update: Increment a value at a particular index.
  2. Query: Retrieve the sum of elements from the start to a particular index.

The algorithm can be broken down into the following steps:

  1. First, normalize the numbers, as the numbers can be negative.
  2. Use a Fenwick Tree to keep track of the count of numbers.
  3. Iterate through the numbers from right to left, and for each number: a. Query the Fenwick Tree to find the count of numbers smaller than the current number. b. Update the Fenwick Tree with the current number’s count.

Here’s the code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class FenwickTree:
    def __init__(self, size):
        self.size = size
        self.tree = [0] * (size + 1)

    def update(self, index, value):
        while index <= self.size:
            self.tree[index] += value
            index += index & -index

    def query(self, index):
        result = 0
        while index > 0:
            result += self.tree[index]
            index -= index & -index
        return result

class Solution:
    def countSmaller(self, nums: List[int]) -> List[int]:
        # Normalizing the numbers
        sorted_nums = sorted(set(nums))
        rank = {num: index + 1 for index, num in enumerate(sorted_nums)}

        # Initialize Fenwick Tree
        fenwick_tree = FenwickTree(len(sorted_nums))

        result = []
        # Iterate from right to left
        for num in reversed(nums):
            # Query for count of smaller numbers
            result.append(fenwick_tree.query(rank[num] - 1))
            # Update the Fenwick Tree
            fenwick_tree.update(rank[num], 1)

        return result[::-1]

Explanation:

  • We’re using a Fenwick Tree to efficiently update and query the count of numbers.
  • We’ve also normalized the numbers to handle negative numbers and large positive numbers.
  • The code iterates from right to left, updating the result with the count of smaller numbers and updating the Fenwick Tree accordingly.

This solution has a time complexity of (O(n \log n)), where (n) is the length of the input array nums.

Identifying Problem Isomorphism

“Count of Smaller Numbers After Self” can be mapped to “How Many Numbers Are Smaller Than the Current Number”.

Reasoning:

Both problems share the central concept of counting numbers in an array that are smaller than the current number. In “Count of Smaller Numbers After Self”, the task is to count the numbers that are smaller and come after the current number in the given array. On the other hand, in “How Many Numbers Are Smaller Than the Current Number”, the task is to count all the numbers that are smaller than the current number in the array, regardless of their position.

These two problems share a similar theme. “Count of Smaller Numbers After Self” is more complex. Because it involves a dynamic condition where the counting changes as you move through the array, whereas in “How Many Numbers Are Smaller Than the Current Number”, the task is static and does not depend on the position of the number in the array. The second problem is easier to solve as it only involves a simple comparison of each number with all others.

The “315. Count of Smaller Numbers After Self” involves concepts of Binary Indexed Trees (Fenwick Tree), Binary Search Trees, Merge Sort, and Divide and Conquer. Here are some simpler problems to prepare for this problem:

  1. 278. First Bad Version: This is a simple binary search problem that will help you understand the basic concept of binary search.

  2. 704. Binary Search: This problem involves implementing a binary search on a sorted array which is a good practice for binary search.

  3. 349. Intersection of Two Arrays: This problem involves using a set to find elements in one array but not another, similar to finding smaller elements in the current problem.

  4. 704. Binary Search: This problem will help you understand the basic principles of binary search, which can be useful for problems involving searching in sorted structures.

  5. 35. Search Insert Position: This problem involves a variant of binary search where the goal is to find the insert position of a target in a sorted array.

  6. 493. Reverse Pairs: This problem involves counting pairs with certain properties in an array, which is somewhat similar to counting smaller elements.

  7. 88. Merge Sorted Array: As a primer for understanding how merge sort works.

  8. 912. Sort an Array: This problem involves sorting an array using any algorithm. It would be beneficial to implement a solution using merge sort to better understand it.

  9. 327. Count of Range Sum: This problem also requires you to count the number of ranges that satisfy a certain condition, which is a similar concept required in the “Count of Smaller Numbers After Self” problem.

  10. 493. Reverse Pairs: This problem requires counting significant pairs in an array and involves concepts similar to the “Count of Smaller Numbers After Self” problem.

Clarification Questions

What are the clarification questions we can ask about this problem?

Identifying Problem Isomorphism

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
    def countSmaller(self, nums: List[int]) -> List[int]:
        res  = [0] * len(nums)
        enum = list(enumerate(nums)) 
        
        self.mergeSort(enum, 0, len(nums) - 1, res)
        return res
    
    def mergeSort(self, enum, start, end, res):
        if start >= end: return
        
        mid = start + (end - start) // 2
        self.mergeSort(enum, start, mid, res)
        self.mergeSort(enum, mid + 1, end, res)
        self.merge(enum, start, mid, end, res)
    
    def merge(self, enum, start, mid, end, res):
        p, q = start, mid + 1
        inversion_count = 0
        
        while p <= mid and q <= end:
            if enum[p][1] <= enum[q][1]:
                res[enum[p][0]] += inversion_count
                p += 1
            else:
                inversion_count += 1
                q += 1
        
        while p <= mid:
            # res[enum[p][0]] += inversion_count
            res[enum[p][0]] += end - mid
            p += 1
        
        enum[start:end+1] = sorted(enum[start:end+1], key=lambda e:e[1])    

Count of Smaller Numbers After Self - This problem involves counting smaller numbers to the right of each number in an array. This is a Number Comparison Problem.

Language Agnostic Coding Drills

To understand the code snippet, we’ll need to break it down into the key components:

  1. Understanding Lists and Enumerate Function: Understanding how lists work in Python and how to use the enumerate function is crucial here. The enumerate function is used to add a counter to the list or any other iterable and returns it as an enumerate object.

  2. Understanding Counters and Indexes: The provided solution uses a counter (res) to store the number of smaller elements on the right for each element. To modify this counter correctly, a solid understanding of indexes and how to use them is necessary.

  3. Understanding Sorting: Sorting is a key aspect of this problem. The code uses the built-in sorted function to sort subarrays of the enumerated list.

  4. Merge Sort Algorithm: The solution is based on a modification of the merge sort algorithm. Understanding how merge sort works, including how it divides the problem and how it combines the solutions, is crucial to understand the code.

  5. Understanding Divide and Conquer Paradigm: The problem is solved using a Divide and Conquer strategy, which involves breaking the problem into smaller subproblems until they can be solved directly. The solutions to the subproblems are then combined to give a solution to the original problem.

  6. Understanding Inversion Count: An inversion in an array is a pair of elements where the larger element appears before the smaller one. The inversion count is used to keep track of the number of smaller numbers on the right of the current number.

After each of these concepts has been independently understood and practiced, they can be combined to understand the solution to this problem. The problem-solving approach is as follows:

  1. Enumerate the input list.
  2. Initialize a result list with zeros.
  3. Use a modified merge sort to sort the enumerated list. In each recursive call of merge sort:
    • Divide the list into two halves.
    • Call the merge sort recursively on each half.
    • Merge the two halves. While merging, if an element from the right half is smaller than an element from the left half, increment a counter. Add the counter’s value to the result list at the index corresponding to the element from the left half. This is because all elements to the right of this element in the original array are smaller.
  4. The result list will contain the count of smaller numbers after each number.

Targeted Drills in Python

I’ll provide Python coding drills for the key components of the problem.

  1. Understanding Lists and Enumerate Function:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Understanding Lists
lst = [4, 2, 1, 3]

# Accessing List Elements
print(lst[0])  # 4
print(lst[-1])  # 3

# Enumerate Function
for i, val in enumerate(lst):
    print(f"Index: {i}, Value: {val}")
  1. Understanding Counters and Indexes:
1
2
3
4
5
6
# Initializing a counter with zeros
counter = [0] * len(lst)

# Incrementing the counter at a certain index
counter[1] += 1
print(counter)  # [0, 1, 0, 0]
  1. Understanding Sorting:
1
2
3
# Sorting a list
lst_sorted = sorted(lst)
print(lst_sorted)  # [1, 2, 3, 4]
  1. Merge Sort Algorithm: The code for the merge sort algorithm is quite long, but here’s a simplified version that sorts a list:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def mergeSort(arr):
    if len(arr) > 1:
        mid = len(arr) // 2
        left = arr[:mid]
        right = arr[mid:]

        mergeSort(left)
        mergeSort(right)

        i = j = k = 0
        while i < len(left) and j < len(right):
            if left[i] < right[j]:
                arr[k] = left[i]
                i += 1
            else:
                arr[k] = right[j]
                j += 1
            k += 1

        while i < len(left):
            arr[k] = left[i]
            i += 1
            k += 1

        while j < len(right):
            arr[k] = right[j]
            j += 1
            k += 1
  1. Understanding Divide and Conquer Paradigm: This concept is demonstrated in the merge sort algorithm above, where the list is divided into halves until it has only one element, and then the halves are merged together.

  2. Understanding Inversion Count: This is the concept specific to the problem, and you could practice with a simpler version of the problem: counting the inversions in an array.

1
2
3
4
5
6
7
def countInversions(arr):
    count = 0
    for i in range(len(arr)):
        for j in range(i+1, len(arr)):
            if arr[i] > arr[j]:
                count += 1
    return count

The final solution for the problem involves combining all these drills and modifying the merge sort and inversion counting algorithms to also handle the index information.