leetcode 1202. Smallest String With Swaps(交换得到最小的字符串)

该博客探讨了一种字符串处理问题,其中给定一个字符串和一系列可交换的字符对。目标是通过无限次交换找到字典顺序最小的字符串。博主介绍了两种解决方案:深度优先搜索(DFS)和并查集(UnionFind)。在DFS方法中,通过建立无向图并遍历寻找连通区域,然后对字符和下标排序。而在UnionFind方法中,通过聚类和检索找到连通区域,同样对字符排序后插入相应下标。这两种方法都旨在最小化字典顺序,而UnionFind方法在效率上优于DFS。

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);
    }   
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

蓝羽飞鸟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值