3149. Find the Minimum Cost Array Permutation
Problem Description
You are given an array nums which is a permutation of [0, 1, 2, ..., n - 1]. Your task is to find another permutation perm of the same set [0, 1, 2, ..., n - 1] that minimizes a specific score.
The score of any permutation perm is calculated as: score(perm) = |perm[0] - nums[perm[1]]| + |perm[1] - nums[perm[2]]| + ... + |perm[n - 1] - nums[perm[0]]|
In this formula:
- Each term calculates the absolute difference between an element in
permat positioniand an element innumsat positionperm[i+1] - The calculation wraps around cyclically, so the last term uses
perm[n-1]andnums[perm[0]]
The goal is to find the permutation perm that produces the minimum possible score. If multiple permutations have the same minimum score, you should return the lexicographically smallest one among them.
For example, if nums = [1, 0, 2], you need to find a permutation of [0, 1, 2] that, when used as indices and values according to the scoring formula, gives the smallest total sum of absolute differences. The permutation should start with 0 when possible for lexicographic ordering, and among all minimum-score permutations, choose the one that comes first in dictionary order.
Intuition
The first key observation is that the score formula creates a cycle: we're essentially creating a circular chain where each element in perm points to the next one, and we sum up the differences between each element and what it points to in nums. Since this is a cycle, we can rotate any valid permutation and get the same score. For example, [0, 1, 2] and [1, 2, 0] would have the same score value.
Given that rotations preserve the score and we want the lexicographically smallest result, we can fix the first element as 0. This simplifies our problem significantly - we only need to find the optimal arrangement of the remaining elements [1, 2, ..., n-1].
The small constraint (n ≤ 14) immediately suggests that we can use bit manipulation and dynamic programming. With at most 14 elements, we have 2^14 = 16384 possible states, which is manageable.
We can think of building the permutation step by step. At each step, we need to decide which unvisited number to visit next. The challenge is that our current choice affects future costs - if we choose number cur after pre, we pay |pre - nums[cur]| now, but this also determines where we'll be when making the next choice.
This naturally leads to a state-based dynamic programming approach where:
- We track which numbers we've already used (using a bitmask)
- We track what the last number we chose was (since this affects the cost of the next choice)
- We compute the minimum remaining cost from each state
Once we know the minimum costs, we can reconstruct the actual permutation by greedily choosing the next number that leads to the optimal total cost while maintaining lexicographic order (trying smaller numbers first).
Learn more about Dynamic Programming and Bitmask patterns.
Solution Implementation
1class Solution: 2 def findPermutation(self, nums: List[int]) -> List[int]: 3 """ 4 Find a permutation of indices [0, 1, ..., n-1] that minimizes the total 5 absolute difference when traversing the array in that order (forming a cycle). 6 7 Args: 8 nums: List of integers 9 10 Returns: 11 List of indices representing the optimal permutation 12 """ 13 14 @cache 15 def calculate_min_cost(visited_mask: int, last_index: int) -> int: 16 """ 17 Calculate minimum cost to visit all remaining nodes starting from last_index. 18 19 Args: 20 visited_mask: Bitmask representing visited indices (1 = visited) 21 last_index: The index of the last visited element 22 23 Returns: 24 Minimum cost to complete the tour 25 """ 26 # If all nodes are visited, return cost to go back to start 27 if visited_mask == (1 << n) - 1: 28 return abs(nums[last_index] - nums[0]) 29 30 min_cost = float('inf') 31 32 # Try visiting each unvisited node 33 for next_index in range(1, n): 34 # Check if next_index is not visited (bit is 0) 35 if (visited_mask >> next_index) & 1 == 0: 36 # Calculate cost: edge cost + remaining tour cost 37 cost = abs(nums[last_index] - nums[next_index]) + \ 38 calculate_min_cost(visited_mask | (1 << next_index), next_index) 39 min_cost = min(min_cost, cost) 40 41 return min_cost 42 43 def construct_path(visited_mask: int, current_index: int) -> None: 44 """ 45 Reconstruct the optimal path by following the decisions that led to minimum cost. 46 47 Args: 48 visited_mask: Bitmask representing visited indices 49 current_index: Current position in the tour 50 """ 51 # Add current index to the result path 52 result_path.append(current_index) 53 54 # If all nodes are visited, we're done 55 if visited_mask == (1 << n) - 1: 56 return 57 58 # Get the optimal cost from current state 59 optimal_cost = calculate_min_cost(visited_mask, current_index) 60 61 # Find which next move achieves this optimal cost 62 for next_index in range(1, n): 63 # Check if next_index is not visited 64 if (visited_mask >> next_index) & 1 == 0: 65 # Calculate cost of moving to next_index 66 edge_cost = abs(nums[current_index] - nums[next_index]) 67 remaining_cost = calculate_min_cost(visited_mask | (1 << next_index), next_index) 68 69 # If this path gives optimal cost, follow it 70 if edge_cost + remaining_cost == optimal_cost: 71 construct_path(visited_mask | (1 << next_index), next_index) 72 break 73 74 # Initialize variables 75 n = len(nums) 76 result_path = [] 77 78 # Start from index 0 (with bitmask having only bit 0 set) 79 construct_path(1, 0) 80 81 return result_path 821class Solution { 2 // Memoization table: dp[mask][lastIndex] stores minimum cost 3 // mask: bitmask representing visited positions 4 // lastIndex: the last visited position 5 private Integer[][] dp; 6 7 // Input array 8 private int[] nums; 9 10 // Result array to store the permutation 11 private int[] result; 12 13 // Length of the input array 14 private int n; 15 16 /** 17 * Finds the permutation that minimizes the sum of absolute differences 18 * between consecutive elements, starting and ending at index 0 19 * 20 * @param nums the input array 21 * @return the optimal permutation as indices 22 */ 23 public int[] findPermutation(int[] nums) { 24 n = nums.length; 25 result = new int[n]; 26 this.nums = nums; 27 28 // Initialize DP table: 2^n states for mask, n states for last position 29 dp = new Integer[1 << n][n]; 30 31 // Start building the path from position 0 32 buildPath(1, 0, 0); 33 34 return result; 35 } 36 37 /** 38 * Dynamic programming function to find minimum cost 39 * 40 * @param mask bitmask representing visited positions (1 = visited) 41 * @param lastPos the last visited position index 42 * @return minimum cost to complete the tour from current state 43 */ 44 private int dfs(int mask, int lastPos) { 45 // Base case: all positions visited, return cost to go back to start 46 if (mask == (1 << n) - 1) { 47 return Math.abs(lastPos - nums[0]); 48 } 49 50 // Return memoized result if already computed 51 if (dp[mask][lastPos] != null) { 52 return dp[mask][lastPos]; 53 } 54 55 int minCost = Integer.MAX_VALUE; 56 57 // Try visiting each unvisited position (except position 0) 58 for (int nextPos = 1; nextPos < n; ++nextPos) { 59 // Check if position nextPos is not visited yet 60 if ((mask >> nextPos & 1) == 0) { 61 // Calculate cost: distance to next position + remaining cost 62 int currentCost = Math.abs(lastPos - nums[nextPos]) + 63 dfs(mask | (1 << nextPos), nextPos); 64 minCost = Math.min(minCost, currentCost); 65 } 66 } 67 68 // Memoize and return the result 69 return dp[mask][lastPos] = minCost; 70 } 71 72 /** 73 * Reconstructs the optimal path by following the DP decisions 74 * 75 * @param mask bitmask representing visited positions 76 * @param currentPos current position index 77 * @param stepIndex current step number in the result array 78 */ 79 private void g(int mask, int currentPos, int stepIndex) { 80 // Store current position in result array 81 result[stepIndex] = currentPos; 82 83 // Base case: all positions visited 84 if (mask == (1 << n) - 1) { 85 return; 86 } 87 88 // Get the optimal cost from current state 89 int optimalCost = dfs(mask, currentPos); 90 91 // Find which next position achieves the optimal cost 92 for (int nextPos = 1; nextPos < n; ++nextPos) { 93 // Check if position is unvisited 94 if ((mask >> nextPos & 1) == 0) { 95 // Check if this move leads to optimal solution 96 int moveCost = Math.abs(currentPos - nums[nextPos]) + 97 dfs(mask | (1 << nextPos), nextPos); 98 99 if (moveCost == optimalCost) { 100 // Found the optimal next move, continue building path 101 g(mask | (1 << nextPos), nextPos, stepIndex + 1); 102 break; 103 } 104 } 105 } 106 } 107} 1081class Solution { 2public: 3 vector<int> findPermutation(vector<int>& nums) { 4 int n = nums.size(); 5 vector<int> result; 6 7 // dp[mask][last]: minimum cost to visit all positions set in mask, ending at position 'last' 8 // -1 indicates uncomputed state 9 int dp[1 << n][n]; 10 memset(dp, -1, sizeof(dp)); 11 12 // Dynamic programming function to compute minimum cost 13 // mask: bitmask representing visited positions (1 = visited, 0 = unvisited) 14 // lastPos: the last position in the current path 15 function<int(int, int)> computeMinCost = [&](int mask, int lastPos) { 16 // Base case: all positions visited, return cost to go back to start 17 if (mask == (1 << n) - 1) { 18 return abs(lastPos - nums[0]); 19 } 20 21 // Check memoization 22 int* dpValue = &dp[mask][lastPos]; 23 if (*dpValue != -1) { 24 return *dpValue; 25 } 26 27 // Try all unvisited positions as next position 28 *dpValue = INT_MAX; 29 for (int nextPos = 1; nextPos < n; ++nextPos) { 30 // Check if nextPos is unvisited (bit is 0) 31 if ((mask >> nextPos & 1) ^ 1) { 32 int cost = abs(lastPos - nums[nextPos]) + 33 computeMinCost(mask | (1 << nextPos), nextPos); 34 *dpValue = min(*dpValue, cost); 35 } 36 } 37 38 return *dpValue; 39 }; 40 41 // Reconstruct the optimal path 42 // mask: bitmask representing visited positions 43 // currentPos: current position in the path 44 function<void(int, int)> reconstructPath = [&](int mask, int currentPos) { 45 // Add current position to result 46 result.push_back(currentPos); 47 48 // Base case: all positions visited 49 if (mask == (1 << n) - 1) { 50 return; 51 } 52 53 // Get the optimal cost from current state 54 int optimalCost = computeMinCost(mask, currentPos); 55 56 // Find which next position achieves the optimal cost 57 for (int nextPos = 1; nextPos < n; ++nextPos) { 58 // Check if nextPos is unvisited 59 if ((mask >> nextPos & 1) ^ 1) { 60 int costThroughNext = abs(currentPos - nums[nextPos]) + 61 computeMinCost(mask | (1 << nextPos), nextPos); 62 63 // If this path achieves optimal cost, follow it 64 if (costThroughNext == optimalCost) { 65 reconstructPath(mask | (1 << nextPos), nextPos); 66 break; 67 } 68 } 69 } 70 }; 71 72 // Start from position 0 (bit 0 is set in initial mask) 73 reconstructPath(1, 0); 74 75 return result; 76 } 77}; 781/** 2 * Finds the optimal permutation of array indices that minimizes the total distance 3 * when traversing elements in sequence, starting and ending at index 0 4 * @param nums - Input array of numbers 5 * @returns Array of indices representing the optimal permutation 6 */ 7function findPermutation(nums: number[]): number[] { 8 const arrayLength: number = nums.length; 9 const resultPath: number[] = []; 10 11 // Memoization table: dp[visitedMask][lastIndex] = minimum cost 12 // visitedMask uses bit representation to track visited indices 13 const memoTable: number[][] = Array.from( 14 { length: 1 << arrayLength }, 15 () => Array(arrayLength).fill(-1) 16 ); 17 18 /** 19 * Dynamic programming function to find minimum cost path 20 * @param visitedMask - Bitmask representing visited indices 21 * @param lastVisitedIndex - The last visited index in the path 22 * @returns Minimum cost to complete the path from current state 23 */ 24 const calculateMinimumCost = (visitedMask: number, lastVisitedIndex: number): number => { 25 // Base case: all indices visited, return cost to go back to start 26 if (visitedMask === (1 << arrayLength) - 1) { 27 return Math.abs(lastVisitedIndex - nums[0]); 28 } 29 30 // Return memoized result if already calculated 31 if (memoTable[visitedMask][lastVisitedIndex] !== -1) { 32 return memoTable[visitedMask][lastVisitedIndex]; 33 } 34 35 let minimumCost: number = Infinity; 36 37 // Try visiting each unvisited index 38 for (let currentIndex = 1; currentIndex < arrayLength; ++currentIndex) { 39 // Check if currentIndex is not visited (bit is 0) 40 if (((visitedMask >> currentIndex) & 1) ^ 1) { 41 // Calculate cost: distance to current + cost of remaining path 42 const totalCost: number = Math.abs(lastVisitedIndex - nums[currentIndex]) + 43 calculateMinimumCost(visitedMask | (1 << currentIndex), currentIndex); 44 minimumCost = Math.min(minimumCost, totalCost); 45 } 46 } 47 48 // Memoize and return the result 49 return (memoTable[visitedMask][lastVisitedIndex] = minimumCost); 50 }; 51 52 /** 53 * Reconstructs the optimal path by following the minimum cost decisions 54 * @param visitedMask - Bitmask representing visited indices 55 * @param lastVisitedIndex - The last visited index in the path 56 */ 57 const reconstructPath = (visitedMask: number, lastVisitedIndex: number): void => { 58 // Add current index to result path 59 resultPath.push(lastVisitedIndex); 60 61 // Base case: all indices visited 62 if (visitedMask === (1 << arrayLength) - 1) { 63 return; 64 } 65 66 // Get the optimal cost from current state 67 const optimalCost: number = calculateMinimumCost(visitedMask, lastVisitedIndex); 68 69 // Find which next index achieves the optimal cost 70 for (let currentIndex = 1; currentIndex < arrayLength; ++currentIndex) { 71 // Check if currentIndex is not visited 72 if (((visitedMask >> currentIndex) & 1) ^ 1) { 73 // Check if this path achieves the optimal cost 74 const pathCost: number = Math.abs(lastVisitedIndex - nums[currentIndex]) + 75 calculateMinimumCost(visitedMask | (1 << currentIndex), currentIndex); 76 77 if (pathCost === optimalCost) { 78 // Continue reconstruction with this choice 79 reconstructPath(visitedMask | (1 << currentIndex), currentIndex); 80 break; 81 } 82 } 83 } 84 }; 85 86 // Start path reconstruction from index 0 (with bitmask having bit 0 set) 87 reconstructPath(1, 0); 88 89 return resultPath; 90} 91Solution Approach
The solution uses memoized recursion with state compression to find the optimal permutation. Here's how the implementation works:
State Representation: We use a bitmask mask to represent which numbers have been selected. If the i-th bit of mask is 1, it means number i has been included in our permutation. For example, mask = 5 (binary: 101) means we've selected numbers 0 and 2.
Dynamic Programming Function dfs(mask, pre): This function calculates the minimum score possible when:
- We've already selected the numbers indicated by
mask - The last selected number was
pre
The function works as follows:
-
Base case: If
mask == (1 << n) - 1(all bits set, meaning all numbers selected), we've completed the cycle. Return|pre - nums[0]|to close the cycle back to the first element. -
Recursive case: For each unselected number
cur(wheremask >> cur & 1 ^ 1checks if bitcuris 0):- Calculate the cost of selecting
curnext:|pre - nums[cur]| - Add the minimum cost for the remaining choices:
dfs(mask | 1 << cur, cur) - Track the minimum total cost across all choices
- Calculate the cost of selecting
The @cache decorator memoizes results to avoid recomputing the same states.
Permutation Construction with g(mask, pre): Once we know the minimum costs, we reconstruct the actual permutation:
- Add the current number
preto our answer list - If we've selected all numbers (
mask == (1 << n) - 1), we're done - Otherwise, get the minimum cost from the current state:
res = dfs(mask, pre) - Try each unselected number
curin ascending order (for lexicographic ordering):- Check if selecting
curachieves the minimum cost:|pre - nums[cur]| + dfs(mask | 1 << cur, cur) == res - If yes, recursively build the rest of the permutation with
g(mask | 1 << cur, cur)and break (ensuring lexicographically smallest choice)
- Check if selecting
Main Execution: The solution starts by calling g(1, 0), which means:
- Initial mask is
1(binary:0...001), indicating only number 0 is selected - Starting with
pre = 0(first element is 0 for lexicographic ordering)
The time complexity is O(n^2 * 2^n) due to the state space and transitions, while the space complexity is O(n * 2^n) for memoization.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with nums = [1, 0, 2].
Step 1: Understanding the Score Calculation For any permutation perm, the score is: |perm[0] - nums[perm[1]]| + |perm[1] - nums[perm[2]]| + |perm[2] - nums[perm[0]]|
Step 2: Initialize DFS Computation We start with perm[0] = 0 (for lexicographic ordering). Now we need to find the minimum cost using dfs(mask, pre):
-
dfs(001, 0): mask=1 (binary 001) means only 0 is selected, pre=0- Can choose 1: cost =
|0 - nums[1]| + dfs(011, 1)=|0 - 0| + dfs(3, 1)=0 + dfs(3, 1) - Can choose 2: cost =
|0 - nums[2]| + dfs(101, 2)=|0 - 2| + dfs(5, 2)=2 + dfs(5, 2)
- Can choose 1: cost =
-
dfs(011, 1): mask=3 means 0,1 selected, pre=1- Only 2 available: cost =
|1 - nums[2]| + dfs(111, 2)=|1 - 2| + dfs(7, 2)=1 + dfs(7, 2)
- Only 2 available: cost =
-
dfs(101, 2): mask=5 means 0,2 selected, pre=2- Only 1 available: cost =
|2 - nums[1]| + dfs(111, 1)=|2 - 0| + dfs(7, 1)=2 + dfs(7, 1)
- Only 1 available: cost =
-
dfs(111, 2): All selected, pre=2, return to start- Return
|2 - nums[0]|=|2 - 1|= 1
- Return
-
dfs(111, 1): All selected, pre=1, return to start- Return
|1 - nums[0]|=|1 - 1|= 0
- Return
Step 3: Compute Minimum Costs Working backwards:
dfs(7, 2)= 1dfs(7, 1)= 0dfs(3, 1)= 1 + 1 = 2dfs(5, 2)= 2 + 0 = 2dfs(1, 0)= min(0 + 2, 2 + 2) = min(2, 4) = 2
Step 4: Construct the Permutation Now we build the permutation using g(mask, pre):
Starting with g(1, 0):
- Add 0 to answer:
ans = [0] - Check minimum cost:
dfs(1, 0)= 2 - Try cur=1:
|0 - nums[1]| + dfs(3, 1)= 0 + 2 = 2 ✓ (equals minimum) - Recurse with
g(3, 1)
Continue with g(3, 1):
- Add 1 to answer:
ans = [0, 1] - Only option is cur=2:
|1 - nums[2]| + dfs(7, 2)= 1 + 1 = 2 - Recurse with
g(7, 2)
Finally g(7, 2):
- Add 2 to answer:
ans = [0, 1, 2] - All bits set, we're done
Result: The optimal permutation is [0, 1, 2] with score = 2
We can verify:
|0 - nums[1]| + |1 - nums[2]| + |2 - nums[0]|= |0 - 0| + |1 - 2| + |2 - 1|= 0 + 1 + 1 = 2
Time and Space Complexity
Time Complexity: O(n² × 2ⁿ)
The time complexity is determined by the dynamic programming function dfs:
- The function uses memoization with
@cache, creating at most2ⁿ × nunique states (wheremaskhas2ⁿpossible values representing subsets, andprehasnpossible values representing the previous index) - For each state
(mask, pre), the function iterates through allnpositions to find unvisited nodes (the for loop from 1 to n) - Each state is computed only once due to memoization
- Therefore, the total time complexity is
O(n × 2ⁿ × n) = O(n² × 2ⁿ)
The reconstruction function g has the same traversal pattern but doesn't add to the overall complexity since it only traces back the optimal path once.
Space Complexity: O(n × 2ⁿ)
The space complexity consists of:
- The memoization cache storing up to
2ⁿ × nstates, each takingO(1)space - The recursion stack depth which is at most
O(n)(visiting each element once) - The answer array
answhich storesO(n)elements - Since
2ⁿ × ndominates, the overall space complexity isO(n × 2ⁿ)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Cycle Closure in Base Case
Pitfall: A common mistake is forgetting to close the cycle properly or using the wrong indices. In the base case, developers often return abs(pre - nums[pre]) or abs(nums[pre] - pre) instead of the correct abs(pre - nums[perm[0]]).
Why it happens: The scoring formula |perm[n-1] - nums[perm[0]]| for the last term is confusing. When we reach the base case, pre represents perm[n-1], and we need to connect back to nums[perm[0]]. Since we started with index 0, perm[0] = 0, so we should return abs(pre - nums[0]).
Solution: Always remember that in the cyclic tour, the last element connects back to wherever we started. Since we fix the starting point as 0, the base case should return abs(pre - nums[0]).
2. Mixing Up Index and Value in the Scoring Formula
Pitfall: Confusing what represents an index and what represents a value in the formula |perm[i] - nums[perm[i+1]]|. Developers might incorrectly implement it as |nums[perm[i]] - nums[perm[i+1]]| or |perm[i] - perm[i+1]|.
Why it happens: The formula uses perm both as a source of values (left side) and indices (right side), which is counterintuitive.
Solution: Think of it this way:
perm[i]is the VALUE at position i in our permutationperm[i+1]is used as an INDEX into the nums array- So we compare: value at position i vs. nums element at index given by position i+1
3. Starting from Arbitrary Position Instead of Fixed Position
Pitfall: Trying to consider all possible starting positions by iterating through different initial values, which breaks the lexicographical ordering requirement and increases complexity unnecessarily.
Why it happens: Developers might think they need to try all starting points to find the global minimum, not realizing that fixing the start at 0 is both correct and ensures lexicographical ordering.
Solution: Always start with 0 (calling g(1, 0)). Since it's a cycle, the minimum score is the same regardless of where we "cut" the cycle to represent it as a sequence. Starting with 0 guarantees the lexicographically smallest result among all minimum-score permutations.
4. Incorrect Bitmask Operations
Pitfall: Using wrong bit operations to check or set visited status:
- Using
mask & (1 << cur)instead of(mask >> cur) & 1to check if bit is set - Using
mask + (1 << cur)instead ofmask | (1 << cur)to set a bit
Why it happens: Bit manipulation can be tricky, and different equivalent forms can be confusing.
Solution: Stick to consistent patterns:
- Check if visited:
(mask >> cur) & 1 == 1 - Check if unvisited:
(mask >> cur) & 1 == 0 - Mark as visited:
mask | (1 << cur)
5. Not Breaking After Finding First Valid Choice in Path Construction
Pitfall: In the construct_path function, continuing to explore other options after finding the first valid next move, which could lead to incorrect permutations or violate lexicographical ordering.
Why it happens: Developers might think they need to explore all paths that achieve minimum cost.
Solution: Since we iterate through indices in ascending order (0 to n-1), the first valid choice we find is automatically the lexicographically smallest. Always break immediately after recursing into the first valid path.
How does merge sort divide the problem into subproblems?
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Bitmask Introduction Prerequisite Dynamic Programming Introduction problems dynamic_programming_intro Interview Priority Low Bitmask DP is an advanced technique that is relatively less likely to appear in interviews If you're short on time focus on more common patterns first However understanding bitmasks is valuable for competitive programming and occasional hard interview problems A bitmask
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!