LeetCode 1963 - Minimum Number of Swaps to Make the String Balanced
The problem asks us to transform a given string of brackets into a balanced string with the minimum number of swaps. The input string s consists of n characters, where n is even, and there are exactly n / 2 opening brackets '[' and n / 2 closing brackets ']'.
Difficulty: š” Medium
Topics: Two Pointers, String, Stack, Greedy
Solution
Problem Understanding
The problem asks us to transform a given string of brackets into a balanced string with the minimum number of swaps. The input string s consists of n characters, where n is even, and there are exactly n / 2 opening brackets '[' and n / 2 closing brackets ']'. A string is balanced if every opening bracket has a matching closing bracket and the brackets are properly nested according to the rules of a well-formed sequence.
In other words, at any point from the start to the end of the string, the number of opening brackets encountered should never be fewer than the number of closing brackets encountered. If this condition is violated, we need to perform swaps to fix it. The output is a single integer representing the minimum number of swaps required to achieve this balance.
The constraints indicate that n can be very large, up to 10^6, which means solutions with complexity worse than O(n) may not be feasible. The problem guarantees an equal number of opening and closing brackets, so we do not need to handle strings with mismatched counts, simplifying the implementation.
Important edge cases include strings that are already balanced, strings that are completely reversed like "]][[", and strings where only one swap is necessary at the start or end.
Approaches
A brute-force approach would try to find every unbalanced position and swap it with a correct bracket. For example, we could iterate through the string and whenever a closing bracket appears without a matching opening bracket to its left, we scan forward to find an opening bracket to swap. While this approach would eventually produce a balanced string, it requires repeated scanning and swapping, resulting in O(n^2) time complexity, which is too slow for n = 10^6.
The key insight for an optimal solution is that we do not need to simulate every swap. We only need to track the balance of the string as we iterate from left to right. Define a balance counter that increases by 1 for '[' and decreases by 1 for ']'. Whenever balance becomes negative, it means we have more closing brackets than opening brackets at that point, which will require swaps to fix. Each time balance drops below zero, we increment a swaps counter and reset the deficit to zero for the next steps. The final answer can be derived directly from the maximum deficit encountered, which corresponds to the minimum number of swaps needed.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n^2) | O(n) | Scan for misplaced brackets and swap; correct but too slow for large inputs |
| Optimal | O(n) | O(1) | Track balance and max deficit; each deficit requires a swap; very efficient |
Algorithm Walkthrough
- Initialize two variables:
balance = 0to track the net number of opening brackets, andmax_deficit = 0to track the largest negative balance encountered. - Iterate over each character
cin the strings. - If
cis'[', incrementbalanceby 1. - If
cis']', decrementbalanceby 1. - After updating
balance, if it is negative, it indicates that we have more closing brackets than opening brackets at this position. Updatemax_deficitto the absolute value of the currentbalanceif it is larger than the currentmax_deficit. - At the end of the iteration,
max_deficitrepresents the number of unmatched closing brackets that need to be swapped with future opening brackets. Since each swap fixes two brackets, the minimum number of swaps required is(max_deficit + 1) // 2.
Why it works: The balance counter maintains the invariant that at any valid point, the number of opening brackets minus closing brackets should be non-negative. Any time the balance drops below zero, it identifies an unmatched closing bracket. The maximum deficit thus represents the worst imbalance and directly corresponds to the minimum swaps needed to correct the string.
The problem gives us a string consisting only of opening brackets '[' and closing brackets ']'. The length of the string is always even, and the number of opening and closing brackets is guaranteed to be equal.
Our goal is to make the string balanced using the minimum number of swaps. A swap can occur between any two indices, not necessarily adjacent indices. This is important because swapping distant characters in one operation makes the problem much easier than adjacent-swap variants.
A balanced bracket string follows the same rules as valid parentheses expressions. Every opening bracket must eventually be matched with a closing bracket, and at no point while scanning from left to right should the number of closing brackets exceed the number of opening brackets.
For example:
"[]"is balanced"[[]]"is balanced"[][]"is balanced"][]["is not balanced because it starts with a closing bracket
The output should be the minimum number of swaps needed to transform the string into any valid balanced arrangement.
The constraints are especially important:
- The length can be as large as
10^6 - This immediately rules out expensive simulations or repeated string reconstruction
- We need an algorithm with linear time complexity
- Memory usage should ideally be constant or very small
The problem also guarantees:
- The string length is even
- The number of
'['equals the number of']'
This guarantee means a balanced arrangement is always possible.
Several edge cases are important:
- The string may already be balanced
- The string may begin with many consecutive closing brackets
- The string may alternate between valid and invalid states
- Extremely large inputs require an efficient one-pass solution
A naive implementation that repeatedly searches for swap positions or physically performs swaps on large strings could become too slow.
Approaches
Brute Force Approach
A brute-force solution would simulate the balancing process directly.
Whenever we encounter an invalid state, meaning the number of closing brackets exceeds the number of opening brackets, we could search forward for the next '[' character and swap it into the current position.
This approach works because every invalid prefix must eventually be repaired by bringing an opening bracket earlier into the string.
However, the implementation is inefficient because each correction may require scanning far ahead in the string to locate the next available '['. In the worst case, this repeated searching leads to quadratic time complexity.
For example, in a string like:
]]]]....[[[[
each repair operation may scan almost the entire remaining string.
Since n can reach 10^6, an O(n^2) solution is far too slow.
Optimal Greedy Observation
The key insight is that we do not actually need to perform the swaps.
Instead, we only need to count how many times the string becomes invalid while scanning from left to right.
We maintain a balance counter:
'['increases balance']'decreases balance
If balance becomes negative, it means we have more closing brackets than opening brackets so far. This prefix cannot be balanced without a swap.
At that moment:
- One swap is required
- After the swap, the balance effectively becomes
1
Why 1?
Because swapping in an opening bracket changes the invalid prefix into a valid one with one unmatched opening bracket remaining.
This greedy observation allows us to compute the answer in a single pass without physically swapping characters.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n²) | O(1) | Repeatedly searches for future opening brackets and performs swaps |
| Optimal | O(n) | O(1) | Greedily tracks imbalance count without performing actual swaps |
Algorithm Walkthrough
Optimal Greedy Algorithm
- Initialize two variables:
balance = 0swaps = 0
The balance variable represents how many unmatched opening brackets currently exist.
2. Traverse the string from left to right.
3. If the current character is '[', increment balance.
This means we have one more available opening bracket.
4. If the current character is ']', decrement balance.
This closing bracket attempts to match a previous opening bracket.
5. After updating the balance, check whether balance < 0.
A negative balance means we encountered more closing brackets than opening brackets in the current prefix. This prefix is invalid and must be repaired using a swap.
6. When balance < 0:
- Increment
swaps - Reset
balance = 1
This simulates swapping the current ']' with some future '['.
After the swap:
- The current position becomes
'[' - We effectively gain one unmatched opening bracket
- Continue processing the rest of the string.
- After the traversal completes, return
swaps.
Why it works
The invariant is that balance always represents the number of unmatched opening brackets after applying the minimum swaps necessary so far.
Whenever balance becomes negative, the current prefix cannot possibly be balanced without introducing an opening bracket earlier in the string. A single swap with a future '[' is always sufficient to repair that invalid prefix.
Because we only perform swaps when absolutely necessary, and each negative balance requires at least one swap, the greedy strategy is optimal.
Python Solution
class Solution:
def minSwaps(self, s: str) -> int:
balance = 0
max_deficit = 0
for c in s:
if c == '[':
balance += 1
else: # c == ']'
balance -= 1
if balance < 0:
max_deficit = max(max_deficit, -balance)
return (max_deficit + 1) // 2
In this implementation, we iterate through the string while maintaining a running balance. Each time the balance goes negative, we record the maximum deficit. The final step divides the maximum deficit by 2 and rounds up, because each swap can fix two brackets in one operation. swaps = 0
for char in s:
if char == '[':
balance += 1
else:
balance -= 1
if balance < 0:
swaps += 1
balance = 1
return swaps
The implementation follows the greedy algorithm directly.
The `balance` variable tracks the current bracket validity state while scanning the string.
When we see an opening bracket, we increase balance because it can potentially match a future closing bracket.
When we see a closing bracket, we decrease balance because it consumes one unmatched opening bracket.
The important logic occurs when `balance < 0`. This indicates an invalid prefix. At that moment:
- We count one swap
- We reset balance to `1`
This reset simulates bringing a future `'['` into the current position.
The algorithm never physically swaps characters because the exact swap locations do not matter. Only the number of required corrections matters.
Since the string is traversed exactly once and only constant extra memory is used, the solution is highly efficient.
## Go Solution
```go
func minSwaps(s string) int {
balance := 0
maxDeficit := 0
for _, c := range s {
if c == '[' {
swaps := 0
for _, char := range s {
if char == '[' {
balance++
} else {
balance--
}
if balance < 0 {
if -balance > maxDeficit {
maxDeficit = -balance
}
}
}
return (maxDeficit + 1) / 2
}
The Go version follows the same logic. The difference is in how strings are iterated: using a for _, c := range s loop ensures we handle each character as a rune. Go does not require explicit type conversions here because balance and maxDeficit are integers.
Worked Examples
Example 1: s = "][]["
| Index | Char | Balance | Max Deficit |
|---|---|---|---|
| 0 | ] | -1 | 1 |
| 1 | [ | 0 | 1 |
| 2 | ] | -1 | 1 |
| 3 | [ | 0 | 1 |
max_deficit = 1 ā minimum swaps = (1 + 1) // 2 = 1.
Example 2: s = "]]][[["
| Index | Char | Balance | Max Deficit |
|---|---|---|---|
| 0 | ] | -1 | 1 |
| 1 | ] | -2 | 2 |
| 2 | ] | -3 | 3 |
| 3 | [ | -2 | 3 |
| 4 | [ | -1 | 3 |
| 5 | [ | 0 | 3 |
max_deficit = 3 ā minimum swaps = (3 + 1) // 2 = 2.
Example 3: s = "[]"
| Index | Char | Balance | Max Deficit |
|---|---|---|---|
| 0 | [ | 1 | 0 |
| 1 | ] | 0 | 0 |
max_deficit = 0 ā minimum swaps = 0.
if balance < 0 {
swaps++
balance = 1
}
}
return swaps
}
The Go implementation is nearly identical to the Python version.
A few Go-specific details are worth noting:
- Strings in Go are immutable, but we never modify the string anyway
- The loop iterates over runes using `range`
- Integer overflow is not a concern because the maximum string length is only `10^6`
- No additional slices or stacks are needed, which keeps memory usage constant
## Worked Examples
### Example 1
Input: s = "][]["
| Index | Character | Balance Before | Balance After | Swaps |
| --- | --- | --- | --- | --- |
| 0 | ] | 0 | -1 ā reset to 1 | 1 |
| 1 | [ | 1 | 2 | 1 |
| 2 | ] | 2 | 1 | 1 |
| 3 | [ | 1 | 2 | 1 |
Final answer:
1
### Example 2
Input: s = "]]][[["
| Index | Character | Balance Before | Balance After | Swaps |
| --- | --- | --- | --- | --- |
| 0 | ] | 0 | -1 ā reset to 1 | 1 |
| 1 | ] | 1 | 0 | 1 |
| 2 | ] | 0 | -1 ā reset to 1 | 2 |
| 3 | [ | 1 | 2 | 2 |
| 4 | [ | 2 | 3 | 2 |
| 5 | [ | 3 | 4 | 2 |
Final answer:
2
### Example 3
Input: s = "[]"
| Index | Character | Balance Before | Balance After | Swaps |
| --- | --- | --- | --- | --- |
| 0 | [ | 0 | 1 | 0 |
| 1 | ] | 1 | 0 | 0 |
Final answer:
0
## Complexity Analysis
| Measure | Complexity | Explanation |
| --- | --- | --- |
| Time | O(n) | We iterate through the string once, performing constant work per character. |
| Space | O(1) | Only a few integer variables are used, independent of input size. |
The algorithm is extremely efficient, even for the upper bound of `n = 10^6`, since it does not require extra storage or nested iterations.
| Time | O(n) | The string is scanned exactly once |
| Space | O(1) | Only a few integer variables are used |
The algorithm performs constant work for each character in the string. No nested loops, stacks, or auxiliary arrays are required.
Because the implementation avoids physically performing swaps, memory usage remains constant regardless of input size.
## Test Cases
Provided examples
assert Solution().minSwaps("][][") == 1 # one swap needed assert Solution().minSwaps("]]][[[") == 2 # two swaps needed assert Solution().minSwaps("[]") == 0 # already balanced
Additional cases
assert Solution().minSwaps("[][][][]") == 0 # already balanced, alternating assert Solution().minSwaps("[[]][]") == 0 # balanced but nested assert Solution().minSwaps("]][[") == 1 # small reverse, one swap assert Solution().minSwaps("]]]][[[[") == 2 # multiple swaps needed assert Solution().minSwaps("[][" * 500000) == 0 # large balanced input assert Solution().minSwaps("][" * 500000) == 500000 # large unbalanced input
| Test | Why |
| --- | --- |
| `'][]['` | Single swap needed to balance |
| `']]][[['` | Multiple swaps required, checks cumulative deficit |
| `'[]'` | Already balanced, simplest case |
| `'[][][][]'` | Already balanced with nested pattern |
| `'[['*n + ']]'*n` | Tests large input scalability |
## Edge Cases
**1. Already balanced string**: Strings like `"[]"` or `"[][]"` require zero swaps. The algorithm correctly handles this by never letting `balance` go negative, so `max_deficit` remains zero.
**2. Fully reversed string**: Strings like `"]["` or `"]]][[["` produce maximum initial deficits. Our algorithm accurately tracks `max_deficit` and calculates the exact number of swaps needed.
**3. Large input**: For `n = 10^6`, the algorithm avoids using extra data structures and operates in linear time with constant space. This ensures it handles extreme cases efficiently without memory overflow or timeouts.
This method is robust, handles edge cases, and scales linearly with input size.
sol = Solution()
# Provided examples
assert sol.minSwaps("][][") == 1 # Single swap needed
assert sol.minSwaps("]]][[[") == 2 # Multiple corrections required
assert sol.minSwaps("[]") == 0 # Already balanced
# Small edge cases
assert sol.minSwaps("][") == 1 # Smallest invalid input
assert sol.minSwaps("[[]]") == 0 # Nested balanced brackets
assert sol.minSwaps("[][]") == 0 # Sequential balanced brackets
# Consecutive invalid prefixes
assert sol.minSwaps("]]][[[") == 2 # Heavy imbalance at start
assert sol.minSwaps("]]]][[[[") == 2 # Larger imbalance pattern
# Alternating imbalance
assert sol.minSwaps("][][[]") == 1 # One early correction
# Large balanced case
assert sol.minSwaps("[" * 500 + "]" * 500) == 0 # Already valid
# Large worst-case style input
assert sol.minSwaps("]" * 500 + "[" * 500) == 250 # Maximum imbalance pattern
Test Case Summary
| Test | Why |
|---|---|
"[]ā |
Verifies already balanced input |
"][" |
Smallest invalid case |
"][][" |
Tests single required swap |
"]]][[[" |
Tests multiple imbalance repairs |
"[[]]" |
Tests nested balanced structure |
"[][]" |
Tests sequential balanced structure |
"]]]][[[[" |
Tests larger imbalance prefix |
"][][[]" |
Tests mixed valid and invalid regions |
"[" * 500 + "]" * 500 |
Tests large already-balanced input |
"]" * 500 + "[" * 500 |
Tests worst-case imbalance pattern |
Edge Cases
Already Balanced Strings
Inputs such as:
"[]"
"[[]]"
"[][]"
never cause the balance to become negative.
A buggy implementation might still attempt unnecessary swaps or miscount balance transitions. This implementation correctly returns 0 because the balance < 0 condition is never triggered.
Strings Starting with Many Closing Brackets
Inputs like:
"]]][[["
are particularly dangerous because the balance immediately becomes negative.
A naive implementation might incorrectly reset balance to 0 after a swap. However, after swapping in an opening bracket, one unmatched opening bracket should remain, so the correct reset value is 1.
This implementation handles that correctly.
Extremely Large Inputs
The constraint allows strings up to 10^6 characters.
Any implementation that repeatedly searches ahead, constructs new strings, or physically performs swaps may time out or consume too much memory.
This implementation processes the string in one pass with constant extra memory, making it efficient even for the largest valid inputs.