Pyramid Transition Matrix
MEDIUMDescription
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 <= 60 <= allowed.length <= 216allowed[i].length == 3- The letters in all input strings are from the set
{'A', 'B', 'C', 'D', 'E', 'F'}. - All the values of
allowedare 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
allowedlist into aMap<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>orMap<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, returntrue. - If
currentRowis in the memoization table (meaning we've already proved it's unsolvable), returnfalse.
- If
- 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 validnextRow. - For each position in the
nextRow, iterate through all possible characters. Recursively callfindPathfor the next position. - When a full
nextRowis constructed, callsolve(nextRow, memo). - If
solvereturnstrue, a path is found, so propagatetrueall the way up.
- Use a helper function to generate and test possible next rows one character at a time. This helper function, say
- Memoize Failures: If the
findPathfunction explores all possibilities forcurrentRowand fails to find a solution, addcurrentRowto the memoization set before returningfalse.
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:
- We preprocess the
allowedlist into a map for quick lookups, just like in the brute-force approach. - We use a
Set<String>to store the rows that we have already processed and confirmed cannot lead to a valid pyramid top. - 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). - 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.
- Once a complete
nextRowis formed, we recursively callsolveon it. - If any of these recursive calls eventually return
true, we have found a solution. - If, after trying all possible valid next rows, none lead to a solution, we add the
currentRowto our failure set and returnfalse.
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
Pros and Cons
- 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.
- 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
Similar Questions
5 related questions you might find useful
Algorithms:
Patterns:
Companies:
Subscribe to Scale Engineer newsletter
Learn about System Design, Software Engineering, and interview experiences every week.
No spam, unsubscribe at any time.