LeetCode 3551 - Minimum Swaps to Sort by Digit Sum

We are given an array nums consisting of distinct positive integers. The task is to reorder this array into a new array sorted by a custom key: the sum of digits of each integer. If two integers have the same digit sum, the smaller integer must appear first among them.

LeetCode Problem 3551

Difficulty: 🟡 Medium
Topics: Array, Hash Table, Sorting

Solution

Problem Understanding

We are given an array nums consisting of distinct positive integers. The task is to reorder this array into a new array sorted by a custom key: the sum of digits of each integer. If two integers have the same digit sum, the smaller integer must appear first among them.

The output is not the sorted array itself but the minimum number of swaps required to transform the original array into this sorted order. A swap exchanges the values at two distinct indices.

The constraints indicate that 1 <= nums.length <= 10^5, so any solution worse than O(n log n) will be too slow. Since each number can be as large as 10^9, computing digit sums must be efficient but still constant time per number.

The key structural guarantee is that all integers are distinct, which ensures that the induced permutation from original order to sorted order is well-defined and invertible. This is essential for cycle decomposition in the optimal solution.

Edge cases include arrays of length 1 (already sorted), arrays already in correct order (0 swaps), and cases where many numbers share the same digit sum but differ in value, which tests tie-breaking correctness.

Approaches

Brute-force approach

A direct but inefficient approach is to repeatedly transform the array toward the target ordering using greedy swaps. One could compute the target sorted array, then for each position i, if nums[i] is not the correct element, search linearly for the correct element and swap it into place. This works because each swap places at least one element into its final position, but the search step costs linear time per swap, and there can be up to O(n) swaps, leading to O(n^2) complexity.

This approach is correct because it explicitly simulates a valid sequence of swaps that eventually reaches the sorted configuration, but it is too slow for 10^5 elements.

Optimal insight

The problem reduces to transforming one permutation into another. Once we compute the target ordering, each element has a unique destination index. This defines a permutation P where P[i] is the index where the element at position i must go.

The minimum number of swaps required to transform a permutation into the identity permutation is a classical result: it equals

$$\sum_{\text{cycles}} (\text{cycle length} - 1) = n - #\text{cycles}$$

Thus, the problem becomes:

  1. Sort elements by (digit_sum, value)
  2. Build the permutation from original indices to sorted indices
  3. Count cycles in this permutation

This yields an O(n log n) solution dominated by sorting.

Complexity comparison

Approach Time Complexity Space Complexity Notes
Brute Force O(n²) O(n) Repeatedly find correct element and swap
Optimal O(n log n) O(n) Sort + permutation cycle decomposition

Algorithm Walkthrough

We proceed by transforming the array into a permutation problem.

  1. For each index i, compute the digit sum of nums[i]. This defines a key function f(x) equal to the sum of decimal digits of x. This step is necessary to impose the required ordering.
  2. Construct a list of tuples (f(nums[i]), nums[i], i). The inclusion of the original index i is critical because we must track how elements move from their original positions.
  3. Sort this list lexicographically by (digit sum, value). This produces the final target order. Sorting ensures correctness because Python’s lexicographic ordering enforces the primary and secondary constraints exactly as required.
  4. Build an array target_index such that for each original index i, we determine its position in the sorted array. This defines a permutation P where P[i] = j means the element at index i must move to index j.
  5. To compute the minimum swaps, we decompose permutation P into disjoint cycles. We maintain a visited array. For each unvisited index i, we traverse i → P[i] → P[P[i]] → ... until returning to a visited node, counting the cycle length k.
  6. Each cycle of length k requires exactly k - 1 swaps to fix, because each swap can place one element into its final position while reducing the cycle size by one.
  7. Sum k - 1 over all cycles to obtain the final answer.

Why it works

The correctness follows from the structure of permutation groups. Any permutation decomposes uniquely into disjoint cycles. A cycle of length k requires at least k - 1 swaps because each swap can reduce the number of misplaced elements in that cycle by at most one. The constructive strategy of repeatedly swapping the current element into its correct position achieves this lower bound, making the result optimal.

Python Solution

from typing import List

class Solution:
    def minSwaps(self, nums: List[int]) -> int:
        def digit_sum(x: int) -> int:
            s = 0
            while x > 0:
                s += x % 10
                x //= 10
            return s

        n = len(nums)

        arr = []
        for i, x in enumerate(nums):
            arr.append((digit_sum(x), x, i))

        arr.sort()

        target_index = [0] * n
        for sorted_pos, (_, _, original_idx) in enumerate(arr):
            target_index[original_idx] = sorted_pos

        visited = [False] * n
        swaps = 0

        for i in range(n):
            if visited[i] or target_index[i] == i:
                visited[i] = True
                continue

            cycle_len = 0
            j = i

            while not visited[j]:
                visited[j] = True
                j = target_index[j]
                cycle_len += 1

            swaps += cycle_len - 1

        return swaps

Implementation Explanation

The function begins by defining a helper digit_sum, which computes the sum of digits in constant time relative to the number of digits. We then construct an annotated array storing each element’s digit sum, value, and original index.

Sorting this array establishes the target ordering. We then translate this ordering into a permutation mapping target_index, which encodes where each original position must move.

Finally, we compute cycle decomposition over this permutation. Each cycle contributes cycle_len - 1 swaps, which are accumulated into the final result.

Go Solution

package main

import "sort"

func digitSum(x int) int {
    s := 0
    for x > 0 {
        s += x % 10
        x /= 10
    }
    return s
}

func minSwaps(nums []int) int {
    n := len(nums)

    type node struct {
        sum int
        val int
        idx int
    }

    arr := make([]node, n)
    for i, x := range nums {
        arr[i] = node{digitSum(x), x, i}
    }

    sort.Slice(arr, func(i, j int) bool {
        if arr[i].sum == arr[j].sum {
            return arr[i].val < arr[j].val
        }
        return arr[i].sum < arr[j].sum
    })

    target := make([]int, n)
    for sortedPos, v := range arr {
        target[v.idx] = sortedPos
    }

    visited := make([]bool, n)
    swaps := 0

    for i := 0; i < n; i++ {
        if visited[i] || target[i] == i {
            visited[i] = true
            continue
        }

        cycleLen := 0
        j := i

        for !visited[j] {
            visited[j] = true
            j = target[j]
            cycleLen++
        }

        swaps += cycleLen - 1
    }

    return swaps
}

Go-specific notes

Go requires explicit struct definitions for sorting tuples, unlike Python’s native tuple sorting. Additionally, sort.Slice is used with a comparator function to enforce lexicographic ordering. Slice initialization is explicit, and boolean arrays are used for visitation tracking. Integer behavior is safe since values are within standard int range for constraints.

Worked Examples

Example 1: nums = [37, 100]

Digit sums:

  • 37 → 10
  • 100 → 1

Sorted by (digit sum, value):

  • (1, 100), (10, 37) → [100, 37]

Permutation mapping:

Index i nums[i] target position
0 37 1
1 100 0

Permutation cycles:

  • 0 → 1 → 0 forms one cycle of length 2

Swaps = 2 - 1 = 1

Example 2: nums = [22, 14, 33, 7]

Digit sums:

  • 22 → 4
  • 14 → 5
  • 33 → 6
  • 7 → 7

Already sorted, so:

target[i] = i for all i

All nodes are fixed points, so no cycles of length > 1

Swaps = 0

Example 3: nums = [18, 43, 34, 16]

Digit sums:

  • 18 → 9
  • 43 → 7
  • 34 → 7
  • 16 → 7

Sorted:

  • 16 (7), 34 (7), 43 (7), 18 (9)

Permutation mapping:

i value target
0 18 3
1 43 2
2 34 1
3 16 0

Cycle decomposition:

  • 0 → 3 → 0 (cycle length 2)
  • 1 → 2 → 1 (cycle length 2)

Swaps = (2-1) + (2-1) = 2

Complexity Analysis

Measure Complexity Explanation
Time O(n log n) Sorting dominates; digit sum and cycle traversal are linear
Space O(n) Arrays for tuples, mapping, and visited tracking

The logarithmic factor arises solely from sorting the annotated array. All other operations, including digit sum computation and cycle decomposition, are linear scans over the input.

Test Cases

assert Solution().minSwaps([37, 100]) == 1  # basic swap case
assert Solution().minSwaps([22, 14, 33, 7]) == 0  # already sorted
assert Solution().minSwaps([18, 43, 34, 16]) == 2  # two independent cycles
assert Solution().minSwaps([1]) == 0  # single element
assert Solution().minSwaps([10, 2, 11, 20]) == Solution().minSwaps([2, 10, 11, 20])  # stability under equal digit sums
assert Solution().minSwaps([99, 1, 10, 100]) >= 0  # sanity check non-negative
Test Why
[37, 100] minimal non-trivial swap
[22, 14, 33, 7] already sorted case
[18, 43, 34, 16] multiple cycles
[1] smallest input
mixed equal digit sums tie-breaking correctness

Edge Cases

A single-element array is the simplest case where no swaps are required. The permutation consists of a single fixed point, and the cycle decomposition immediately yields zero swaps.

Already sorted arrays are important because they test whether the algorithm correctly identifies fixed points and avoids unnecessary cycle traversal. In such cases, every index maps to itself.

Cases with identical digit sums are critical because correctness depends on secondary ordering by numeric value. Without proper lexicographic sorting on (digit sum, value), the permutation would be incorrect and cycle decomposition would yield wrong results.