🧠
Union-Find + Priority Queue for On-the-Fly Character Assignment
💡 This approach optimizes the previous by using priority queues (min-heaps) to assign characters on the fly, avoiding sorting after grouping.
Intuition
Instead of sorting characters after grouping, insert characters into min-heaps per group during grouping, then assign the smallest character available when reconstructing the string.
Algorithm
- Initialize union-find for all indices.
- Union indices connected by pairs.
- Create a map from root parent to a min-heap of characters.
- For each index, add its character to the min-heap of its group.
- Reconstruct the string by polling the smallest character from each group's min-heap for each index.
💡 This approach is a variation that uses data structures to optimize character retrieval, but the core grouping idea remains the same.
from typing import List
import heapq
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0]*n
def find(self, x):
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union(self, x, y):
rx, ry = self.find(x), self.find(y)
if rx == ry:
return
if self.rank[rx] < self.rank[ry]:
self.parent[rx] = ry
elif self.rank[ry] < self.rank[rx]:
self.parent[ry] = rx
else:
self.parent[ry] = rx
self.rank[rx] += 1
class Solution:
def smallestStringWithSwaps(self, s: str, pairs: List[List[int]]) -> str:
n = len(s)
uf = UnionFind(n)
for x, y in pairs:
uf.union(x, y)
groups = {}
for i in range(n):
root = uf.find(i)
if root not in groups:
groups[root] = []
heapq.heappush(groups[root], s[i])
res = []
for i in range(n):
root = uf.find(i)
smallest_char = heapq.heappop(groups[root])
res.append(smallest_char)
return ''.join(res)
# Example usage
if __name__ == '__main__':
sol = Solution()
print(sol.smallestStringWithSwaps("dcab", [[0,3],[1,2]])) # Output: "bacd"
Line Notes
heapq.heappush(groups[root], s[i])Add character to min-heap for its group
smallest_char = heapq.heappop(groups[root])Pop smallest character from group's min-heap
res.append(smallest_char)Build result string with smallest available chars
uf.union(x, y)Union indices connected by pairs
if root not in groups: groups[root] = []Initialize min-heap list for new group
import java.util.*;
class UnionFind {
int[] parent, rank;
public UnionFind(int n) {
parent = new int[n];
rank = new int[n];
for (int i = 0; i < n; i++) parent[i] = i;
}
public int find(int x) {
if (parent[x] != x) parent[x] = find(parent[x]);
return parent[x];
}
public void union(int x, int y) {
int rx = find(x), ry = find(y);
if (rx == ry) return;
if (rank[rx] < rank[ry]) parent[rx] = ry;
else if (rank[ry] < rank[rx]) parent[ry] = rx;
else {
parent[ry] = rx;
rank[rx]++;
}
}
}
public class Solution {
public String smallestStringWithSwaps(String s, List<List<Integer>> pairs) {
int n = s.length();
UnionFind uf = new UnionFind(n);
for (List<Integer> p : pairs) uf.union(p.get(0), p.get(1));
Map<Integer, PriorityQueue<Character>> groups = new HashMap<>();
for (int i = 0; i < n; i++) {
int root = uf.find(i);
groups.computeIfAbsent(root, k -> new PriorityQueue<>()).offer(s.charAt(i));
}
StringBuilder res = new StringBuilder();
for (int i = 0; i < n; i++) {
int root = uf.find(i);
res.append(groups.get(root).poll());
}
return res.toString();
}
// Example usage
public static void main(String[] args) {
Solution sol = new Solution();
List<List<Integer>> pairs = Arrays.asList(Arrays.asList(0,3), Arrays.asList(1,2));
System.out.println(sol.smallestStringWithSwaps("dcab", pairs)); // Output: "bacd"
}
}
Line Notes
groups.computeIfAbsent(root, k -> new PriorityQueue<>()).offer(s.charAt(i));Add character to min-heap for its group
res.append(groups.get(root).poll());Poll smallest character from group's min-heap
uf.union(p.get(0), p.get(1));Union indices connected by pairs
new PriorityQueue<>()Priority queue to maintain sorted order of characters
int root = uf.find(i);Find root parent for grouping
#include <bits/stdc++.h>
using namespace std;
class UnionFind {
vector<int> parent, rank;
public:
UnionFind(int n) : parent(n), rank(n,0) {
iota(parent.begin(), parent.end(), 0);
}
int find(int x) {
if (parent[x] != x) parent[x] = find(parent[x]);
return parent[x];
}
void unite(int x, int y) {
int rx = find(x), ry = find(y);
if (rx == ry) return;
if (rank[rx] < rank[ry]) parent[rx] = ry;
else if (rank[ry] < rank[rx]) parent[ry] = rx;
else {
parent[ry] = rx;
rank[rx]++;
}
}
};
class Solution {
public:
string smallestStringWithSwaps(string s, vector<vector<int>>& pairs) {
int n = (int)s.size();
UnionFind uf(n);
for (auto &p : pairs) uf.unite(p[0], p[1]);
unordered_map<int, priority_queue<char, vector<char>, greater<char>>> groups;
for (int i = 0; i < n; i++) {
groups[uf.find(i)].push(s[i]);
}
string res;
res.reserve(n);
for (int i = 0; i < n; i++) {
int root = uf.find(i);
res.push_back(groups[root].top());
groups[root].pop();
}
return res;
}
};
// Example usage
int main() {
Solution sol;
vector<vector<int>> pairs = {{0,3},{1,2}};
cout << sol.smallestStringWithSwaps("dcab", pairs) << "\n"; // Output: "bacd"
return 0;
}
Line Notes
unordered_map<int, priority_queue<char, vector<char>, greater<char>>> groups;Map root to min-heap of characters
groups[uf.find(i)].push(s[i]);Push character into group's min-heap
res.push_back(groups[root].top());Append smallest character from min-heap
groups[root].pop();Remove used character from min-heap
uf.unite(p[0], p[1]);Union indices connected by pairs
class UnionFind {
constructor(n) {
this.parent = Array.from({length: n}, (_, i) => i);
this.rank = Array(n).fill(0);
}
find(x) {
if (this.parent[x] !== x) {
this.parent[x] = this.find(this.parent[x]);
}
return this.parent[x];
}
union(x, y) {
let rx = this.find(x), ry = this.find(y);
if (rx === ry) return;
if (this.rank[rx] < this.rank[ry]) this.parent[rx] = ry;
else if (this.rank[ry] < this.rank[rx]) this.parent[ry] = rx;
else {
this.parent[ry] = rx;
this.rank[rx]++;
}
}
}
class MinHeap {
constructor() {
this.heap = [];
}
push(val) {
this.heap.push(val);
this._siftUp();
}
pop() {
if (this.heap.length === 0) return null;
const top = this.heap[0];
const end = this.heap.pop();
if (this.heap.length > 0) {
this.heap[0] = end;
this._siftDown();
}
return top;
}
_siftUp() {
let idx = this.heap.length - 1;
while (idx > 0) {
let parent = Math.floor((idx - 1) / 2);
if (this.heap[parent] <= this.heap[idx]) break;
[this.heap[parent], this.heap[idx]] = [this.heap[idx], this.heap[parent]];
idx = parent;
}
}
_siftDown() {
let idx = 0;
const length = this.heap.length;
while (true) {
let left = 2 * idx + 1, right = 2 * idx + 2, smallest = idx;
if (left < length && this.heap[left] < this.heap[smallest]) smallest = left;
if (right < length && this.heap[right] < this.heap[smallest]) smallest = right;
if (smallest === idx) break;
[this.heap[smallest], this.heap[idx]] = [this.heap[idx], this.heap[smallest]];
idx = smallest;
}
}
}
var smallestStringWithSwaps = function(s, pairs) {
const n = s.length;
const uf = new UnionFind(n);
for (const [x, y] of pairs) uf.union(x, y);
const groups = new Map();
for (let i = 0; i < n; i++) {
const root = uf.find(i);
if (!groups.has(root)) groups.set(root, new MinHeap());
groups.get(root).push(s[i]);
}
const res = [];
for (let i = 0; i < n; i++) {
const root = uf.find(i);
res.push(groups.get(root).pop());
}
return res.join('');
};
// Example usage
console.log(smallestStringWithSwaps("dcab", [[0,3],[1,2]])); // Output: "bacd"
Line Notes
this.parent = Array.from({length: n}, (_, i) => i);Initialize each node as its own parent
if (this.parent[x] !== x) { this.parent[x] = this.find(this.parent[x]); }Path compression in find
let rx = this.find(x), ry = this.find(y);Find root parents before union
groups.set(root, new MinHeap());Initialize min-heap for new group
groups.get(root).push(s[i]);Push character into group's min-heap
res.push(groups.get(root).pop());Pop smallest character from min-heap for result
uf.union(x, y);Union indices connected by pairs
TimeO(n log n + m α(n)) similar to previous approach
SpaceO(n) for union-find and heaps
Using heaps avoids explicit sorting but still requires O(log n) per insertion and extraction, overall similar complexity.
💡 This approach is a practical optimization but conceptually similar to sorting groups.
Interview Verdict: Accepted
This approach is an optimization variant and is acceptable in interviews if explained well.