LeetCode 1866 - Number of Ways to Rearrange Sticks With K Sticks Visible
The problem gives us n sticks with unique lengths from 1 to n. We must arrange these sticks in some order so that exactly k sticks are visible when looking from the left side. A stick is visible if every stick before it is shorter.
Difficulty: 🔴 Hard
Topics: Math, Dynamic Programming, Combinatorics
Solution
Problem Understanding
The problem gives us n sticks with unique lengths from 1 to n. We must arrange these sticks in some order so that exactly k sticks are visible when looking from the left side.
A stick is visible if every stick before it is shorter. In other words, a stick is visible if it creates a new maximum height while scanning from left to right.
For example, in the arrangement [1,3,2,5,4]:
1is visible because nothing is before it.3is visible because it is taller than1.2is not visible because3blocks it.5is visible because it is taller than everything before it.4is not visible because5blocks it.
So there are exactly 3 visible sticks.
The task is to count how many permutations of the numbers 1 through n produce exactly k visible sticks. Since the number of permutations grows extremely quickly, we return the answer modulo 10^9 + 7.
The constraints are important:
1 <= n <= 10001 <= k <= n
A brute force solution would require checking all n! permutations, which becomes impossible even for moderate values of n. For example:
10! = 3,628,80015!is already over one trillion1000!is astronomically large
This immediately suggests that we need a combinatorial or dynamic programming solution.
Several edge cases are important:
k = 1, only one stick is visible. This happens when the tallest stick appears first.k = n, every stick is visible. This only happens when the sticks are in strictly increasing order.n = 1, there is only one arrangement and one visible stick.- Large values like
n = 1000, where recursion without memoization or factorial enumeration would fail.
Approaches
Brute Force Approach
The most straightforward idea is to generate every permutation of the sticks and count how many visible sticks each arrangement produces.
For every permutation:
- Scan from left to right.
- Track the tallest stick seen so far.
- Whenever a taller stick appears, increment the visible count.
- If the visible count equals
k, increment the answer.
This approach is correct because it explicitly checks every possible arrangement.
However, it is far too slow. There are n! permutations, and each permutation requires O(n) work to count visible sticks. The total complexity becomes O(n × n!), which is completely infeasible for n = 1000.
Key Insight
Instead of constructing full permutations directly, we can build the answer incrementally using dynamic programming.
Suppose we already know the number of valid arrangements for n - 1 sticks. How can we insert the largest stick n?
The largest stick is special because it is always visible whenever it appears.
There are two possibilities:
- Place stick
nat the front.
- It becomes visible.
- The remaining
n - 1sticks must contributek - 1visible sticks. - Contribution:
dp[n-1][k-1]
2. Place stick n somewhere other than the front.
- Since
nis the tallest stick, it will block visibility for everything after it. - It does not create a new visible count beyond what already existed before it.
- There are
n - 1possible insertion positions after the first position. - Contribution:
(n - 1) * dp[n-1][k]
This gives the recurrence:
$$dp[n][k] = dp[n-1][k-1] + (n-1) \times dp[n-1][k]$$
This recurrence is the core insight that makes the problem tractable.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n × n!) | O(n) | Enumerates every permutation |
| Optimal Dynamic Programming | O(n × k) | O(n × k) | Uses combinatorial recurrence |
Algorithm Walkthrough
- Define
dp[i][j]as the number of ways to arrangeisticks such that exactlyjsticks are visible from the left. - Initialize the base case:
dp[1][1] = 1- With one stick, there is exactly one arrangement and one visible stick.
- Iterate from
i = 2ton.
- We compute all answers involving
isticks using previously computed states.
- For each
jfrom1tomin(i, k):
- Consider where to place the largest stick
i.
- Case 1, place stick
iat the beginning:
- The largest stick becomes visible immediately.
- The remaining
i - 1sticks must contributej - 1visible sticks. - Add:
dp[i-1][j-1]
6. Case 2, place stick i somewhere after the first position:
- There are
i - 1choices. - Since stick
iis tallest, it does not increase the visible count when inserted later. - Add:
(i - 1) * dp[i-1][j]
7. Combine both contributions:
$$dp[i][j] = dp[i-1][j-1] + (i-1)\times dp[i-1][j]$$
8. Apply modulo 10^9 + 7 after every update to avoid overflow.
9. Return dp[n][k].
Why it works
The recurrence partitions all valid arrangements into two disjoint categories based on the position of the largest stick. Every valid permutation belongs to exactly one of these categories, and both categories preserve the visibility count in predictable ways. Since the recurrence exhausts all possibilities without overlap, the DP computes the exact number of valid arrangements.
Python Solution
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 sticks in range(2, n + 1):
for visible in range(1, min(sticks, k) + 1):
place_front = dp[sticks - 1][visible - 1]
place_elsewhere = (
(sticks - 1) * dp[sticks - 1][visible]
) % MOD
dp[sticks][visible] = (
place_front + place_elsewhere
) % MOD
return dp[n][k]
The implementation directly mirrors the recurrence relation.
The dp table stores the number of valid arrangements for every combination of stick count and visible count.
The outer loop iterates over the number of sticks currently being considered. The inner loop computes all possible visible counts for that stick count.
For every state:
place_frontcorresponds to placing the largest stick first.place_elsewherecorresponds to inserting the largest stick into one of the remaining positions.
The modulo operation is applied after multiplication and addition to ensure values remain within bounds.
Finally, dp[n][k] contains the desired answer.
Go Solution
func rearrangeSticks(n int, k int) int {
const MOD int = 1_000_000_007
dp := make([][]int, n+1)
for i := range dp {
dp[i] = make([]int, k+1)
}
dp[1][1] = 1
for sticks := 2; sticks <= n; sticks++ {
limit := k
if sticks < k {
limit = sticks
}
for visible := 1; visible <= limit; visible++ {
placeFront := dp[sticks-1][visible-1]
placeElsewhere := int(
(int64(sticks-1) * int64(dp[sticks-1][visible])) % int64(MOD),
)
dp[sticks][visible] =
(placeFront + placeElsewhere) % MOD
}
}
return dp[n][k]
}
The Go implementation follows the same recurrence and DP structure as the Python version.
The main Go-specific consideration is integer overflow. Multiplication between (sticks - 1) and dp[sticks-1][visible] can exceed 32-bit integer limits, so the multiplication is performed using int64 before converting back to int.
Slices are used to construct the two-dimensional DP table dynamically.
Worked Examples
Example 1
Input:
n = 3, k = 2
We build the DP table step by step.
Initial state:
| i | j | dp[i][j] |
|---|---|---|
| 1 | 1 | 1 |
For i = 2:
j = 1
$$dp[2][1] = dp[1][0] + 1 \times dp[1][1]$$
$$= 0 + 1 \times 1 = 1$$
j = 2
$$dp[2][2] = dp[1][1] + 1 \times dp[1][2]$$
$$= 1 + 0 = 1$$
Current table:
| i | j | dp[i][j] |
|---|---|---|
| 2 | 1 | 1 |
| 2 | 2 | 1 |
For i = 3:
j = 1
$$dp[3][1] = dp[2][0] + 2 \times dp[2][1]$$
$$= 0 + 2 \times 1 = 2$$
j = 2
$$dp[3][2] = dp[2][1] + 2 \times dp[2][2]$$
$$= 1 + 2 \times 1 = 3$$
j = 3
$$dp[3][3] = dp[2][2] + 2 \times dp[2][3]$$
$$= 1 + 0 = 1$$
Final answer:
dp[3][2] = 3
Example 2
Input:
n = 5, k = 5
To have all 5 sticks visible, every stick must be taller than all previous sticks.
That only happens in strictly increasing order:
[1,2,3,4,5]
The DP recurrence eventually produces:
| i | j | dp[i][j] |
|---|---|---|
| 5 | 5 | 1 |
Final answer:
1
Example 3
Input:
n = 20, k = 11
The DP fills states incrementally up to dp[20][11].
After all computations:
dp[20][11] = 647427950
This value is already modulo 10^9 + 7.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n × k) | Each DP state is computed once |
| Space | O(n × k) | DP table stores all states |
The DP table contains (n + 1) × (k + 1) states. Every state requires constant work using the recurrence relation, so the total runtime is O(n × k).
The space complexity is also O(n × k) because we store the entire table. This is efficient enough for n <= 1000.
Test Cases
sol = Solution()
# Provided examples
assert sol.rearrangeSticks(3, 2) == 3 # Example 1
assert sol.rearrangeSticks(5, 5) == 1 # Strictly increasing order
assert sol.rearrangeSticks(20, 11) == 647427950 # Large example
# Smallest input
assert sol.rearrangeSticks(1, 1) == 1 # Single stick
# Only one visible stick
assert sol.rearrangeSticks(2, 1) == 1 # [2,1]
assert sol.rearrangeSticks(3, 1) == 2 # [3,1,2], [3,2,1]
# All sticks visible
assert sol.rearrangeSticks(2, 2) == 1 # [1,2]
assert sol.rearrangeSticks(3, 3) == 1 # [1,2,3]
# Intermediate values
assert sol.rearrangeSticks(4, 2) == 11 # Known DP result
assert sol.rearrangeSticks(4, 3) == 6 # Known DP result
# Larger boundary-style checks
assert sol.rearrangeSticks(10, 1) == 362880 # (10-1)!
assert sol.rearrangeSticks(10, 10) == 1 # Increasing order only
# Validity checks
assert sol.rearrangeSticks(6, 2) > 0 # General nontrivial case
assert sol.rearrangeSticks(8, 4) > 0 # Medium-sized state
| Test | Why |
|---|---|
rearrangeSticks(3, 2) |
Verifies sample input |
rearrangeSticks(5, 5) |
Tests all-visible scenario |
rearrangeSticks(20, 11) |
Tests large DP computation |
rearrangeSticks(1, 1) |
Smallest valid input |
rearrangeSticks(3, 1) |
Tests single visible stick behavior |
rearrangeSticks(3, 3) |
Tests strictly increasing arrangement |
rearrangeSticks(4, 2) |
Verifies intermediate DP transitions |
rearrangeSticks(10, 1) |
Confirms factorial-related pattern |
rearrangeSticks(8, 4) |
General medium-sized correctness case |
Edge Cases
One important edge case is k = 1. In this scenario, only the first stick can be visible. The tallest stick must therefore appear first, otherwise another stick would become visible before it. A buggy implementation might incorrectly count additional permutations. The DP handles this naturally through the recurrence, and the resulting values match (n - 1)!.
Another critical edge case is k = n. Every stick must be visible, which means the arrangement must be strictly increasing. There is exactly one valid arrangement. The recurrence correctly preserves this property because every new largest stick must be inserted at the front-visible position.
The smallest input, n = 1 and k = 1, is also important. Without a proper base case, the recurrence would reference invalid states and produce zero instead of one. Initializing dp[1][1] = 1 ensures the DP builds correctly from the smallest valid configuration.
A subtle edge case involves modulo arithmetic. Intermediate multiplication values can become extremely large, especially near n = 1000. Without taking modulo after multiplication and addition, integer overflow could occur in languages like Go. The implementation avoids this by applying modulo consistently during every transition.