把每片雪花的 12 种旋转写法规范化为最小表示,再用哈希集合 O(n) 判重,判断是否存在两片同形状的雪花。
OJ: roj
题目 ID: 3037
难度:普及+/提高-
标签:哈希表字符串模拟
创建: 2026-10-01 11:28
更新: 2026-10-07 13:50
形式化题目
把每片雪花看成一个六元组
- 旋转:
,下标对 6 取模, ; - 翻转:
。
两片雪花形状相同,当且仅当其中一片可以通过若干次旋转和翻转变成另一片。等价地,定义等价类
则
给定 Twin snowflakes found.,不存在则输出 No two snowflakes are alike.。
正解
思路
朴素做法与瓶颈
最直接的想法是枚举所有雪花对
问题出在把"形状"当成了两片雪花之间的关系来验证。其实形状是每片雪花自己的属性:第
只要给每片雪花先算出一个"形状编号",判重就退化成集合判重,配对彻底消失。
关键观察:等价类只需要一个代表元
性质 1(等价类闭合):若
因为
、 ,"旋转 + 翻转"生成的变换群是闭合的:对 再做一次群变换,结果仍然落在 里;两边互相包含即相等。
也就是说,同形状的雪花拥有完全相同的旋转集合。
性质 2(最小表示是精确指纹):定义
(按六元组字典序取最小的那个)。那么
- 正向
:若 ,由性质 1 有 ,两边取最小值自然相等。这保证不漏判。 - 反向
:设 。由 和性质 1 得 ,同理 ,所以 ,即 。这保证不误判。
反向这一步是关键:它说明最小表示是精确指纹,不像数值哈希那样需要讨论碰撞概率。两片雪花的 12 种写法只要有一个重合,它们的最小表示就必然相等。
样例推演
下面把样例第一片雪花 1 2 3 4 5 6 的全部 12 种等价写法列出(k 表示从第 k 个角开始读),加粗的是最小表示。
| 方向与起点 | 六元组 | 是否最小表示 |
|---|---|---|
| 顺时针 k=0 | 1 2 3 4 5 6 |
是 |
| 顺时针 k=1 | 2 3 4 5 6 1 |
否 |
| 顺时针 k=2 | 3 4 5 6 1 2 |
否 |
| 顺时针 k=3 | 4 5 6 1 2 3 |
否 |
| 顺时针 k=4 | 5 6 1 2 3 4 |
否 |
| 顺时针 k=5 | 6 1 2 3 4 5 |
否 |
| 逆时针 k=0 | 6 5 4 3 2 1 |
否 |
| 逆时针 k=1 | 5 4 3 2 1 6 |
否 |
| 逆时针 k=2 | 4 3 2 1 6 5 |
否 |
| 逆时针 k=3 | 3 2 1 6 5 4 |
否 |
| 逆时针 k=4 | 2 1 6 5 4 3 |
否 |
| 逆时针 k=5 | 1 6 5 4 3 2 |
否 |
最小表示(形状指纹)是 1 2 3 4 5 6。
第二片雪花 4 3 2 1 6 5 表面上和第一片毫无共同之处,但它的第 逆时针 k=2 种写法恰好就是 1 2 3 4 5 6:
| 方向与起点 | 六元组 | 是否最小表示 |
|---|---|---|
| 顺时针 k=0 | 4 3 2 1 6 5 |
否 |
| 顺时针 k=1 | 3 2 1 6 5 4 |
否 |
| 顺时针 k=2 | 2 1 6 5 4 3 |
否 |
| 顺时针 k=3 | 1 6 5 4 3 2 |
否 |
| 顺时针 k=4 | 6 5 4 3 2 1 |
否 |
| 顺时针 k=5 | 5 4 3 2 1 6 |
否 |
| 逆时针 k=0 | 5 6 1 2 3 4 |
否 |
| 逆时针 k=1 | 6 1 2 3 4 5 |
否 |
| 逆时针 k=2 | 1 2 3 4 5 6 |
是 |
| 逆时针 k=3 | 2 3 4 5 6 1 |
否 |
| 逆时针 k=4 | 3 4 5 6 1 2 |
否 |
| 逆时针 k=5 | 4 5 6 1 2 3 |
否 |
两片的最小表示都是 1 2 3 4 5 6,因此形状相同。
这两张表要一起看:第一张说明"12 种写法里挑最小"这个动作,第二张说明"看起来不同的两片可能挑出同一个最小表示"。注意逆时针也要取满 6 个起点——只取顺时针 6 个会漏掉第二片这种情况,这是本题最容易写错的地方。
算法流程
按输入顺序扫描,维护一个哈希集合 seen:
- 读入一片雪花,算它的最小表示
key; - 若
key已经在seen里,说明前面出现过同形状雪花,输出Twin snowflakes found.并立即结束(剩余输入不必再读); - 否则把
key放进seen,继续下一片; - 循环正常结束都没命中,说明所有等价类互不相同,输出
No two snowflakes are alike.。
下表演示样例在 seen 上的推进过程。表格的行按输入顺序排列,"桶内已有"表示该最小表示之前是否被别的雪花占据:
| 第 i 片雪花 | 最小表示 | 桶内已有 | 结论 |
|---|---|---|---|
1:1 2 3 4 5 6 |
1 2 3 4 5 6 |
无 | 放入桶 |
2:4 3 2 1 6 5 |
1 2 3 4 5 6 |
第 1 片 | Twin snowflakes found. |
第一片进入桶后,第二片算出同一个最小表示,查表立即命中——整个过程只做了两次
实现细节
- 用
tuple而不是 list 作为集合元素:元组可哈希,可以直接放进set。 - 切片拼接
f[k:] + f[:k]恰好就是;翻转用 f[::-1]。 - 12 个候选里允许出现重复元组(
0 0 0 0 0 0、1 2 1 2 1 2这类自带对称性的雪花,12 个候选里只有 1 个、2 个不同写法),min对重复不敏感,不必先去重——去重反而更慢。 - 读入用
islice(data, 6)按 token 个数取 6 个整数,不依赖换行位置,输入被压成一行也能正确工作。
代码
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-01 11:28
# update_at: 2026-10-01 11:36
import sys
from itertools import islice
TWIN = "Twin snowflakes found."
ALIKE = "No two snowflakes are alike."
def shape_key(flake: tuple[int, ...]) -> tuple[int, ...]:
"""雪花的最小表示:顺时针 6 个旋转 + 逆时针 6 个旋转里最小的那个六元组。
形状相同的两片雪花,12 个旋转组成的集合完全相同,取最小值后必然相等;
反过来最小值相等就说明两个集合有公共元素,即形状相同。所以最小表示是
“形状”的精确指纹,不含哈希碰撞。元组比较由 CPython 在 C 层完成,
比在 Python 里手写数值哈希还快。
"""
reverse = flake[::-1] # 逆时针方向读到的序列
return min(
[flake[k:] + flake[:k] for k in range(6)] # 顺时针 6 个旋转
+ [reverse[k:] + reverse[:k] for k in range(6)] # 逆时针 6 个旋转
)
def solve() -> None:
data = map(int, sys.stdin.buffer.read().split())
n = next(data)
seen: set[tuple[int, ...]] = set()
for _ in range(n):
flake = tuple(islice(data, 6)) # 每行 6 个角,正好构成一片雪花
key = shape_key(flake)
if key in seen:
print(TWIN)
return
seen.add(key)
print(ALIKE)
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-06 15:26
* update_at: 2026-10-06 15:26
*/
// 雪花判重:把每片雪花 12 种写法(顺时针 6 个起点 + 逆时针 6 个起点)
// 规范化成字典序最小的六元组作为形状指纹,指纹相同当且仅当两片雪花同形状。
// 用哈希集合扫描判重,O(n)。
#include <cstdio>
#include <set>
typedef long long ll;
const int MAXN = 100005;
int n;
ll a[MAXN][6]; // a[i][j] 表示第 i 片雪花第 j 个角的长度(下标 0..5)
// 形状指纹:六元组最小表示,自定义比较直接放进 set
struct SnowKey {
ll v[6];
};
bool operator<(const SnowKey &x, const SnowKey &y) {
for (int i = 0; i < 6; ++i) {
if (x.v[i] != y.v[i]) return x.v[i] < y.v[i];
}
return false;
}
std::set<SnowKey> seen; // 已经出现过的形状指纹集合
// 计算第 idx 片雪花的最小表示,枚举 12 种写法取字典序最小
SnowKey make_key(int idx) {
SnowKey best;
bool first = true;
// dir = 0 顺时针,dir = 1 逆时针;k 表示从第 k 个角开始读
for (int dir = 0; dir < 2; ++dir) {
for (int k = 0; k < 6; ++k) {
SnowKey cur;
for (int t = 0; t < 6; ++t) {
int j;
if (dir == 0)
j = (k + t) % 6; // 顺时针:从 k 往后读
else
j = (k - t + 6) % 6; // 逆时针:从 k 往前读
cur.v[t] = a[idx][j];
}
if (first || cur < best) { // 取 12 种写法里字典序最小的
best = cur;
first = false;
}
}
}
return best;
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
for (int j = 0; j < 6; ++j) scanf("%lld", &a[i][j]);
SnowKey key = make_key(i);
// 指纹已经出现过,说明前面存在同形状的雪花
if (seen.count(key)) {
printf("Twin snowflakes found.\n");
return 0;
}
seen.insert(key);
}
printf("No two snowflakes are alike.\n");
return 0;
}复杂度
时间复杂度
空间复杂度 seen 最多存
实测(20 组真实数据全部通过):
对比朴素做法:
总结
- 题目的核心是把"两两比较形状"转化为"给每片雪花算一个形状编号,再判重"。
- 形状编号取 12 种写法(顺时针 6 个 + 逆时针 6 个)中的字典序最小者,由性质 1、2 可知它是等价类的精确代表元,既不会漏判也不会误判。
- 翻转必须算进去,且逆时针方向要取满 6 个起点;漏掉任何一个都会在"只有翻转才相同"的数据上给出错误答案。
- 时间
、空间 , 时 Python 也能轻松通过。
图示解析
这张图串起本题从建模到得到答案的主线:
输入 n 片雪花
`- 朴素做法的瓶颈: 两两比较 => O(n^2),n=1e5 时约 3.6e11 次操作
`- 关键观察: 形状是雪花自己的属性,不是两片之间的关系
|- 每片雪花展开 12 种写法 (顺时针 6 + 逆时针 6)
| `- 取字典序最小 => 最小表示 key = 形状的唯一指纹
`- 用哈希集合扫描判重
|- key 已出现 => Twin snowflakes found. (立即输出)
`- 扫描结束 => No two snowflakes are alike.先看瓶颈节点:它说明了为什么必须放弃配对。再看中间分支:最小表示把 if key in seen: print(TWIN); return 与循环结束后的 print(ALIKE)。