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 perm at position i and an element in nums at position perm[i+1]
  • The calculation wraps around cyclically, so the last term uses perm[n-1] and nums[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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 82
1class 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} 108
1class 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}; 78
1/** 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} 91

Solution 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:

  1. 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.

  2. Recursive case: For each unselected number cur (where mask >> cur & 1 ^ 1 checks if bit cur is 0):

    • Calculate the cost of selecting cur next: |pre - nums[cur]|
    • Add the minimum cost for the remaining choices: dfs(mask | 1 << cur, cur)
    • Track the minimum total cost across all choices

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:

  1. Add the current number pre to our answer list
  2. If we've selected all numbers (mask == (1 << n) - 1), we're done
  3. Otherwise, get the minimum cost from the current state: res = dfs(mask, pre)
  4. Try each unselected number cur in ascending order (for lexicographic ordering):
    • Check if selecting cur achieves 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)

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 Evaluator

Example 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)
  • 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)
  • 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)
  • dfs(111, 2): All selected, pre=2, return to start

    • Return |2 - nums[0]| = |2 - 1| = 1
  • dfs(111, 1): All selected, pre=1, return to start

    • Return |1 - nums[0]| = |1 - 1| = 0

Step 3: Compute Minimum Costs Working backwards:

  • dfs(7, 2) = 1
  • dfs(7, 1) = 0
  • dfs(3, 1) = 1 + 1 = 2
  • dfs(5, 2) = 2 + 0 = 2
  • dfs(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 most 2ⁿ × n unique states (where mask has 2ⁿ possible values representing subsets, and pre has n possible values representing the previous index)
  • For each state (mask, pre), the function iterates through all n positions 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ⁿ × n states, each taking O(1) space
  • The recursion stack depth which is at most O(n) (visiting each element once)
  • The answer array ans which stores O(n) elements
  • Since 2ⁿ × n dominates, the overall space complexity is O(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 permutation
  • perm[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) & 1 to check if bit is set
  • Using mask + (1 << cur) instead of mask | (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.

Loading...
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

How does merge sort divide the problem into subproblems?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More