1505. Minimum Possible Integer After at Most K Adjacent Swaps On Digits
Problem Description
You are given a string num
that represents the digits of a very large integer, and an integer k
representing the maximum number of swaps you can perform.
In each swap operation, you can exchange any two adjacent digits in the number. You can perform at most k
such swaps.
Your task is to find the minimum possible integer you can create by performing up to k
adjacent swaps, and return it as a string.
For example:
- If
num = "4321"
andk = 4
, you could swap adjacent digits to eventually get"1234"
(the minimum possible) - If
num = "100"
andk = 1
, you could swap once to get"010"
which equals"10"
The challenge involves strategically choosing which adjacent digits to swap to minimize the final number, while staying within the constraint of at most k
swaps. Since you can only swap adjacent digits, moving a digit multiple positions requires multiple swaps.
Intuition
To minimize the number, we want smaller digits to appear earlier in the result. The key insight is to greedily select the smallest available digit that can be moved to the current position within our remaining swap budget.
Consider building the result digit by digit from left to right. For each position, we want to find the smallest digit that we can afford to move there. Moving a digit from position j
to position i
(where j > i
) requires j - i
adjacent swaps to bubble it leftward.
However, there's a complication: as we select digits and "remove" them from their original positions, the distances between remaining digits change. If we've already selected some digits between positions i
and j
, the actual number of swaps needed is less than j - i
because there are gaps.
This leads us to think about tracking which positions have been "used" and calculating the actual distance dynamically. The actual distance becomes: (original position j) - (current position i) - (number of used positions between i and j)
.
We can use a Binary Indexed Tree (Fenwick Tree) to efficiently track and query how many positions have been used in any range. For each digit value (0-9), we maintain a queue of positions where that digit appears. Then, for each position in our result:
- Try each digit from 0 to 9
- Check if we can afford to move the first occurrence of that digit to the current position
- If yes, select it, mark its position as used, and update our remaining swaps
- The first affordable digit we find (starting from 0) is optimal for this position
The Binary Indexed Tree allows us to query "how many positions are used up to index x
" in O(log n)
time, making the distance calculation efficient.
Learn more about Greedy and Segment Tree patterns.
Solution Approach
The solution uses a Binary Indexed Tree (Fenwick Tree) combined with a greedy approach to efficiently find the minimum number.
Data Structures Used:
-
Binary Indexed Tree (BIT): Tracks which positions have been "used" (digits already selected). It supports:
update(x, delta)
: Mark positionx
as used by addingdelta
(1 in our case)query(x)
: Get count of used positions from 1 tox
-
Position Queues: A dictionary
pos
wherepos[d]
is a deque containing all positions where digitd
appears in the original string (1-indexed).
Algorithm Steps:
-
Initialize Data Structures:
pos = defaultdict(deque) for i, v in enumerate(num, 1): pos[int(v)].append(i)
Store all positions (1-indexed) where each digit appears.
-
Build Result Greedily: For each position
i
from 1 ton
:- Try digits 0 through 9 in order
- For each digit
v
, check if we have any occurrences left inpos[v]
- If yes, get the leftmost occurrence at position
j = pos[v][0]
-
Calculate Actual Distance:
dist = tree.query(n) - tree.query(j) + j - i
This formula computes:
tree.query(n) - tree.query(j)
: Number of used positions afterj
j - i
: Original distance between positions- The actual swaps needed =
(j - i) + (used positions after j)
-
Make Selection:
- If
dist <= k
, we can afford to move this digit:- Deduct
k -= dist
- Remove this position from
pos[v]
usingpopleft()
- Add digit
v
to result - Mark position
j
as used:tree.update(j, 1)
- Break to move to next position
- Deduct
- If
-
Return Result: Join all selected digits to form the final minimum number.
Time Complexity: O(n * 10 * log n)
where n
is the length of the string. For each of n
positions, we check up to 10 digits, and each BIT operation takes O(log n)
.
Space Complexity: O(n)
for storing positions and the BIT structure.
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 num = "4321"
and k = 4
.
Initial Setup:
- Original string: "4321"
- Position queues (1-indexed):
- pos[1] = [4]
- pos[2] = [3]
- pos[3] = [2]
- pos[4] = [1]
- BIT initialized with all zeros
- Result = []
Building Position 1:
- Try digit 0: No occurrences available, skip
- Try digit 1: pos[1] has [4]
- Position j = 4 (digit '1' is at position 4)
- Calculate distance:
tree.query(4) - tree.query(4) + 4 - 1 = 0 - 0 + 3 = 3
- Cost is 3 swaps, we have k=4, so we can afford it!
- Select digit '1', update k = 4 - 3 = 1
- Remove position 4 from pos[1]: pos[1] = []
- Mark position 4 as used: tree.update(4, 1)
- Result = ['1']
Building Position 2:
- Try digit 0: No occurrences, skip
- Try digit 1: No occurrences left, skip
- Try digit 2: pos[2] has [3]
- Position j = 3
- Calculate distance:
tree.query(4) - tree.query(3) + 3 - 2 = 1 - 0 + 1 = 2
- Note: tree.query(4) = 1 because position 4 is marked as used
- Cost is 2 swaps, but we only have k=1 remaining, cannot afford
- Try digit 3: pos[3] has [2]
- Position j = 2
- Calculate distance:
tree.query(4) - tree.query(2) + 2 - 2 = 1 - 0 + 0 = 1
- Cost is 1 swap, we have k=1, we can afford it!
- Select digit '3', update k = 1 - 1 = 0
- Mark position 2 as used: tree.update(2, 1)
- Result = ['1', '3']
Building Position 3:
- k = 0 (no swaps left)
- Try digit 0: No occurrences, skip
- Try digit 1: No occurrences left, skip
- Try digit 2: pos[2] has [3]
- Position j = 3
- Calculate distance:
tree.query(4) - tree.query(3) + 3 - 3 = 1 - 1 + 0 = 0
- Note: Both positions 2 and 4 are marked as used
- Cost is 0 swaps, we can afford it!
- Select digit '2'
- Result = ['1', '3', '2']
Building Position 4:
- Only digit '4' remains at position 1
- Distance = 0 (it's the only one left)
- Result = ['1', '3', '2', '4']
Final Answer: "1324"
This demonstrates how the algorithm greedily selects the smallest available digit that can be moved within the swap budget, using the BIT to track which positions have been "consumed" to accurately calculate movement costs.
Solution Implementation
1from collections import defaultdict, deque
2from typing import List
3
4
5class BinaryIndexedTree:
6 """
7 Binary Indexed Tree (Fenwick Tree) for efficient range sum queries and updates.
8 Used to track which positions have been used/moved.
9 """
10
11 def __init__(self, n: int) -> None:
12 """
13 Initialize a Binary Indexed Tree with size n.
14
15 Args:
16 n: Size of the tree (1-indexed)
17 """
18 self.size = n
19 self.tree = [0] * (n + 1) # 1-indexed array
20
21 @staticmethod
22 def lowbit(x: int) -> int:
23 """
24 Get the lowest set bit of x.
25 This helps navigate the tree structure.
26
27 Args:
28 x: Input number
29
30 Returns:
31 The value of the lowest set bit
32 """
33 return x & -x
34
35 def update(self, x: int, delta: int) -> None:
36 """
37 Add delta to position x and update all affected parent nodes.
38
39 Args:
40 x: Position to update (1-indexed)
41 delta: Value to add
42 """
43 while x <= self.size:
44 self.tree[x] += delta
45 x += BinaryIndexedTree.lowbit(x)
46
47 def query(self, x: int) -> int:
48 """
49 Get the prefix sum from position 1 to x.
50
51 Args:
52 x: End position for the prefix sum (1-indexed)
53
54 Returns:
55 Sum of elements from 1 to x
56 """
57 result = 0
58 while x > 0:
59 result += self.tree[x]
60 x -= BinaryIndexedTree.lowbit(x)
61 return result
62
63
64class Solution:
65 def minInteger(self, num: str, k: int) -> str:
66 """
67 Find the smallest possible integer by swapping adjacent digits at most k times.
68
69 Args:
70 num: Input number as string
71 k: Maximum number of adjacent swaps allowed
72
73 Returns:
74 The lexicographically smallest string after at most k swaps
75 """
76 # Store positions of each digit (0-9) in the original string
77 # Using 1-indexed positions for the Binary Indexed Tree
78 digit_positions = defaultdict(deque)
79 for index, digit_char in enumerate(num, 1):
80 digit_value = int(digit_char)
81 digit_positions[digit_value].append(index)
82
83 result = []
84 n = len(num)
85
86 # Binary Indexed Tree to track which positions have been used
87 # When a position is used, we mark it with 1
88 used_positions_tree = BinaryIndexedTree(n)
89
90 # Process each position in the result
91 for current_position in range(1, n + 1):
92 # Try digits from 0 to 9 to get the smallest possible digit
93 for digit in range(10):
94 positions_queue = digit_positions[digit]
95
96 if positions_queue:
97 # Get the leftmost occurrence of this digit
98 original_position = positions_queue[0]
99
100 # Calculate the actual distance to move this digit to current position
101 # Distance = original_position - current_position - (number of used positions before original_position)
102 used_before = used_positions_tree.query(original_position)
103 distance = original_position - current_position - used_before
104
105 # Alternative calculation using the formula in original code:
106 # total_used = used_positions_tree.query(n)
107 # distance = total_used - used_before + original_position - current_position
108
109 if distance <= k:
110 # We can afford to move this digit to the current position
111 k -= distance
112 positions_queue.popleft()
113 result.append(str(digit))
114
115 # Mark this position as used
116 used_positions_tree.update(original_position, 1)
117 break
118
119 return ''.join(result)
120
1class Solution {
2 public String minInteger(String num, int k) {
3 // Create queues to store positions of each digit (0-9)
4 // Each queue stores 1-indexed positions of where that digit appears
5 Queue<Integer>[] digitPositions = new Queue[10];
6 for (int i = 0; i < 10; i++) {
7 digitPositions[i] = new ArrayDeque<>();
8 }
9
10 // Store the positions of each digit in the original string
11 // Using 1-indexed positions for the Binary Indexed Tree
12 int stringLength = num.length();
13 for (int i = 0; i < stringLength; i++) {
14 int digit = num.charAt(i) - '0';
15 digitPositions[digit].offer(i + 1);
16 }
17
18 // Build the result string
19 StringBuilder result = new StringBuilder();
20
21 // Binary Indexed Tree to track which positions have been used
22 BinaryIndexedTree fenwickTree = new BinaryIndexedTree(stringLength);
23
24 // Process each position in the result string
25 for (int currentPosition = 1; currentPosition <= stringLength; currentPosition++) {
26 // Try each digit from smallest to largest (greedy approach)
27 for (int digit = 0; digit < 10; digit++) {
28 if (!digitPositions[digit].isEmpty()) {
29 Queue<Integer> currentQueue = digitPositions[digit];
30 int originalPosition = currentQueue.peek();
31
32 // Calculate the actual distance to move this digit
33 // tree.query(n) - tree.query(j): number of used positions after j
34 // j - i: original distance between positions
35 // Total gives the actual swaps needed considering already moved elements
36 int swapsNeeded = fenwickTree.query(stringLength) - fenwickTree.query(originalPosition)
37 + originalPosition - currentPosition;
38
39 // If we have enough swaps remaining, use this digit
40 if (swapsNeeded <= k) {
41 k -= swapsNeeded;
42 currentQueue.poll();
43 result.append(digit);
44 // Mark this position as used in the tree
45 fenwickTree.update(originalPosition, 1);
46 break;
47 }
48 }
49 }
50 }
51
52 return result.toString();
53 }
54}
55
56class BinaryIndexedTree {
57 private int size;
58 private int[] tree;
59
60 /**
61 * Initialize a Binary Indexed Tree with given size
62 * @param n the size of the tree
63 */
64 public BinaryIndexedTree(int n) {
65 this.size = n;
66 this.tree = new int[n + 1]; // 1-indexed array
67 }
68
69 /**
70 * Update the value at position x by adding delta
71 * @param x the position to update (1-indexed)
72 * @param delta the value to add
73 */
74 public void update(int x, int delta) {
75 while (x <= size) {
76 tree[x] += delta;
77 x += lowbit(x); // Move to next position to update
78 }
79 }
80
81 /**
82 * Query the prefix sum from 1 to x
83 * @param x the end position of the range (1-indexed)
84 * @return the sum of elements from position 1 to x
85 */
86 public int query(int x) {
87 int sum = 0;
88 while (x > 0) {
89 sum += tree[x];
90 x -= lowbit(x); // Move to previous range
91 }
92 return sum;
93 }
94
95 /**
96 * Get the lowest bit of x (rightmost set bit)
97 * @param x the input number
98 * @return the value of the lowest bit
99 */
100 public static int lowbit(int x) {
101 return x & -x;
102 }
103}
104
1class BinaryIndexedTree {
2public:
3 int size; // Size of the array
4 vector<int> tree; // Fenwick tree array (1-indexed)
5
6 // Constructor: Initialize BIT with given size
7 BinaryIndexedTree(int n) : size(n), tree(n + 1) {}
8
9 // Update the BIT by adding delta at position index
10 void update(int index, int delta) {
11 while (index <= size) {
12 tree[index] += delta;
13 index += lowbit(index); // Move to next responsible node
14 }
15 }
16
17 // Query the prefix sum from 1 to index
18 int query(int index) {
19 int sum = 0;
20 while (index > 0) {
21 sum += tree[index];
22 index -= lowbit(index); // Move to parent node
23 }
24 return sum;
25 }
26
27 // Get the lowest set bit of x
28 int lowbit(int x) {
29 return x & -x;
30 }
31};
32
33class Solution {
34public:
35 string minInteger(string num, int k) {
36 // Store positions of each digit (0-9) in queues
37 vector<queue<int>> digitPositions(10);
38 int n = num.size();
39
40 // Populate queues with 1-indexed positions of each digit
41 for (int i = 0; i < n; ++i) {
42 digitPositions[num[i] - '0'].push(i + 1);
43 }
44
45 // Binary Indexed Tree to track used positions
46 BinaryIndexedTree* bit = new BinaryIndexedTree(n);
47 string result = "";
48
49 // Process each position in the result string
50 for (int currentPos = 1; currentPos <= n; ++currentPos) {
51 // Try each digit from 0 to 9 (greedy approach)
52 for (int digit = 0; digit < 10; ++digit) {
53 queue<int>& posQueue = digitPositions[digit];
54
55 if (!posQueue.empty()) {
56 int originalPos = posQueue.front();
57
58 // Calculate the actual distance considering already used positions
59 // bit->query(n) - bit->query(originalPos): count of used positions after originalPos
60 // originalPos - currentPos: base distance without considering used positions
61 int swapDistance = bit->query(n) - bit->query(originalPos) + originalPos - currentPos;
62
63 // If we can move this digit to current position within k swaps
64 if (swapDistance <= k) {
65 k -= swapDistance; // Decrease remaining swaps
66 posQueue.pop(); // Remove used position
67 result += (digit + '0'); // Append digit to result
68 bit->update(originalPos, 1); // Mark position as used
69 break; // Move to next position
70 }
71 }
72 }
73 }
74
75 return result;
76 }
77};
78
1// Binary Indexed Tree (Fenwick Tree) for efficient range sum queries
2let size: number; // Size of the array
3let tree: number[]; // Fenwick tree array (1-indexed)
4
5// Initialize BIT with given size
6function initializeBIT(n: number): void {
7 size = n;
8 tree = new Array(n + 1).fill(0);
9}
10
11// Get the lowest set bit of x
12function lowbit(x: number): number {
13 return x & -x;
14}
15
16// Update the BIT by adding delta at position index
17function update(index: number, delta: number): void {
18 while (index <= size) {
19 tree[index] += delta;
20 index += lowbit(index); // Move to next responsible node
21 }
22}
23
24// Query the prefix sum from 1 to index
25function query(index: number): number {
26 let sum = 0;
27 while (index > 0) {
28 sum += tree[index];
29 index -= lowbit(index); // Move to parent node
30 }
31 return sum;
32}
33
34function minInteger(num: string, k: number): string {
35 // Store positions of each digit (0-9) in queues
36 const digitPositions: number[][] = Array.from({ length: 10 }, () => []);
37 const n = num.length;
38
39 // Populate arrays with 1-indexed positions of each digit
40 for (let i = 0; i < n; i++) {
41 const digit = parseInt(num[i]);
42 digitPositions[digit].push(i + 1);
43 }
44
45 // Initialize Binary Indexed Tree to track used positions
46 initializeBIT(n);
47 let result = "";
48
49 // Process each position in the result string
50 for (let currentPos = 1; currentPos <= n; currentPos++) {
51 // Try each digit from 0 to 9 (greedy approach)
52 for (let digit = 0; digit < 10; digit++) {
53 const posArray = digitPositions[digit];
54
55 if (posArray.length > 0) {
56 const originalPos = posArray[0];
57
58 // Calculate the actual distance considering already used positions
59 // query(n) - query(originalPos): count of used positions after originalPos
60 // originalPos - currentPos: base distance without considering used positions
61 const swapDistance = query(n) - query(originalPos) + originalPos - currentPos;
62
63 // If we can move this digit to current position within k swaps
64 if (swapDistance <= k) {
65 k -= swapDistance; // Decrease remaining swaps
66 posArray.shift(); // Remove used position from front
67 result += digit.toString(); // Append digit to result
68 update(originalPos, 1); // Mark position as used in BIT
69 break; // Move to next position
70 }
71 }
72 }
73 }
74
75 return result;
76}
77
Time and Space Complexity
Time Complexity: O(n² log n)
The algorithm consists of:
- Building position queues:
O(n)
where we iterate through the string once - Main loop with nested operations:
- Outer loop runs
n
times (for each position in result) - Inner loop checks 10 digits (constant)
- For each valid digit:
tree.query(n)
andtree.query(j)
: Each query operation takesO(log n)
due to the BIT structuretree.update(j, 1)
: Update operation takesO(log n)
- In worst case, we perform BIT operations for each position
- Outer loop runs
Since we have n
iterations and each iteration potentially performs O(log n)
BIT operations, the total time complexity is O(n² log n)
.
The n²
factor comes from the fact that for each of the n
positions, we might need to check positions that require calculating distances using the BIT, and in worst case scenarios, we examine many positions before finding one within the allowed swaps k
.
Space Complexity: O(n)
The space usage includes:
pos
dictionary with deques storing all positions:O(n)
total across all digitsans
list storing the result:O(n)
- BinaryIndexedTree with array
c
of sizen+1
:O(n)
- Other variables:
O(1)
The total space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Distance Calculation Formula
Pitfall: Many implementations incorrectly calculate the number of swaps needed to move a digit from position j
to position i
. A common mistake is using simple arithmetic like j - i
without accounting for digits that have already been moved.
Why it happens: When we select and move digits earlier in the process, they effectively "disappear" from their original positions. This changes the actual distance between remaining digits.
Example:
- Original string:
"4321"
, k = 4 - After selecting '1' from position 4 and moving it to position 1 (3 swaps)
- The remaining digits are conceptually at positions 1, 2, 3 (not 2, 3, 4)
- If we now want to move '2' to position 2, it only needs 1 swap, not 2
Solution: Use the Binary Indexed Tree to track used positions and adjust the distance calculation:
# Correct formula used_after_j = tree.query(n) - tree.query(j) actual_distance = j - i - (tree.query(j - 1)) # OR equivalently: actual_distance = tree.query(n) - tree.query(j) + j - i
2. Using 0-Indexed Positions with Binary Indexed Tree
Pitfall: Binary Indexed Trees traditionally use 1-based indexing. Using 0-based indexing can cause index out of bounds errors or incorrect calculations.
Why it happens: The BIT's lowbit
operation and tree navigation logic rely on 1-based indexing. Position 0 would cause infinite loops in queries/updates.
Solution: Always use 1-based indexing when working with BIT:
# Correct: enumerate starting from 1
for i, v in enumerate(num, 1):
pos[int(v)].append(i)
# Incorrect: using 0-based indexing
for i, v in enumerate(num): # Don't do this
pos[int(v)].append(i)
3. Not Breaking After Finding a Valid Digit
Pitfall: Forgetting to break
after successfully placing a digit, causing the algorithm to try placing multiple digits at the same position.
Solution: Always include a break
statement after successfully placing a digit:
if dist <= k:
k -= dist
pos[v].popleft()
ans.append(str(v))
tree.update(j, 1)
break # Critical: move to next position
4. Modifying the Deque While Iterating
Pitfall: Some might try to iterate through all positions of a digit and modify the deque simultaneously, leading to unexpected behavior.
Solution: Always check only the first element (pos[v][0]
) and use popleft()
to remove it:
# Correct approach if pos[v]: # Check if deque is not empty j = pos[v][0] # Peek at first element # ... calculate distance ... if dist <= k: pos[v].popleft() # Remove only after confirming we'll use it
5. Integer vs String Confusion
Pitfall: Mixing up integer values and string characters when building the result, leading to type errors or incorrect output format.
Solution: Be consistent with type conversions:
# Store positions by integer value
pos[int(v)].append(i) # Convert char to int for indexing
# But append string to result
ans.append(str(v)) # Convert back to string for output
In a binary min heap, the minimum element can be found in:
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Segment Tree Faster Range Queries For this article we want to introduce the idea of a Segment Tree Segment Trees allow us to quickly perform range queries as well as range updates Suppose we have an array and we want to know the sum of a particular range of numbers as well
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!