Facebook Pixel

2189. Number of Ways to Build House of Cards 🔒

Problem Description

You have n playing cards and want to build a house of cards following specific rules:

Structure Rules:

  • A house of cards consists of one or more rows containing triangles and horizontal cards
  • Each triangle is formed by leaning two cards against each other
  • Between every pair of adjacent triangles in the same row, exactly one horizontal card must be placed
  • Triangles in rows above the first row must be placed on top of horizontal cards from the row below
  • Within each row, triangles are placed in the leftmost available positions

Card Count Pattern:

  • Row 0 (bottom): Uses 2 cards (1 triangle, 0 horizontal cards)
  • Row 1: Uses 5 cards (2 triangles, 1 horizontal card between them)
  • Row 2: Uses 8 cards (3 triangles, 2 horizontal cards between them)
  • Row k: Uses 3k + 2 cards (k+1 triangles, k horizontal cards)

Goal: Find the number of distinct ways to build a complete house of cards using exactly n cards. Two houses are considered distinct if they have at least one row with a different number of cards.

Example: If n = 7, you could build:

  • A house with just row 0 (2 cards) and row 1 (5 cards) = 7 total
  • No other valid configuration exists using all 7 cards

The problem essentially asks: in how many ways can you express n as a sum of numbers of the form 3k + 2 where each k value can be used at most once?

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

Let's think about what we're really being asked to do. We need to use exactly n cards to build a house, where each row uses 3k + 2 cards (for row number k starting from 0).

The key insight is that we're essentially partitioning the number n into a sum of distinct values from the sequence: 2, 5, 8, 11, 14, ... (which follows the pattern 3k + 2 for k = 0, 1, 2, ...).

For example, if n = 16, we could have:

  • 16 = 2 + 14 (rows 0 and 4)
  • 16 = 5 + 11 (rows 1 and 3)
  • 16 = 2 + 5 + 8 (rows 0, 1, and 2 - won't work as row 2 can't be placed without row 1 below it)

Wait, there's another constraint! The rows must form a valid house structure. If we use row k, all rows from 0 to k-1 must also be present to support it. This means we can only skip rows at the top, not in the middle.

So the real question becomes: starting from row 0, which consecutive rows should we build, and at what point should we stop?

This leads us to a decision-making process: for each row k, we have two choices:

  1. Skip this row and all subsequent rows - meaning we're done building and must have used exactly n cards
  2. Build this row - use 3k + 2 cards and continue deciding for row k+1

This binary choice at each level naturally suggests a recursive approach where we track:

  • How many cards we have left (n)
  • Which row we're currently considering (k)

The base cases are:

  • If 3k + 2 > n: We can't build this row (not enough cards)
  • If 3k + 2 = n: We must build this row and stop (uses all remaining cards perfectly)
  • Otherwise: Try both options (build or skip) and sum the possibilities

Learn more about Math and Dynamic Programming patterns.

Solution Approach

The solution implements a recursive approach with memoization to count the number of distinct house configurations.

Core Function Design:

We define dfs(n, k) where:

  • n = remaining number of cards
  • k = current row number being considered (starting from 0)
  • Returns: number of ways to build valid houses with the remaining cards

Implementation Details:

  1. Calculate cards needed for current row: x = 3 * k + 2

  2. Base Cases:

    • If x > n: Cannot build row k (insufficient cards), return 0
    • If x == n: Must build row k (uses all remaining cards perfectly), return 1
  3. Recursive Cases: When x < n, we have two choices:

    • Build row k: Use x cards, continue with dfs(n - x, k + 1)
    • Skip row k (and all subsequent rows): Stop building, continue with dfs(n, k + 1)

    The total count is the sum of both choices.

Why This Works:

The recursion explores all valid ways to partition n into sums of the form 3k + 2 where:

  • Each value can be used at most once (each row is unique)
  • Values must be consecutive starting from k = 0 (structural constraint of house of cards)

Memoization with @cache:

The @cache decorator automatically memoizes the function results, preventing redundant calculations when the same (n, k) pair is encountered multiple times. This optimization is crucial as many recursive paths may lead to the same subproblems.

Starting Point:

The solution begins with dfs(n, 0), meaning we start with all n cards available and consider building from row 0.

Time Complexity: O(n²) as we have at most n different values for remaining cards and n different row numbers.

Space Complexity: O(n²) for the memoization cache plus O(n) for the recursion stack depth.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through the solution with n = 12 cards.

We start with dfs(12, 0):

  • Row 0 needs 3*0 + 2 = 2 cards
  • Since 2 < 12, we have two choices:
    1. Build row 0: Use 2 cards, call dfs(10, 1)
    2. Skip row 0: Call dfs(12, 1)

Path 1: Build row 0 → dfs(10, 1):

  • Row 1 needs 3*1 + 2 = 5 cards
  • Since 5 < 10, we have two choices:
    1. Build row 1: Use 5 cards, call dfs(5, 2)
    2. Skip row 1: Call dfs(10, 2)

Path 1.1: Build rows 0 and 1 → dfs(5, 2):

  • Row 2 needs 3*2 + 2 = 8 cards
  • Since 8 > 5, we cannot build row 2
  • Return 0

Path 1.2: Build row 0, skip row 1 → dfs(10, 2):

  • Row 2 needs 8 cards
  • Since 8 < 10, we have two choices:
    1. Build row 2: Use 8 cards, call dfs(2, 3)
    2. Skip row 2: Call dfs(10, 3)

Path 1.2.1: Build rows 0 and 2 → dfs(2, 3):

  • Row 3 needs 3*3 + 2 = 11 cards
  • Since 11 > 2, cannot build row 3
  • Return 0

Path 1.2.2: Build row 0, skip rows 1 and 2 → dfs(10, 3):

  • Row 3 needs 11 cards
  • Since 11 > 10, cannot build row 3
  • Return 0

So Path 1 gives us: 0 + 0 = 0 ways

Path 2: Skip row 0 → dfs(12, 1):

  • Row 1 needs 5 cards
  • Since 5 < 12, we have two choices:
    1. Build row 1: Use 5 cards, call dfs(7, 2)
    2. Skip row 1: Call dfs(12, 2)

Path 2.1: Skip row 0, build row 1 → dfs(7, 2):

  • Row 2 needs 8 cards
  • Since 8 > 7, cannot build row 2
  • Return 0

Path 2.2: Skip rows 0 and 1 → dfs(12, 2):

  • Row 2 needs 8 cards
  • Since 8 < 12, we have two choices:
    1. Build row 2: Use 8 cards, call dfs(4, 3)
    2. Skip row 2: Call dfs(12, 3)

Path 2.2.1: Skip rows 0-1, build row 2 → dfs(4, 3):

  • Row 3 needs 11 cards
  • Since 11 > 4, cannot build row 3
  • Return 0

Path 2.2.2: Skip rows 0-2 → dfs(12, 3):

  • Row 3 needs 11 cards
  • Since 11 < 12, we have two choices:
    1. Build row 3: Use 11 cards, call dfs(1, 4)
    2. Skip row 3: Call dfs(12, 4)

Continuing this process, we eventually find that 12 = 2 + 10 doesn't work (10 isn't in our sequence), and 12 = 5 + 7 doesn't work either (7 isn't in our sequence). However, we discover that row 3 alone uses 11 cards, which is close but not exact.

After fully exploring the recursion tree, we find there are 0 valid ways to use exactly 12 cards, as no combination of rows from our sequence {2, 5, 8, 11, 14, ...} sums to exactly 12.

The memoization ensures that if we encounter the same (n, k) state again, we return the cached result immediately rather than recalculating.

Solution Implementation

1from functools import cache
2
3class Solution:
4    def houseOfCards(self, n: int) -> int:
5        @cache
6        def dfs(remaining_cards: int, base_size: int) -> int:
7            """
8            Dynamic programming function to count ways to build house of cards.
9          
10            Args:
11                remaining_cards: Number of cards left to use
12                base_size: Current base level size (k-th level uses 3*k+2 cards)
13          
14            Returns:
15                Number of valid ways to build the house
16            """
17            # Calculate cards needed for current level with base_size
18            cards_needed = 3 * base_size + 2
19          
20            # Base case: cannot build if required cards exceed remaining
21            if cards_needed > remaining_cards:
22                return 0
23          
24            # Base case: found exact match, one valid way
25            if cards_needed == remaining_cards:
26                return 1
27          
28            # Recursive case: either use current level and continue building up,
29            # or skip to trying a larger base size
30            use_current_level = dfs(remaining_cards - cards_needed, base_size + 1)
31            skip_current_level = dfs(remaining_cards, base_size + 1)
32          
33            return use_current_level + skip_current_level
34      
35        # Start with all n cards and base size 0
36        return dfs(n, 0)
37
1class Solution {
2    // Memoization table to store computed results
3    // dp[remainingCards][lastRowSize] stores the number of ways to build with remainingCards cards
4    // when the last row had lastRowSize triangles
5    private Integer[][] dp;
6
7    /**
8     * Calculates the number of ways to build a house of cards with exactly n cards.
9     * Each triangle uses 2 cards, and each horizontal card uses 1 card.
10     * Row i (0-indexed) must have exactly i triangles.
11     * 
12     * @param n The total number of cards to use
13     * @return The number of valid ways to build the house of cards
14     */
15    public int houseOfCards(int n) {
16        // Initialize memoization table
17        // Maximum possible rows is approximately n/3 (each row needs at least 3 cards)
18        dp = new Integer[n + 1][n / 3];
19      
20        // Start building from row 0 with n cards remaining
21        return countWays(n, 0);
22    }
23
24    /**
25     * Recursive helper function to count the number of valid ways to build.
26     * 
27     * @param remainingCards The number of cards left to use
28     * @param currentRowTriangles The number of triangles in the current row being built
29     * @return The number of valid ways to complete the house from this state
30     */
31    private int countWays(int remainingCards, int currentRowTriangles) {
32        // Calculate cards needed for current row
33        // Each triangle needs 2 cards + 1 horizontal card between triangles
34        // Formula: 2 * triangles + (triangles - 1) = 3 * triangles - 1
35        // But for k triangles, we need 3k + 2 cards total (2k for triangles + k for bases)
36        int cardsNeededForCurrentRow = 3 * currentRowTriangles + 2;
37      
38        // Base case: Not enough cards for current row
39        if (cardsNeededForCurrentRow > remainingCards) {
40            return 0;
41        }
42      
43        // Base case: Exact match - found one valid way
44        if (cardsNeededForCurrentRow == remainingCards) {
45            return 1;
46        }
47      
48        // Check memoization table
49        if (dp[remainingCards][currentRowTriangles] != null) {
50            return dp[remainingCards][currentRowTriangles];
51        }
52      
53        // Two choices:
54        // 1. Use current row and move to next row (with one more triangle)
55        int useCurrentRow = countWays(remainingCards - cardsNeededForCurrentRow, currentRowTriangles + 1);
56      
57        // 2. Skip to a row with more triangles (try next size)
58        int skipToLargerRow = countWays(remainingCards, currentRowTriangles + 1);
59      
60        // Store and return the total count
61        dp[remainingCards][currentRowTriangles] = useCurrentRow + skipToLargerRow;
62        return dp[remainingCards][currentRowTriangles];
63    }
64}
65
1class Solution {
2public:
3    int houseOfCards(int n) {
4        // Create memoization table
5        // dp[remainingCards][currentLevel] stores number of ways to build with remainingCards starting from currentLevel
6        // Maximum levels possible is n/3 + 1 (since minimum cards per level is 3*k+2, starting from k=0)
7        int dp[n + 1][n / 3 + 1];
8        memset(dp, -1, sizeof(dp));
9      
10        // Recursive function with memoization to count number of ways
11        // Parameters:
12        // - remainingCards: number of cards left to use
13        // - currentLevel: current level being built (0-indexed)
14        auto dfs = [&](this auto&& dfs, int remainingCards, int currentLevel) -> int {
15            // Calculate cards needed for current level
16            // Formula: 3 * level + 2 (level 0 needs 2, level 1 needs 5, level 2 needs 8, etc.)
17            int cardsNeeded = 3 * currentLevel + 2;
18          
19            // Base case: not enough cards for current level
20            if (cardsNeeded > remainingCards) {
21                return 0;
22            }
23          
24            // Base case: exact number of cards used
25            if (cardsNeeded == remainingCards) {
26                return 1;
27            }
28          
29            // Return memoized result if already computed
30            if (dp[remainingCards][currentLevel] != -1) {
31                return dp[remainingCards][currentLevel];
32            }
33          
34            // Two choices:
35            // 1. Use current level and continue building (subtract cards used)
36            // 2. Skip current level and try next level
37            int useCurrentLevel = dfs(remainingCards - cardsNeeded, currentLevel + 1);
38            int skipCurrentLevel = dfs(remainingCards, currentLevel + 1);
39          
40            // Store and return the total number of ways
41            return dp[remainingCards][currentLevel] = useCurrentLevel + skipCurrentLevel;
42        };
43      
44        // Start building from level 0 with n cards
45        return dfs(n, 0);
46    }
47};
48
1/**
2 * Calculates the number of ways to build a house of cards using exactly n cards
3 * Each level k requires 3k + 2 cards (k starts from 0)
4 * @param n - Total number of cards available
5 * @returns Number of valid ways to build the house of cards
6 */
7function houseOfCards(n: number): number {
8    // Memoization table: memo[remainingCards][currentLevel]
9    // Initialize with -1 to indicate uncomputed states
10    const memo: number[][] = Array(n + 1)
11        .fill(0)
12        .map(() => Array(Math.floor(n / 3) + 1).fill(-1));
13  
14    /**
15     * Recursive helper function with memoization
16     * @param remainingCards - Number of cards left to use
17     * @param currentLevel - Current level being built (0-indexed)
18     * @returns Number of ways to complete the house from this state
19     */
20    const dfs = (remainingCards: number, currentLevel: number): number => {
21        // Calculate cards needed for the current level
22        const cardsNeeded = currentLevel * 3 + 2;
23      
24        // Base case: Not enough cards for current level
25        if (cardsNeeded > remainingCards) {
26            return 0;
27        }
28      
29        // Base case: Exact match - found one valid way
30        if (cardsNeeded === remainingCards) {
31            return 1;
32        }
33      
34        // Check memoization table
35        if (memo[remainingCards][currentLevel] === -1) {
36            // Two choices:
37            // 1. Use cards for current level and continue building
38            // 2. Skip current level (stop building here)
39            memo[remainingCards][currentLevel] = 
40                dfs(remainingCards - cardsNeeded, currentLevel + 1) + 
41                dfs(remainingCards, currentLevel + 1);
42        }
43      
44        return memo[remainingCards][currentLevel];
45    };
46  
47    // Start building from level 0 with n cards
48    return dfs(n, 0);
49}
50

Time and Space Complexity

The time complexity is O(n^2) and the space complexity is O(n^2).

Time Complexity Analysis: The function uses memoization with @cache decorator. The state space is defined by two parameters: n and k.

  • The parameter n can range from 0 to n (the input value)
  • The parameter k is bounded by the condition 3 * k + 2 ≤ n, which means k ≤ (n - 2) / 3, so k can be at most O(n)
  • Therefore, there are O(n) * O(n) = O(n^2) possible unique states
  • Each state is computed only once due to memoization, and each computation takes O(1) time (excluding recursive calls)
  • Thus, the overall time complexity is O(n^2)

Space Complexity Analysis: The space complexity consists of two components:

  • The memoization cache stores up to O(n^2) states (as analyzed above)
  • The recursion depth is at most O(n) in the worst case, since k increases by at least 1 in each recursive call and is bounded by O(n)
  • The dominant factor is the cache storage of O(n^2)
  • Therefore, the total space complexity is O(n^2)

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

Common Pitfalls

1. Misunderstanding the Problem Constraints

Pitfall: A common mistake is thinking that houses must be built with consecutive rows starting from row 0. This leads to incorrectly assuming that skipping a row means you cannot build any more rows above it.

Why it happens: The problem statement mentions that "triangles in rows above the first row must be placed on top of horizontal cards from the row below," which might be misinterpreted as requiring all rows to be consecutive.

Correct Understanding: The recursion actually explores building different complete houses, not partial houses with gaps. When we "skip" row k, we're not creating a gap - we're choosing to end our house at row k-1 and counting that as one complete configuration.

2. Incorrect Base Case Handling

Pitfall: Writing the base cases as:

if remaining_cards == 0:
    return 1  # Wrong!
if cards_needed > remaining_cards:
    return 0

Issue: This would count incomplete houses (those with leftover cards) as valid configurations. If we have 0 remaining cards after building some rows, that's only valid if we intentionally stopped building, not if we're forced to stop mid-recursion.

Solution: The correct base cases ensure we only count configurations that use exactly n cards:

if cards_needed > remaining_cards:
    return 0  # Can't build this row
if cards_needed == remaining_cards:
    return 1  # Must build this row to use all cards

3. Forgetting Memoization

Pitfall: Implementing the recursive solution without memoization:

def dfs(remaining_cards, base_size):
    # recursive logic without @cache

Issue: This creates exponential time complexity as the same subproblems are solved repeatedly. For example, we might reach state (10, 3) through multiple different paths of building/skipping earlier rows.

Solution: Always use memoization (@cache decorator or manual dictionary) to store computed results:

@cache
def dfs(remaining_cards, base_size):
    # recursive logic with automatic memoization

4. Off-by-One Error in Row Indexing

Pitfall: Confusing the row number with the number of triangles or using 1-based indexing:

cards_needed = 3 * (base_size + 1) + 2  # Wrong formula!

Issue: Row k (0-indexed) uses 3k + 2 cards, not 3(k+1) + 2 cards.

Solution: Maintain consistent 0-based indexing throughout:

  • Row 0: 3(0) + 2 = 2 cards
  • Row 1: 3(1) + 2 = 5 cards
  • Row k: 3k + 2 cards
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of these pictures shows the visit order of a depth-first search?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More