算法 {哈希}
理论上 他是 log ( N ) \log(N) log(N), 但是 因为他是动态的过程, 所以 实际效率还不错 虽然会比(取模哈希)慢几个常数;
template< class _Type> class __Hash_Mapping{
public:
unordered_map< _Type, int> Map; // `S: Map.size()`, 所有元素的哈希值为`[0, S)`;
__Hash_Mapping(){}
void Initialize(){ Map.clear();}
void Add( _Type _a){ if( Map.find( _a) == Map.end()){ Map[ _a] = Map.size();}}
int Get_hash( _Type _a) const{
if( Map.find( _a) == Map.end()){ return -1;} return Map.at( _a);}
}; // class __Hash_Mapping
__Hash_Mapping< ?> Hash_Mapping;
@TODO( Hash_Mapping.Initialize());
template < class _Type> class Hash_discretization{
public:
const int __Range_maximum;
_Type * __Array;
int Array_size; // 所有哈希的值 是`[0, Array_size)`;
Hash_discretization( int _range_maximum)
:
__Range_maximum( _range_maximum){
__Array = new _Type[ __Range_maximum];
Initialize();
}
~Hash_discretization(){
delete[] __Array;
}
void Initialize(){
Array_size = 0;
}
void Add( _Type _a){
// ASSERT_( Array_size < __Range_maximum);
__Array[ Array_size] = _a;
++ Array_size;
}
void Discretize(){
sort( __Array, __Array + Array_size);
Array_size = unique( __Array, __Array + Array_size) - __Array;
}
int Get_hash( _Type _tar){
int l = 0, r = Array_size - 1, mid;
while( l < r){
mid = ( l + r) >> 1;
if( __Array[ mid] < _tar){
l = mid + 1;
}
else{
r = mid;
}
}
if( __Array[ l] == _tar){
return l;
}
return -1;
}
}; // class Hash_discretization
字符串str="abc"(a為低位) 他的哈希值為A*(Base^2) + B*Base + C(在模Mod下) 其中ABC為abc對應的Base進制數;
這個哈希算法是絕對的 只與當前字符串有關 與其他背景都無關;
比如S="abcdef" 他的子字符串bcd的哈希值 你通過{1:預處理S 來獲得子串[1:3]的哈希值; 2:直接求bcd的哈希值} 兩者是一樣的! 都等於B*Base^2 + C*Base + D;
@DELI;
映射从1开始
假如是从0开始映射, 则abc 和 bc, 他们的对应的P进制是: (0) (1) (2) 和 (1) (2)
如果采用(前高位)的哈希顺序, 则他们对应的10进制值都是: (1 * P + 2), 就冲突了;
其原因在于: 两者的长度不同; 如果长度相同, 不会出现: 前导零的情况;
一般, 最好都从(1)开始映射
@DELIMITER
字符串的哈希冲突
由于字符串哈希, 不像整数哈希是有探测法(即整数哈希 不会出现哈希冲突, 探测法解决了这点)
而字符串哈希没有探测法, 因此, 字符串哈希是会发生冲突的; 假如他发生冲突, 目前是没有解决办法的!
只能去尝试更换: 字符串哈希的取模值: 将(默认的1e9+7) 改为: (1e9 + 21)
namespace __HashString{
//< 哈希算法: 字符串`str="abc"`(`a`為低位) 他的哈希值為`A*(Base^2) + B*Base + C`(在模`Mod`下) 其中`ABC`為`abc`對應的`Base`進制數, 即*字符串低位 其實是高量級*!
static constexpr int __Base_ = 131;
static constexpr int __Mod_ = int(1e9) + 7;
int __Hash_char( char _c){
if( (_c >= 'a') && (_c <= 'z')){ return _c - 'a' + 1;}
else if( (_c >= 'A') && (_c <= 'Z')){ return 27 + _c - 'A';}
else if( (_c >= '0') && (_c <= '9')){ return 53 + _c - '0';}
ASSERT_( 0);
return _c;
}
int Get_hash( const char * _str, int _length){
int ANS = __Hash_char( _str[ 0]);
for( int i = 1; i < _length; ++i){
ANS = ((long long)ANS * __Base_ % __Mod_ + __Hash_char( _str[ i])) % __Mod_;
}
return ANS;
}
template< int _MaximumLength> class __HashSubStrings{
public:
int __PrefixSum[ _MaximumLength];
int Power[ _MaximumLength];
int __Length;
__HashSubStrings(){}
void Initialize( const char * _str, int _length){
__Length = _length;
__PrefixSum[ 0] = __Hash_char( _str[ 0]), Power[ 0] = 1;
for( int i = 1; i < __Length; ++i){
__PrefixSum[ i] = ((long long)__PrefixSum[ i - 1] * __Base_ % __Mod_ + __Hash_char( _str[ i])) % __Mod_;
Power[ i] = (long long)Power[ i - 1] * __Base_ % __Mod_;
}
}
int Get_hash( int _left, int _right){
ASSERT_( (_left >= 0) && (_left <= _right) && (_right < __Length));
int ans = __PrefixSum[ _right]; if( _left != 0){ ans = ((ans - (long long)__PrefixSum[ _left - 1] * Power[ _right-_left + 1] % __Mod_) + __Mod_) % __Mod_;}
return ans;
}
}; // __HashSubStrings
} // __HashString
__HashString::__HashSubStrings< @TODO> HashSubStrings;
如果哈希冲突了, 将1e9 + 7改为1e9 + 21;
@DELI;
這個int Mod_, 你不能把他設置成一個long long的數, 這就錯誤了, 因為會導致溢出 (兩個long long的乘積 溢出);
. 只有說 你把Mod_給去掉 改為對unsigned long long自然溢出, 此時 溢出是正確的 (因為溢出 就相當於對2^64取模);
對於S: "abcdef", 獲取其(後綴+前綴)的字符串的哈希值;
比如要得到efabcd這個字符串的哈希值 令暴力算法O(N)得到的結果為H(即E*Base^n + F*... + D), 而實際上 你可以通過O(1)獲得他的哈希值: 令A:S.Get_hash(4,5); B:S.Get_hash(0,3), 即A為ef的哈希值(E*Base + F) B為abcd的哈希值(A*Base^3 + ... + D), 則efabcd的哈希值為 A * Power[4] + B == H;
例题: @LINK: https://editor.csdn.net/md/?not_checkout=1&articleId=132818193;
原理: 两个不同的P进制数, 他对应的十进制数, 是不同的;
假如P = 26, 两个P进制数: (a1) (a2) (a3) 和 (b1) (b2) (b3)
如果存在: ai != bi, 那么其对应的10进制数, 即(a1 * P^2 + a2 * P + a3)是不同的
这就像是, 假如P为熟悉的十进制, 那么(012) 和 (022)是不同的;
因此, 对于一个P进制数(也可以看做是一个P进制字符串), 他所对应的十进制数, 就称为他的(哈希值)
基于此, 假如一个字符串有P个不同的字符, 则对应一个P进制, 每个字符映射到[0, P)之中
由于其对应的十进制数, 会爆出LL, 需要使用取模操作;
需求: 对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這些數 他們的初始哈希值 都是相同的, 即出現了 哈希衝突;
哈希衝突: 的質數; 拉鏈法等價於: 由於探測法和 #取模数为质数# @DELI; #探测算法# 不要写成: #二次探测# 这样, k*k + init也是int范围的; 對於整數 很簡單, 選擇一個取模數 上面的算法 也可以稱為(單哈希), 假如說 對於
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為算法
模板
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_, 取模数(也是哈希表长度), 必须是质数 (否则容易冲突)
要放入一个值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;
}
应该不会超过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, (對於四則運算 即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即可;