1202. 交换字符串中的元素

1202. 交换字符串中的元素

1202. 交换字符串中的元素

难度中等

给你一个字符串 s,以及该字符串中的一些「索引对」数组 pairs,其中 pairs[i] = [a, b] 表示字符串中的两个索引(编号从 0 开始)。

你可以 任意多次交换pairs 中任意一对索引处的字符。

返回在经过若干次交换后,s 可以变成的按字典序最小的字符串。

示例 1:

输入:s = "dcab", pairs = [[0,3],[1,2]]
输出:"bacd"
解释: 
交换 s[0] 和 s[3], s = "bcad"
交换 s[1] 和 s[2], s = "bacd"

示例 2:

输入:s = "dcab", pairs = [[0,3],[1,2],[0,2]]
输出:"abcd"
解释:
交换 s[0] 和 s[3], s = "bcad"
交换 s[0] 和 s[2], s = "acbd"
交换 s[1] 和 s[2], s = "abcd"

示例 3:

输入:s = "cba", pairs = [[0,1],[1,2]]
输出:"abc"
解释:
交换 s[0] 和 s[1], s = "bca"
交换 s[1] 和 s[2], s = "bac"
交换 s[0] 和 s[1], s = "abc"

提示:

  • 1 <= s.length <= 10^5
  • 0 <= pairs.length <= 10^5
  • 0 <= pairs[i][0], pairs[i][1] < s.length
  • s 中只含有小写英文字母

函数签名:

func smallestStringWithSwaps(s string, pairs [][]int) string

分析

根据 pairs 可以得到一系列联通的顶点,假设 i1, i2, ..., ik 这些位置相邻的点是联通的,可以发现这些点是可以通过交换任意排序的。

比如,要让 i1ik 交换,而中间的点不变,只需要先将 i1 一直换到 最后,类似冒泡的方式,再类似地把现在居于倒数第二位的 ik 冒泡交换到最开始。

任意指定两个点,都可以在其他点不动的情况下交换这两个点。

因为这样的原因,要使得最终结果字典序最小,只需要把每个联通分量里的点按照对应字符字典序排列。

用并查集来事先处理联通分量的情况,之后将每个连通分量里的点排序,最后构建结果即可。

func smallestStringWithSwaps(s string, pairs [][]int) string {
	n := len(s)
	uf := makeUnionFind(n)
	for _, v := range pairs {
		uf.Union(v[0], v[1])
	}
	m := make(map[int][]byte, n)
	for i := range uf {
		root := uf.Find(i)
		m[root] = append(m[root], s[i])
	}
	for _, b := range m {
		sort.Slice(b, func(i, j int) bool {
			return b[i] < b[j]
		})
	}
	res := make([]byte, n)
	for i := range res {
		root := uf.Find(i)
		res[i] = m[root][0]
		m[root] = m[root][1:]
	}
	return string(res)
}

type UnionFind []int

func makeUnionFind(n int) UnionFind {
	uf := make([]int, n)
	for i := range uf {
		uf[i] = i
	}
	return uf
}

func (uf UnionFind) Union(x, y int) {
	rootX, rootY := uf.Find(x), uf.Find(y)
	uf[rootX] = rootY
}

func (uf UnionFind) Find(x int) int {
	for x != uf[x] {
		x, uf[x] = uf[x], uf[uf[x]]
	}
	return x
}

时间复杂度: O(nlog n + m ),其中 n 为字符串长度,m 为索引对数量。并查集的操作可以看作常数级。题解中 Find 用了路径压缩,也可以在 Union 的时候按秩合并。最坏情况下所有点联通,需要把整个字符串排序。

空间复杂度: O(n)。