算法 {{哈希函数,哈希表,哈希冲突},取模哈希(探测法|拉链法),离散化,整数元组的哈希}
{哈希函数,哈希表,哈希冲突}
定义
一般当我们谈到哈希, 就是指: 将一个任意类型T 映射成一个uint32
, 这称之为哈希函数; (参见qHash
)
维护着这些要哈希的对象T 的集合, 叫做哈希表 (参见QHash
);
具体过程如下:
0: 对于T t; 其哈希值为h
, 则哈希表里 会维护一个{h, t}
的pair; 哈希表里的所有pair, 一定满足: 其所有的t
均不同(因为没有必要记录重复元素 因为哈希表就是维护对象与其哈希值 而一个确定的对象他的哈希值是唯一的), 但所有的h
值 可能会发生重复(即哈希冲突); 比如{1, "a"}, {2, "b"}, {1, "c"}
这是一个合法的哈希表;
1: 当我们要往哈希表里 放一个{h,t}
时, 他会去查询 当前哈希表里 所有pair.first == h
的pair 记作S
, 然后再去比较 是否存在S.second == t
, 如果存在 则操作无影响, 否则 把{h,t}
放入哈希表里; (因此T类型 除了要支持哈希函数外 必须还要支持==
运算符);
2: 此时 既然对象已经哈希成一个uint32
了, 然后你可以再使用拉链法/探测法的取模哈希法 再进行进一步处理;
class ST{
D0 d0;
D1 d1;
//> 这个函数 你可以发生*哈希冲突*, 比如两个不同的对象`!(a==b)` 他俩的哈希值一样`a.GetHash()==b.GetHash()`, 这是没问题的, 只不过会影响效率, 因为哈希冲突是不可避免的;
uint32 GetHash(){ return d0.GetHash() ^ d1.GetHash();}
//> 他非常重要, 你必须保证 你自己是*如何定义 两个ST对象的同一性的*;
bool operator==( ST const& _a){ return (d0==_a.d0)&&(d1==_a.d1);}
};
错误
错误: #哈希函数 必须保证: 两个不同对象的哈希值 是不同的#;
这是错误的, 只能说尽可能不发生, 否则为什么还有哈希冲突这个术语呢? 他就是会发生的;
但 两个相同对象的哈希值 必须相同, 这句话是正确的;
整数元组的哈希
定义
对于(a0,a1,a2)
假设a0=[0,A0) , a1=[0,A1), a2=[0,A2)
, 这个元组 可以哈希成一个[0, A0*A1*A2)
之间的整数 计算公式是a0*A1*A2 + a1*A2 + a2
;
而且他支持逆哈希, 即给定哈希值d
, 那么(a0=(d/A2/A1%A0), a1=(d/A2%A1), a2=(d%A2))
;
例题
比如状压DP, 就会用到这个技巧, 每个列都是[0, Ci)
范围的数, 然后通过哈希 把他们变成一个整数 表示当前整个行(即所有列)的状态;
动态哈希(Map)
性质
理论上 他是 log ( N ) \log(N) log(N), 但是 因为他是动态的过程, 所以 实际效率还不错 虽然会比(取模哈希)慢几个常数;
算法
模板代码
template< class _Type> class ___Hash_Dynamically{
public:
std::map< _Type, int> __Map; // 所有元素的哈希值为`[0, Map.size()-1]`;
void Initialize(){ __Map.clear();}
void Insert( const _Type & _a){ if( __Map.find( _a) == __Map.end()){ __Map[ _a] = __Map.size();}}
int Get_hash( const _Type & _a) const{ if( __Map.find( _a) == __Map.end()){ return -1;} return __Map.at( _a);}
int GetHash_andInsert( const _Type & _a){ if( __Map.find( _a) == __Map.end()){ __Map[ _a] = __Map.size();} return __Map.at( _a);}
}; // class ___Hash_Dynamically
静态哈希(离散化)
代碼
template< class _element> struct ___Hash_Sorting{
std::vector<_element> Map; // `b=Map[a]`表示`b`的哈希值為`a`, 即`a<->b`是一一對應的;
void Initialize( int _maxCount){ // `maxCount: Add函数的最大调用次数`;
Map.reserve( _maxCount); Map.clear();
}
void Add( _element const& _a){ Map.push_back( _a);}
void Work(){ std::sort( Map.begin(), Map.end()); Map.erase( std::unique( Map.begin(), Map.end()), Map.end());}
int Get_lowerBound( const _element & _t) const{ // 滿足`>=t`的*最小元素的下標*(如果不存在返回`-1`);
if( Map.empty()){ return -1;}
int l = 0, r = Map.size()-1;
while( l < r){ int mid = ( l + r) >> 1; if( Map[ mid] >= _t){ r = mid;}else{ l = mid + 1;}}
if( Map[l] < _t){ return -1;}
return l;
}
int Get_hash( const _element & _tar) const{ // 如果不存在則返回`-1` 否則`Map[返回值]==tar`;
auto ind = Get_lowerBound( _tar); if( ind==-1 || Map[ind]!=_tar){ return -1;}
return ind;
}
}; // class ___Hash_Sorting
取模哈希(探测法|拉链法)
性质
需求: 对unordered_map/unordered_set
的效率上的优化;
换句话说, 探测法|拉链法 都可以被(STL容器)所替代, 只不过 他效率更高;
.
即, 维护一个整数集合S, 然后有一些操作{查询某个整数是否在S里, 向S里添加一个整数, …}
@DELI;
給定
N
N
N個整數
a
a
a(可以是負數), 選擇一個
>
N
>N
>N的質數
P
P
P, 定義數組T map[ P]
初始都置為Invalid
(確保所有
a
≠
I
n
v
a
l
i
d
a \neq Invalid
a=Invalid), 對於a=map[b]
: @IF(a!=Invalid
)[a的哈希值為b], @ELSE[沒有元素的哈希值為b]; 即 任意兩個數 他倆的哈希值必須要保證是不同的;
哈希算法:
令init = (a%P+P)%P
非負餘數 作為a的初始哈希值, 顯然 對於
a
+
k
∗
P
a + k*P
a+k∗P這些數 他們的初始哈希值 都是相同的, 即出現了 哈希衝突;
哈希衝突:
1(拉鏈法): 將map
變成vector< T> map[ P]
, 即對於衝突的元素 都放到map[ init]
裡面去 來區分他們;
缺點:
.
1: 此時a的哈希值 不再是一個[0,P)
的整數, 而是2個數{h1, h2}
表示a在map[ h1][ h2]
這個位置, 這不好 假如我們希望他的哈希值是[0, P)
範圍的;
.
2: 假如所有數都是a + k*P
, 顯然此時 時間變成了
O
(
N
)
O(N)
O(N), 超時了;
2(探測法): 為了讓a的哈希值 是一個[0, P)
的整數 就不能使用上述的鏈錶, 依次的查詢x = [a/ a+(1*1)/ a+(2*2)/ a+(3*3)/ ...]的%P的非負餘數
如果map[x] = Invalid
那麼當前數的哈希值為x
; 且要讓
P
>
2
∗
N
P > 2*N
P>2∗N 即讓P比較大;
缺點: 大多數情況沒問題, 但是跟上面的2:
一樣 假如所有數都是a + k*P
此時同樣是退化到
O
(
N
)
O(N)
O(N)超時;
.
解決辦法: 再哈希, 樸素做法是a + k*k
將他變成a + k*L
其中L = a%PP
PP
為<P
的質數;
算法
模板
拉鏈法等價於: set
(即添加/查詢一個元素, 但你無法獲得一個元素的哈希值);
探測法等價於: map
(在set
的基礎上 還添加了 將一個元素映射到[0,P)
的整數, 換句話說 等價於離散化哈希);
因此 探測法可以完全替代拉鏈法;
由於探測法和Hash_Mapping
等價, 而且 其實unordered_map
效率也很好 大多數情況不會被卡 大概也就慢個幾倍的時間, 因此 基本很少用探測法 你可以先使用Hash_Mapping
不行再用探測法;
代碼
class __Hash_Modular{
//< 取模哈希 對整數(可以是負數)進行哈希 得到一個`>=0`的哈希, 他可以完全被`Map< Type,int>`所替代 只不過後者是$O(\log{N})$ 而取模哈希是接近$O(1)$的;
// . 當`@LINK:@LOC_0`發生, 說明衝突次數太多了, 可以增大`__MaxDetectionCount_` 但這不是好辦法, 最好是可以考慮*再哈希* 即將探測`k*k`改為`k*L` L為`_a % 99991`;
public:
using Type_ = @TODO; // 必須是*整數*類型;
static constexpr int __Modular_ = @TODO; // [ 100003, 500009, 1000003, 5000011, 10000019, 50000017];
//< `N: 要進行哈希的元素個數`, 確保`Modular_ > 2*N`;
static constexpr Type_ __Invalid_ = INT64_0x80; // 確保所有要進行哈希的元素 都不會等於該值;
static constexpr int __MaxDetectionCount_ = 202; // 二次檢測的最大次數 (也對應`Add/Get_hash`函數的最大時間複雜度);
Type_ __Map[ __Modular_]; // `a=Map[b]`: @IF(a!=`Invalid`)[a的哈希值為b], @ELSE[暫時還沒有元素的哈希值為b]
__Hash_Modular(){}
void Initialize(){
memset( __Map, 0x80, sizeof( __Map));
}
int Add( Type_ _a){ // 返回值為 `a`的哈希值;
ASSERT_WEAK_( _a != __Invalid_);
int init = _a % __Modular_; if( init < 0){ init += __Modular_;}
if( __Map[ init] == __Invalid_){ __Map[ init] = _a; return init;}
else if( __Map[ init] == _a){ return init;}
for( int k = 1; k <= __MaxDetectionCount_; ++k){
int id = (init + k * k) % __Modular_;
if( __Map[ id] == __Invalid_){ __Map[ id] = _a; return id;}
else if( __Map[ id] == _a){ return id;}
}
ASSERT_( 0); // @MARK:@LOC_0
return -1;
}
int Get_hash( Type_ _a){
//< 返回值: @IF(`!= -1`)[表示a的哈希值 說明其之前調用過`Add`函數] @ELSE[該元素不存在];
ASSERT_WEAK_( _a != __Invalid_);
int init = _a % __Modular_; if( init < 0){ init += __Modular_;}
if( __Map[ init] == __Invalid_){ return -1;}
else if( __Map[ init] == _a){ return init;}
for( int k = 1; k <= __MaxDetectionCount_; ++k){
int id = (init + k * k) % __Modular_;
if( __Map[ id] == __Invalid_){ return -1;}
else if( __Map[ id] == _a){ return id;}
}
ASSERT_( 0); // @MARK:@LOC_0
return -1;
}
}; // class __Hash_Modular
__Hash_Modular Hash_Modular;
@TODO( Hash_Modular.Initialize());
筆記
#取模数为质数#
要放入一个LL的值A, 他的下标是A % Hash_size_
, 然后再探测; 即Ll_ Hash_index[ Hash_size_]
Hash_size_, 取模数(也是哈希表长度), 必须是质数 (否则容易冲突)
@DELI;
#探测算法#
要放入一个值A, int init = A % Hash_size_
, 然后从init位置开始, 开始探测;
不要写成: int init = Hash_index[ A % Hash_size_];
这点反映对哈希的理解程度, Hash_index里存放的是: 键值; 键值通过% Hash_size_
后, 是index, 数组下标;
现在init要获取的是(下标), 不是(键值)
#二次探测#
目前看来, 二次探测要比(线性探测)要更好
但应该没必要循环k = [0, .., Hash_size)
吧? 这太大了, 而且k * k
就爆int了, 还得转LL;
int Get_hash( int _key){
int init = _key % Hash_size_;
int id;
for( int k = 0; k < 10000; ++k){
id = (init + k * k) % Hash_size_;
if( ( Hash_key[ id] == Hash_invalid) || (Hash_key[ id] == _key)){
Hash_key[ id] = _key;
return id;
}
}
ASSERT_( 0);
return id;
}
这样, k*k + init也是int范围的;
应该不会超过10000(虽然理论上是应该到Hash_size_), 但假如他循环比10000次还多, 那应该设哈希表设计的问题, 跟平方探测应该没问题;
取模哈希(四則運算)
定義
對於整數a
, 執行a += x, a -= x, a *= x
(且x
也是整數 不涉及到除法運算), 然後判斷 兩個整數a,b
是否相等;
如果所有數 都是在long long
範圍裡 這當然很簡單, 但是假如他會爆出long long
, 那麼此時就可以使用哈希;
算法
模板
很簡單, 選擇一個取模數M=1e9+7
, 對於(判斷兩個整數a,b
是否相等這個操作 他等價於a%M == b%M
), 對於(四則運算 因為取模也是可以進行+,-,*
的);
具體代碼也很簡單, 你之前是: long long a = ?;
(即所有涉及到的整數 都是long long
類型), 現在就把他變成為Modular a = ?%Mod
類型即可;
雙哈希
上面的算法 也可以稱為(單哈希), 假如說 對於Modular a = 1e9
, 執行a += 7
, 此時a==0
, 然後此時a
就代表所有0 + k*M
, 假如此時你要判斷a
是否等於0
(此時a=1e9+7
顯然他不等於0
) 他倆的哈希值都是0
, 即發生了哈希衝突;
為了解決哈希衝突, 此時我們再設置一個Mod2 = 1e9+9
(與1e9+7
互為孿生質數), 原來是Modular a
, 現在變成pair<Modular, Mod2> a
, (對於四則運算 即a.first += ?, a.second += ?
) (對於判斷相等 即對應為 同時判斷這個2個哈希值是否相等);
.
以上述例子, 對於1e9
他的哈希值為{1e9, 1e9}
, 進行+=7
操作後為{0, 1e9+7}
, 那麼判斷他是否等於0
(0
的哈希值為{0,0}
), 顯然{0,1e9+7} != {0,0}
;
具體代碼也很簡單 原來是Modular a = ?%Mod;
現在變成pair<Mod1, Mod2> a = {?%M1, ?%M2}
即可;