The Pyramid Transition Matrix problem asks you to determine if you can build a pyramid from a given bottom row of blocks, using a list of allowed triples that specify which block can be placed atop every pair of blocks.
Specifically, given a string bottom
representing the bottom row (where each character is a block), and a list of strings allowed
(each a length-3 string like "ABC", meaning "if A and B are adjacent, you can put C above them"), determine if you can build the pyramid all the way up to a single block at the top.
C
atop two adjacent blocks A
and B
if "ABC"
is in allowed
.
true
if at least one way exists to build the pyramid to the top, else false
.
At first glance, the problem seems to involve checking all possible ways to stack blocks, given the constraints. For each level, every pair of adjacent blocks determines which blocks could possibly go above them. This suggests a recursive or backtracking approach: try all possible combinations, and see if any lead to a completed pyramid.
A brute-force solution would try every possible combination for every level, but this can quickly become infeasible as the pyramid grows taller and the number of allowed triples increases. Therefore, we look for ways to optimize:
Let's break down the solution into clear steps:
This approach ensures we efficiently explore all valid paths, while avoiding redundant computation.
Let's use the following example:
bottom = "BCD"
allowed = ["BCC", "CDE", "CEA", "FFF"]
Step-by-step:
BCD
. The pairs are ["B", "C"]
and ["C", "D"]
.
("B", "C")
, allowed top is 'C'
(from "BCC").
("C", "D")
, allowed top is 'E'
(from "CDE").
"CE"
.
"CE"
, the only pair is ("C", "E")
.
("C", "E")
, allowed top is 'A'
(from "CEA").
"A"
(length 1).
true
.
If at any point no allowed top exists for a pair, or no next row can be formed, that path fails and we backtrack.
Brute-force approach:
n
, and each pair allows up to k
possible tops, the total number of possible rows at a level is up to k^{n-1}
.
O(k^{n^2})
.
Optimized approach (with memoization):
O(k^n)
unique rows of length n
, and each is computed once, the total number of subproblems is much smaller.
k^{n-1}
possible next rows, but memoization keeps the total manageable for small n
.
O(k^n)
entries, and the mapping uses O(|allowed|)
space.
The Pyramid Transition Matrix problem is a classic example of recursive backtracking with memoization. By mapping pairs to possible tops, and caching subproblem results, we efficiently explore all valid ways to build the pyramid from the bottom up. The key insight is to treat the problem as constructing all possible next rows for each level, and to use memoization to avoid redundant work. This makes the solution both elegant and practical for reasonable input sizes.
from collections import defaultdict
from functools import lru_cache
class Solution:
def pyramidTransition(self, bottom: str, allowed: list[str]) -> bool:
mapping = defaultdict(set)
for triplet in allowed:
a, b, c = triplet
mapping[(a, b)].add(c)
@lru_cache(None)
def can_build(row):
if len(row) == 1:
return True
# For each pair, get possible tops
candidates = []
for i in range(len(row) - 1):
pair = (row[i], row[i+1])
if pair not in mapping:
return False
candidates.append(mapping[pair])
# Try all combinations for next row
def dfs(idx, path):
if idx == len(candidates):
return can_build(''.join(path))
for c in candidates[idx]:
if dfs(idx + 1, path + [c]):
return True
return False
return dfs(0, [])
return can_build(bottom)
#include <vector>
#include <string>
#include <unordered_map>
#include <unordered_set>
using namespace std;
class Solution {
public:
unordered_map<string, unordered_set<char>> mapping;
unordered_map<string, bool> memo;
bool canBuild(const string& row) {
if (row.size() == 1) return true;
if (memo.count(row)) return memo[row];
vector<unordered_set<char>> candidates;
for (int i = 0; i < row.size() - 1; ++i) {
string pair = row.substr(i, 2);
if (!mapping.count(pair)) return memo[row] = false;
candidates.push_back(mapping[pair]);
}
return memo[row] = dfs(candidates, 0, "", row);
}
bool dfs(const vector<unordered_set<char>>& candidates, int idx, string path, const string& orig) {
if (idx == candidates.size()) {
return canBuild(path);
}
for (char c : candidates[idx]) {
if (dfs(candidates, idx + 1, path + c, orig)) return true;
}
return false;
}
bool pyramidTransition(string bottom, vector<string>& allowed) {
for (const string& triplet : allowed) {
string key = triplet.substr(0, 2);
mapping[key].insert(triplet[2]);
}
return canBuild(bottom);
}
};
import java.util.*;
class Solution {
Map<String, Set<Character>> mapping = new HashMap<>();
Map<String, Boolean> memo = new HashMap<>();
public boolean pyramidTransition(String bottom, List<String> allowed) {
for (String triplet : allowed) {
String key = triplet.substring(0, 2);
char val = triplet.charAt(2);
mapping.computeIfAbsent(key, k -> new HashSet<>()).add(val);
}
return canBuild(bottom);
}
private boolean canBuild(String row) {
if (row.length() == 1) return true;
if (memo.containsKey(row)) return memo.get(row);
List<Set<Character>> candidates = new ArrayList<>();
for (int i = 0; i < row.length() - 1; ++i) {
String pair = row.substring(i, i + 2);
if (!mapping.containsKey(pair)) {
memo.put(row, false);
return false;
}
candidates.add(mapping.get(pair));
}
boolean result = dfs(candidates, 0, new StringBuilder());
memo.put(row, result);
return result;
}
private boolean dfs(List<Set<Character>> candidates, int idx, StringBuilder path) {
if (idx == candidates.size()) {
return canBuild(path.toString());
}
for (char c : candidates.get(idx)) {
path.append(c);
if (dfs(candidates, idx + 1, path)) return true;
path.deleteCharAt(path.length() - 1);
}
return false;
}
}
/**
* @param {string} bottom
* @param {string[]} allowed
* @return {boolean}
*/
var pyramidTransition = function(bottom, allowed) {
const mapping = {};
for (const triplet of allowed) {
const key = triplet.slice(0, 2);
if (!mapping[key]) mapping[key] = new Set();
mapping[key].add(triplet[2]);
}
const memo = new Map();
function canBuild(row) {
if (row.length === 1) return true;
if (memo.has(row)) return memo.get(row);
const candidates = [];
for (let i = 0; i < row.length - 1; ++i) {
const pair = row[i] + row[i+1];
if (!mapping[pair]) {
memo.set(row, false);
return false;
}
candidates.push(Array.from(mapping[pair]));
}
function dfs(idx, path) {
if (idx === candidates.length) {
return canBuild(path.join(''));
}
for (const c of candidates[idx]) {
path.push(c);
if (dfs(idx + 1, path)) return true;
path.pop();
}
return false;
}
const res = dfs(0, []);
memo.set(row, res);
return res;
}
return canBuild(bottom);
};