1866. Number of Ways to Rearrange Sticks With K Sticks Visible - Explanation
Prerequisites
Before attempting this problem, you should be comfortable with:
- Recursion - Understanding how to break down problems into smaller subproblems with base cases
- Dynamic Programming - Recognizing overlapping subproblems and using memoization or tabulation
- Combinatorics - Counting arrangements and understanding the recurrence relation for permutations
- Modular Arithmetic - Working with results modulo 10^9 + 7 to prevent integer overflow
1. Recursion
Intuition
Consider placing sticks from tallest to shortest. The tallest stick must be visible. For the remaining sticks, we decide where to place each one relative to previously placed sticks. If a stick is placed at the leftmost available position, it becomes visible. Otherwise, it hides behind a taller stick already placed to its left.
Algorithm
- Define
dfs(N, K)whereNis the number of sticks remaining andKis how many still need to be visible. - Base cases:
- If
N == K, all remaining sticks must be visible (only one way: place them in increasing order from left), return1. - If
N == 0orK == 0(but not both), it's impossible, return0.
- If
- Two choices for the shortest remaining stick:
- Place it at the leftmost position, making it visible:
dfs(N-1, K-1). - Place it in one of the
N-1positions behind a taller stick:(N-1) * dfs(N-1, K).
- Place it at the leftmost position, making it visible:
- Return the sum modulo
10^9 + 7.
Time & Space Complexity
- Time complexity:
- Space complexity: for recursion stack.
2. Dynamic Programming (Top-Down)
Intuition
The recursive solution has overlapping subproblems. By caching results in a 2D table, we avoid redundant computation.
Algorithm
- Create a memoization table of size
(n+1) x (k+1)initialized to-1. - In
dfs(N, K), check if the result is already computed. - Apply the same recurrence:
dfs(N-1, K-1) + (N-1) * dfs(N-1, K). - Store and return the result modulo
10^9 + 7.
class Solution:
def rearrangeSticks(self, n: int, k: int) -> int:
MOD = 10**9 + 7
dp = [[-1] * (k + 1) for _ in range(n + 1)]
def dfs(N, K):
if N == K:
return 1
if N == 0 or K == 0:
return 0
if dp[N][K] != -1:
return dp[N][K]
dp[N][K] = (dfs(N - 1, K - 1) + (N - 1) * dfs(N - 1, K)) % MOD
return dp[N][K]
return dfs(n, k)Time & Space Complexity
- Time complexity:
- Space complexity:
Where represents the total number of sticks, and denotes the number of sticks that must be visible from the left side.
3. Dynamic Programming (Bottom-Up)
Intuition
We fill the DP table iteratively from smaller subproblems to larger ones. dp[N][K] represents the number of ways to arrange N sticks with exactly K visible.
Algorithm
- Create a DP table of size
(n+1) x (k+1)initialized to0. - Set
dp[1][1] = 1(one stick, one visible). - For each
Nfrom2ton, and eachKfrom1tok:dp[N][K] = (dp[N-1][K-1] + (N-1) * dp[N-1][K]) % MOD
- Return
dp[n][k].
class Solution:
def rearrangeSticks(self, n: int, k: int) -> int:
MOD = 10**9 + 7
dp = [[0] * (k + 1) for _ in range(n + 1)]
dp[1][1] = 1
for N in range(2, n + 1):
for K in range(1, k + 1):
dp[N][K] = (dp[N - 1][K - 1] + (N - 1) * dp[N - 1][K]) % MOD
return dp[n][k]Time & Space Complexity
- Time complexity:
- Space complexity:
Where represents the total number of sticks, and denotes the number of sticks that must be visible from the left side.
4. Dynamic Programming (Space Optimized)
Intuition
Each row only depends on the previous row, so we can use a 1D array and update it carefully to avoid overwriting values we still need.
Algorithm
- Use a 1D DP array of size
k + 1. - Set
dp[1] = 1. - For each
Nfrom2ton:- Iterate through
Kfrom1tok, tracking the previous value before updating. - Update
dp[K] = (prev + (N-1) * dp[K]) % MOD.
- Iterate through
- Return
dp[k].
Time & Space Complexity
- Time complexity:
- Space complexity:
Where represents the total number of sticks, and denotes the number of sticks that must be visible from the left side.
Common Pitfalls
Misunderstanding the Recurrence Relation
The key insight is that we place sticks from tallest to shortest. The shortest remaining stick can either be placed at the leftmost position (becoming visible, contributing dfs(N-1, K-1)) or in one of N-1 positions behind a taller stick (contributing (N-1) * dfs(N-1, K)). A common mistake is thinking the multiplier should be N instead of N-1, or confusing which stick is being placed.
Integer Overflow in the Multiplication Term
The term (N-1) * dfs(N-1, K) can cause integer overflow in languages like Java and C++ when both values are large. Always cast to long before multiplication: (long)(N-1) * dfs(N-1, K) % MOD. Failing to do this produces incorrect results for large inputs.
Incorrect Base Cases
The base cases require careful handling. When N == K, all remaining sticks must be visible (return 1). When N == 0 or K == 0 (but not both equal), it is impossible (return 0). A common error is returning 1 when K == 0 regardless of N, or not handling the N == K case before checking K == 0.
Space-Optimized DP Update Order
In the space-optimized version, updating dp[K] requires the old value of dp[K-1] (stored in prev) and the current dp[K]. If you update in the wrong order or forget to save prev before modifying dp[K], you will use already-updated values and get wrong answers. Always save the current value before overwriting: tmp = dp[K]; dp[K] = ...; prev = tmp.