Special Permutations
MedPrompt
You are given a 0-indexed integer array nums containing n distinct positive integers. A permutation of nums is called special if:
- For all indexes
0 <= i < n - 1, eithernums[i] % nums[i+1] == 0ornums[i+1] % nums[i] == 0.
Return the total number of special permutations. As the answer could be large, return it modulo 109 + 7.
Example 1:
Input: nums = [2,3,6]
Output: 2
Explanation: [3,6,2] and [2,6,3] are the two special permutations of nums.Example 2:
Input: nums = [1,4,3]
Output: 2
Explanation: [3,1,4] and [4,1,3] are the two special permutations of nums.
Constraints:
2 <= nums.length <= 141 <= nums[i] <= 109
Approaches
3 approaches with complexity analysis and trade-offs.
This approach involves generating every possible arrangement (permutation) of the nums array. For each generated permutation, it performs a check to see if it satisfies the "special" condition. If it does, a counter is incremented. This method is the most straightforward to conceptualize but is computationally very expensive.
Algorithm
- Initialize a counter
countto 0. - Create a recursive helper function,
generatePermutations(nums, current_permutation, used_flags), to generate all permutations ofnums. - The function works as follows:
- Base Case: If the
current_permutationhasnelements, it's a complete permutation. Check if it's a special permutation using a helper functionisSpecial(). - If it is special, increment the
count(with modulo arithmetic). - Recursive Step: Iterate through each number in the original
numsarray. If a number hasn't been used, mark it as used, add it tocurrent_permutation, and make a recursive call. After the call returns, backtrack by removing the number and unmarking it.
- Base Case: If the
- The
isSpecial(permutation)helper function iterates fromi = 0ton-2and checks ifperm[i] % perm[i+1] == 0orperm[i+1] % perm[i] == 0for all adjacent pairs. If any pair fails this condition, it returnsfalse. - Start the process by calling
generatePermutationswith an empty permutation. - Return the final
count.
Walkthrough
The core idea is to explore all n! permutations of the input array. We can use a recursive helper function to build these permutations. The function maintains a list for the current permutation being built and a boolean array to keep track of which numbers from the original array have been used.
Once a full permutation of length n is formed, we validate it. The validation involves iterating through the permutation from the first to the second-to-last element and checking the divisibility condition for each adjacent pair (nums[i] and nums[i+1]). If all pairs in the permutation satisfy the condition, we increment our total count of special permutations. Since the answer can be large, the count is maintained modulo 10^9 + 7.
class Solution { long count = 0; int MOD = 1_000_000_007; public int specialPerm(int[] nums) { List<Integer> p = new ArrayList<>(); boolean[] used = new boolean[nums.length]; generatePermutations(nums, p, used); return (int) count; } private void generatePermutations(int[] nums, List<Integer> p, boolean[] used) { if (p.size() == nums.length) { if (isSpecial(p)) { count = (count + 1) % MOD; } return; } for (int i = 0; i < nums.length; i++) { if (!used[i]) { used[i] = true; p.add(nums[i]); generatePermutations(nums, p, used); p.remove(p.size() - 1); // Backtrack used[i] = false; } } } private boolean isSpecial(List<Integer> p) { for (int i = 0; i < p.size() - 1; i++) { if (p.get(i) % p.get(i + 1) != 0 && p.get(i + 1) % p.get(i) != 0) { return false; } } return true; }}Complexity
Time
O(n! * n) - There are `n!` possible permutations to generate. For each permutation, we take O(n) time to check if it's special. This makes the approach infeasible for `n > 10`.
Space
O(n) - The space is dominated by the recursion stack depth and the list used to store the current permutation, both of which can go up to size `n`.
Trade-offs
Pros
Simple to understand and implement.
Correctly solves the problem for very small values of
n.
Cons
Extremely inefficient due to its factorial time complexity.
Will result in a 'Time Limit Exceeded' error on platforms like LeetCode for constraints like n=14.
This approach improves upon brute-force by integrating the validity check into the permutation generation process. Instead of generating a full permutation and then checking it, we build the permutation one element at a time. We only add a new element if it satisfies the "special" condition with the previous element. If at any point an element cannot be added, we prune this entire branch of the search tree, thus avoiding the generation of many invalid permutations.
Algorithm
- Use a recursive Depth-First Search (DFS) function,
dfs(current_permutation, used_flags), to build special permutations element by element. - The function works as follows:
- Base Case: If the
current_permutationsize isn, we have successfully built a full special permutation. Increment the total count (modulo10^9 + 7) and return. - Recursive Step: Iterate through all numbers
nums[i]. - If
nums[i]has not been used yet, check if it can be appended to thecurrent_permutation. - The condition is: the permutation is empty, OR
nums[i]is divisible by the last element, OR the last element is divisible bynums[i]. - If the condition is met, mark
nums[i]as used, add it to the permutation, and make a recursive call:dfs(new_permutation, new_used_flags). - After the recursive call returns, backtrack by removing
nums[i]from the permutation and unmarking it as used. This allows exploring other possibilities.
- Base Case: If the
- The initial call is made with an empty permutation.
Walkthrough
We employ a recursive depth-first search (DFS) strategy. The recursive function explores paths that could form a special permutation. It keeps track of the permutation being built and the numbers that have already been used.
At each step of the recursion, we try to append an unused number to the current partial permutation. A number can be appended only if it's the first number in the permutation or if it satisfies the divisibility rule with the last number currently in the permutation. If it does, we add it and recurse. If not, we discard this choice and try the next unused number. This pruning of invalid paths makes the search more efficient than the naive brute-force method.
class Solution { long count = 0; int MOD = 1_000_000_007; int n; public int specialPerm(int[] nums) { this.n = nums.length; boolean[] used = new boolean[n]; dfs(new ArrayList<>(), used, nums); return (int) count; } private void dfs(List<Integer> currentPerm, boolean[] used, int[] nums) { if (currentPerm.size() == n) { count = (count + 1) % MOD; return; } for (int i = 0; i < n; i++) { if (!used[i]) { if (currentPerm.isEmpty() || (currentPerm.get(currentPerm.size() - 1) % nums[i] == 0) || (nums[i] % currentPerm.get(currentPerm.size() - 1) == 0)) { used[i] = true; currentPerm.add(nums[i]); dfs(currentPerm, used, nums); currentPerm.remove(currentPerm.size() - 1); // Backtrack used[i] = false; } } } }}Complexity
Time
O(n!) - In the worst case, the complexity is still factorial. However, for average cases where the graph of divisibility is sparse, the performance is much better than O(n! * n) because many branches are pruned early.
Space
O(n) - For the recursion stack depth and the list storing the current permutation.
Trade-offs
Pros
Significantly more efficient than the brute-force approach due to pruning.
Conceptually builds on the idea of permutation generation, making it relatively easy to understand.
Cons
The time complexity is still factorial in the worst-case scenario (when all numbers are mutually divisible, forming a complete graph).
It can be too slow for the given constraints as it doesn't store results of subproblems, leading to redundant computations.
This is the most efficient approach for the given constraints. It uses dynamic programming with bitmasking to avoid the redundant computations inherent in the recursive backtracking solution. The state of our DP is defined by the subset of numbers used in the permutation and the last number of the permutation.
Algorithm
- This problem can be modeled as finding the number of Hamiltonian paths in a graph where vertices are numbers and an edge exists if one divides the other.
- We use a 2D array
memo[mask][lastIdx]for memoization.maskis a bitmask representing the set of used numbers, andlastIdxis the index of the last number in the partial permutation. memo[mask][lastIdx]stores the number of special permutations using numbers inmaskand ending withnums[lastIdx].- Define a recursive function
solve(mask, lastIdx):- Base Case: If
maskhas only one bit set (i.e.,mask == (1 << lastIdx)), it's a permutation of length 1. Return 1. - Memoization: If
memo[mask][lastIdx]is already computed, return the stored value. - Recursive Step: Calculate the mask for the subproblem:
prevMask = mask ^ (1 << lastIdx). Iterate through all possible previous elementsnums[prev](whereprevis from 0 ton-1). - If
nums[prev]is inprevMaskand satisfies the divisibility condition withnums[lastIdx], recursively callsolve(prevMask, prev)and add the result to a running total (with modulo). - Store the computed total in
memo[mask][lastIdx]and return it.
- Base Case: If
- The final answer is the sum of
solve((1 << n) - 1, i)for allifrom 0 ton-1, as any number can be the last element of a full permutation.
Walkthrough
We can think of this problem as finding the number of Hamiltonian paths in a graph. The vertices are the numbers from the nums array, and an edge connects two vertices u and v if u % v == 0 or v % u == 0.
A Hamiltonian path visits each vertex exactly once. We need to count all such paths.
We define a DP state dp[mask][i], which stores the number of special partial permutations that use the set of numbers represented by mask and end with the number nums[i]. The mask is an integer where the j-th bit is set to 1 if nums[j] has been used.
This can be implemented with a top-down recursive approach with memoization. The function solve(mask, last_idx) computes our DP state. The base case is a permutation of length 1. In the recursive step, we try to extend shorter valid permutations by one element. To find dp[mask][i], we look at all valid permutations for the sub-mask mask excluding i, and for each of those permutations ending in j, we check if i can follow j. If it can, we add dp[sub-mask][j] to our result for dp[mask][i].
The final answer is the sum of dp[final_mask][i] for all i, where final_mask has all n bits set, representing all numbers being used.
import java.util.Arrays; class Solution { int n; int[] nums; long[][] memo; int MOD = 1_000_000_007; public int specialPerm(int[] nums) { this.n = nums.length; this.nums = nums; this.memo = new long[1 << n][n]; for (long[] row : memo) { Arrays.fill(row, -1); } long totalCount = 0; int finalMask = (1 << n) - 1; for (int i = 0; i < n; i++) { totalCount = (totalCount + solve(finalMask, i)) % MOD; } return (int) totalCount; } private long solve(int mask, int lastIdx) { // Base case: if only one element is in the permutation, there's one way. if (mask == (1 << lastIdx)) { return 1; } if (memo[mask][lastIdx] != -1) { return memo[mask][lastIdx]; } long count = 0; int prevMask = mask ^ (1 << lastIdx); for (int prev = 0; prev < n; prev++) { // Check if 'prev' was in the previous sub-permutation if ((prevMask & (1 << prev)) != 0) { // Check if the current element can follow the previous one if (nums[lastIdx] % nums[prev] == 0 || nums[prev] % nums[lastIdx] == 0) { count = (count + solve(prevMask, prev)) % MOD; } } } return memo[mask][lastIdx] = count; }}Complexity
Time
O(n^2 * 2^n) - There are `n * 2^n` states `(mask, lastIdx)`. For each state, we iterate through `n` possible previous elements to compute the result. Thus, the total time is `n * 2^n * n`.
Space
O(n * 2^n) - We use a 2D array for memoization of size `(1 << n) x n`.
Trade-offs
Pros
Highly efficient and guaranteed to pass within the time limits for n <= 14.
It's a standard and powerful technique for permutation-related problems on small sets (Traveling Salesperson Problem, etc.).
Cons
The space complexity of O(n * 2^n) can be large, though it's acceptable for n <= 14.
Can be less intuitive to grasp compared to straightforward backtracking.
Solutions
Solution
class Solution {public int specialPerm(int[] nums) { final int mod = (int)1 e9 + 7; int n = nums.length; int m = 1 << n; int[][] f = new int[m][n]; for (int i = 1; i < m; ++i) { for (int j = 0; j < n; ++j) { if ((i >> j & 1) == 1) { int ii = i ^ (1 << j); if (ii == 0) { f[i][j] = 1; continue; } for (int k = 0; k < n; ++k) { if (nums[j] % nums[k] == 0 || nums[k] % nums[j] == 0) { f[i][j] = (f[i][j] + f[ii][k]) % mod; } } } } } int ans = 0; for (int x : f[m - 1]) { ans = (ans + x) % mod; } return ans; }}class Solution {public: int specialPerm(vector<int> &nums) { const int mod = 1e9 + 7; int n = nums.size(); int m = 1 << n; int f[m][n]; memset(f, 0, sizeof(f)); for (int i = 1; i < m; ++i) { for (int j = 0; j < n; ++j) { if ((i >> j & 1) == 1) { int ii = i ^ (1 << j); if (ii == 0) { f[i][j] = 1; continue; } for (int k = 0; k < n; ++k) { if (nums[j] % nums[k] == 0 || nums[k] % nums[j] == 0) { f[i][j] = (f[i][j] + f[ii][k]) % mod; } } } } } int ans = 0; for (int x : f[m - 1]) { ans = (ans + x) % mod; } return ans; }};class Solution: def specialPerm(self, nums: List[int]) -> int: mod = 10 ** 9 + 7 n = len(nums) m = 1 << n f = [[0] * n for _ in range(m)] for i in range(1, m): for j, x in enumerate(nums): if i >> j & 1: ii = i ^ (1 << j) if ii == 0: f[i][j] = 1 continue for k, y in enumerate(nums): if x % y == 0 or y % x == 0: f[i][j] = (f[i][j] + f[ii][k]) % mod return sum(f[- 1]) % modVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.