Markdown lesson 118 total lessons

Geometric Sequence Triplets

Stay in the lesson flow with grouped chapter navigation on the left and markdown content optimized for long reading sessions.

Geometric Sequence Triplets

A geometric sequence triplet is a sequence of three numbers where each successive number is obtained by multiplying the preceding number by a constant called the common ratio.

Let’s examine three triplets to understand how this works:

  • (1, 2, 4): This is a geometric sequence with a ratio of 2‾\underline{\text{2}}2​ (i.e., [1, 1⋅ 2‾\underline{\text{2}}2​ = 2, 2⋅ 2‾\underline{\text{2}}2​ = 4]).
  • (5, 15, 45): This is a geometric sequence with a ratio of 3‾\underline{\text{3}}3​ (i.e., [5, 5⋅ 3‾\underline{\text{3}}3​ = 15, 15⋅ 3‾\underline{\text{3}}3​ = 45]).
  • (2, 3, 4): Not a geometric sequence.

Given an array of integers and a common ratio r, find all triplets of indexes (i, j, k) that follow a geometric sequence for i < j < k. It’s possible to encounter duplicate triplets in the array.

Example:

Image represents five rows demonstrating a geometric sequence. Each row begins with a bracketed sequence of numbers: [2, 1, 2, 4, 8], with some rows having additional numbers (8) at the end.  The numbers 2, 4, and 8 within the brackets are highlighted in peach-colored circles, and small numbers beneath them indicate their position within the sequence (0, 2, 3, 4, 5).  Following each bracketed sequence is a colon and then an equation demonstrating the pattern: (2, 22 = 4, 42 = 8).  The first number in the equation (2) corresponds to the first highlighted number in the bracket, and subsequent numbers are generated by multiplying the preceding number by 2. The last row shows a variation where the sequence starts with 1 instead of 2, resulting in a modified equation: (1, 12 = 2, 22 = 4).  The overall image illustrates the concept of a geometric progression where each term is obtained by multiplying the previous term by a constant value (in this case, 2).

Input: nums = [2, 1, 2, 4, 8, 8], `r` = 2
Output: 5

Explanation:

  • Triplet [2, 4, 8] occurs at indexes (0, 3, 4), (0, 3, 5), (2, 3, 4), (2, 3, 5).
  • Triplet [1, 2, 4] occurs at indexes (1, 2, 3).

Intuition

For a triplet to form a geometric sequence, it has to adhere to two main rules:

  1. It consists of three values that follow a geometric sequence with a common ratio r.

  2. The three values forming the triplet must appear in the same order within the array as they do in the geometric sequence. This means for a geometric triplet (nums[i], nums[j], nums[k]), the indexes must follow the order i < j < k.

How can we represent a geometric sequence so that it follows rule 1? Let’s say the first number is x. The second number is the first number multiplied by r (i.e., x⋅r), and the third is the second number multiplied by r (i.e., x⋅r⋅r = x⋅r^2). So, a triplet in a geometric sequence can be represented as (x, x⋅r, x⋅r^2).

A brute force approach is to iterate over every possible triplet in the array and check if any of these triplets follow a geometric progression. However, it would take three nested for-loops to search through all the triplets, resulting in a time complexity of O(n3)O(n^3)O(n3), where nnn denotes the length of the input array. Can we do better?

An important observation here is that if we know one value of a triplet, we can calculate what the other two values should be.

This is because all three values are related by the common ratio r. So, for any number x in the array, we just need to find the values x⋅r and x⋅r^2 to form a geometric triplet (x, x⋅r, x⋅r^2). However, we could run into issues when using this triplet representation. While it’s clear the values x⋅r and x⋅r^2 must be positioned to the right of x, we have to be careful since the order of these values matters: we don’t want to accidentally identify a triplet such as (x, x⋅r^2, x⋅r), which is invalid:

Image represents a diagram illustrating an ordering problem in a data structure.  A single 'X' at the top points downwards via a grey arrow to a horizontal array-like structure enclosed in square brackets [ ]. This array contains several elements represented by dots (.) as placeholders, with a single 'X' and then 'xr²' and 'xr' appearing in the array. Below the array, a grey box highlights the text '(x, xr², xr)' labeled as 'triplet valves,' indicating that these three elements are expected to form a triplet. The text 'appear in the wrong order' explains that the order of these elements within the array does not match the expected order defined in the triplet valves.  The diagram visually shows the incorrect arrangement of the triplet within a larger data structure, highlighting the problem of out-of-order elements.

We can work around this issue by using the (x/r, x, x⋅r) triplet representation, which allows us to always maintain order by looking for x/r to the left of x and x⋅r to the right:

Image represents a comparison of two search operations within a data structure, likely an array.  The left side shows a light-blue rectangular box representing an array containing several elements (represented by black dots). A gray line extends below the box, indicating a range, with a downward-pointing triangle marking a midpoint. Below the line, the text 'find x/r' suggests a search operation aiming to find an element 'x' at or near the middle ('r' possibly representing a range or index). The right side mirrors this structure but uses a peach-colored rectangle.  The text 'find xr' suggests a different search operation, possibly indicating a search for 'x' within a range defined by 'r', but with a different search algorithm or criteria implied by the asterisk (). The letter 'X' separates the two diagrams, visually distinguishing the two search methods.  Both diagrams use brackets to visually represent the beginning and end of the array.

One way we can find the x/r and x⋅r values is by linearly searching through the left and right subarrays. This linear search would need to be done for each number in the array, resulting in an O(n2)O(n^2)O(n2) time complexity. While this is an improvement from the brute force solution, it would be great if we had a way to find those values faster.

Hash maps
A hash map would be a great way to solve this problem, as it allows us to query specific values in constant time.

What we would need are two hash maps:

  • A hash map that contains numbers to the left of each x (left_map).
  • A hash map that contains numbers to the right of each x (right_map).

Image represents a diagram illustrating a data structure concept, likely within the context of a two-pointer approach or similar algorithm.  The diagram shows two rectangular boxes, one light blue labeled implicitly as a 'left_map' and the other peach-colored labeled implicitly as a 'right_map'. Each box contains several dots (...), representing data elements within each respective map.  A horizontal line extends below each box, ending in a downward-pointing triangle, visually indicating a storage location. Below each line, the text 'store in' is written, followed by 'left_map' under the light blue box and 'right_map' under the peach box.  The two boxes are separated by the letter 'X', suggesting a separation or comparison point between the two data structures. The overall structure suggests that data is stored and potentially processed separately in 'left_map' and 'right_map', with the 'X' possibly representing a pivot or comparison point in an algorithm.

Hash maps allow us to query for both x/r in the left hash map and query for x⋅r in the right hash map in constant time on average. Note that a hash map would be preferred over a hash set because hash maps can also store the frequency of each value it stores. This is crucial since the array might contain duplicates, and we need to know the frequency of each value to accurately identify all possible triplets.

Finding all (x/r, x, xr) triplets
Our goal is to find all triplets that follow a geometric sequence, representing each number in the array as the middle (x) number of a triplet.

Before we find a triplet’s x/r value, we need to check if x is divisible by r. If it’s not, it’s impossible to form a triplet from the current value of x. Otherwise, we can proceed to look for the triplet.

For any element x, there could be multiple instances of x/r in left_map and multiple instances of x⋅r in right_map, implying that multiple triplets can be formed using x as the middle value. So, to get the total number of triplets that can be formed with x in the middle, multiply the frequencies of x⋅r and x/r:

Image represents a diagram illustrating a coding pattern, possibly related to data structures or algorithms.  The diagram shows two parallel structures, each representing a set of numbers. The left structure, labeled 'x/r = 2' in light blue, depicts a set [2, 1, 2], where two instances of the number 2 are connected by a line labeled 'two 2's' to a central node.  The right structure, labeled 'x/r = 8' in orange, shows a set [8, 8], with two instances of 8 connected by a line labeled 'two 8's' to the same central node. A downward-pointing arrow labeled 'x' connects the two structures, indicating a transformation or operation. The central node, connected to both structures, is labeled '2 * 2 = 4', suggesting a calculation based on the number of instances in each structure. Finally, an arrow pointing to the right indicates the result: '4 instances of [2, 4, 8]', implying that the combined result of the operation on the two input structures produces four instances of the combined set [2, 4, 8].

This overall methodology can be summarized in the following steps:

Image represents a flowchart illustrating a coding pattern for counting triplets.  The top shows two arrays, a light-blue array labeled [...] on the left representing values of x/r, and a peach-colored array labeled [...] on the right representing values of x*r, separated by the variable 'x'.  Step ①, labeled 'check that x%r == 0', indicates a condition that must be met.  Step ②, labeled 'get frequency of x/r from left_map', shows an arrow pointing from the left array to step ④.  Similarly, step ③, labeled 'get frequency of x*r from right_map', shows an arrow pointing from the right array to step ④.  Step ④, labeled 'multiply the frequencies to get the number of triplets with this x in the middle', indicates that the frequencies obtained in steps ② and ③ are multiplied to determine the count of triplets where 'x' is the central element, satisfying the condition in step ①.  The arrows visually represent the data flow, showing how the frequencies from the left and right arrays are combined to calculate the final result.

Note that if either x/r or x⋅r are not found in their hash maps, their frequency is 0 by default.


Let’s implement this strategy using the example below:

Image represents a one-dimensional array or list enclosed in square brackets [ and ], containing the integer sequence [2, 1, 2, 4, 8, 8].  This array is separated by a comma from the assignment r = 2, indicating a variable r is assigned the integer value 2.  The arrangement shows the array as a data structure, and the assignment suggests a potential relationship between the array and the variable r, possibly representing a parameter or index related to the array's processing or manipulation within a larger algorithm or code.  There are no URLs or other parameters present beyond the numerical values and variable assignment.

To ensure the hash maps always contain the correct values, we’d need to incorporate a dynamic strategy that involves updating the hash maps as we go because the values in both hash maps will be different depending on the position of x in the array.

Since we’re traversing the array from left to right, we should initially fill the right hash map with all values in the array. This is because, before the start of the iteration, every element is a potential candidate for x⋅r. Meanwhile, the left hash map is initially empty because there are no preceding elements to consider as potential x/r values:

Image represents a visual depiction of a data transformation process, likely related to frequency counting or mapping.  The input is a list of numbers [2, 1, 2, 4, 8, 8] and a value r = 2.  This input is processed to generate two maps: left_map and right_map.  right_map is a table with two columns: x*r and freq. It shows the results of multiplying each element in the input list by r (2) in the x*r column, and the frequency of each resulting value in the freq column. For example, 22=4 appears once, 12=2 appears twice, 4*2=8 appears twice. left_map is an empty table with columns x/r and freq, suggesting it would contain the results of dividing each input element by r and their corresponding frequencies, but these values are not shown in the image.  The overall diagram illustrates a process where an input list is transformed based on a given value r, resulting in frequency counts displayed in two separate tables representing different operations (multiplication and division by r).


Now let’s look for triplets. Start by representing the first value as the middle value (x) of a triplet.

First, let’s update right_map. We should remove the current value (2) from right_map since this 2 is not to the right of itself. There are two 2’s in right_map, so let’s reduce its frequency to 1:

Image represents a diagram illustrating a coding pattern, possibly related to frequency counting or mapping.  The diagram shows an input array x = [2, 1, 2, 4, 8, 8] and a value r = 2.  This input is processed to create two maps: left_map and right_map.  left_map is empty, suggesting it's a placeholder or the result of a computation not yet shown. right_map is a table with two columns: x*r (representing elements of the input array multiplied by r) and freq (representing the frequency of each element in x*r).  The right_map contains the following entries: x*r values of 2, 1, 4, 8 and their corresponding frequencies in freq as 2, 1, 1, 2 respectively. An arrow points from the freq column value of 2 (corresponding to x*r = 2) to the number 1, indicating a potential further processing step or mapping not fully depicted.  The overall structure suggests a process where an input array is manipulated based on a given value r, resulting in frequency counts displayed in the right_map, with a possible additional transformation indicated by the arrow.

Next, check if x/r is an integer. In this case, it is, so let’s find the number of triplets with x as the middle number by multiplying the frequencies of x/r and x⋅r, which we can get from the respective hash maps. Since left_map doesn’t contain x/r at this point, its frequency is 0:

The image represents a code snippet illustrating a calculation process. On the left, an array [2 1 2 4 8 8] is shown, with a variable r assigned the value 2.  A downward-pointing arrow suggests this array is input to the process. On the right, a light-grey box contains a conditional statement and a series of calculations. The condition x % r == 0 checks if a variable x (not explicitly defined in the image but implied to be an element from the input array) is divisible by r. If true, the count variable is updated. The update involves multiplying values from two maps, left_map and right_map, using x/r and x*r as indices respectively.  The example shows the calculation for x=2, resulting in count += left_map[2/2] * right_map[2*2], which simplifies to count += left_map[1] * right_map[4].  Subsequent lines show further additions to count, ultimately resulting in count += 0, suggesting that the initial multiplication resulted in zero, and subsequent additions also contributed zero.  The overall structure depicts a conditional update of a counter based on array elements and lookups in two maps (left_map and right_map).

Before moving on to the next value, let’s add the current number to the left_map because it now becomes a potential x/r value for future triplets in the array:

Image represents a comparison of two maps, labeled 'left_map' and 'right_map,' each depicted as a table with two columns.  The left column in both maps is labeled 'x/r' in the left map and 'xr' in the right map, representing some form of input data (possibly x divided by r and x multiplied by r respectively). The right column in both maps is labeled 'freq,' indicating frequency.  'left_map' contains two rows: one with '2' in the 'x/r' column and '1' in the 'freq' column, and another with an empty 'x/r' cell and an empty 'freq' cell. 'right_map' contains four rows showing different pairings of 'xr' and 'freq': (2,1), (1,1), (4,1), and (8,2).  The maps are presented side-by-side for visual comparison, highlighting the difference in data representation and frequency distribution between the 'x/r' and 'x*r' transformations.


Repeating this process for the rest of the array allows us to find all geometric triplets with a ratio of r. To clarify, the hash maps in the upcoming diagrams represent their state at the current position of x in the array. This means that left_map includes values to the left of the current x, and the right_map includes values to the right of it.

Image represents a diagram illustrating a coding pattern, possibly related to data processing or algorithm design.  At the top, an input array [2 1 2 4 8 8] and a parameter r = 2 are shown.  A downward-pointing arrow indicates the processing of the input array. Below, two tables are presented side-by-side, labeled 'left_map' and 'right_map'.  'left_map' contains a column labeled 'x/r' with values (presumably resulting from dividing elements of the input array by r) and a column labeled 'freq' representing their frequencies. Similarly, 'right_map' has columns 'x*r' (likely representing multiplication of input array elements by r) and 'freq' showing their frequencies.  A light gray rectangular box displays the condition x % r != 0 → can't form a triplet → continue, indicating that if the remainder of an element divided by r is not zero, a triplet cannot be formed, and the process continues to the next element.  The overall diagram suggests a frequency analysis or counting process involving division and multiplication by a given parameter r, with a conditional check for triplet formation.


Image represents a diagram illustrating a coding pattern, possibly related to frequency counting or data manipulation.  At the top, an input array [2 1 2 4 8 8] and a value r = 2 are given. Below, two tables labeled 'left_map' and 'right_map' are shown.  'left_map' contains two columns: 'x/r' representing values after integer division by r, and 'freq' representing their frequencies.  Similarly, 'right_map' has columns 'x*r' (values after multiplication by r) and 'freq'.  The values in these tables seem pre-calculated based on the input array. A dashed-line box to the right shows a calculation process.  It starts with a condition x % r == 0, implying a check for divisibility by r. If true, a count variable is updated by adding the product of a value from 'left_map' (indexed by x/r) and a value from 'right_map' (indexed by x*r). An example calculation is shown, where x is implicitly 2, resulting in left_map[2/2] * right_map[2*2], which simplifies to 1 * 1, adding 1 to the count.  The entire process suggests an algorithm that uses pre-computed frequency maps ('left_map' and 'right_map') to efficiently process the input array based on divisibility by r.


Image represents a diagram illustrating a coding pattern, possibly related to frequency counting or data manipulation.  At the top, an input array [2 1 2 4 8 8] and a value r = 2 are given.  Below, two tables labeled 'left_map' and 'right_map' are shown.  'left_map' has two columns: 'x/r' (representing the element of the input array divided by r) and 'freq' (frequency). It contains the values 2 (x/r) and 2 (freq) in the first row, and 1 (x/r) and 1 (freq) in the second row. 'right_map' similarly has columns 'x*r' (element multiplied by r) and 'freq', with values 2 and 0, 1 and 0, 4 and 0, and 8 and 2. To the right, a dashed box shows a calculation:  x % r == 0 → count += left_map[x/r] * right_map[x*r]. This formula is then broken down step-by-step, substituting values from the input array and the maps: += left_map[4/2] * right_map[4*2], += 2 * 2, and finally += 4, demonstrating how the count is updated based on the values in the 'left_map' and 'right_map' tables.  The overall diagram visualizes a process where an input array is processed using two pre-computed frequency maps ('left_map' and 'right_map') to calculate a final count.


Image represents a coding pattern illustration focusing on a calculation involving two maps, left_map and right_map, and an input array [2, 1, 2, 4, 8, 8] with r set to 2.  An arrow points downwards from 'x' suggesting an input value. left_map and right_map are presented as tables with two columns: the first labeled 'x/r' or 'x*r' respectively, representing the result of integer division by r or multiplication by r, and the second labeled 'freq', likely representing frequency counts.  The left_map contains entries (2,2), (1,1), (4,1), (8,1), while right_map contains (2,0), (1,0), (4,0), (8,1). A dashed box to the right shows a calculation process:  x % r == 0 (checking if x is a multiple of r) leads to count += left_map[x/r] * right_map[x*r].  The calculation is further broken down with example values, showing count being updated by adding the product of left_map[8/2] (which is 1) and right_map[8*2] (which is 1), resulting in a final count of 0.  The diagram illustrates how the maps are used to perform a calculation based on the input array and the value of r.


Image represents a diagram illustrating a coding pattern, possibly for counting occurrences based on modulo operation.  At the top, an input array [2 1 2 4 8 8] and a value r = 2 are given. Below, two tables labeled 'left_map' and 'right_map' are shown side-by-side.  Each table has two columns: the first column in 'left_map' lists the elements of the input array divided by r (integer division), while the second column represents their frequencies. Similarly, 'right_map' has its first column listing elements of the input array multiplied by r, and the second column showing their frequencies.  A dashed-line box to the right contains a calculation sequence. It starts with a condition x % r == 0, indicating that if an element x modulo r equals 0, a counter (count) is updated. The update involves multiplying the frequency from 'left_map' (using the integer division of x by r as the index) with the frequency from 'right_map' (using x multiplied by r as the index). The example calculation shows how this is done for the element 8: left_map[8/2] (which is 1) is multiplied by right_map[8*2] (which is 0), resulting in 0 being added to the counter.  The final result of the calculation is 0 in this specific example.

Implementation

Python

JavaScript

Java

SVG Image

from typing import List
from collections import defaultdict
  
def geometric_sequence_triplets(nums: List[int], r: int) -> int:
    # Use 'defaultdict' to ensure the default value of 0 is returned when
    # accessing a key that doesn’t exist in the hash map. This effectively sets
    # the default frequency of all elements to 0.
    left_map = defaultdict(int)
    right_map = defaultdict(int)
    count = 0
    # Populate 'right_map' with the frequency of each element in the array.
    for x in nums:
        right_map[x] += 1
    # Search for geometric triplets that have x as the center.
    for x in nums:
        # Decrement the frequency of x in 'right_map' since x is now being
        # processed and is no longer to the right.
        right_map[x] -= 1
        if x % r == 0:
            count += left_map[x // r] * right_map[x * r]
        # Increment the frequency of x in 'left_map' since it'll be a part of the
        # left side of the array once we iterate to the next value of `x`.
        left_map[x] += 1
    return count

Complexity Analysis

Time complexity: The time complexity of geometric_sequence_triplets is O(n)O(n)O(n) because we iterate through the nums array and perform constant-time hash map operations at each iteration.

Space complexity: The space complexity is O(n)O(n)O(n) because the hash maps can grow up to nnn in size.