Pyramid Transition Matrix

MEDIUM

Description

You are stacking blocks to form a pyramid. Each block has a color, which is represented by a single letter. Each row of blocks contains one less block than the row beneath it and is centered on top.

To make the pyramid aesthetically pleasing, there are only specific triangular patterns that are allowed. A triangular pattern consists of a single block stacked on top of two blocks. The patterns are given as a list of three-letter strings allowed, where the first two characters of a pattern represent the left and right bottom blocks respectively, and the third character is the top block.

  • For example, "ABC" represents a triangular pattern with a 'C' block stacked on top of an 'A' (left) and 'B' (right) block. Note that this is different from "BAC" where 'B' is on the left bottom and 'A' is on the right bottom.

You start with a bottom row of blocks bottom, given as a single string, that you must use as the base of the pyramid.

Given bottom and allowed, return true if you can build the pyramid all the way to the top such that every triangular pattern in the pyramid is in allowed, or false otherwise.

 

Example 1:

Input: bottom = "BCD", allowed = ["BCC","CDE","CEA","FFF"]
Output: true
Explanation: The allowed triangular patterns are shown on the right.
Starting from the bottom (level 3), we can build "CE" on level 2 and then build "A" on level 1.
There are three triangular patterns in the pyramid, which are "BCC", "CDE", and "CEA". All are allowed.

Example 2:

Input: bottom = "AAAA", allowed = ["AAB","AAC","BCD","BBE","DEF"]
Output: false
Explanation: The allowed triangular patterns are shown on the right.
Starting from the bottom (level 4), there are multiple ways to build level 3, but trying all the possibilites, you will get always stuck before building level 1.

 

Constraints:

  • 2 <= bottom.length <= 6
  • 0 <= allowed.length <= 216
  • allowed[i].length == 3
  • The letters in all input strings are from the set {'A', 'B', 'C', 'D', 'E', 'F'}.
  • All the values of allowed are unique.

Approaches

Checkout 2 different approaches to solve Pyramid Transition Matrix. Click on different approaches to view the approach and algorithm in detail.

Backtracking with Memoization

This approach significantly optimizes the brute-force backtracking by using memoization, a technique common in dynamic programming. The key observation is that the same intermediate row configuration can be reached from multiple different lower rows. Instead of re-computing whether a pyramid can be built from this intermediate row every time it's encountered, we can store the result the first time we compute it and look it up for subsequent encounters.

Algorithm

  • Preprocessing: Convert the allowed list into a Map<String, List<Character>> for efficient lookups. The key is the 2-character base, and the value is a list of possible top blocks.
  • Memoization: Use a Set<String> or Map<String, Boolean> to store the results for subproblems. A subproblem is identified by the string of a given row. We can store rows that are determined to be 'unsolvable'.
  • Recursive Solver: Create a recursive function, solve(currentRow, memo), that checks if a pyramid can be built.
  • Base Cases:
    • If currentRow.length() == 1, return true.
    • If currentRow is in the memoization table (meaning we've already proved it's unsolvable), return false.
  • Recursive Step:
    • Use a helper function to generate and test possible next rows one character at a time. This helper function, say findPath(currentRow, nextRowBuilder, index, memo), will try to build a valid nextRow.
    • For each position in the nextRow, iterate through all possible characters. Recursively call findPath for the next position.
    • When a full nextRow is constructed, call solve(nextRow, memo).
    • If solve returns true, a path is found, so propagate true all the way up.
  • Memoize Failures: If the findPath function explores all possibilities for currentRow and fails to find a solution, add currentRow to the memoization set before returning false.

We enhance the recursive solution by adding a cache (memoization table) to keep track of rows from which it's impossible to build a pyramid. This avoids redundant computations.

The algorithm is a top-down dynamic programming approach:

  1. We preprocess the allowed list into a map for quick lookups, just like in the brute-force approach.
  2. We use a Set<String> to store the rows that we have already processed and confirmed cannot lead to a valid pyramid top.
  3. The main recursive function solve(currentRow) first checks its base cases: if the row is of length 1 (success) or if the row is already in our failure set (cached failure).
  4. If it's a new row, we proceed to find a valid path upwards. We do this with a helper function that constructs the next row piece by piece. This helper tries every possible character for the first position of the next row, then for each of those, every possible character for the second, and so on.
  5. Once a complete nextRow is formed, we recursively call solve on it.
  6. If any of these recursive calls eventually return true, we have found a solution.
  7. If, after trying all possible valid next rows, none lead to a solution, we add the currentRow to our failure set and return false.

This way, any given row configuration is fully explored only once. Subsequent calls with the same row will return the cached result instantly.

import java.util.*;

class Solution {
    public boolean pyramidTransition(String bottom, List<String> allowed) {
        Map<String, List<Character>> transitions = new HashMap<>();
        for (String s : allowed) {
            String key = s.substring(0, 2);
            transitions.computeIfAbsent(key, k -> new ArrayList<>()).add(s.charAt(2));
        }
        // Memoization set for rows that cannot form a pyramid
        Set<String> memo = new HashSet<>();
        return solve(bottom, transitions, memo);
    }

    private boolean solve(String currentRow, Map<String, List<Character>> transitions, Set<String> memo) {
        if (currentRow.length() == 1) {
            return true;
        }
        if (memo.contains(currentRow)) {
            return false;
        }

        boolean canMakePyramid = findPathForNextRow(currentRow, 0, new StringBuilder(), transitions, memo);
        
        if (!canMakePyramid) {
            memo.add(currentRow);
        }
        
        return canMakePyramid;
    }

    private boolean findPathForNextRow(String currentRow, int index, StringBuilder nextRowBuilder, Map<String, List<Character>> transitions, Set<String> memo) {
        if (index == currentRow.length() - 1) {
            // A complete next row has been formed, now solve for it
            return solve(nextRowBuilder.toString(), transitions, memo);
        }

        String key = currentRow.substring(index, index + 2);
        if (!transitions.containsKey(key)) {
            return false; // No transition possible, this path is invalid
        }

        for (char c : transitions.get(key)) {
            nextRowBuilder.append(c);
            if (findPathForNextRow(currentRow, index + 1, nextRowBuilder, transitions, memo)) {
                return true; // A valid pyramid was found down this path
            }
            nextRowBuilder.deleteCharAt(nextRowBuilder.length() - 1); // Backtrack
        }

        return false; // No character choice at this index led to a solution
    }
}

Complexity Analysis

Time Complexity: O(sum(|S_k| * C^(k-1)))Space Complexity: O(S * N + N^2)

Pros and Cons

Pros:
  • Drastically more efficient than the brute-force approach by avoiding re-computation.
  • Feasible and passes within time limits for the given constraints.
  • Represents a classic application of dynamic programming (via memoization) to a search problem.
Cons:
  • The worst-case time complexity is still exponential, although it performs well given the problem's constraints.
  • Space complexity can be significant if the number of reachable intermediate row configurations is large.

Code Solutions

Checking out 3 solutions in different languages for Pyramid Transition Matrix. Click on different languages to view the code.

class Solution {
private
  int[][] f = new int[7][7];
private
  Map<String, Boolean> dp = new HashMap<>();
public
  boolean pyramidTransition(String bottom, List<String> allowed) {
    for (String s : allowed) {
      int a = s.charAt(0) - 'A', b = s.charAt(1) - 'A';
      f[a][b] |= 1 << (s.charAt(2) - 'A');
    }
    return dfs(bottom, new StringBuilder());
  }
  boolean dfs(String s, StringBuilder t) {
    if (s.length() == 1) {
      return true;
    }
    if (t.length() + 1 == s.length()) {
      return dfs(t.toString(), new StringBuilder());
    }
    String k = s + "." + t.toString();
    if (dp.containsKey(k)) {
      return dp.get(k);
    }
    int a = s.charAt(t.length()) - 'A', b = s.charAt(t.length() + 1) - 'A';
    int cs = f[a][b];
    for (int i = 0; i < 7; ++i) {
      if (((cs >> i) & 1) == 1) {
        t.append((char)('A' + i));
        if (dfs(s, t)) {
          dp.put(k, true);
          return true;
        }
        t.deleteCharAt(t.length() - 1);
      }
    }
    dp.put(k, false);
    return false;
  }
}

Video Solution

Watch the video walkthrough for Pyramid Transition Matrix



Algorithms:

Depth-First SearchBreadth-First Search

Patterns:

Bit Manipulation

Companies:

Subscribe to Scale Engineer newsletter

Learn about System Design, Software Engineering, and interview experiences every week.

No spam, unsubscribe at any time.