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.
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:
- Sort elements by
(digit_sum, value) - Build the permutation from original indices to sorted indices
- 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.
- For each index
i, compute the digit sum ofnums[i]. This defines a key functionf(x)equal to the sum of decimal digits ofx. This step is necessary to impose the required ordering. - Construct a list of tuples
(f(nums[i]), nums[i], i). The inclusion of the original indexiis critical because we must track how elements move from their original positions. - 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. - Build an array
target_indexsuch that for each original indexi, we determine its position in the sorted array. This defines a permutationPwhereP[i] = jmeans the element at indeximust move to indexj. - To compute the minimum swaps, we decompose permutation
Pinto disjoint cycles. We maintain a visited array. For each unvisited indexi, we traversei → P[i] → P[P[i]] → ...until returning to a visited node, counting the cycle lengthk. - Each cycle of length
krequires exactlyk - 1swaps to fix, because each swap can place one element into its final position while reducing the cycle size by one. - Sum
k - 1over 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.