7 条题解
-
0
#include <bits/stdc++.h> unordered_map<string,int> mp; // 存储一片雪花的六个角的长度(下标1~6) string a[7]; int n; // 向哈希表中添加一种雪花的字符串表示 // x1: 起始角编号(1~6) // x2: 步长,1表示顺时针,-1表示逆时针 void add(int x1, int x2) { string s=""; int i=x1; do { s+=a[i]; // 拼接当前角的长度 i=(i+x2+5)%6+1; // 计算下一个角的编号,循环取模 }while(i!=x1); // 回到起始角时结束 if(mp[s]) // 如果该字符串已经出现过 { cout<<"Twin snowflakes found."; // 找到相同的雪花 exit(0); // 直接退出程序 }else{ mp[s]++;//没有出现过,加入哈希表,key对应的value+1 } } int main() { ios::sync_with_stdio(0); // 加速输入输出 cin.tie(0); cout.tie(0); cin>>n; // 读入雪花数量 for (int i=1;i<=n;i++) { for(int j=1;j<=6;j++) { cin>>a[j]; // 读入一片雪花的六个角 } for(int j=1;j<=6;j++) { //相当于每个雪花维护了两种字符串,一种顺时针,一种逆时针 //每种字符串有6个(对应6个旋转角度),加入哈希表 add(j,-1); // 以第j个角为起点,逆时针生成字符串 add(j,1); // 以第j个角为起点,顺时针生成字符串 } } cout<<"No two snowflakes are alike."; // 没有找到相同的雪花 } -
0
之前的样例代码会报错,命名冲突
非常抱歉!这是我的疏忽。在较新的 C++ 标准库中(如 GCC 14 对应的 C++14/17 环境),
<type_traits>头文件会被<iostream>间接包含,其中定义了模板类std::is_same。当你使用了using namespace std;后,自定义的函数名is_same会与标准库产生命名冲突。在 NOI 竞赛中,这种“命名空间污染”是导致编译失败的常见原因。
以下是修正后的标准题解代码。我已将函数名改为
check_same,并增加了::全局作用域限定符以确保万无一失。一、 修正后的标准代码 (C++14)
#include <iostream> #include <vector> #include <cstdio> using namespace std; // 哈希表长度选择一个较大的质数 const int MOD = 999983; struct Snowflake { int len[6]; }; // 使用拉链法处理哈希冲突 vector<Snowflake> hash_table[MOD]; /** * 核心判定函数:比较两个雪花是否同构 * 易错点:必须同时考虑顺时针和逆时针的 6 种起点,共 12 种情况 */ bool check_same(const Snowflake& s1, const Snowflake& s2) { for (int i = 0; i < 6; ++i) { // 1. 顺时针方向比对 (Clockwise) bool match_cw = true; for (int j = 0; j < 6; ++j) { if (s1.len[j] != s2.len[(i + j) % 6]) { match_cw = false; break; } } if (match_cw) return true; // 2. 逆时针方向比对 (Counter-clockwise / Reflection) bool match_ccw = true; for (int j = 0; j < 6; ++j) { // (i - j + 6) % 6 处理负数索引,实现逆时针循环 if (s1.len[j] != s2.len[(i - j + 6) % 6]) { match_ccw = false; break; } } if (match_ccw) return true; } return false; } /** * 设计具有旋转不变性的哈希函数 */ int get_hash(const Snowflake& s) { long long sum = 0; long long xor_val = 0; for (int i = 0; i < 6; ++i) { sum += s.len[i]; xor_val ^= s.len[i]; } // 组合特征值并对质数取模 return (int)((sum + xor_val) % MOD); } int main() { int n; // 使用 fast I/O 或 scanf 应对 10^5 数据量 if (scanf("%d", &n) != 1) return 0; bool found = false; for (int i = 0; i < n; ++i) { Snowflake current; for (int j = 0; j < 6; ++j) { scanf("%d", ¤t.len[j]); } if (found) continue; // 已找到答案则跳过后续计算,但仍需读完数据 int h = get_hash(current); // 在对应的哈希桶中遍历比对 // 使用 :: 显式调用全局作用域下的 check_same,彻底规避命名冲突 for (const auto& existing : hash_table[h]) { if (::check_same(current, existing)) { found = true; break; } } // 若当前桶内无匹配,则存入 if (!found) { hash_table[h].push_back(current); } } if (found) { printf("Twin snowflakes found.\n"); } else { printf("No two snowflakes are alike.\n"); } return 0; }
二、 竞赛避坑指南:如何预防“命名冲突”?
在 NOI 考场上,如果遇到
ambiguous(歧义)错误,请记住以下三招:- 全局作用域符
::: 在调用自己的函数时,前面加上::(例如::check_same)。这告诉编译器:“请从全局命名空间找这个函数,不要去std命名空间里瞎猜。” - 避开敏感词汇:
以下词汇在
using namespace std;之后极易发生冲突,尽量避开或加后缀:is_same,count,rank,next,prevdistance,search,find,matchy1,y0,j1,j0(这些在<math.h>中是贝塞尔函数)
- 不使用全局
using: 虽然在比赛中为了快习惯写using namespace std;,但更好的习惯是在函数内部使用,或者只对特定的类使用,如using std::vector;。
三、 性能与空间总结
- 时间复杂度:平均 ,其中 是比对常数。由于 ,哈希冲突极少,处理 数据约耗时 。
- 空间复杂度:,每个雪花占 字节,加上
vector开销,总内存约 以内,远低于 限制。 - 关键点:一定要处理**逆时针(翻转)**的情况。题目样例中
1 2 3 4 5 6和4 3 2 1 6 5就是典型的翻转同构。
- 全局作用域符
-
0
作为 NOI 教练,制作测试数据(Data Generator)是必备技能。为了确保你创建的 OJ 题目具有高区分度,我们需要针对哈希碰撞、旋转/翻转判定、边界值、最大规模这几个维度进行全方位覆盖。
以下是为您准备的 C++14 标准数据生成器脚本。它会自动生成
1.in ~ 10.in及其对应的.out文件。一、 数据生成策略说明
- Test 1-2 (入门级): 较小,手工构造简单的相同或不同序列。
- Test 3-4 (变换验证):重点测试旋转(Rotation)和翻转(Reflection)的识别。
- Test 5 (极端规模-无重复):,全随机数据,极低概率重复。测试哈希表在大规模下的查询性能。
- Test 6 (极端规模-全重复):,所有雪花完全一致。测试代码在大量重复时的响应速度。
- Test 7 (哈希杀手):生成大量数字之和(Sum)相同但结构不同的序列,如
[1,5,1,1,1,1]和[2,4,1,1,1,1]。测试哈希函数的鲁棒性。 - Test 8-9 (大数边界):元素取值接近上限 ,测试整数溢出风险。
- Test 10 (综合强度):随机分布,包含多对双胞胎,测试算法的泛化能力。
二、 数据生成器源码 (C++14)
#include <iostream> #include <fstream> #include <vector> #include <string> #include <algorithm> #include <random> #include <chrono> using namespace std; // --- 逻辑部分:用于生成对应的 .out 文件 --- const int MOD = 999983; struct Snowflake { int len[6]; }; vector<Snowflake> head[MOD]; // 重命名为 check_same 避免与 std::is_same 冲突 bool check_same(const Snowflake& s1, const Snowflake& s2) { for (int i = 0; i < 6; ++i) { bool m1 = true, m2 = true; for (int j = 0; j < 6; ++j) { if (s1.len[j] != s2.len[(i + j) % 6]) m1 = false; if (s1.len[j] != s2.len[(i - j + 6) % 6]) m2 = false; } if (m1 || m2) return true; } return false; } int get_hash(const Snowflake& s) { long long sum = 0, xor_v = 0, sq_v = 0; for (int i = 0; i < 6; ++i) { long long val = s.len[i]; sum += val; xor_v ^= val; sq_v += val * val; } return (int)((sum + xor_v + sq_v) % MOD); } string solve_problem(int n, const vector<vector<int>>& data) { for (int i = 0; i < MOD; ++i) head[i].clear(); for (int i = 0; i < n; ++i) { Snowflake s; for (int j = 0; j < 6; ++j) s.len[j] = data[i][j]; int h = get_hash(s); for (auto& old : head[h]) if (check_same(s, old)) return "Twin snowflakes found."; head[h].push_back(s); } return "No two snowflakes are alike."; } // --- 生成器部分 --- mt19937 rng(chrono::steady_clock::now().time_since_epoch().count()); vector<int> rotate_or_flip(vector<int> s) { int start = rng() % 6; bool flip = rng() % 2; vector<int> res(6); for (int i = 0; i < 6; ++i) { if (!flip) res[i] = s[(start + i) % 6]; else res[i] = s[(start - i + 6) % 6]; } return res; } void generate_test(int test_id) { string in_name = to_string(test_id) + ".in"; string out_name = to_string(test_id) + ".out"; ofstream fout(in_name); int n; vector<vector<int>> data; const int MAX_VAL = 10000000; // --- 调小后的规模策略 --- if (test_id <= 2) n = 10; // 非常小 else if (test_id <= 4) n = 1000; // 中规模 else n = 20000; // 原本 100,000,现在调小为 20,000 if (test_id == 5) { // 随机数据 for (int i = 0; i < n; ++i) { vector<int> s(6); for (int j = 0; j < 6; ++j) s[j] = rng() % MAX_VAL; data.push_back(s); } } else if (test_id == 6) { // 全一致数据 vector<int> base = {100, 200, 300, 400, 500, 600}; for (int i = 0; i < n; ++i) data.push_back(base); } else if (test_id == 7) { // 哈希杀手:和相同但序列不同 for (int i = 0; i < n; ++i) { if (i % 2 == 0) data.push_back({10, 20, 30, 40, 50, 60}); else data.push_back({60, 50, 40, 30, 20, 10}); } } else if (test_id <= 9) { // 边界大数 for (int i = 0; i < n; ++i) { vector<int> s(6); for (int j = 0; j < 6; ++j) s[j] = MAX_VAL - (rng() % 100); data.push_back(s); } } else { // 综合随机(混合双胞胎) for (int i = 0; i < n; ++i) { if (i > 0 && (rng() % 5 == 0)) { // 20% 概率生成双胞胎 data.push_back(rotate_or_flip(data[rng() % i])); } else { vector<int> s(6); for (int j = 0; j < 6; ++j) s[j] = rng() % MAX_VAL; data.push_back(s); } } } fout << n << "\n"; for (auto& row : data) { for (int i = 0; i < 6; ++i) fout << row[i] << (i == 5 ? "" : " "); fout << "\n"; } fout.close(); ofstream fans(out_name); fans << solve_problem(n, data) << endl; fans.close(); cout << "Generated Case " << test_id << " (n=" << n << ")" << endl; } int main() { for (int i = 1; i <= 10; ++i) { generate_test(i); } return 0; }
三、 优化与注意事项(NOI 竞赛级标准)
-
时间复杂度管理:
- 生成器内部自带了一个
solve函数用于生成答案。由于采用了999983规模的哈希桶,处理 的数据大约仅需 0.1s - 0.3s,完全不会出现生成超时的情况。 - 避免 :在生成“双胞胎”数据时(Test 10),我们是随机选取之前已经生成的某片雪花并进行变换,而不是两两比对,确保了 生成。
- 生成器内部自带了一个
-
空间与栈溢出防御:
- 非递归实现:
is_same和get_hash全部采用循环编写,杜绝了递归带来的栈溢出风险。 - 内存释放:每个测试点结束后通过
head[i].clear()清空哈希桶,防止内存叠加导致程序崩溃。
- 非递归实现:
-
异常防御 (Exception):
- 除零保护:题目中 的范围是 ,代码中不涉及除法运算(除取模
MOD外),而MOD为常数质数,不存在除零风险。 - 溢出保护:哈希计算中使用
long long累加sum,防止 6 个 相加超出int范围。
- 除零保护:题目中 的范围是 ,代码中不涉及除法运算(除取模
-
随机性质量:
- 使用了
std::mt19937(梅森旋转算法)。相比rand(),它的周期更长、分布更均匀,在生成高强度测试数据时是首选。
- 使用了
四、 如何使用
- 将上述代码保存为
gen.cpp。 - 使用命令编译:
g++ gen.cpp -o gen -O2 -std=c++14。 - 运行
./gen。 - 当前目录下会立即出现
1.in, 1.out ... 10.in, 10.out。你可以直接将这些文件打包上传到青岛、洛谷或自己搭建的本地 OJ 系统。
-
0
STL不提供标准的开箱即用的哈希函数吗?
这是一个非常敏锐的问题!作为一个竞赛教练,我必须告诉你:STL 确实提供了哈希函数,但在 NOI 竞赛(尤其是处理“同构判定”类题目)中,它往往不是“开箱即用”的,甚至有时是一把“双刃剑”。
让我们在草稿纸上把 STL 的哈希机制拆解开来看。
一、 STL 里的哈希工具箱:
std::hash在 C++11 及以后的标准(包括 C++14)中,STL 提供了一个模板类
std::hash<T>。1. 它支持什么?
它只为基本类型提供了特化版本:
- 所有的基本数值类型(
int,long long,double,char等)。 - 指针类型。
std::string和std::bitset。
2. 它不支持什么?(竞赛选手的痛点)
- 不直接支持容器:你不能直接对
std::vector<int>、std::pair<int, int>或普通的数组int a[6]使用std::hash。 - 不支持自定义结构体:对于我们定义的
struct Snowflake,STL 根本不知道该怎么哈希它。
二、 启发式思考:为什么本题不能直接用 STL 的哈希?
请你在草稿纸上画出下面这两个雪花:
- 雪花 A:
[1, 2, 3, 4, 5, 6] - 雪花 B:
[2, 3, 4, 5, 6, 1](由 A 旋转得到)
1. 语义鸿沟 (Semantic Gap)
即便你通过某种黑科技(比如使用
boost库或手动特化模板)让std::hash能够处理数组,std::hash的逻辑也是基于序列的每一个元素。std::hash(雪花A)计算的是:第一位是 1,第二位是 2...std::hash(雪花B)计算的是:第一位是 2,第二位是 3...- 结果:这两个哈希值会完全不同!
2. 我们的需求
我们需要的是 “旋转/镜像无关哈希”。STL 的
std::hash只是一个通用的、针对固定序列的数学映射,它无法理解业务上的“同构”逻辑。
三、 深度解析:STL 哈希在竞赛中的“陷阱”
如果你决定强行使用
std::unordered_map并配合std::hash<int>,你可能会遇到以下两个问题:1. 效率问题(常数巨大)
std::unordered_map的常数通常比手写哈希表大 3-5 倍。在 NOI 这种对 1.0s 时限卡得很死的比赛中,这几倍的差距往往就是 AC 与 TLE 的区别。2. 被 Hack 的风险(Anti-Hash Test Cases)
这是竞赛选手的必修课:
- 在许多 GCC 编译器实现中,
std::hash<int>的实现竟然是return x;(直接返回原值)。 - 后果:如果出题人构造一组数据,所有数字都是模 的同余数,你的
unordered_map会产生大量的哈希冲突,复杂度瞬间从 退化到 。
四、 如何在竞赛中“科学”地使用 STL 哈希?
如果你非常想用 STL,也不是不行。在 C++14 下,你可以采用 “Hash Combine” (哈希组合) 的策略。
草稿纸上的伪代码实现:
// 这种写法模仿了 boost 库的 hash_combine,常用于组合多个值的哈希 template <class T> inline void hash_combine(size_t &seed, const T &v) { // 这是一个经典的魔数,用来尽量打散二进制位 seed ^= std::hash<T>{}(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2); } // 针对雪花的同构哈希:先排序(由于只有6个数,排序极快) size_t get_snowflake_hash(const Snowflake& s) { auto sorted_len = s.len; // 注意:这里只是为了演示,实际上本题用 Sum 和 XOR 更快 std::sort(std::begin(sorted_len), std::end(sorted_len)); size_t seed = 0; for(int x : sorted_len) { hash_combine(seed, x); } return seed; }
五、 教练的总结与关键词
在读题和写代码时,看到“哈希”需求,请立刻在大脑中搜索这几个关键词:
- “等价性定义”:哈希值是否需要满足旋转、翻转、大小写无关?如果需要,STL 默认的
std::hash绝不可直接用。 - “哈希冲突概率”:
MOD是否足够大?是否需要双哈希? - “防御性”:如果用
unordered_map,是否需要加入随机种子(chrono::steady_clock::now().time_since_epoch().count())来防止出题人针对?
图形化思考建议: 把
std::hash想象成一个复印机,你给它什么,它复印什么;而这道题需要的哈希是一个特征提取器,无论你把雪花怎么摆放,它都能提取出那个不变的核心特征。这就是为什么在 NOI 中,我们更倾向于手写那个简单的
(Sum + XOR) % Prime。它虽然看起来“简陋”,但它精准地抓住了题目的对称性特征,且速度极快。 - 所有的基本数值类型(
-
0
示范代码中的哈希算法和大质数是怎么选取的?
这是一个非常深入且具有实战意义的问题。在 NOI 竞赛中,哈希函数的设计(Hash Function)和模数(Modulo/Prime)的选择直接关系到你的程序是会 “AC” 还是被构造数据 “Hack” 到掉分。
作为教练,我带你从数学本质和竞赛经验两个维度来拆解这个选择逻辑。
一、 哈希算法的设计:寻找“不变量”
雪花问题的特殊性在于:同一片雪花有 12 种表示方式(6种旋转 2种翻转)。 设计的核心原则: 无论雪花如何旋转或翻转,计算出的哈希值 必须完全一致。
1. 为什么选择
Sum(和) 与XOR(异或)?在示范代码中,我使用了
(sum + xor_val) % MOD。- 加法交换律:。无论从哪个角开始读,6 个数字的和是不变的。
- 异或交换律:。同理,异或值也不变。
- 为什么要叠加?
- 只用
Sum:序列[1, 5, 1, 1, 1, 1]和[2, 4, 1, 1, 1, 1]的和都是 10,会碰撞。 - 只用
XOR:序列[1, 2, 3, 1, 2, 3]的异或值是 0,很多对称序列异或值都是 0,碰撞极高。 - 组合拳:
Sum + XOR大大降低了不同组合产生相同哈希值的概率。
- 只用
2. 更高级的思路(进阶提示)
如果你追求更低的碰撞率,可以考虑 多重对称指纹:
- (利用平方和进一步区分数字组合)
- 或者直接将 6 个数排序后做一个标准的字符串哈希(这样就彻底无视了旋转和翻转的顺序)。
二、 大质数的选择:为什么是 999,983?
在草稿纸上,我们要根据数据规模 来推算模数。
1. 为什么要用“质数” (Prime)?
哈希表本质是取模运算:。
- 如果 是合数(比如 ),那么如果你的 恰好是 因数的倍数(比如全是偶数),它们会大量挤在某些特定的位置上,导致“聚集”现象。
- 质数可以最大程度地让哈希值在桶中分布均匀,哪怕你的 存在某种等差规律。
2. 为什么选 左右?
- 数据规模 。
- 负载因子 (Load Factor):通常建议哈希表的长度(桶数)是数据量的 5 到 10 倍。
- 如果 太小(如 ),平均每个桶会有 1 个元素,冲突概率变大。
- 如果 选在 左右,每个桶的平均长度只有 。这意味着绝大多数时候,你计算出哈希值后,对应的桶是空的或只有一个元素,查询时间接近 。
3. 竞赛常用“明星质数”表
在草稿纸的角落里,建议你记下这几个在 NOI 赛场上经过验证的、碰撞率低的质数:
- 10^5 级别:
100003,1313131(常用作 Base),999983 - 10^6 级别:
1000003,19260817(非常有名的质数) - 大模数(双哈希用):
10^9+7,10^9+9,998244353
三、 启发式图形化思考:哈希碰撞的代价
请在草稿纸上画出这两个场景的比对:
场景 A:垃圾哈希函数 / 模数选太小
Bucket[1]: [雪花A] -> [雪花B] -> [雪花C] -> [雪花D] ... (链表极长) 查找雪花E时:需要跟 A, B, C, D 逐一进行 12 次比对。 性能:接近 O(N^2),瞬间 TLE。场景 B:优秀的哈希函数 + 大质数模数
Bucket[1]: [雪花A] Bucket[2]: (空) Bucket[3]: [雪花B] Bucket[4]: [雪花C] 查找雪花E时:计算 H(E)=2,发现 Bucket[2] 是空的,秒出结果。 性能:稳定 O(N)。
四、 读题关键词与底层防御
- 关键词:“可能相同”。这就在暗示你:哈希只是第一步筛选,最后必须通过
is_same函数进行严格比对。哈希只是为了把“不可能相同”的绝大部分雪花先过滤掉。 - 防御式编程建议:
在 NOI 赛场上,为了防止题目数据故意针对你的哈希函数(即所谓 Hash Killer),你可以采取:
- 双哈希:计算两个不同的哈希值,只有当两个值都匹配时才进桶比对。
- 随机化:在哈希函数里加入一个随机种子(
rand()),让攻击者无法预知你的碰撞点。
总结: 选择
999983是因为它比 大 10 倍左右,且是一个强质数。选择Sum + XOR是因为它计算极快且具有旋转不变性。在竞赛中,稳健性比极致的数学美感更重要。 -
0
是否可以用string来存储雪花,用STL的map来做哈希?
这是一个非常棒的启发式问题!在 NOI 竞赛中,选择合适的容器(STL)和数据结构往往决定了你的程序是 AC(通过)还是 TLE(超时)或 MLE(内存超限)。
作为教练,我首先给出一个直观的结论:在 NOI 级别竞赛中,不建议使用
string存储雪花,也不建议使用std::map做此类哈希。让我们在草稿纸上拆解一下为什么这样做会带来风险,以及更好的替代方案。
一、 为什么
std::string存储雪花是“危险”的?我们可以从空间效率和准确性两个角度来分析:
-
数据的歧义(准确性问题):
- 假设有两个雪花序列:
[1, 23, 4, 5, 6, 7]和[12, 3, 4, 5, 6, 7]。 - 如果你简单地拼接成字符串,它们都会变成
"1234567"。 - 修正建议:如果你一定要用字符串,必须加分隔符(如
"1-23-4-5-6-7"),但这会进一步增大空间开销。
- 假设有两个雪花序列:
-
效率开销(时间与空间):
- 转换开销:
to_string()或sprintf将整数转为字符串是一个极其耗时的 操作。对于 个数据,每个转 6 次,常数巨大。 - 空间膨胀:雪花的数字最大是 (7位数字)。
int存储:6 * 4字节 = 24字节。string存储:"10000000-"这种长度平均在 40-50 字节,且std::string还有额外的堆内存管理开销(通常每个 string 对象有 24-32 字节的固定开销)。
- 结论:在 NOI 这种对内存限制(通常 128MB/256MB)和时间限制(1s)要求严格的比赛中,
string显得过于笨重。
- 转换开销:
二、 为什么
std::map并不适合做这里的哈希?在 C++ STL 中,
std::map的底层是红黑树(平衡二叉搜索树),而std::unordered_map才是真正的哈希表。-
复杂度差异:
std::map:查找和插入是 。总复杂度 。std::unordered_map:查找和插入平均是 ,最坏 。- 但在竞赛中,由于
map的常数(树的旋转、指针跳转)远大于数组,实际表现往往比 的手写哈希慢得多。
-
关键问题:如何处理“同构”?
- 如果你把雪花直接存进
map<vector<int>, bool>,map只会识别完全一致的vector。 - 思考:雪花的 12 种旋转和翻转怎么处理?
- 如果你把 12 种情况都存进
map,空间会爆炸。 - 如果你只存一种,你必须先求出雪花的**“最小表示法”**。
- 如果你把 12 种情况都存进
- 如果你把雪花直接存进
三、 进阶:启发式优化建议
如果你想用更高级的 STL 来简化代码,我建议你采取以下路径:
方案 A:最小表示法 + 排序/Map(适合练习,竞赛慎用)
- 写一个函数
get_min(Snowflake s),通过循环位移和翻转,找出 12 种序列中字典序最小的一个。 - 将这 个“最小序列”存入一个
vector<vector<int>>。 - 对这个
vector进行一次全排序 ,然后遍历看相邻是否有相同的。
方案 B:NOI 选手的标配(自定义哈希 + 数组链表)
这是我上一条回复中给出的方案,也是金牌选手最常用的方法。
为什么手写哈希优于 STL?
- 内存连续性:数组在内存中是连续的,对 CPU 缓存友好。
- 可控性:你可以自定义哈希函数(比如 Sum 和 XOR 结合),极大减少冲突。
四、 总结:读题时的“避坑”指南
当你看到题目要求判断“某种变换下的等价性”且数据范围是 时:
- 看到“等价” 想到 哈希 或 最小表示法。
- 看到数字很大但个数多 不要用 string,坚持用
int数组。 - 看到时间紧、内存紧 不要用
map,优先考虑unordered_map(注意防 hack)或手写数组哈希。
草稿纸演练提示: 请你在纸上画一下:
- 左边画一个红黑树(
map),想象每次插入都要进行 次节点比较。 - 右边画一个数组桶(
hash_table),想象每次插入通过计算下标直接定位。 你会发现,数组桶在处理这种大数据量时的效率优势是压倒性的。
加油,多尝试不同的实现方式,你会对底层性能有更深的理解!
-
-
0
这是一份针对 NOI 竞赛选手的标准题解。本题的核心考点在于哈希表的应用与环形序列同构的判定。
一、 题解标准代码 (C++14)
此代码采用拉链法哈希,结合严谨的 12 种同构情况校验。
#include <iostream> #include <vector> #include <cstdio> using namespace std; // 哈希表长度选择一个较大的质数,减少碰撞概率 const int MAXN = 100005; const int MOD = 999983; struct Snowflake { int len[6]; }; // 存储所有的雪花,雪花以拉链法挂在 hash_table 下 vector<Snowflake> hash_table[MOD]; // 核心判定函数:比较两个雪花是否可能相同 bool is_same(const Snowflake& s1, const Snowflake& s2) { for (int i = 0; i < 6; ++i) { // 枚举 s2 的起始位置 // 情况 1: 顺时针方向旋转 bool match = true; for (int j = 0; j < 6; ++j) { if (s1.len[j] != s2.len[(i + j) % 6]) { match = false; break; } } if (match) return true; // 情况 2: 逆时针方向旋转(翻转) match = true; for (int j = 0; j < 6; ++j) { // (i - j + 6) % 6 是逆时针索引处理的技巧 if (s1.len[j] != s2.len[(i - j + 6) % 6]) { match = false; break; } } if (match) return true; } return false; } // 设计哈希函数:将6个数映射为一个索引 // 易错点:仅用 Sum 可能会造成大量哈希碰撞,建议结合 Sum 和 XOR 或 Prod int get_hash(const Snowflake& s) { long long sum = 0; long long xor_val = 0; for (int i = 0; i < 6; ++i) { sum += s.len[i]; xor_val ^= s.len[i]; } // 使用 sum 和 xor 组合减少冲突,最后对大质数取模 return (int)((sum + xor_val) % MOD); } int main() { int n; if (scanf("%d", &n) != 1) return 0; bool found = false; for (int i = 0; i < n; ++i) { Snowflake current; for (int j = 0; j < 6; ++j) { scanf("%d", ¤t.len[j]); } if (found) continue; // 如果已经找到了,则只读不处理 int h = get_hash(current); // 在对应的哈希桶中查找 for (const auto& existing : hash_table[h]) { if (is_same(current, existing)) { found = true; break; } } // 若没找到,放入桶中 if (!found) { hash_table[h].push_back(current); } } if (found) { printf("Twin snowflakes found.\n"); } else { printf("No two snowflakes are alike.\n"); } return 0; }
二、 复杂度分析思考过程
1. 时间复杂度
- 读取与哈希计算:。
- 哈希冲突判定:
- 平均情况:假设哈希函数足够随机且均匀,每个桶的平均长度为 。每次插入时进行
is_same比较,该函数常数为 12。总期望复杂度为 。当 时,接近 。 - 最坏情况:所有雪花的特征值全部相同(碰撞到同一个桶),复杂度退化为 。在 NOI 竞赛中,若遇到精心构造的数据,需小心。
- 平均情况:假设哈希函数足够随机且均匀,每个桶的平均长度为 。每次插入时进行
- 结论:在常规数据下,该算法表现为 ,能轻松通过 的数据。
2. 空间复杂度
- 存储开销:
hash_table存储了 个Snowflake结构体,每个结构体 6 个int。 - 分析: 字节 MB。加上
vector的额外指针开销,总空间远小于题目常见的 128MB 或 256MB 限制。
三、 时间复杂度优化建议
- 最小表示法 (Minimal Representation):
对于每个雪花序列,求出其字典序最小的循环同构串。例如
3 4 1 2的最小表示是1 2 3 4。- 将每片雪花的正向和反向序列共 12 种情况中,字典序最小的那一个作为该雪花的“标准态”。
- 优化后:可以直接对这 个“标准态”序列进行排序 () 或直接存入哈希表,省去了
is_same中繁琐的循环比对。
- 更强的哈希函数:
为了防止被 hack(最坏情况),可以使用双哈希(Double Hashing)或者使用
std::array配合更复杂的特征值。 - 手写链表:
在 NOI 这种对时间极其敏感的比赛中,使用
vector可能会有额外的常数开销。可以使用静态数组模拟链表(Head/Next 数组)来进一步压低常数。
四、 读题理解关键词总结
在处理此类“序列判定”或“同构”题目时,务必关注以下词汇:
- "Order around" (环形顺序):暗示这是一个环,首尾相接。
- "Clockwise or counter-clockwise" (顺时针或逆时针):暗示序列可以反转 (Reverse)。
- "Start from any" (任意起始):暗示序列可以循环移位 (Rotation)。
- "Corresponding" (对应相同):意味着只要有一种位置对应的可能性成立,两个个体即为相同(无需所有排列组合)。
- :该数量级意味着你的算法必须优于 。任何包含双重 循环的暴力逻辑都必须通过哈希或排序来剪枝。
- 1
信息
- ID
- 14976
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 5
- 标签
- 递交数
- 4
- 已通过
- 3
- 上传者