You are given a string s, and an array of pairs of indices in the string pairs where pairs[i] = [a, b] indicates 2 indices(0-indexed) of the string.
You can swap the characters at any pair of indices in the given pairs any number of times.
Return the lexicographically smallest string that s can be changed to after using the swaps.
Example 1:
Input: s = “dcab”, pairs = [[0,3],[1,2]]
Output: “bacd”
Explaination:
Swap s[0] and s[3], s = “bcad”
Swap s[1] and s[2], s = “bacd”
Example 2:
Input: s = “dcab”, pairs = [[0,3],[1,2],[0,2]]
Output: “abcd”
Explaination:
Swap s[0] and s[3], s = “bcad”
Swap s[0] and s[2], s = “acbd”
Swap s[1] and s[2], s = “abcd”
给一个字符串s,pairs里面是一对对的数,每一对表示这两个下标对应的字符可以交换位置。
而且可以无限次交换。
问如何利用这些交换使得字符串在字典顺序上最小。
思路:
乍一看像是字符串的问题,实则是graph问题。
把连续的可以交换的点看成一个连通区域,那么这个区域内任意两点都是可以交换的(因为不限制交换次数,总有办法把它们换到想去的地方)。
比如上面example1, 0和3,0和2,1和2可以交换,连在一起就是0,1,2,3可以互换。
既然可以互换,那么就按字典顺序最小的给对应的字符排个序,把排过序的字符插入到排过序的对应下标处,变成了"abcd"。
所以需要解决两个问题:
1)找连通区域。
2)把连通区域内的下标和字符排序,排好序的字符放在排好序的下标处。
方法1:
DFS
找连通区域可以通过建立无向图(邻接链表的形式),用DFS把相连的下标(可以交换的下标)放到一个list,
同时把对应的字符也放入list。
然后对这两个list排序,把排好序的字符放入排好序的下标处。
(因为取出的下标就是这些字符的位置,字符已经按字典顺序排好,要从左往右依次放入排好序的字符,所以这些下标也要排序)。
class Solution {
List<Integer>[] graph;
boolean[] visited;
public String smallestStringWithSwaps(String s, List<List<Integer>> pairs) {
int n = s.length();
visited = new boolean[n];
graph = new ArrayList[n];
char[] res = new char[n];
for(int i = 0; i < n; i++) {
graph[i] = new ArrayList<Integer>();
}
//构建无向图
for(List<Integer> pair : pairs) {
graph[pair.get(0)].add(pair.get(1));
graph[pair.get(1)].add(pair.get(0));
}
//遍历每个顶点,找连通区域
for(int i = 0; i < n; i++) {
if(visited[i]) continue;
//每个装的都是一个连通区域
List<Character> chars = new ArrayList<>();
List<Integer> ids = new ArrayList<>();
dfs(s, i, chars, ids);
//把获得到的连通区字符和下标排序,按下标把字符插入到string数组中
Collections.sort(chars);
Collections.sort(ids);
for(int j = 0; j < ids.size(); j++) {
res[ids.get(j)] = chars.get(j);
}
}
return new String(res);
}
void dfs(String s, int idx, List<Character> chars, List<Integer> ids) {
if(visited[idx]) return;
chars.add(s.charAt(idx));
ids.add(idx);
visited[idx] = true;
for(int adj : graph[idx]) {
dfs(s, adj, chars, ids);
}
}
}
方法2:
Union Find
找连通区域还可以用union-find方法,
简单说下,union就类似于聚类,每个可交换的点(有边相连的点)顺藤摸瓜找到它的root,
所以这些点就聚成以root为首的类,把同一类的点直接接到root下面。
下次检索类别的时候就不需要再通过 点1,点2,点3…来找到root,而可以一步找到root。缩短了时间。
find类似于检索,就是找到每个点所属的root。
同一root的点就是一个连通区域。
下面是UnionFind类
rank表示一个root下有多少个点,当然聚类是往点多的类去靠拢。
class UnionFind {
int[] root;
int[] rank;
public UnionFind(int n) {
root = new int[n];
rank = new int[n];
for(int i = 0; i < n; i++) {
root[i] = i;
rank[i] = 1;
}
}
int find(int node) {
if(node == root[node]) return node;
return root[node] = find(root[node]);
}
void union(int n1, int n2) {
int r1 = find(n1);
int r2 = find(n2);
//不是同一个root,要union
if(r1 != r2) {
if(rank[r1] >= rank[r2]) {
root[r2] = r1;
rank[r2] += rank[r1];
} else {
root[r1] = r2;
rank[r1] += rank[r2];
}
}
}
}
然后思路就和上面DFS差不多,也是先找连通区域,存入连通区的下标和字符。
然后排序,注意这里不需要给下标排序,因为下标存入的时候是按顺序访问的。
只需要给字符排序,放入对应的下标中。
这个方法比DFS快很多。
public String smallestStringWithSwaps(String s, List<List<Integer>> pairs) {
int n = s.length();
UnionFind uf = new UnionFind(n);
HashMap<Integer, List<Integer>> map = new HashMap<>();
char[] res = new char[n];
for(List<Integer> pair : pairs) {
uf.union(pair.get(0), pair.get(1));
}
for(int i = 0; i < n; i++) {
int root = uf.find(i);
map.putIfAbsent(root, new ArrayList<Integer>());
map.get(root).add(i); //按下标顺序加的,所以连通区的下标已经是排好序的
}
for(List<Integer> ids : map.values()) {
char[] chars = new char[ids.size()]; //利用和下标数量一样直接建立数组,比ArrayList快很多
for(int i = 0; i < ids.size(); i++) {
chars[i] = s.charAt(ids.get(i));
}
Arrays.sort(chars);
for(int i = 0; i < ids.size(); i ++) {
res[ids.get(i)] = chars[i];
}
}
return new String(res);
}
}