LeetCode 893中等贪心 · 排序
特殊等价字符串组 图解题解
这道题到底在问什么
给定字符串数组 words,每步可交换某个串里任意两个偶数下标处的字符,或任意两个奇数下标处的字符。两串若能经若干次操作变得完全相同,就是「特殊等价」。求把 words 按特殊等价关系分组后,组的数量。
- 输入
- words=["abcd","cdab","cbad","xyzz","zzxy","zzyx"]
- 输出
- 3
- 输入
- words=["abc","acb","bac","bca","cab","cba"]
- 输出
- 3
最优解:一步一步想明白
- 3记住这把钥匙:签名 = 排序后的偶数位 加 排序后的奇数位。下面每个串都现算它的签名,再看集合里有没有见过。
- 4先把 4 个串摆出来。我们要逐个算签名、放进集合。集合现在是空的,0 组。
- 5处理「abcd」。先看偶数下标 0 和 2,绿色这两个字符是 a 和 c,奇数位先灰着不动。
- 6把偶数位的 a、c 排个序,得到 ac。这就是签名的前半截,顺序固定下来了。
- 7再看奇数下标 1 和 3,绿色换到这两个字符 b 和 d,偶数位这回灰掉。
- 8奇数位的 b、d 排序得 bd。前半 ac 接上后半 bd,「abcd」的签名就是 ac·bd。
- 9拿 ac·bd 去集合里查,没见过,于是加进去,组数变成 1。
- 10处理「cdab」。先看偶数下标 0 和 2,绿色这两个字符是 c 和 a,奇数位先灰着不动。
- 11把偶数位的 c、a 排个序,得到 ac。这就是签名的前半截,顺序固定下来了。
- 12再看奇数下标 1 和 3,绿色换到这两个字符 d 和 b,偶数位这回灰掉。
- 13奇数位的 d、b 排序得 bd。前半 ac 接上后半 bd,「cdab」的签名就是 ac·bd。
- 14拿 ac·bd 去集合里查,发现第 1 组里已有同款签名,「cdab」和它们特殊等价,并进去,组数不变还是 1。
- 15处理「xyzz」。先看偶数下标 0 和 2,绿色这两个字符是 x 和 z,奇数位先灰着不动。
- 16把偶数位的 x、z 排个序,得到 xz。这就是签名的前半截,顺序固定下来了。
- 17再看奇数下标 1 和 3,绿色换到这两个字符 y 和 z,偶数位这回灰掉。
- 18奇数位的 y、z 排序得 yz。前半 xz 接上后半 yz,「xyzz」的签名就是 xz·yz。
- 19拿 xz·yz 去集合里查,没见过,于是加进去,组数变成 2。
- 20处理「zzyx」。先看偶数下标 0 和 2,绿色这两个字符是 z 和 y,奇数位先灰着不动。
- 21把偶数位的 z、y 排个序,得到 yz。这就是签名的前半截,顺序固定下来了。
- 22再看奇数下标 1 和 3,绿色换到这两个字符 z 和 x,偶数位这回灰掉。
- 23奇数位的 z、x 排序得 xz。前半 yz 接上后半 xz,「zzyx」的签名就是 yz·xz。
- 24拿 yz·xz 去集合里查,没见过,于是加进去,组数变成 3。
- 254 个串算完,集合里留下 3 个不同签名。abcd 和 cdab 撞同一个签名归一组,xyzz 和 zzyx 各自单独一组(注意 xyzz 与 zzyx 并不等价),最终 3 组。
⚠️ 容易写错的地方
✗ 错:把整个串排序当签名
✓ 对:偶数位、奇数位分别排序再拼
跨奇偶位不能交换,整串排序会把本不等价的串错判成一组
✗ 错:偶数位和奇数位拼接顺序在不同串间不一致
✓ 对:所有串统一「偶在前奇在后」或统一「奇在前偶在后」
顺序规则必须全程一致,签名才有可比性
✗ 错:用下标从 1 开始数奇偶
✓ 对:题目按 0 起始下标,下标 0 是偶数位
错位会把偶数位当奇数位,签名全错
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def numSpecialEquivGroups(self, words: List[str]) -> int:
s = {''.join(sorted(word[::2]) + sorted(word[1::2])) for word in words}
return len(s)C++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
int numSpecialEquivGroups(vector<string>& words) {
unordered_set<string> s;
for (auto& word : words) {
string a = "", b = "";
for (int i = 0; i < word.size(); ++i) {
if (i & 1)
a += word[i];
else
b += word[i];
}
sort(a.begin(), a.end());
sort(b.begin(), b.end());
s.insert(a + b);
}
return s.size();
}
};Java
import java.util.*;
class Solution {
public int numSpecialEquivGroups(String[] words) {
Set<String> s = new HashSet<>();
for (String word : words) {
s.add(convert(word));
}
return s.size();
}
private String convert(String word) {
List<Character> a = new ArrayList<>();
List<Character> b = new ArrayList<>();
for (int i = 0; i < word.length(); ++i) {
char ch = word.charAt(i);
if (i % 2 == 0) {
a.add(ch);
} else {
b.add(ch);
}
}
Collections.sort(a);
Collections.sort(b);
StringBuilder sb = new StringBuilder();
for (char c : a) {
sb.append(c);
}
for (char c : b) {
sb.append(c);
}
return sb.toString();
}
}复杂度
时间
O(n·L·log L)
n 个串,每串长 L,各排序一次
空间
O(n·L)
集合里最多存 n 个签名,每个长 L
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 特殊等价字符串组 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
不排序,用计数代替签名行不行?+
可以。给偶数位的 26 个字母计数、奇数位的 26 个字母计数,把两个计数向量拼起来当签名(比如转成定长字符串)。这样省掉排序,单串处理降到 O(L),对长串更省。排序版胜在写法短、好记。
题目说所有串等长,如果不等长会怎样?+
不等长的两个串绝不可能特殊等价,因为操作不改变长度。签名天然带着各奇偶位的字符个数,长度不同签名必然不同,会被分到不同组,逻辑依旧成立,不用特判。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 特殊等价字符串组 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。