2245. Maximum Trailing Zeros in a Cornered Path


Problem Description

The problem requires us to find a path within a 2D grid of positive integers that results in the maximum number of trailing zeros when the values of the path's cells are multiplied together. A path in this context is defined as a sequence of adjacent cells that travel in a straight line and can make at most one turn, moving horizontally before the turn and vertically after, or vice versa. The goal is to calculate the path with the highest number of trailing zeros in the product of its values. As only a single turn is allowed, this creates a "corner" in the path, hence the term "cornered path."

To break it down, we consider each cell and try to extend paths to it from all four directions (up, down, left, right). When calculating the path’s product, the factorization of each number is considered, focusing on the number of 2s and 5s because a trailing zero in a number is created by the pair of 2 and 5 in the number's prime factorization. The strategy involves favoring moves that increase the count of both 2s and 5s, as the number of trailing zeros is constrained by the smaller count of either prime factor in the product.

Intuition

The intuition behind the solution is based on dynamic programming and the understanding of how trailing zeros are formed in a number obtained by multiplying integers. Since a trailing zero is created from the factors of 2 and 5, the number of trailing zeros is equal to the minimum of the count of 2s and 5s in the prime factorization of the number.

As we look for paths with a single corner, we analyze each cell as if it were the corner and calculate the number of twos and fives that could be accumulated in paths that end at this corner cell from the four possible directions. This process enables us to compute all possible cornered paths crossing this cell.

To do this, the solution uses dynamic programming to keep track of the cumulative count of 2s and 5s for both rows and columns up to the current cell. This is accomplished by pre-computing the cumulative sums horizontally (r2, r5) and vertically (c2, c5) with respect to each cell for both prime factors.

When iterating over each cell, we consider this cell as the potential corner and calculate the total twos and fives if a path would have come from the left or above, turned at our corner cell, and extended to the right or below, and vice versa. We then take the minimum count of twos and fives for each considered path. We do this for all four possible cornered paths that could meet at each cell, resulting in four combinations to consider for each cell (starting horizontally then vertically, vertically then horizontally for both upward and downward, and leftward and rightward extensions).

The solution keeps track of the best (maximum) result as it evaluates each cell. This maximum is the maximum number of trailing zeros found in any cornered path in the grid, which is the required output.

Learn more about Prefix Sum patterns.

Not Sure What to Study? Take the 2-min Quiz to Find Your Missing Piece:

A heap is a ...?

Solution Approach

The provided solution approach is a systematic example of dynamic programming. This programming technique avoids computing the same subproblems repeatedly by storing their results in a memory structure. Here's a detailed explanation of the algorithm and the data structures used:

  1. Data Structures: Four auxiliary 2D arrays r2, r5, c2, c5 are initialized to store the cumulative count of 2s and 5s in the prime factorization of the numbers along the rows and columns of the grid. The size of each is (m+1) x (n+1) to simplify boundary conditions where m is the number of rows and n is the number of columns.

  2. Pre-computation: The first step involves a sweep through each element x of the grid with indexes i and j. For each element, the count of 2s (s2) and 5s (s5) in its prime factorization is calculated by repeatedly dividing x by 2 and 5 until no more division is possible. These counts are then added to obtain cumulative sums. Specifically, r2[i][j] is the cumulative count of 2s along row i up to column j, and c2[i][j] is the cumulative count of 2s along column j up to row i. The same goes for r5 and c5 with the count of 5s.

  3. Iterating for Paths: After building the cumulative grids, the algorithm proceeds by considering each cell as a potential corner of a cornered path. It calculates the maximum number of trailing zeros for every path that ends at this cell from any of the four directions (left, right, up, down).

  4. Calculating Trailing Zeros: For each cell (i, j), we calculate the product of the path coming from four possible directions as mentioned before:

    • Path a: Move horizontally, then vertically up.
    • Path b: Move horizontally, then vertically down.
    • Path c: Move vertically, then horizontally right.
    • Path d: Move vertically, then horizontally left.

    For each potential path, we use the cumulative counts of 2s and 5s to calculate the total factor count for each direction up to the corner and after the turn.

    Example of calculating for the path a:

    a = min(r2[i][j] + c2[i - 1][j], r5[i][j] + c5[i - 1][j])

    Here, r2[i][j] + c2[i - 1][j] represents the count of 2s when moving horizontally to (i, j) and then up, whereas r5[i][j] + c5[i - 1][j] represents the count of 5s for the same path. The minimum of these two represents the count of trailing zeros that can be achieved on this path, since trailing zeros depend on the pairs of 2s and 5s.

  5. Tracking Maximum: Within the nested loop, the solution updates the maximum number of trailing zeros found (ans) whenever a path with more trailing zeros than previously found is encountered.

  6. Returning the Result: Once all cells have been considered as potential corners, the solution returns the maximum trailing zeros calculated, stored in ans.

By doing all of these steps, the solution efficiently calculates the maximum number of trailing zeros possible in any cornered path within the given grid.

Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:

Which algorithm is best for finding the shortest distance between two points in an unweighted graph?

Example Walkthrough

Let's consider a small 3x3 grid with the following values to illustrate the solution approach:

1Grid:
2------
3  20   2   5
4  32   5  10
5  5    2  25

Step 1: Initialize Data Structures We'll create auxiliary arrays r2, r5, c2, c5 with dimensions (4x4) which include an extra row and column to handle boundaries easily.

Step 2: Pre-computation We pre-compute the cumulative counts of 2s and 5s along each row and column:

1Grid Values:     r2:     r5:     c2:     c5:
2------          ------  ------  ------  ------
3  20   2   5      1     1        1     1
4  32   5  10      5     0        6     1
5  5    2  25      0     1        1     2
6
7Note: We are considering that pre-computation has filled the arrays; the values here represent the count of 2s and 5s, not the cumulative sum.

Step 3: Iterate for Paths Consider each cell to be the potential corner of the path. Let's take (2,2) (the center cell with value 5) as an example:

Step 4: Calculating Trailing Zeros For cell (2,2), we will now calculate the product of paths with a potential turn at this cell:

  • Path a: Coming from left then turning up, we take minimum of (cumulative 2s from left + cumulative 2s from above) and (cumulative 5s from left + cumulative 5s from above).
  • Path b: Coming from left then turning down works similarly.
  • Path c: Coming from above then turning right.
  • Path d: Coming from above then turning left.

For (2,2), assuming r2, r5, c2, c5 have been filled with cumulative sums:

1a = min(r2[2][2] + c2[1][2], r5[2][2] + c5[1][2])
2b = min(r2[2][2] + c2[3][2], r5[2][2] + c5[3][2])
3c = min(c2[2][2] + r2[2][3], c5[2][2] + r5[2][3])
4d = min(c2[2][2] + r2[2][1], c5[2][2] + r5[2][1])

Let's assume for simplicity that these calculations yield:

1a = 1   (because if we had 1 "2" and 2 "5s", the min is 1)
2b = 1
3c = 1
4d = 1
5
6This means all paths through `(2,2)` as a corner can give us a product with at most 1 trailing zero.

Step 5: Tracking Maximum We compare the trailing zeros from a, b, c, and d and update our answer if we find a bigger number of trailing zeros. In our case, let's assume a gives us the maximum trailing zeros so far.

Step 6: Returning the Result After iterating through all cells, the highest number of trailing zeros found will be our answer. Assume that after full calculation, the maximum trailing zeros obtained from any path that turns at a corner is 1, so ans = 1.

By following these steps, we would find the maximum number of trailing zeros obtainable in any 'cornered' path in this 3x3 grid.

Solution Implementation

1class Solution:
2    def maxTrailingZeros(self, grid: List[List[int]]) -> int:
3        rows, cols = len(grid), len(grid[0])
4      
5        # Initialize counters for the number of 2s and 5s in each row and column
6        row_twos = [[0] * (cols + 1) for _ in range(rows + 1)]
7        col_twos = [[0] * (cols + 1) for _ in range(rows + 1)]
8        row_fives = [[0] * (cols + 1) for _ in range(rows + 1)]
9        col_fives = [[0] * (cols + 1) for _ in range(rows + 1)]
10      
11        # Precompute the count of 2s and 5s in the prefixes of each row and column
12        for i in range(1, rows + 1):
13            for j in range(1, cols + 1):
14                twos_count = fives_count = 0
15                current_val = grid[i - 1][j - 1]
16                while current_val % 2 == 0:
17                    current_val //= 2
18                    twos_count += 1
19                while current_val % 5 == 0:
20                    current_val //= 5
21                    fives_count += 1
22                row_twos[i][j] = row_twos[i][j - 1] + twos_count
23                col_twos[i][j] = col_twos[i - 1][j] + twos_count
24                row_fives[i][j] = row_fives[i][j - 1] + fives_count
25                col_fives[i][j] = col_fives[i - 1][j] + fives_count
26      
27        max_zeros = 0  # Variable to store the maximum number of trailing zeros found
28      
29        # Iterate through each grid cell to calculate the maximum trailing zeros
30        for i in range(1, rows + 1):
31            for j in range(1, cols + 1):
32                # Calculate for each direction and take the smallest count between 2s and 5s
33                # since they determine the number of trailing zeros
34                up = min(row_twos[i][j] + col_twos[i - 1][j], row_fives[i][j] + col_fives[i - 1][j])
35                down = min(row_twos[i][j] + col_twos[rows][j] - col_twos[i][j], row_fives[i][j] + col_fives[rows][j] - col_fives[i][j])
36                left = min(row_twos[i][cols] - row_twos[i][j] + col_twos[i][j], row_fives[i][cols] - row_fives[i][j] + col_fives[i][j])
37                right = min(
38                    row_twos[i][cols] - row_twos[i][j - 1] + col_twos[rows][j] - col_twos[i][j],
39                    row_fives[i][cols] - row_fives[i][j - 1] + col_fives[rows][j] - col_fives[i][j],
40                )
41              
42                # Update the maximum number of trailing zeros if a larger number is found
43                max_zeros = max(max_zeros, up, down, left, right)
44      
45        return max_zeros  # Return the maximum number of trailing zeros
46
1class Solution {
2
3    public int maxTrailingZeros(int[][] grid) {
4        // Retrieve dimensions of the grid
5        int rows = grid.length, cols = grid[0].length;
6      
7        // Create prefix sum arrays for the count of 2s and 5s for rows and columns
8        int[][] rowTwos = new int[rows + 1][cols + 1];
9        int[][] colTwos = new int[rows + 1][cols + 1];
10        int[][] rowFives = new int[rows + 1][cols + 1];
11        int[][] colFives = new int[rows + 1][cols + 1];
12      
13        // Fill in the prefix sum arrays by factorizing each number into its 2s and 5s components
14        for (int i = 1; i <= rows; ++i) {
15            for (int j = 1; j <= cols; ++j) {
16                int value = grid[i - 1][j - 1];
17                int countTwos = 0, countFives = 0;
18              
19                // Count the number of 2s in the factorization
20                while (value % 2 == 0) {
21                    value /= 2;
22                    countTwos++;
23                }
24              
25                // Count the number of 5s in the factorization
26                while (value % 5 == 0) {
27                    value /= 5;
28                    countFives++;
29                }
30              
31                // Update prefix sums for rows and columns
32                rowTwos[i][j] = rowTwos[i][j - 1] + countTwos;
33                colTwos[i][j] = colTwos[i - 1][j] + countTwos;
34                rowFives[i][j] = rowFives[i][j - 1] + countFives;
35                colFives[i][j] = colFives[i - 1][j] + countFives;
36            }
37        }
38
39        // Initialize the answer to zero
40        int maxZeros = 0;
41      
42        // Iterate over every position to calculate the maximum trailing zeros
43        for (int i = 1; i <= rows; ++i) {
44            for (int j = 1; j <= cols; ++j) {
45                // Calculate the count of trailing zeros for four possible L shapes
46                int a = Math.min(rowTwos[i][j] + colTwos[i - 1][j], rowFives[i][j] + colFives[i - 1][j]);
47                int b = Math.min(rowTwos[i][j] + colTwos[rows][j] - colTwos[i][j], rowFives[i][j] + colFives[rows][j] - colFives[i][j]);
48                int c = Math.min(rowTwos[i][cols] - rowTwos[i][j] + colTwos[i][j], rowFives[i][cols] - rowFives[i][j] + colFives[i][j]);
49                int d = Math.min(rowTwos[i][cols] - rowTwos[i][j - 1] + colTwos[rows][j] - colTwos[i][j],
50                                 rowFives[i][cols] - rowFives[i][j - 1] + colFives[rows][j] - colFives[i][j]);
51              
52                // Find the maximum count of trailing zeros among the L shapes
53                maxZeros = Math.max(maxZeros, Math.max(a, Math.max(b, Math.max(c, d))));
54            }
55        }
56
57        // Return the maximum count of trailing zeros
58        return maxZeros;
59    }
60}
61
1class Solution {
2public:
3    // Helper function to count the number of factors in factorization
4    int countFactors(int num, int factor) {
5        int count = 0;
6        while (num % factor == 0) {
7            ++count;
8            num /= factor;
9        }
10        return count;
11    }
12  
13    // Main function to calculate the maximum trailing zeros in any path in the grid
14    int maxTrailingZeros(vector<vector<int>>& grid) {
15        int numRows = grid.size();
16        int numCols = grid[0].size();
17      
18        // Prefix sum arrays for count of 2's and 5's in rows and columns
19        vector<vector<int>> prefixSumRows2(numRows + 1, vector<int>(numCols + 1));
20        vector<vector<int>> prefixSumCols2(numRows + 1, vector<int>(numCols + 1));
21        vector<vector<int>> prefixSumRows5(numRows + 1, vector<int>(numCols + 1));
22        vector<vector<int>> prefixSumCols5(numRows + 1, vector<int>(numCols + 1));
23      
24        // Pre-computing the prefix sums for 2's and 5's count both row-wise and column-wise
25        for (int i = 1; i <= numRows; ++i) {
26            for (int j = 1; j <= numCols; ++j) {
27                int value = grid[i - 1][j - 1];
28              
29                // Count the number of 2's and 5's in the factorization of the value
30                int count2 = countFactors(value, 2);
31                int count5 = countFactors(value, 5);
32              
33                // Update the prefix sums for rows and columns
34                prefixSumRows2[i][j] = prefixSumRows2[i][j - 1] + count2;
35                prefixSumCols2[i][j] = prefixSumCols2[i - 1][j] + count2;
36                prefixSumRows5[i][j] = prefixSumRows5[i][j - 1] + count5;
37                prefixSumCols5[i][j] = prefixSumCols5[i - 1][j] + count5;
38            }
39        }
40      
41        int maxZeros = 0; // The maximum number of trailing zeros found
42      
43        // Checking every possible path for each cell in the grid
44        for (int i = 1; i <= numRows; ++i) {
45            for (int j = 1; j <= numCols; ++j) {
46                // Paths can be created by going up or down in columns and left or right in rows
47                int upLeft = min(prefixSumRows2[i][j] + prefixSumCols2[i - 1][j], prefixSumRows5[i][j] + prefixSumCols5[i - 1][j]);
48                int downLeft = min(prefixSumRows2[i][j] + prefixSumCols2[numRows][j] - prefixSumCols2[i][j], prefixSumRows5[i][j] + prefixSumCols5[numRows][j] - prefixSumCols5[i][j]);
49                int upRight = min(prefixSumRows2[i][numCols] - prefixSumRows2[i][j] + prefixSumCols2[i][j], prefixSumRows5[i][numCols] - prefixSumRows5[i][j] + prefixSumCols5[i][j]);
50                int downRight = min(prefixSumRows2[i][numCols] - prefixSumRows2[i][j - 1] + prefixSumCols2[numRows][j] - prefixSumCols2[i][j], prefixSumRows5[i][numCols] - prefixSumRows5[i][j - 1] + prefixSumCols5[numRows][j] - prefixSumCols5[i][j]);
51              
52                // Take the maximum of the trailing zeros found in all four directions
53                maxZeros = max({maxZeros, upLeft, downLeft, upRight, downRight});
54            }
55        }
56      
57        return maxZeros; // Return the maximum number of trailing zeros
58    }
59};
60
1function maxTrailingZeros(grid: number[][]): number {
2    // Get the dimensions of the grid
3    const rowCount = grid.length;
4    const colCount = grid[0].length;
5
6    // Create 2D arrays to store counts of factors 2 and 5 on each row and column
7    const rowFactorsOf2 = Array.from({ length: rowCount + 1 }, () => new Array(colCount + 1).fill(0));
8    const colFactorsOf2 = Array.from({ length: rowCount + 1 }, () => new Array(colCount + 1).fill(0));
9    const rowFactorsOf5 = Array.from({ length: rowCount + 1 }, () => new Array(colCount + 1).fill(0));
10    const colFactorsOf5 = Array.from({ length: rowCount + 1 }, () => new Array(colCount + 1).fill(0));
11
12    // Calculate the running count of factors 2 and 5 in each row and column
13    for (let i = 1; i <= rowCount; ++i) {
14        for (let j = 1; j <= colCount; ++j) {
15            let cellVal = grid[i - 1][j - 1];
16            let countOf2 = 0;
17            let countOf5 = 0;
18            while (cellVal % 2 === 0) {
19                cellVal = Math.floor(cellVal / 2);
20                ++countOf2;
21            }
22            while (cellVal % 5 === 0) {
23                cellVal = Math.floor(cellVal / 5);
24                ++countOf5;
25            }
26            rowFactorsOf2[i][j] = rowFactorsOf2[i][j - 1] + countOf2;
27            colFactorsOf2[i][j] = colFactorsOf2[i - 1][j] + countOf2;
28            rowFactorsOf5[i][j] = rowFactorsOf5[i][j - 1] + countOf5;
29            colFactorsOf5[i][j] = colFactorsOf5[i - 1][j] + countOf5;
30        }
31    }
32
33    // Initialize variable for the maximum number of trailing zeros
34    let maxZeros = 0;
35
36    // Check each cell to calculate the maximum number of trailing zeros
37    // by combining the counts of factors 2 and 5 from rows and columns
38    for (let i = 1; i <= rowCount; ++i) {
39        for (let j = 1; j <= colCount; ++j) {
40            // Paths of four directions: up, down, left, and right
41            const upPath = Math.min(rowFactorsOf2[i][j] + colFactorsOf2[i - 1][j], rowFactorsOf5[i][j] + colFactorsOf5[i - 1][j]);
42            const downPath = Math.min(rowFactorsOf2[i][j] + colFactorsOf2[rowCount][j] - colFactorsOf2[i][j], rowFactorsOf5[i][j] + colFactorsOf5[rowCount][j] - colFactorsOf5[i][j]);
43            const leftPath = Math.min(rowFactorsOf2[i][colCount] - rowFactorsOf2[i][j] + colFactorsOf2[i][j], rowFactorsOf5[i][colCount] - rowFactorsOf5[i][j] + colFactorsOf5[i][j]);
44            const rightPath = Math.min(
45                rowFactorsOf2[i][colCount] - rowFactorsOf2[i][j - 1] + colFactorsOf2[rowCount][j] - colFactorsOf2[i][j],
46                rowFactorsOf5[i][colCount] - rowFactorsOf5[i][j - 1] + colFactorsOf5[rowCount][j] - colFactorsOf5[i][j],
47            );
48
49            // Update the maximum zeros with the highest count among the paths
50            maxZeros = Math.max(maxZeros, upPath, downPath, leftPath, rightPath);
51        }
52    }
53
54    // Return the maximum number of trailing zeros found
55    return maxZeros;
56}
57
Not Sure What to Study? Take the 2-min Quiz:

What's the relationship between a tree and a graph?

Time and Space Complexity

The time complexity of the code is determined by several nested loops and the operations within these loops.

  • There are two for-loops used to initialize the required arrays for counting the number of 2s and 5s (r2, c2, r5, and c5). Each of these loops runs m times for the rows and n times for the columns, resulting in a total of O(m * n) operations for this initialization step.

  • Within the nested loops, counting the number of 2s and 5s is done in constant time per cell because the while-loops terminate once the factors of 2 and 5 are taken out. This results in the while-loops running in constant time per cell and does not add more than an O(m * n) complexity.

  • There is another nested loop for calculating the maximum path. This nested loop also iterates m * n times.

  • Inside this nested loop, we perform a constant number of arithmetic operations and comparisons to calculate the minimum and maximum of the number of trailing zeros from different paths. These calculations are constant-time operations because they rely on precomputed counts of 2s and 5s from the r2, c2, r5, and c5 arrays.

Due to the above, the total time complexity is O(m * n) for the loops and constant operations in the order of m * n, hence it remains O(m * n).

The space complexity is determined by the additional arrays that are created to store the number of 2s and 5s for each row and column.

  • The r2, c2, r5, and c5 arrays each are sized (m + 1) * (n + 1), which gives us 4 * (m + 1) * (n + 1) in terms of space requirement. In complexity terms, this is O(m * n).

Since no other significant space-consuming structures are used, the total space complexity of the code is O(m * n).

In summary:

  • Time Complexity: O(m * n)
  • Space Complexity: O(m * n)

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

Fast Track Your Learning with Our Quick Skills Quiz:

What's the output of running the following function using the following tree as input?

1def serialize(root):
2    res = []
3    def dfs(root):
4        if not root:
5            res.append('x')
6            return
7        res.append(root.val)
8        dfs(root.left)
9        dfs(root.right)
10    dfs(root)
11    return ' '.join(res)
12
1import java.util.StringJoiner;
2
3public static String serialize(Node root) {
4    StringJoiner res = new StringJoiner(" ");
5    serializeDFS(root, res);
6    return res.toString();
7}
8
9private static void serializeDFS(Node root, StringJoiner result) {
10    if (root == null) {
11        result.add("x");
12        return;
13    }
14    result.add(Integer.toString(root.val));
15    serializeDFS(root.left, result);
16    serializeDFS(root.right, result);
17}
18
1function serialize(root) {
2    let res = [];
3    serialize_dfs(root, res);
4    return res.join(" ");
5}
6
7function serialize_dfs(root, res) {
8    if (!root) {
9        res.push("x");
10        return;
11    }
12    res.push(root.val);
13    serialize_dfs(root.left, res);
14    serialize_dfs(root.right, res);
15}
16

Recommended Readings


Got a question? Ask the Teaching Assistant anything you don't understand.

Still not clear? Ask in the Forum,  Discord or Submit the part you don't understand to our editors.


TA 👨‍🏫