Taro Logo

Find the Minimum Cost Array Permutation

Hard
Google logo
Google
2 views
Topics:
ArraysDynamic ProgrammingBit Manipulation

You are given an array nums which is a permutation of [0, 1, 2, ..., n - 1]. The score of any permutation of [0, 1, 2, ..., n - 1] named perm is defined as:

score(perm) = |perm[0] - nums[perm[1]]| + |perm[1] - nums[perm[2]]| + ... + |perm[n - 1] - nums[perm[0]]|

Return the permutation perm which has the minimum possible score. If multiple permutations exist with this score, return the one that is lexicographically smallest among them.

For example:

nums = [1,0,2]

Output: [0,1,2]

Explanation:

The lexicographically smallest permutation with minimum cost is [0,1,2]. The cost of this permutation is |0 - 0| + |1 - 2| + |2 - 1| = 2.

Another example:

nums = [0,2,1]

Output: [0,2,1]

Explanation:

The lexicographically smallest permutation with minimum cost is [0,2,1]. The cost of this permutation is |0 - 1| + |2 - 2| + |1 - 0| = 2.

Constraints:

  • 2 <= n == nums.length <= 14
  • nums is a permutation of [0, 1, 2, ..., n - 1].

Solution


Naive Solution: Brute Force

The most straightforward approach is to generate all possible permutations of the input array nums and calculate the score for each permutation. We keep track of the minimum score encountered so far and the lexicographically smallest permutation that achieves this minimum score.

Algorithm:

  1. Generate all permutations of [0, 1, 2, ..., n - 1].
  2. For each permutation perm, calculate its score as |perm[0] - nums[perm[1]]| + |perm[1] - nums[perm[2]]| + ... + |perm[n - 1] - nums[perm[0]]|.
  3. Maintain a variable min_score to store the minimum score seen so far, initialized to infinity.
  4. Maintain a variable best_perm to store the lexicographically smallest permutation with the minimum score.
  5. If the score of the current permutation is less than min_score, update min_score and best_perm.
  6. If the score of the current permutation is equal to min_score, update best_perm only if the current permutation is lexicographically smaller than best_perm.
  7. Return best_perm.

Code (Python):

import itertools

def calculate_score(perm, nums):
    score = 0
    n = len(nums)
    for i in range(n):
        score += abs(perm[i] - nums[perm[(i + 1) % n]])
    return score

def min_permutation_score_brute_force(nums):
    n = len(nums)
    indices = list(range(n))
    permutations = itertools.permutations(indices)
    
    min_score = float('inf')
    best_perm = None

    for perm in permutations:
        perm = list(perm)
        score = calculate_score(perm, nums)

        if score < min_score:
            min_score = score
            best_perm = perm
        elif score == min_score:
            if perm < best_perm:
                best_perm = perm

    return best_perm

Big O Runtime: O(n! * n), where n is the length of the input array. Generating all permutations takes O(n!), and calculating the score for each permutation takes O(n).

Big O Space: O(n), where n is the length of the input array. This is due to the space required to store the current permutation.

Optimal Solution: Dynamic Programming with Bitmasking

Since the constraint 2 <= n <= 14 is relatively small, we can optimize the solution using dynamic programming with bitmasking. The main idea is to use a bitmask to represent which indices have already been included in the current partial permutation.

Algorithm:

  1. Define dp[mask][last] where mask is a bitmask representing the set of used indices and last is the last index used in the permutation. dp[mask][last] stores the minimum cost to build a partial permutation using indices represented by mask and ending with index last.
  2. Initialize dp[1 << i][i] = 0 for all i from 0 to n - 1.
  3. Iterate through all possible masks from 1 to (1 << n) - 1.
  4. For each mask, iterate through all possible last indices i that are included in the mask (mask & (1 << i) is true).
  5. For each i, iterate through all possible next indices j that are not included in the mask (mask & (1 << j) is false).
  6. Update dp[mask | (1 << j)][j] = min(dp[mask | (1 << j)][j], dp[mask][i] + abs(i - nums[j])).
  7. After filling the DP table, we need to find the minimum cost to complete the cycle. We iterate through all possible last indices i that can be the second to last node in a minimal cycle, and then iterate through all possible j which represents the last node in the optimal cycle.
  8. Return the permutation. We can reconstruct the permutation by backtracking from the final state.

Code (Python):

import sys

def min_permutation_score_dp(nums):
    n = len(nums)
    dp = [[float('inf')] * n for _ in range(1 << n)]
    parent = [[None] * n for _ in range(1 << n)]

    for i in range(n):
        dp[1 << i][i] = 0

    for mask in range(1, 1 << n):
        for i in range(n):
            if mask & (1 << i):
                for j in range(n):
                    if not (mask & (1 << j)):
                        new_mask = mask | (1 << j)
                        cost = dp[mask][i] + abs(i - nums[j])
                        if cost < dp[new_mask][j]:
                            dp[new_mask][j] = cost
                            parent[new_mask][j] = i

    min_cost = float('inf')
    start_node = -1
    second_to_last = -1

    for i in range(n):
        cost = dp[(1 << n) - 1][i]
        for j in range(n):
            if i != j:
                cost += abs(i - nums[j])
                if cost < min_cost:
                    min_cost = cost
                    start_node = j
                    second_to_last = i
                cost -= abs(i - nums[j])

    perm = [0] * n
    perm[n - 1] = start_node
    mask = (1 << n) - 1
    current = second_to_last

    for i in range(n - 2, -1, -1):
        perm[i] = current
        next_node = parent[mask][current]
        mask ^= (1 << current)
        current = next_node

    lexicographically_smallest_perm = perm
    return lexicographically_smallest_perm

Big O Runtime: O(n2 * 2n), where n is the length of the input array. The outer loops iterate through all masks (2n) and all possible pairs of last and next indices (n2). Backtracking to reconstruct the permutation takes O(n) time.

Big O Space: O(n * 2n), where n is the length of the input array. This is due to the space required to store the dp table and the parent table.

Edge Cases

  • Empty Input: The problem states 2 <= n, so an empty input array is not a valid case.
  • Small Input (n = 2): The DP solution still works correctly for small inputs. The brute force solution also works. The complexity is small for n=2, so both methods should be fast.
  • Large Input (n = 14): The DP solution is optimized for n <= 14. The brute force solution will be very slow.