算法竞赛(xcpc)中的hash技巧
所谓哈希(hash),即把一些需要比较的信息映射为一个具体的值,做到 O ( 1 ) O(1) O(1)对比,这个信息可能是一个字符串、集合、树等等
常见的库函数hash表
在标准库实现里,每个元素的散列值是将值对一个质数取模得到的,更具体地说,是 这个列表 中的质数(g++ 6 及以前版本的编译器,因此可以通过向容器中插入这些模数的倍数来达到制造大量哈希冲突的目的。
std::unordered_map<Key, Value>
- 复杂度平均O(1),最坏能由于碰撞被卡到O(n)
- 适用于:频率计数,map映射,状态记录
- 注意其默认不支持pair<int, int>作为键。
缺点
- 默认哈希函数容易被卡
- 容易被卡到O(n),即被卡常
unordered_map<int, int> cnt;
cnt[x]++; // 对 x 计数
自定义哈希函数
为了避免被卡常,我们可以自定义哈希函数,使用自定义哈希函数可以有效避免构造hack产生的大量哈希冲突。
要想使用自定义哈希函数,需要定义一个结构体,并在结构体中重载 ()
运算符,像这样:
struct my_hash {
static uint64_t splitmix64(uint64_t x) {
x += 0x9e3779b97f4a7c15;
x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9;
x = (x ^ (x >> 27)) * 0x94d049bb133111eb;
return x ^ (x >> 31);
}
size_t operator()(uint64_t x) const {
static const uint64_t FIXED_RANDOM =
chrono::steady_clock::now().time_since_epoch().count();
return splitmix64(x + FIXED_RANDOM);
}
// 针对 std::pair<int, int> 作为主键类型的哈希函数
size_t operator()(pair<uint64_t, uint64_t> x) const {
static const uint64_t FIXED_RANDOM =
chrono::steady_clock::now().time_since_epoch().count();
return splitmix64(x.first + FIXED_RANDOM) ^
(splitmix64(x.second + FIXED_RANDOM) >> 1);
}
};
// unordered_map<int, int, my_hash> my_map;
//unordered_map<pair<int, int>, int, my_hash> my_pair_map;
std::map
- 基于红黑树实现,稳定O(logn)
- 排序容器
- 比unordered_map慢,但不会被卡
std::unordered_set
- 存储不重复的集合元素,支持快速判重
- insert、count、erase均摊O(1)
__gnu_pbds::gp_hash_table<Key, Value, Hash>
- 来自GNU PBDS,是C++ STL的增强版
- 用来替代unordered_map,抗卡常能力很强
- 自带高质量哈希函数
- 但在主流竞赛的支持性未知
#include <ext/pb_ds/assoc_container.hpp>
__gnu_pbds::gp_hash_table<u64, int> mp;
使用gp_hash_table替代unordered_map快了600ms
字符串hash(rolling hash)
这里参考了cup-pyy:一种科学的字符串哈希方法。
字符串hash本质上是把两个子串看成两个B进制下的数,然后比较两个数是否相等,但是因为数字本身会非常大,我们很难直接比较,所以我们把数字对一个大数取模后再进行比较.
常见的错误hash
- 1e9级别的单模hash:不特意去卡都很容易出锅,根据生日攻击理论,比较次数达到 O ( s q r t ( 1 0 9 ) ) O(sqrt(10^9)) O(sqrt(109))就有50%的概率crash。
- 非质数模数:比如最常见的自然溢出,即对 2 64 2^{64} 264取模,这种会被Thue-Morse序列卡掉
- 固定模数的hash:在其他比赛和线下赛基本没问题,但是在一些具有Hack机制的比赛(Codeforces),这可以通过一些数学方法计算出冲突情况的解来卡掉hash。
- 效率问题:在一些非期望解的情况下,可能出现单模wa,多模tle的情况,这是因为取模操作的常数巨大。
为了避免被卡的问题,我们选取mod= 2 61 − 1 2^{61}-1 261−1,随机底数的hash。我们有一些方法优化在这个模数下的加法和乘法。
using u64 = unsigned long long;
constexpr int N = 2e5 + 10;
constexpr u64 mod = (1ull << 61) - 1;
u64 power[N];
mt19937_64 rng(chrono::steady_clock::now().time_since_epoch().count());
uniform_int_distribution<u64> dist(mod / 2, mod - 1);
const u64 base = dist(rng);
u64 add(u64 a, u64 b) {
a += b;
if(a >= mod) a -= mod;
return a;
}
u64 mul(u64 a, u64 b) {
__uint128_t c = __uint128_t(a) * b;
return add(c >> 61, c & mod);
}
//合并两个字符串hash
u64 merge(u64 h1, u64 h2, int len2) {
return add(mul(h1, power[len2]), h2);
}
//初始化
void init() {
power[0] = 1;
for(int i = 1; i < N; ++i) {
power[i] = mul(power[i - 1], base);
}
}
vector<u64> build(const string &s) {
int sz = s.size();;
vector<u64> hashed(sz + 1);
for(int i = 0; i < sz; ++i) {
hashed[i + 1] = add(mul(hashed[i], base), s[i]);
}
return hashed;
}
template<typename T>
vector<u64> build(const vector<T> &s) {
int sz = s.size();
vector<u64> hashed(sz + 1);
for(int i = 0; i < sz; ++i) {
hashed[i + 1] = add(mul(hashed[i], base), s[i]);
}
return hashed;
}
// 查询 l, r的子串hash
u64 query(const vector<u64> &s, int l, int r) {
return add(s[r], mod - mul(s[l - 1], power[r - l + 1]));
}
// 查询两个字符串的最长lcp
int lcp(const vector<u64> &a, int l1, int r1, const vector<u64> &b, int l2, int r2) {
int len = min(r1 - l1 + 1, r2 - l2 + 1);
int l = 0, r = len;
int res = 0;
while(l <= r) {
int mid = (l + r) / 2;
if(query(a, l1, l1 + mid - 1) == query(b, l2, l2 + mid - 1)) {
res = mid;
l = mid + 1;
} else r = mid - 1;
}
return res;
}
组合哈希
线性合并哈希(有序)
适用于:数组、字符串、tuple、pair等
使用类似于字符串哈希的方法。
异或组合哈希 (无序)
适用于无序集合、多重集合。
由于异或的特殊性(两个相同数异或为0),异或哈希具有灵活的应用,常见的可以判断在一个集合中一个数的出现次数是奇数还是偶数。
如2025牛客多校Equal、2025杭电多校性质不同的数字。
pair/tuple哈希
对于pair和tuple我们使用自定义unorder_map的hash函数来进行hash。
struct my_hash {
static uint64_t splitmix64(uint64_t x) {
x += 0x9e3779b97f4a7c15;
x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9;
x = (x ^ (x >> 27)) * 0x94d049bb133111eb;
return x ^ (x >> 31);
}
size_t operator()(uint64_t x) const {
static const uint64_t FIXED_RANDOM =
chrono::steady_clock::now().time_since_epoch().count();
return splitmix64(x + FIXED_RANDOM);
}
// 针对 std::pair<int, int> 作为主键类型的哈希函数
size_t operator()(pair<uint64_t, uint64_t> x) const {
static const uint64_t FIXED_RANDOM =
chrono::steady_clock::now().time_since_epoch().count();
return splitmix64(x.first + FIXED_RANDOM) ^
(splitmix64(x.second + FIXED_RANDOM) >> 1);
}
};