118. Pascal's Triangle


Problem Description

Given an integer numRows, the task is to return the first numRows of Pascal's triangle. In Pascal's triangle, each number is the sum of the two numbers directly above it except for the boundaries, which are always 1. The triangle starts with a 1 at the top. Then, each subsequent row contains one more number than the previous row, and these numbers are positioned such that they form a triangle. The challenge here is to compute these numbers using the given property of Pascal's triangle and to represent the triangle in the form of a list of lists, where each inner list corresponds to a row in the triangle.

For example, the first 3 rows of Pascal's triangle look like this:

1               //First row (only one element)
1 1             //Second row (two 1s at the boundaries)
1 2 1           //Third row (1s at the boundaries, middle is sum of the two numbers above)

Intuition

The solution to generating Pascal's triangle is a direct application of its properties. The key idea is to start with the first row [1], and then generate each following row based on the previous row. For any new row, aside from the first and last elements which are 1, each element is obtained by adding the two numbers directly above it in the triangle. With this approach, we can iteratively construct each row and add it to the final result.

This can be implemented by initializing a list with the first row as its only element, then using a loop to construct each subsequent row. Inside the loop, we can use list comprehension to generate the middle values of the row by summing pairs of consecutive elements from the last row. We use the pairwise function (assuming Python 3.10+) to obtain the pairs, and then enclose the generated middle values with [1] on both ends to complete the row. After constructing each row, we append it to our result list. We repeat this process numRows - 1 times since the first row is already initialized.

The Python code encapsulates this logic in the generate function, with f holding the result list of lists and g being the new row being constructed each iteration. Finally, the function returns f after the completion of the loop.

Learn more about Dynamic Programming patterns.

Solution Approach

In the provided code, the problem is approached by considering each row in Pascal's Triangle to be a list and the entire triangle to be a list of these lists.

The main steps of the implementation are as follows:

  1. Initialize a list, f, with a single element that is the first row of Pascal's Triangle, which is simply [1].

  2. Loop from 0 to numRows - 1 (the -1 is because we already have the first row). This loop will build one row at a time to add to our f list.

  3. For each iteration, construct a new row, g. The first element is always 1. This is done by simply specifying [1] at the start.

  4. The middle elements of the row g are created by taking pairs of consecutive elements from the last row available in f and summing them up. This is done using list comprehension and the pairwise function. pairwise takes an iterable and returns an iterator over pairs of consecutive elements. For example, pairwise([1,2,3]) would produce (1, 2), (2, 3).

    In the code, we have [a + b for a, b in pairwise(f[-1])], which sums each pair of consecutive elements from the last row f[-1] to create the middle elements of the new row g.

  5. After the middle elements are calculated, [1] is appended to the end of the list to create the last element of the row, which is also 1.

  6. Row g is then added to the list f.

  7. This process repeats until all numRows rows are generated.

  8. Finally, the list f is returned, which contains the representation of Pascal's Triangle up to numRows.

This solution uses nested lists to represent Pascal's Triangle, list comprehension to generate each row, and a loop to build the triangle one row at a time. The pairwise function streamlines the process of summing adjacent elements from the previous row to build the interior of each new row.

Here is the critical part of the Python solution:

f = [[1]]  # Initial Pascal's Triangle with the first row.
for i in range(numRows - 1):
    g = [1] + [a + b for a, b in pairwise(f[-1])] + [1]  # Constructing the new row
    f.append(g)  # Appending the new row to the triangle
return f  # The complete Pascal's Triangle

The algorithm has a time complexity of O(n^2), where n is the number of rows, since each element of the triangle is computed exactly once.

Ready to land your dream job?

Unlock your dream job with a 2-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through the solution with numRows = 5 to illustrate the generation of Pascal's Triangle:

  1. We start with the initial list f which already contains the first row [1].

  2. We begin the loop with i ranging from 0 to numRows - 1, which is 0 to 4 in this case. Remember, the first row is already accounted for, so we effectively need to generate 4 more rows.

  3. For i = 0, we construct the second row:

    g = [1] + [a + b for a, b in pairwise(f[-1])] + [1]

    • f[-1] is [1], so pairwise(f[-1]) doesn't generate any pairs since there's only one element.
    • We add [1] at the beginning and end.

    So, g = [1] + [] + [1] which is simply [1, 1]. We append [1, 1] to f.

  4. Our list f now looks like [[1], [1, 1]].

  5. For i = 1, we construct the third row:

    f[-1] is [1, 1], pairwise(f[-1]) yields one pair: (1, 1).

    • We then add the sums of these pairs to [1] and enclose with [1] at the end.

    So, g = [1] + [1 + 1] + [1] which gives us [1, 2, 1]. We append [1, 2, 1] to f.

  6. Our list f now looks like [[1], [1, 1], [1, 2, 1]].

  7. For i = 2, constructing the fourth row:

    f[-1] is [1, 2, 1], pairwise(f[-1]) gives (1, 2) and (2, 1).

    • The sums are [1 + 2, 2 + 1] which is [3, 3].

    So, g = [1] + [3, 3] + [1] which forms [1, 3, 3, 1]. We append [1, 3, 3, 1] to f.

  8. Our list f now looks like [[1], [1, 1], [1, 2, 1], [1, 3, 3, 1]].

  9. For i = 3, the final loop to construct the fifth row:

    f[-1] is [1, 3, 3, 1], pairwise(f[-1]) yields (1, 3), (3, 3), and (3, 1).

    • The sums are [1 + 3, 3 + 3, 3 + 1] which is [4, 6, 4].

    g = [1] + [4, 6, 4] + [1], forming the row [1, 4, 6, 4, 1]. We append this row to f.

  10. Our final list f, representing the first 5 rows of Pascal's Triangle, now looks like [[1], [1, 1], [1, 2, 1], [1, 3, 3, 1], [1, 4, 6, 4, 1]].

The function would then return this list f.

Executing these steps with a proper function such as the given generate function will yield the desired Pascal's Triangle up to the specified row. Each iteration builds upon the previous, aligning perfectly with the properties of Pascal's Triangle.

Solution Implementation

1class Solution:
2    def generate(self, numRows: int) -> List[List[int]]:
3        # Initialize the first row of Pascal's Triangle
4        pascal_triangle = [[1]]
5      
6        # Generate each row of Pascal's Triangle
7        for i in range(1, numRows):
8            # The first element of each row is always 1
9            new_row = [1]
10          
11            # Calculate the intermediate elements of the row
12            # by adding the pairs of adjacent elements from the previous row
13            for j in range(1, i):
14                sum_of_elements = pascal_triangle[i - 1][j - 1] + pascal_triangle[i - 1][j]
15                new_row.append(sum_of_elements)
16          
17            # The last element of each row is also always 1
18            new_row.append(1)
19          
20            # Append the newly generated row to Pascal's Triangle
21            pascal_triangle.append(new_row)
22      
23        # Return the completed Pascal's Triangle
24        return pascal_triangle
25
1class Solution {
2    public List<List<Integer>> generate(int numRows) {
3        // Initialize the main list that will hold all rows of Pascal's Triangle
4        List<List<Integer>> triangle = new ArrayList<>();
5      
6        // The first row of Pascal's Triangle is always [1]
7        triangle.add(List.of(1));
8      
9        // Loop through each row (starting from the second row)
10        for (int rowIndex = 1; rowIndex < numRows; ++rowIndex) {
11            // Initialize the list to hold the current row's values
12            List<Integer> row = new ArrayList<>();
13          
14            // The first element in each row is always 1
15            row.add(1);
16          
17            // Compute the values within the row (excluding the first and last element)
18            for (int j = 0; j < triangle.get(rowIndex - 1).size() - 1; ++j) {
19                // Calculate each element as the sum of the two elements above it
20                row.add(triangle.get(rowIndex - 1).get(j) + triangle.get(rowIndex - 1).get(j + 1));
21            }
22          
23            // The last element in each row is always 1
24            row.add(1);
25          
26            // Add the computed row to the triangle list
27            triangle.add(row);
28        }
29      
30        // Return the fully constructed list of rows of Pascal's Triangle
31        return triangle;
32    }
33}
34
1class Solution {
2public:
3    // Function to generate Pascal's Triangle with the given number of rows.
4    vector<vector<int>> generate(int numRows) {
5        // Initialize a 2D vector to hold the rows of Pascal's Triangle.
6        vector<vector<int>> pascalsTriangle;
7
8        // The first row of Pascal's Triangle is always [1].
9        pascalsTriangle.push_back(vector<int>(1, 1));
10
11        // Generate the subsequent rows of Pascal's Triangle.
12        for (int rowIndex = 1; rowIndex < numRows; ++rowIndex) {
13            // Initialize the new row starting with a '1'.
14            vector<int> newRow;
15            newRow.push_back(1);
16
17            // Fill in the values between the first and last '1' of the row.
18            for (int elementIndex = 0; elementIndex < pascalsTriangle[rowIndex - 1].size() - 1; ++elementIndex) {
19                // The new value is the sum of the two values directly above it.
20                newRow.push_back(pascalsTriangle[rowIndex - 1][elementIndex] + pascalsTriangle[rowIndex - 1][elementIndex + 1]);
21            }
22
23            // The last value in a row of Pascal's Triangle is always '1'.
24            newRow.push_back(1);
25
26            // Append the newly created row to Pascal's Triangle.
27            pascalsTriangle.push_back(newRow);
28        }
29      
30        // Return the fully generated Pascal's Triangle.
31        return pascalsTriangle;
32    }
33};
34
1// Generates Pascal's Triangle with a given number of rows
2function generate(numRows: number): number[][] {
3    // Initialize the triangle with the first row
4    const triangle: number[][] = [[1]];
5
6    // Populate the triangle row by row
7    for (let rowIndex = 1; rowIndex < numRows; ++rowIndex) {
8        // Initialize the new row starting with '1'
9        const row: number[] = [1];
10
11        // Calculate each value for the current row based on the previous row
12        for (let j = 0; j < triangle[rowIndex - 1].length - 1; ++j) {
13            row.push(triangle[rowIndex - 1][j] + triangle[rowIndex - 1][j + 1]);
14        }
15
16        // End the current row with '1'
17        row.push(1);
18
19        // Add the completed row to the triangle
20        triangle.push(row);
21    }
22
23    // Return the fully generated Pascal's Triangle
24    return triangle;
25}
26

Time and Space Complexity

The provided code generates Pascal's Triangle with numRows levels. Here is the analysis of its time and space complexity:

Time Complexity

The time complexity of the code can be understood by analyzing the operations inside the for-loop that generates each row of Pascal's Triangle.

  • There are numRows - 1 iterations since the first row is initialized before the loop.
  • Inside the loop, pairwise(f[-1]) generates tuples of adjacent elements from the last row which takes O(j) time, where j is the number of elements in the previous row (since every step inside the enumeration would be constant time).
  • The list comprehension [a + b for a, b in pairwise(f[-1])] performs j - 1 additions, so this is also O(j) where j is the size of the previous row.
  • Appending the first and last 1 is O(1) each for a total of O(2) which simplifies to O(1).

As the rows of Pascal's Triangle increase by one element each time, summing up the operations over all rows gives us a total time of approximately O(1) for the first row, plus O(2) + O(3) + ... + O(numRows) for the numRows - 1 remaining rows. This results in a time complexity of O(numRows^2).

Therefore, the overall time complexity is O(numRows^2).

Space Complexity

The space complexity is determined by the space required to store the generated rows of Pascal's Triangle.

  • f is initialized with a single row containing one 1 which we can consider O(1).
  • In each iteration of the loop, a new list with i + 2 elements is created (since each new row has one more element than the previous one) and appended to f.

Summing this up, the space required will be O(1) for the first row, plus O(2) + O(3) + ... + O(numRows) for the rest of the rows.

This results in a space complexity of O(numRows^2) since the space required is proportional to the square of the number of rows due to the nature of Pascal's Triangle.

Therefore, the overall space complexity is O(numRows^2).

Learn more about how to find time and space complexity quickly using problem constraints.


Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:
Question 1 out of 10

What does the following code do?

1def f(arr1, arr2):
2  i, j = 0, 0
3  new_arr = []
4  while i < len(arr1) and j < len(arr2):
5      if arr1[i] < arr2[j]:
6          new_arr.append(arr1[i])
7          i += 1
8      else:
9          new_arr.append(arr2[j])
10          j += 1
11  new_arr.extend(arr1[i:])
12  new_arr.extend(arr2[j:])
13  return new_arr
14
1public static List<Integer> f(int[] arr1, int[] arr2) {
2  int i = 0, j = 0;
3  List<Integer> newArr = new ArrayList<>();
4
5  while (i < arr1.length && j < arr2.length) {
6      if (arr1[i] < arr2[j]) {
7          newArr.add(arr1[i]);
8          i++;
9      } else {
10          newArr.add(arr2[j]);
11          j++;
12      }
13  }
14
15  while (i < arr1.length) {
16      newArr.add(arr1[i]);
17      i++;
18  }
19
20  while (j < arr2.length) {
21      newArr.add(arr2[j]);
22      j++;
23  }
24
25  return newArr;
26}
27
1function f(arr1, arr2) {
2  let i = 0, j = 0;
3  let newArr = [];
4  
5  while (i < arr1.length && j < arr2.length) {
6      if (arr1[i] < arr2[j]) {
7          newArr.push(arr1[i]);
8          i++;
9      } else {
10          newArr.push(arr2[j]);
11          j++;
12      }
13  }
14  
15  while (i < arr1.length) {
16      newArr.push(arr1[i]);
17      i++;
18  }
19  
20  while (j < arr2.length) {
21      newArr.push(arr2[j]);
22      j++;
23  }
24  
25  return newArr;
26}
27

Recommended Readings

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