LeetCode 2741 - Special Permutations

The problem asks us to count the number of special permutations of a given array of distinct positive integers nums.

LeetCode Problem 2741

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

  1. Define a modulo constant MOD = 10^9 + 7 to handle large numbers.
  2. Initialize a 2D DP table dp with dimensions [1 << n][n] filled with -1 to indicate uncomputed states.
  3. Define a recursive helper function dfs(mask, last) where mask tracks used elements and last is the index of the last element used in the permutation.
  4. If mask equals (1 << n) - 1, all elements are used and we have found a valid permutation, so return 1.
  5. If dp[mask][last] is not -1, return the cached value to avoid recomputation.
  6. Initialize a counter count = 0. Iterate through all indices j from 0 to n-1. If j is unused in mask and nums[last] % nums[j] == 0 or nums[j] % nums[last] == 0, recursively call dfs(mask | (1 << j), j) and add the result to count.
  7. Cache count in dp[mask][last] and return it.
  8. Initialize a total counter. Loop over each starting index i and add dfs(1 << i, i) to the total.
  9. 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