Appearance
哈希本质均为“单映射”,就是通过某些手段,将原数据变换成新数据。
若哈希在“单映射”的同时,越满足“双映射”,则哈希越“强”。
哈希表:
哈希不存在“冲突”的问题,从存储上,以下两种方法都解决了不同元素映射至同一个位置的问题。
但是在确定唯一位置的目的上,花费了额外的时间,导致在刻意构造下,效率和线性表一致。
在非特意构造下,可认为插入、删除、查询均为
开放寻址法:
此方法用于基本存储数据的基本数据结构还是线性表。
取一个模数
拉链法:
此方法用于基本存储数据的基本数据结构是链表和线性表(或者其它任意数据结构)。
取一个模数
事实上,开放寻址法和拉链法可通过修改哈希函数,优化效率。
开放寻址法还可以修改步长。拉链法也可修改位置上的数据结构。
基于开放寻址法的哈希表实现:
cpp
mt19937_64 rnd(chrono::steady_clock::now().time_since_epoch().count());
struct Hash_Map {
bool vis[N];
int a[N], b[N];
stack<int> q;
Hash_Map() { memset(a, -1, sizeof a); }
void clear() {
while (q.size()) {
auto now = q.top();
q.pop();
vis[now] = 0;
a[now] = -1;
}
}
const u64 s1 = rnd(), s2 = rnd(), s3 = rnd();
u64 f(u64 x) {
x += s1;
x = (x ^ (x >> 33)) * s2;
x = (x ^ (x >> 30)) * s3;
return x;
}
bool find(u64 x) {
int now = f(x) % N;
while (vis[now] && a[now] != x) {
now = (now + 1) % N;
}
return vis[now] && a[now] == x;
}
int get(u64 x) {
int now = f(x) % N;
while (vis[now] && a[now] != x) {
now = (now + 1) % N;
}
if (!vis[now])
q.push(now);
vis[now] = 1, a[now] = x;
return now;
}
int &operator[](const u64 x) { return b[get(x)]; }
};多项式模数哈希:
优点:
- 可以将字符串映射到
中,方便存储。 - 模运算具有优秀的性质,可以支持很多其它操作(比如修改字符串中的某一个字符,只需要加减取模即可)。
缺点:
- 映射范围较小容易产生哈希冲突。
自然溢出:
使用 unsigned long long 进行运算,不使用取模,任凭数据在底层自动“溢出”
优点:
- 运算速度快,
缺点:
- 本质是对
取模,模数过于特殊,十分容易卡。
哈希冲突空间
考虑:
冲突的情况即为同余方程解的数目。
若
那么为了使冲突空间尽可能大,就要使
多项式哈希冲突概率:
设模数为
假设字符串随机分布,即哈希值随机分布(每次从
根据“生日悖论”,在随机
若产生冲突,则字符串哈希用于“判断两个字符串是否相同”的目的即失效。
所以在随机情况下,
但是上述结论建立在随机的情况下,在实际场景中,往往可以通过对特定模数的特定构造,达到
双模数多项式哈希:
即使用两个模数,分别进行多项式模数哈希,只有在两者结果均相同时,认为字符串相同。
同样使用“生日攻击”的情况下,随机
同样地,双模数可以推广到
集合哈希:
判断两个序列升序后是否相同。
哈希做法:
哈希的“单映射”就像是假言推理中的必要条件。如果能做到“双映射”那就是充要条件。
所以,构造哈希函数,也可以视作找必要条件的过程。
例如此处,
但是也会出现
因此,需要通过构造更多的必要条件,来尽可能地实现充要条件。
必要条件越多,哈希冲突的概率就越小。
此处,可以增加
同样的,还可以增加四次方和、五次方和、六次方和等等。
时间复杂度取决于具体问题限制要求,如果静态不带修,
随机化
如果只维护平方和,会出现形如
此时,再相等的概率就较小了。
实际上就是对每个元素再做一次哈希。
多项式哈希做法:
update on 2025.4.20
2024 郑州 G 补充了使用多项式哈希维护集合哈希的更加稳定做法。
其实也是 naive 的。
本质和序列哈希相同,将元素视作序列下标,维护
sum hash
上文的做法就是一种 Sum Hash。
Sum Hash 用于判断集合是否相同。
对于集合
其中,
还有一种更普遍的是:
update on 2025.4.20:
2024 郑州 G 题引申了
但是如果不带修的话,可以为序列每一个初始值随机映射一个权值,再进行
2024 郑州 G 题需要维护区间加,这就使得序列两个数的差值不能发生改变,在此条件下,hack 仍能构造。
xor hash
xor 运算的一个性质:
所以使用 xor 可以很好地维护数字出现次数的奇偶性。
若某一个集合内所有数字出现次数均为偶数次,那么
那么反过来,
同样地,仍然可以借助随机化的策略,将集合中每个数映射到一个随机值上,再使用异或和为
xor hash 应用:
排列,是一个特殊的序列,其中每个数在
并且同一个长度的所有排列的异或和相同。即:排列的异或和只和排列的长度有关。所以
判断一个序列是不是一个排列,就可以通过序列的异或和和对应长度排列的异或和比较得到。
不过同样的,异或和会出现相同的不充分情况:
xor hash 再扩展:
能用 xor hash 出现次数为偶数的情况,是基于
同样地,只需要构造一种运算,使得
异或是二进制的不进位加法,所以只要把二元运算定义成
同样将每个数随机映射到一个数上,以避免冲突。
