题目描述
思路解析
一句话答案:LeetCode 893 特殊等价字符串组:只能交换同奇偶下标的字符,于是给每个串按偶数位排序加奇数位排序拼出签名,扔进集合去重,组数就是集合大小,时间 O(n·L log L)。
只能同奇偶换字符,求分成几组
给一个字符串数组 words,每个串等长。允许的操作只有两种:把某个串里两个偶数下标处的字符对调,或两个奇数下标处的对调。两个串若能靠这些操作变得完全一样,就叫特殊等价。按这层关系分组,返回组数。题面例子 words=["abcd","cdab","cbad","xyzz","zzxy","zzyx"] 的答案是 3。
整串排序当签名,会把两组并成一组
判两串等不等价,很容易想到把整串字符排序,排完一样就归一组,这会把不该并的串并到一起。拿 ab 和 ba:整串排序都得 ab,看着一组;可 a 在偶数下标、b 在奇数下标,把 ab 挪成 ba 要让偶数位 a 和奇数位 b 对调,属跨奇偶交换,题目不许,它们其实是两组。整串排序默认任意两字符都能换,比题目宽,才会误判。
偶数位排序加奇数位排序,就是签名
操作只发生在同奇偶位之间,所以不管怎么换,一个串偶数下标上那堆字符的构成永远不变,奇数下标上那堆也不变。这里的构成指多重集,也就是只数每种字母各出现几次、不看排列顺序。于是两串特殊等价,当且仅当偶数位、奇数位的字符多重集分别相同。
要给每个串造个能比对的代表,就把偶数位字符单独排序、奇数位字符单独排序,偶在前奇在后接成一个字符串,这就是签名,也就是把所有互相等价的串映成同一个字符串、方便去重。签名相同即特殊等价,不同即不等价。
每串现算签名,扔进集合数一数
遍历每个串,按下标奇偶拆成两组。下标从 0 起数,0 是偶数位,于是下标 0、2、4… 归偶数位那组,1、3、5… 归奇数位那组。两组各自排序,偶在前奇在后拼成签名,塞进哈希集合。所有串走完,集合自动合掉重复签名,剩几个不同签名就是几组,返回集合大小。
六个串逐个算签名,落到 3 组
拿题面的 words=["abcd","cdab","cbad","xyzz","zzxy","zzyx"] 手算。abcd 偶数位 a、c 排成 ac,奇数位 b、d 排成 bd,签名 acbd。cdab 偶数位 c、a 也排成 ac,奇数位 d、b 也排成 bd,签名同为 acbd。cbad 同理得 acbd,前三个并成一组。
接着 xyzz 偶数位 x、z 排成 xz,奇数位 y、z 排成 yz,签名 xzyz;zzxy 偶数位 z、x 得 xz,奇数位 z、y 得 yz,签名同样 xzyz,和 xyzz 并一组。最后 zzyx 偶数位 z、y 排成 yz,奇数位 z、x 排成 xz,签名 yzxz,跟前面都不同,自己一组。集合留下 acbd、xzyz、yzxz 三个,答案 3。
L log L 排序 n 次,ab 和 ba 为何分家
设 n 个串、每串长 L。每串把两半排序,代价 L·log L,合起来 O(n·L log L);集合最多存 n 个签名、每个长 L,空间 O(n·L)。
两个本来相同的串签名一致,并进同一组。ab 和 ba 字母虽一样,a、b 分处偶奇两位、跨位换不动,签名一个 ab 一个 ba,判成两组,整串排序正会栽在这。长度不同的两串更省事,操作改不了长度,签名长度也不同,必然分开,无需特判。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
记住这把钥匙:签名 = 排序后的偶数位 加 排序后的奇数位。下面每个串都现算它的签名,再看集合里有没有见过。
先把 4 个串摆出来。我们要逐个算签名、放进集合。集合现在是空的,0 组。
处理「abcd」。先看偶数下标 0 和 2,绿色这两个字符是 a 和 c,奇数位先灰着不动。
把偶数位的 a、c 排个序,得到 ac。这就是签名的前半截,顺序固定下来了。
再看奇数下标 1 和 3,绿色换到这两个字符 b 和 d,偶数位这回灰掉。
奇数位的 b、d 排序得 bd。前半 ac 接上后半 bd,「abcd」的签名就是 ac·bd。
拿 ac·bd 去集合里查,没见过,于是加进去,组数变成 1。
处理「cdab」。先看偶数下标 0 和 2,绿色这两个字符是 c 和 a,奇数位先灰着不动。
把偶数位的 c、a 排个序,得到 ac。这就是签名的前半截,顺序固定下来了。
再看奇数下标 1 和 3,绿色换到这两个字符 d 和 b,偶数位这回灰掉。
奇数位的 d、b 排序得 bd。前半 ac 接上后半 bd,「cdab」的签名就是 ac·bd。
拿 ac·bd 去集合里查,发现第 1 组里已有同款签名,「cdab」和它们特殊等价,并进去,组数不变还是 1。
处理「xyzz」。先看偶数下标 0 和 2,绿色这两个字符是 x 和 z,奇数位先灰着不动。
把偶数位的 x、z 排个序,得到 xz。这就是签名的前半截,顺序固定下来了。
再看奇数下标 1 和 3,绿色换到这两个字符 y 和 z,偶数位这回灰掉。
奇数位的 y、z 排序得 yz。前半 xz 接上后半 yz,「xyzz」的签名就是 xz·yz。
拿 xz·yz 去集合里查,没见过,于是加进去,组数变成 2。
处理「zzyx」。先看偶数下标 0 和 2,绿色这两个字符是 z 和 y,奇数位先灰着不动。
把偶数位的 z、y 排个序,得到 yz。这就是签名的前半截,顺序固定下来了。
再看奇数下标 1 和 3,绿色换到这两个字符 z 和 x,偶数位这回灰掉。
奇数位的 z、x 排序得 xz。前半 yz 接上后半 xz,「zzyx」的签名就是 yz·xz。
拿 yz·xz 去集合里查,没见过,于是加进去,组数变成 3。
4 个串算完,集合里留下 3 个不同签名。abcd 和 cdab 撞同一个签名归一组,xyzz 和 zzyx 各自单独一组(注意 xyzz 与 zzyx 并不等价),最终 3 组。
边界先想清:相同串并组;ab 与 ba 因 a、b 跨奇偶不可换,是两组。
两个常见追问:计数代替排序可提速;不等长的串签名必不同、天然分开。
参考代码
from __future__ import annotationsfrom 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)复杂度
- 时间:O(n·L·log L),n 个串,每串长 L,各排序一次
- 空间:O(n·L),集合里最多存 n 个签名,每个长 L
易错点
面试追问把动画讲成自己的话
追问不排序,用计数代替签名行不行?
追问题目说所有串等长,如果不等长会怎样?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
使括号有效的最少添加
LeetCode 921 · 中等 · 沿着 贪心套路 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题