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 positioni
and an element innums
at 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 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 ^ 1
checks if bitcur
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
- 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
pre
to 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
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)
- 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
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
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ⁿ × n
unique states (wheremask
has2ⁿ
possible values representing subsets, andpre
hasn
possible values representing the previous index) - For each state
(mask, pre)
, the function iterates through alln
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 takingO(1)
space - The recursion stack depth which is at most
O(n)
(visiting each element once) - The answer array
ans
which storesO(n)
elements - Since
2ⁿ × n
dominates, 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) & 1
to 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.
In a binary min heap, the maximum element can be found in:
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 and Dynamic Programming Bit manipulation is a crucial aspect of computer programming and one of the most powerful tools for bit manipulation is bitmasks Let's first understand what a bit is A bit is a binary digit It's the smallest piece of data in a computer and can be
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!