2184. Number of Ways to Build Sturdy Brick Wall
Problem Description
The problem entails building a brick wall of specific dimensions, height
and width
, using bricks that come in different fixed widths (each brick is 1 unit high). In the array bricks
, each brick's width is given, and we have an unlimited number of bricks of each size. The objective is to calculate the total number of ways to build the wall without aligning the joints of bricks from adjacent rows—except at the ends of the wall—to ensure the wall's sturdiness.
Each row must be exactly width
units wide, and bricks may not be stacked atop each other; each brick spans the entire height of its row (which is 1 unit tall). The answer could be quite large, so we require the total count modulo 10^9 + 7
.
Intuition
To solve this problem, we can think of each row in the wall as a sequence of bricks that fit perfectly within the given width
. We start by finding all the valid sequences (arrangements) of bricks that will make a single row of the specified width. We represent these sequences as a list summed up to the width
.
Once we have all the valid sequences for a single row, we approach the problem by assessing which sequences can sit above each other without their joints aligning. This links to the concept of graph theory, where we can think of each sequence as a node and edges that connect sequences that can be placed on top of each other (i.e., no aligned joints, except at the ends). With this graph, we can use dynamic programming to find all possible combinations of sequences for the total height
of the wall.
We initialize a matrix of dynamic programming states where dp[i][j]
represents the number of ways the wall can be built up to height i
with the last row's configuration being the j
th sequence. For each level of height, we compute the number of ways to continue building on top of each sequence, adding those from previous layers that are compatible (no aligning joints).
In the provided code, the recursive function dfs
is used to generate all possible sequences that make up a row of width width
. The check
function verifies if two sequences can be placed one on top of the other without joints aligning. The nested for-loops iterate over the combinations of sequences to populate our graph g
with valid sequences that can follow each other. In the final for-loops, we use dynamic programming to compute the number of possible configurations for each level of the wall until we reach the desired height, and return this count modulo 10^9 + 7
as the final answer.
Learn more about Dynamic Programming and Bitmask patterns.
Solution Approach
To implement the solution, we use several programming concepts such as recursion, depth-first search (DFS), dynamic programming, and graph theory. Here's how each part of the solution works:
-
Depth-First Search (DFS): The
dfs
function is where we generate all the sequences of bricks that exactly fit the givenwidth
. We start with an empty sequence (t
list) and extend it by adding bricks (x
frombricks
array) one by one. If adding a brick exceeds thewidth
, we backtrack; this is the essence of DFS. When a sequence hits the exactwidth
, we record it in thes
list. -
Sequences Validation: To validate whether two sequences (rows of bricks) can be placed on top of each other, we create a
check
function. This function compares the cumulative width of bricks in two sequences to ensure that no joints (except at the ends) line up. As it iterates over the bricks, it maintains a running sum of widths (s1
ands2
) for each sequence. If at any point the sums are equal (and not at the end), the sequences are incompatible. -
Graph Building: We construct a graph represented as a dictionary
g
, where each key is a sequence index, and its value is a list of indices of sequences that can be placed on top of the key sequence without joints aligning. This involves checking each sequence against every other valid sequence, facilitating the dynamic programming step. -
Dynamic Programming (DP): With the graph
g
in place, our next step is to count all viable ways to build the wall up to the specifiedheight
. We initialize a DP tabledp
, wheredp[i][j]
represents the number of ways to build the wall to height where thei
th layer corresponds to thej
th sequence froms
. We start by settingdp[0][j]
to 1 for allj
, as there's exactly one way to start the wall with any sequence. Then for each subsequent layer (i
), we add up the ways we can stack each sequence (j
) on top of all the sequences (k
) from the previous layer that are connected in graphg
. We apply the modulo operation (mod
) to keep the numbers within the required range. -
Summing Up Combinations: At the end, to get the total number of combinations, we iterate over the last layer of the
dp
table and sum up all the values, assessing the complete wall's height. This sum, modulo10^9 + 7
, gives us the final answer detailing the number of ways to construct the sturdy wall.
Throughout this process, we make use of:
- Recursion in the
dfs
function to explore all brick sequences. - Graph Theory by constructing a graph where each node represents a possible sequence configuration and edges represent the ability to place one sequence over another.
- Dynamic Programming to efficiently calculate the number of combinations by avoiding recalculating the same subproblems.
The entire approach revolves around breaking down the problem into manageable subproblems (valid sequences of a single row) and then using the properties of these subproblems (whether they can stack on top of each other) to solve the larger problem (building the entire wall) while avoiding repetitive work through memoization in the DP table.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's illustrate the solution approach with a small example where the goal is to build a wall of height
2 and width
4 using bricks of widths 1 and 2. The available brick sizes are represented in the bricks
array: [1, 2]
.
Step 1: Depth-First Search (DFS) for all valid sequences:
We start by generating all sequences of bricks that fit exactly into the width of 4:
- One option is [1, 1, 1, 1] - using four bricks of width 1.
- Another option is [2, 2] - using two bricks of width 2.
- Two more options include [1, 1, 2] and [1, 2, 1], [2, 1, 1] considering different arrangements of width 1 and 2 bricks.
We now have all valid sequences [ [1, 1, 1, 1], [2, 2], [1, 1, 2], [1, 2, 1], [2, 1, 1] ]
.
Step 2: Sequences Validation:
Using the check
function, we will determine which sequences can be placed on top of each other without the joints aligning:
- The sequence [1, 1, 1, 1] can be placed on top of any other sequence since it doesn't create any aligned joints.
- The sequence [2, 2] cannot be placed on top of itself as that would align joints, but it can be placed on top of [1, 1, 2], [1, 2, 1], or [2, 1, 1].
- Similarly, [1, 1, 2] cannot be on top of [2, 2], but it can go on [1, 2, 1] or [2, 1, 1].
- The same logic applies to [1, 2, 1] and [2, 1, 1].
Step 3: Graph Building:
We construct a graph showing which sequences can sit on top of each other:
g = { 0: [1, 2, 3, 4], // [1, 1, 1, 1] can be placed over any sequence. 1: [2, 3, 4], // [2, 2] can be placed over [1, 1, 2], [1, 2, 1], and [2, 1, 1]. 2: [0, 3, 4], // [1, 1, 2] can be placed over [1, 1, 1, 1], [1, 2, 1], and [2, 1, 1]. 3: [0, 1, 4], // ... 4: [0, 1, 2], }
Step 4: Dynamic Programming (DP):
Initialize the DP table. Let dp[i][j]
be the number of ways to build up to the i
-th height, ending with the j
-th sequence:
dp = [ [1, 1, 1, 1, 1], // Base case: 1 way to lay each sequence as the first row. [0, 0, 0, 0, 0], // We will fill this part with the number of ways to make the second row. ]
We iterate over each sequence for the second layer, and add up compatible sequences from the previous layer according to the graph g
:
dp[1][0] = dp[0][1] + dp[0][2] + dp[0][3] + dp[0][4] // 4 ways to place the second sequence over the first dp[1][1] = dp[0][0] + dp[0][2] + dp[0][3] + dp[0][4] // 4 ways dp[1][2] = dp[0][0] + dp[0][3] + dp[0][4] // 3 ways ...
Now, our DP table looks like this:
dp = [ [1, 1, 1, 1, 1], [4, 4, 3, 3, 3], ]
Step 5: Summing Up Combinations:
Lastly, we sum the last row in the dp
table to find the total number of ways to build the wall to the height of 2:
Total combinations = dp[1][0] + dp[1][1] + dp[1][2] + dp[1][3] + dp[1][4] = 4 + 4 + 3 + 3 + 3 = 17.
Since the answer could be very large, we would normally return this number modulo 10^9 + 7
. In our example, the answer is 17.
So, for our small example with a wall of height
2 and width
4 using bricks of width 1 and 2, there are 17 different ways to build a sturdy wall where no two adjacent row joints align (except at the ends).
Solution Implementation
1from typing import List
2from collections import defaultdict
3
4class Solution:
5 def buildWall(self, height: int, width: int, bricks: List[int]) -> int:
6 # Helper function to find all possible ways to build a single layer
7 def build_layer(current_width):
8 if current_width > width:
9 return
10 if current_width == width:
11 all_layers.append(layer[:])
12 return
13 for brick in bricks:
14 layer.append(brick)
15 build_layer(current_width + brick)
16 layer.pop()
17
18 # Function to check if two configurations can be placed one over the other
19 def is_compatible(layer1, layer2):
20 sum1, sum2 = layer1[0], layer2[0]
21 index1, index2 = 1, 1
22 while index1 < len(layer1) and index2 < len(layer2):
23 if sum1 == sum2:
24 return False
25 if sum1 < sum2:
26 sum1 += layer1[index1]
27 index1 += 1
28 else:
29 sum2 += layer2[index2]
30 index2 += 1
31 return True
32
33 mod = 10**9 + 7
34 all_layers = [] # To store all possible configurations for one layer
35 layer = [] # To store the current layer configuration
36 build_layer(0) # Build layers starting with 0 width
37
38 adjacency_list = defaultdict(list) # To store the graph of compatible layers
39 num_layers = len(all_layers)
40
41 # Build the graph
42 for i in range(num_layers):
43 if is_compatible(all_layers[i], all_layers[i]):
44 adjacency_list[i].append(i)
45 for j in range(i + 1, num_layers):
46 if is_compatible(all_layers[i], all_layers[j]):
47 adjacency_list[i].append(j)
48 adjacency_list[j].append(i)
49
50 # Dynamic programming array initialization
51 dp = [[0] * num_layers for _ in range(height)]
52 for j in range(num_layers):
53 dp[0][j] = 1 # Each configuration is one way to build the first layer
54
55 # Compute number of ways to build the wall using DP
56 for i in range(1, height):
57 for j in range(num_layers):
58 for k in adjacency_list[j]:
59 dp[i][j] += dp[i - 1][k]
60 dp[i][j] %= mod
61
62 return sum(dp[-1]) % mod # Return the number of ways modulo 10^9 + 7
63
1class Solution {
2 // Store all the unique row configurations
3 private List<List<Integer>> uniqueRows = new ArrayList<>();
4 // Temporary list to store current row configuration
5 private List<Integer> currentRow = new ArrayList<>();
6 // Modulus value for the result
7 private static final int MOD = (int) 1e9 + 7;
8 // The wall's width and the set of brick lengths
9 private int wallWidth;
10 private int[] brickSizes;
11
12 public int buildWall(int height, int width, int[] bricks) {
13 this.wallWidth = width;
14 this.brickSizes = bricks;
15 // Generate all possible row configurations
16 generateConfigurations(0);
17 int numConfigs = uniqueRows.size();
18 List<Integer>[] graph = new List[numConfigs];
19 Arrays.setAll(graph, i -> new ArrayList<>());
20
21 // Build the adjacency list for the configurations graph
22 for (int i = 0; i < numConfigs; ++i) {
23 for (int j = 0; j < numConfigs; ++j) {
24 if (i != j && canPlaceRows(uniqueRows.get(i), uniqueRows.get(j))) {
25 graph[i].add(j);
26 }
27 }
28 }
29
30 // Dynamic programming array to store counts of ways to build the wall
31 int[][] dp = new int[height][numConfigs];
32 // Initialize base case for the first row
33 for (int j = 0; j < numConfigs; ++j) {
34 dp[0][j] = 1;
35 }
36
37 // Fill the DP array with number of ways to build wall
38 for (int i = 1; i < height; ++i) {
39 for (int j = 0; j < numConfigs; ++j) {
40 for (int k : graph[j]) {
41 dp[i][j] = (dp[i][j] + dp[i - 1][k]) % MOD;
42 }
43 }
44 }
45
46 // Sum up all the ways to build the wall of the specified height
47 int answer = 0;
48 for (int j = 0; j < numConfigs; ++j) {
49 answer = (answer + dp[height - 1][j]) % MOD;
50 }
51 return answer;
52 }
53
54 // Checks if two rows can be placed on top of each other
55 private boolean canPlaceRows(List<Integer> topRow, List<Integer> bottomRow) {
56 int sumTop = topRow.get(0);
57 int sumBottom = bottomRow.get(0);
58 int i = 1, j = 1;
59 while (i < topRow.size() && j < bottomRow.size()) {
60 if (sumTop == sumBottom) {
61 return false; // Cracks align
62 }
63 if (sumTop < sumBottom) {
64 sumTop += topRow.get(i++);
65 } else {
66 sumBottom += bottomRow.get(j++);
67 }
68 }
69 return true; // No alignment of cracks
70 }
71
72 // Depth First Search to generate all possible configurations of one row
73 private void generateConfigurations(int progress) {
74 if (progress > wallWidth) {
75 return; // Exceeds width of wall, invalid configuration
76 }
77 if (progress == wallWidth) {
78 // Found a valid row configuration
79 uniqueRows.add(new ArrayList<>(currentRow));
80 return;
81 }
82 for (int size : brickSizes) {
83 currentRow.add(size);
84 generateConfigurations(progress + size); // Continue to add bricks
85 currentRow.remove(currentRow.size() - 1); // Backtrack to try other bricks
86 }
87 }
88}
89
1#include <vector>
2
3class Solution {
4public:
5 std::vector<int> bricks;
6 int wallWidth;
7 int mod = 1e9 + 7;
8 std::vector<std::vector<int>> layerPatterns;
9 std::vector<int> currentPattern;
10
11 // Main function to compute the number of ways to build the wall
12 int buildWall(int height, int width, std::vector<int>& bricks) {
13 wallWidth = width;
14 this->bricks = bricks;
15
16 // Generate all possible patterns for a single layer of the wall
17 generatePatterns(0);
18 currentPattern.resize(0);
19
20 // Count patterns excluding same crack patterns
21 int patternCount = layerPatterns.size();
22 std::vector<std::vector<int>> graph(patternCount);
23
24 // Build a graph representing compatible patterns
25 for (int i = 0; i < patternCount; ++i) {
26 if (canPlaceAdjacent(layerPatterns[i], layerPatterns[i])) {
27 graph[i].push_back(i);
28 }
29 for (int j = i + 1; j < patternCount; ++j) {
30 if (canPlaceAdjacent(layerPatterns[i], layerPatterns[j])) {
31 graph[i].push_back(j);
32 graph[j].push_back(i);
33 }
34 }
35 }
36
37 // Prepare for dynamic programming
38 std::vector<std::vector<int>> dp(height, std::vector<int>(patternCount));
39
40 // Initialize the base cases
41 for (int j = 0; j < patternCount; ++j) dp[0][j] = 1;
42
43 // Build up solutions from the base cases
44 for (int i = 1; i < height; ++i) {
45 for (int j = 0; j < patternCount; ++j) {
46 for (int k : graph[j]) {
47 dp[i][j] += dp[i - 1][k];
48 dp[i][j] %= mod;
49 }
50 }
51 }
52
53 // Sum all possible configurations for the top layer
54 int result = 0;
55 for (int j = 0; j < patternCount; ++j) {
56 result += dp[height - 1][j];
57 result %= mod;
58 }
59 return result;
60 }
61
62 // Helper function to check if two layer patterns can be placed one over the other
63 bool canPlaceAdjacent(std::vector<int>& a, std::vector<int>& b) {
64 int sum1 = a[0], sum2 = b[0];
65 int i = 1, j = 1;
66 while (i < a.size() && j < b.size()) {
67 if (sum1 == sum2) return false;
68 if (sum1 < sum2) {
69 sum1 += a[i++];
70 } else {
71 sum2 += b[j++];
72 }
73 }
74 return true;
75 }
76
77 // Depth-first search algorithm to generate all possible layer patterns
78 void generatePatterns(int sum) {
79 // If the current sum exceeds the width of the wall, stop the search
80 if (sum > wallWidth) return;
81
82 // If the current sum equals the width of the wall, add the pattern to the results
83 if (sum == wallWidth) {
84 layerPatterns.push_back(currentPattern);
85 return;
86 }
87
88 // Try each type of brick as a possible continuation of the pattern
89 for (int brick : bricks) {
90 currentPattern.push_back(brick);
91 generatePatterns(sum + brick);
92 // Backtrack
93 currentPattern.pop_back();
94 }
95 }
96};
97
1let bricks: number[];
2let wallWidth: number;
3const MOD = 1e9 + 7;
4let layerPatterns: number[][];
5let currentPattern: number[];
6
7/**
8 * Generate all possible patterns for a single layer of the wall
9 */
10function generatePatterns(sum: number) {
11 // Stop search if the current sum exceeds the wall's width
12 if (sum > wallWidth) return;
13
14 // If the current sum equals the wall's width, add the pattern to results
15 if (sum === wallWidth) {
16 layerPatterns.push([...currentPattern]); // Use spread to copy pattern
17 return;
18 }
19
20 // Try each type of brick as a continuation of the current pattern
21 for (let brick of bricks) {
22 currentPattern.push(brick); // Add brick to the current pattern
23 generatePatterns(sum + brick); // Recursively search for patterns
24 currentPattern.pop(); // Remove the last brick to backtrack
25 }
26}
27
28/**
29 * Check if two layer patterns can be placed over each other without aligning cracks
30 */
31function canPlaceAdjacent(a: number[], b: number[]): boolean {
32 let sum1 = a[0], sum2 = b[0];
33 let i = 1, j = 1;
34
35 // Compare the pattern of bricks, ensuring there are no aligning cracks
36 while (i < a.length && j < b.length) {
37 if (sum1 === sum2) return false; // Patterns are not compatible
38 if (sum1 < sum2) sum1 += a[i++];
39 else sum2 += b[j++];
40 }
41
42 return true;
43}
44
45/**
46 * Main function to compute the number of ways to build the wall
47 */
48function buildWall(height: number, width: number, inputBricks: number[]): number {
49 wallWidth = width;
50 bricks = inputBricks;
51 layerPatterns = [];
52 currentPattern = [];
53
54 generatePatterns(0);
55
56 let patternCount = layerPatterns.length;
57 let graph: number[][] = Array.from({length: patternCount}, () => []);
58
59 for (let i = 0; i < patternCount; ++i) {
60 // Build a graph representing compatible patterns
61 for (let j = i; j < patternCount; ++j) {
62 if (canPlaceAdjacent(layerPatterns[i], layerPatterns[j])) {
63 graph[i].push(j);
64 if (j !== i) graph[j].push(i); // Avoid adding the same index when i == j
65 }
66 }
67 }
68
69 // Prepare for dynamic programming with a 2D array
70 let dp: number[][] = Array.from({length: height}, () => Array(patternCount).fill(0));
71
72 // Initialize the base cases for the bottom layer
73 for (let j = 0; j < patternCount; ++j) dp[0][j] = 1;
74
75 // Build up solutions layer by layer
76 for (let i = 1; i < height; ++i) {
77 for (let j = 0; j < patternCount; ++j) {
78 for (let k of graph[j]) {
79 dp[i][j] = (dp[i][j] + dp[i - 1][k]) % MOD;
80 }
81 }
82 }
83
84 // Sum all configurations for the top layer to find the result
85 let result = 0;
86 for (let j = 0; j < patternCount; ++j) {
87 result = (result + dp[height - 1][j]) % MOD;
88 }
89
90 return result;
91}
92
Time and Space Complexity
Time Complexity
The time complexity can be divided into a few parts:
-
DFS (
dfs
function) for generating all possible row combinations: The DFS function is used to generate all possible combinations of bricks that can form a row of the desired width. In the worst case, it generates all permutational combinations. If we denotek
as the maximum number of bricks that a row can take, then the time complexity for this step can be approximated asO(b^k)
, whereb
is the number of brick sizes in thebricks
list. -
Checking compatibility between two rows (
check
function): The check function is used to determine if two rows are compatible, which means no vertical cracks align. Every row combination is checked against every other combination. This gives us time complexity ofO(n^2 * w)
for this part, wheren
is the total number of unique rows andw
is the width of the wall since in the worst scenario each element of a row is checked against each element of another row. -
Populating the graph with compatible layers: Based on the outputs of the check function, a graph is populated. This is also
O(n^2)
as each row is checked against every other row for compatibility. -
Dynamic Programming to calculate the number of ways to build the wall (
dp
loop): The DP array is filled inheight
iterations, and each sub-problem depends on the previous layer's sub-problems. We haveheight * n
sub-problems, and each could potentially depend on alln
other sub-problems from the previous row. Thus the complexity for this part isO(height * n^2)
.
Combining all steps, the total time complexity of the code is:
T(height, width, b) = O(b^k) + O(n^2 * w) + O(n^2) + O(height * n^2)
Since the number of unique rows n
can be pretty large (up to b^k
), the time complexity is dominated by the last term which involves height
, leading to a worst-case scenario of:
T(height, width, b) = O(height * b^(2k))
Space Complexity
The space complexity is determined by the storage required for:
-
The
s
list that stores all possible row combinations: In the worst case, this could store up tob^k
combinations. -
The
g
defaultdict that stores the adjacency list of the graph: Since we're dealing withn
unique rows and each row can be compatible with up ton-1
other rows, in the worst case, this takesO(n^2)
space. -
The
dp
array: Thedp
array is a 2D array withheight
rows andn
columns, so it takesO(height * n)
space. -
Additional space for the
t
list used during DFS and function call stack: Thet
list and the recursion call stack could in the worst case requireO(width)
space each due to the recursive calls depending on the width of the wall.
The space complexity of the code is thus the maximum of the space used by these data structures:
S(height, width, b) = max(O(b^k), O(n^2), O(height * n), O(width))
Since n
can be as large as b^k
, the dominant term is:
S(height, width, b) = O(height * b^(2k)) + O(width)
Considering O(height * b^(2k))
would generally be larger than O(width)
, we can approximate the space complexity as:
S(height, width, b) = O(height * b^(2k))
Learn more about how to find time and space complexity quickly using problem constraints.
What data structure does Breadth-first search typically uses to store intermediate states?
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Bitmask and Dynamic Programming Bit manipulation is a crucial aspect of computer programming and one of the most powerful tools for bit manipulation is bitmasks Let's first understand what a bit is A bit is a binary digit It's the smallest piece of data in a computer and can be
LeetCode Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way we
Want a Structured Path to Master System Design Too? Don’t Miss This!
in example walkthrough, in step 2 i.e. sequence validation, it says 1,1,1,1 with any other sequence, but it has aligned joints at joint 2 with sequence 2,2. can you clarify this?