[leetcode题解] 第1202题Smallest String With Swaps

https://leetcode-cn.com/problems/smallest-string-with-swaps/

分析

需要把这道题看成图论的题目才好做:将字符串中的索引视为图的节点,一个pair对意味着两个节点之间存在一条无向边。那么答案就可以这样来得到:对无向图中的每个连通分量所代表的字符串分别进行排序,再把字符按照索引填入原位置
这是因为交换具有传递性,即a与b可交换,b与c可交换,那么a一定可与c交换(交换次数不受限制)。因此在连通分量中任意两个字符之间都是可交换的,要得到这部分按字典序最小的字符串,自然就是直接排序。

解法

并查集

涉及到图论中的连通分量问题,自然要写并查集,这个很重要,需要事先掌握,下面直接给出采用了路径压缩的并查集代码:

#define MAX 100007

int root[MAX];

void init(int n) {
    for (int i = 0; i < n; ++i) root[i] = i;
}

int find(int x) {
    if (x != root[x]) root[x] = find(root[x]);
    return root[x];
}

void combine(int x, int y) {
    root[find(x)] = find(y);
}

遍历图中所有的边,就可以完成并查集的构造了:

for (auto& pair : pairs) {
    if (find(pair[0]) != find(pair[1])) combine(pair[0], pair[1]);
}

排序思路一

构造好并查集后,同属于一个连通分量的字符具有相同的代表元,可通过find函数来获取。下一步就是思考如何获取各个连通分量代表的字符串,排序,再按正确位置填回去。
要把同属一个连通分量的字符连接成一个字符串,这一步必须用哈希表来建立一个映射关系,连通分量的代表元作为key,value则是存储字符的一个容器。考虑到需要排序,那么优先队列就非常适合作为这里的容器。
当我们把每个字符都放入它所属的优先队列后,再遍历字符串的索引,找到它的代表元,通过映射关系从对应的优先队列中取出一个字符填入该索引处,最终就得到了结果。注意优先队列应选用小根堆。

string smallestStringWithSwaps(string s, vector<vector<int>>& pairs) {
    init(s.size());
    for (auto& pair : pairs) {
        if (find(pair[0]) != find(pair[1])) combine(pair[0], pair[1]);
    }
    unordered_map<int, priority_queue<int, vector<int>, greater<int>>> map;
    for (int i = 0; i < s.size(); ++i) map[find(i)].push(s[i]);
    for (int i = 0; i < s.size(); ++i) {
        int root = find(i);
        if (map[root].size() > 0) {
            s[i] = map[root].top();
            map[root].pop();
        }
    }
    return s;
}

在这个排序思路中,由于容器本身的有序性,我们可以在容器中存放字符而不用管它原来的索引,只用保证每次从容器中取出的字符都是最小的那个即可,填入的位置索引可以由遍历得到。

排序思路二

直接使用优先队列虽然简单方便,但是速度并不快,上述代码用了396ms,而且在最后一步填入时也不是对每个连通分量来分组处理的,可能不是那么容易想到。
考虑更直观的排序方式,使用容器来存储字符的索引,然后对于每一个连通分量来进行排序和填入的处理:利用索引来拼接出一个临时字符串,对其排序,再利用索引来填入。

string smallestStringWithSwaps(string s, vector<vector<int>>& pairs) {
    init(s.size());
    for (auto& pair : pairs) {
        if (find(pair[0]) != find(pair[1])) combine(pair[0], pair[1]);
    }
    unordered_map<int, vector<int>> map;
    for (int i = 0; i < s.size(); ++i) map[find(i)].push_back(i);
    for (auto& [root, indexes] : map) {
        string str = "";
        for (auto i : indexes) str += s[i];
        sort(str.begin(), str.end());
        for (int i = 0; i < str.size(); ++i) s[indexes[i]] = str[i];
    }
    return s;
}

这个方法应该比较容易想到,速度也不错,耗时272ms。

排序思路三

注意到,字符串中只含有小写英文字母,那么就可以采用线性时间复杂度的桶排序,并且由于代表元也是int,哈希表就可以换成更简单的二维数组了:

    string smallestStringWithSwaps(string s, vector<vector<int>>& pairs) {
        int n = s.size();
        init(n);
        for (auto& pair : pairs) {
            if (find(pair[0]) != find(pair[1])) combine(pair[0], pair[1]);
        }
        int map[n][26];
        memset(map, 0, sizeof(map)); // 初始化,不能省略
        for (int i = 0; i < n; ++i) map[find(i)][s[i] - 'a']++;
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < 26; ++j) {
                if (map[find(i)][j] > 0) {
                    s[i] = (char)(j + 'a');
                    map[find(i)][j]--;
                    break;
                }
            }
        }
        return s;
    }

排序的思想很简单,把字符填入对应的桶内即可。要取出最小的那个字符时,只需从小到大遍历所有的桶,从第一个不为空的桶里拿一个即可。填入的方法与思路一相同,都是依次遍历索引,找到此时它所在连通分量中最小的那个字符,填入这个位置。
这个方法速度最快,仅用204ms,击败100%的c++方法,但由于采用桶排序,是以空间换时间的,因此占用的空间较高。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值