LeetCode 2741 - Special Permutations
The problem asks us to count the number of special permutations of a given array of distinct positive integers nums.
Difficulty: 🟡 Medium
Topics: Array, Dynamic Programming, Bit Manipulation, Bitmask
Solution
Problem Understanding
The problem asks us to count the number of special permutations of a given array of distinct positive integers nums. A permutation is special if, for every adjacent pair of elements nums[i] and nums[i+1], one element divides the other evenly, that is, either nums[i] % nums[i+1] == 0 or nums[i+1] % nums[i] == 0.
The input is a 0-indexed array of length n (between 2 and 14) where each number is positive and distinct. The output is a single integer representing the number of special permutations modulo $10^9 + 7$.
The constraints are crucial. Since n can be at most 14, the total number of permutations is $14! \approx 87 \text{ billion}$, which is too large to enumerate naively. This suggests the need for a dynamic programming approach with bitmasking to efficiently track the permutations.
Edge cases that could cause issues include arrays where no element divides another, arrays with primes, or arrays where all elements are powers of a single number. The problem guarantees that all elements are positive and distinct, which prevents division by zero and duplicate complications.
Approaches
A brute-force approach would generate all permutations of nums and check the divisibility condition for each. While correct, this approach is infeasible due to factorial time complexity, especially when n = 14.
The optimal approach relies on dynamic programming with bitmasking. We can define a DP state dp[mask][i] representing the number of special permutations using the subset of elements indicated by mask where the last element used is nums[i]. Transitions occur by trying to append any element nums[j] not yet used (as indicated by mask) that satisfies the divisibility condition with nums[i]. Using memoization ensures we do not recompute states repeatedly, which drastically reduces the computational effort compared to brute force.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n!) | O(n) | Generate all permutations and check divisibility |
| Optimal | O(n * 2^n) | O(n * 2^n) | DP with bitmasking, track subset of elements and last used element |
Algorithm Walkthrough
- Define a modulo constant
MOD = 10^9 + 7to handle large numbers. - Initialize a 2D DP table
dpwith dimensions[1 << n][n]filled with-1to indicate uncomputed states. - Define a recursive helper function
dfs(mask, last)wheremasktracks used elements andlastis the index of the last element used in the permutation. - If
maskequals(1 << n) - 1, all elements are used and we have found a valid permutation, so return 1. - If
dp[mask][last]is not-1, return the cached value to avoid recomputation. - Initialize a counter
count = 0. Iterate through all indicesjfrom 0 ton-1. Ifjis unused inmaskandnums[last] % nums[j] == 0ornums[j] % nums[last] == 0, recursively calldfs(mask | (1 << j), j)and add the result tocount. - Cache
countindp[mask][last]and return it. - Initialize a total counter. Loop over each starting index
iand adddfs(1 << i, i)to the total. - Return
total % MOD.
This works because the DP recursively constructs all valid permutations, memoizing the counts for each subset and last element to avoid redundant computation. The bitmask efficiently represents the subset of used elements.
Python Solution
from typing import List
class Solution:
def specialPerm(self, nums: List[int]) -> int:
MOD = 10**9 + 7
n = len(nums)
dp = [[-1] * n for _ in range(1 << n)]
def dfs(mask: int, last: int) -> int:
if mask == (1 << n) - 1:
return 1
if dp[mask][last] != -1:
return dp[mask][last]
count = 0
for j in range(n):
if not mask & (1 << j) and (nums[last] % nums[j] == 0 or nums[j] % nums[last] == 0):
count = (count + dfs(mask | (1 << j), j)) % MOD
dp[mask][last] = count
return count
total = 0
for i in range(n):
total = (total + dfs(1 << i, i)) % MOD
return total
This implementation defines the DP table, recursively counts valid permutations with memoization, and iterates over all starting elements. The modulo operation ensures the result fits within the required constraints.
Go Solution
func specialPerm(nums []int) int {
const MOD = 1_000_000_007
n := len(nums)
dp := make([][]int, 1<<n)
for i := range dp {
dp[i] = make([]int, n)
for j := range dp[i] {
dp[i][j] = -1
}
}
var dfs func(mask, last int) int
dfs = func(mask, last int) int {
if mask == (1<<n)-1 {
return 1
}
if dp[mask][last] != -1 {
return dp[mask][last]
}
count := 0
for j := 0; j < n; j++ {
if mask&(1<<j) == 0 && (nums[last]%nums[j] == 0 || nums[j]%nums[last] == 0) {
count = (count + dfs(mask|(1<<j), j)) % MOD
}
}
dp[mask][last] = count
return count
}
total := 0
for i := 0; i < n; i++ {
total = (total + dfs(1<<i, i)) % MOD
}
return total
}
The Go implementation mirrors the Python logic. The DP table uses slices, and the bitmask operations work identically. Integer arithmetic is safe since Go handles modulo operations explicitly.
Worked Examples
Example 1: nums = [2,3,6]
We initialize all DP states to -1 and start DFS from each element.
| Step | mask | last | count calculation |
|---|---|---|---|
| Start at 0 (2) | 001 | 0 | Can go to 2 (6) → mask 101, count = dfs(101,2) |
| Continue | 101 | 2 | Can go to 1 (3) → mask 111, count = 1 |
| Start at 1 (3) | 010 | 1 | Can go to 2 (6) → mask 110, count = dfs(110,2) |
| Continue | 110 | 2 | Can go to 0 (2) → mask 111, count = 1 |
| Start at 2 (6) | 100 | 2 | Can go to 0 (2) and 1 (3) → both lead to valid permutations |
Total count = 2.
Example 2: nums = [1,4,3]
Following the same DFS process, valid permutations are [3,1,4] and [4,1,3]. Total count = 2.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n * 2^n) | There are 2^n subsets and n possible last elements for each DP state. Each transition iterates up to n elements, giving overall O(n * 2^n) |
| Space | O(n * 2^n) | The DP table stores results for all subsets and last elements |
The bitmask ensures efficient subset representation. Memoization avoids recalculating repeated subproblems.
Test Cases
# Provided examples
assert Solution().specialPerm([2,3,6]) == 2 # Example 1
assert Solution().specialPerm([1,4,3]) == 2 # Example 2
# Boundary and stress cases
assert Solution().specialPerm([1,2]) == 2 # Minimal size, both orders valid
assert Solution().specialPerm([2,4,8,16]) == 24 # All divisible, all 4! permutations valid
assert Solution().specialPerm([2,3,5,7]) == 0 # All primes, no adjacent divisible pairs
assert Solution().specialPerm([3,6,2,12]) == 8 # Mixed divisible chain permutations
| Test | Why |
|---|---|
| [2,3,6] | Basic example with multiple valid permutations |
| [1,4,3] | Example with 1 in the middle, divisibility both sides |
| [1,2] | Minimal input, ensures both permutations counted |
| [2,4,8 |