A common problem in computer science is to query for a property over a range of contiguous elements in a given set.

Examples of this problem would be

  • For array find the minimum element between

The difficulty in the problem is that there are contiguous subsets of an array.

Approach 1 - Naive Brute Force Approach

def compute_minimum_over_range(A: List):
    n = len(A)
    range_min_query = [[0] * n ] * n 

    for i in range(n):
        for j in range(i, n):
            minimum = A[i]
            for k in range(i, j+1):
                minimum = min(minimum, A[k])
            
            range_min_query[i][j] = minimum
    
    return range_min_query

The naive method iterates over every possible subarray of and computes the minimum over the subarray. Since computing minimum over an array is linear time operation we compute:

which is in

Approach 2 - Dynamic Programming

In the naive, brute force approach we are repeating computations which could be used for speeding up queries for larger ranges.

A simple example would be for

A = [1, 5, -1, 3, 4]

a smart approach would be to notice that has smaller subproblems and and we can get the range_min_queryult in constant time from subproblems when we know them, like observing that:

instead of repeating the computation

so if we save range_min_queryults to the subproblems in memory, the future superproblems will encounter can be sped up.

def compute_minimum_over_range(A: List):
    # use dynamic programming
    n = len(A)
    range_min_query = [[0] * n] * n

    # start from smaller subproblems to larger ones
    for subproblem_size in range(1, n+1):
        for i in range(n):
            j = i + subproblem_size - 1

            if subproblem_size == 1:
                # base case, minimum of one element is itself 
                range_min_query[i][j] = A[i]
            else:
                range_min_query[i][j] = min(range_min_query[i][j-1], range_min_query[i+1][j])
    
    return range_min_query

At the cost of using space we have sped up our algorithm to time.

A query for a specific range we can get the query range_min_queryult in constant time by performing a look up on the range_min_queryult of compute_minimum_over_range dynamic programming table.

Approach 3 - Using Powers of Two optimization

A optimization of the above dynamic programming approach is to exploit the fact that the operation over sets does not care about ovelaps.

For any set , where .

We can optimize by not computing our range minimum query table for every subset size, instead using a sensible scheme such as powers of two. This can be a recurrence relation where is the position and is the power of two we want to query over.

def compute_minimum_over_range(A: List):
    n = len(A)
    K = ceil(log2(n))
    range_min_query = [[0] * n ] * K 

    for k in range(K):
        for i in range(n):
            if k == 0:
                range_min_query[k][i] = A[i]
                continue

            L = range_min_query[k-1][i]
            U = range_min_query[k-1][i + 1<<(k-1)]

            range_min_query[k][i] = min(L, U)

    return range_min_query

This reduces the range minimum query computation table to

Further Approaches

It is possible to do better than time and lookup time for range minimum queries. In fact, it is possible to do preprocessing and lookup through multiple clever data structures I will discuss in the future.