从 map 到 pbds hash。
前言
最近重新整理 STL 的时候,在 map 和 umap 处有了一些讨论,然后又去看了 pbds 中的哈希表,在此稍作总结。
本文讨论基于该题:P11615 【模板】哈希表。
map
map 是 STL 提供的一种有序键值对,内部实现通常是红黑树。
其大多数的单次操作的复杂度均为 \(O(\log n)\),在本题中效率过低。
较为常用,不再赘述。
#include <bits/stdc++.h>
using namespace std;
using ui64 = unsigned long long;
char buf[1 << 23], *p1 = buf, *p2 = buf;
#define gc() (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 1 << 21, stdin), p1 == p2) ? EOF : *p1++)
inline unsigned long long rd()
{ // 读入一个 64 位无符号整数
unsigned long long x = 0;
char ch = gc();
while (!isdigit(ch))
ch = gc();
while (isdigit(ch))
x = x * 10 + (ch ^ 48), ch = gc();
return x;
}
map<ui64, ui64> t;
void _()
{
int n = rd();
ui64 ans = 0;
for (int i = 1; i <= n; ++i)
{
ui64 x = rd(), y = rd();
ans += t[x] * i;
t[x] = y;
}
cout << ans << '\n';
}
int main()
{
int T = 1;
// cin >> T;
while (T--)
_();
return 0;
}
umap
umap 全称 unordered_map,是 STL 提供的一种无序键值对,内部实现基于哈希表。
通常使用方式同 map,即:
unordered_map<ui64, ui64> t;
没有刻意设计的数据下,umap 的复杂度为 \(O(1)\)。
通过制造哈希冲突,在最劣数据下,umap 的复杂度为 \(O(n)\)。
#include <bits/stdc++.h>
using namespace std;
using ui64 = unsigned long long;
char buf[1 << 23], *p1 = buf, *p2 = buf;
#define gc() (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 1 << 21, stdin), p1 == p2) ? EOF : *p1++)
inline unsigned long long rd()
{ // 读入一个 64 位无符号整数
unsigned long long x = 0;
char ch = gc();
while (!isdigit(ch))
ch = gc();
while (isdigit(ch))
x = x * 10 + (ch ^ 48), ch = gc();
return x;
}
unordered_map<ui64, ui64> t;
void _()
{
int n = rd();
ui64 ans = 0;
for (int i = 1; i <= n; ++i)
{
ui64 x = rd(), y = rd();
ans += t[x] * i;
t[x] = y;
}
cout << ans << '\n';
}
int main()
{
int T = 1;
// cin >> T;
while (T--)
_();
return 0;
}
自定义哈希函数
umap 支持自定义哈希函数,可以通过随机数来降低哈希冲突。
如:
mt19937_64 rnd(chrono::steady_clock::now().time_since_epoch().count());
struct rnd_hash
{
ui64 operator()(const ui64 &x) const
{ // 重载运算符实现自定义哈希函数
static const ui64 r = rnd();
return x ^ r;
}
};
unordered_map<ui64, ui64, rnd_hash> t;
不过仍有被卡的风险便是了。
#include <bits/stdc++.h>
using namespace std;
using ui64 = unsigned long long;
char buf[1 << 23], *p1 = buf, *p2 = buf;
#define gc() (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 1 << 21, stdin), p1 == p2) ? EOF : *p1++)
inline unsigned long long rd()
{ // 读入一个 64 位无符号整数
unsigned long long x = 0;
char ch = gc();
while (!isdigit(ch))
ch = gc();
while (isdigit(ch))
x = x * 10 + (ch ^ 48), ch = gc();
return x;
}
mt19937_64 rnd(chrono::steady_clock::now().time_since_epoch().count());
struct rnd_hash
{
ui64 operator()(const ui64 &x) const
{
static const ui64 r = rnd();
return x ^ r;
}
};
unordered_map<ui64, ui64, rnd_hash> t;
void _()
{
int n = rd();
ui64 ans = 0;
for (int i = 1; i <= n; ++i)
{
ui64 x = rd(), y = rd();
ans += t[x] * i;
t[x] = y;
}
cout << ans << '\n';
}
int main()
{
int T = 1;
// cin >> T;
while (T--)
_();
return 0;
}
更优的哈希设计
在这片文章中,提到了一种更优秀的防卡哈希设计,即著名的 splitmix64。
struct custom_hash {
static uint64_t splitmix64(uint64_t x) {
// http://xorshift.di.unimi.it/splitmix64.c
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);
}
};
具体原理不需要了解,赛时直接看板子即可。
不过依旧很危险,不确定是我实现原因还是运气不好。
#include <bits/stdc++.h>
using namespace std;
using ui64 = unsigned long long;
char buf[1 << 23], *p1 = buf, *p2 = buf;
#define gc() (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 1 << 21, stdin), p1 == p2) ? EOF : *p1++)
inline unsigned long long rd()
{ // 读入一个 64 位无符号整数
unsigned long long x = 0;
char ch = gc();
while (!isdigit(ch))
ch = gc();
while (isdigit(ch))
x = x * 10 + (ch ^ 48), ch = gc();
return x;
}
struct custom_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);
}
};
unordered_map<ui64, ui64, custom_hash> t;
void _()
{
int n = rd();
ui64 ans = 0;
for (int i = 1; i <= n; ++i)
{
ui64 x = rd(), y = rd();
ans += t[x] * i;
t[x] = y;
}
cout << ans << '\n';
}
int main()
{
int T = 1;
// cin >> T;
while (T--)
_();
return 0;
}
hash_table
众所不一定周知,pbds 中有两个哈希表:
cc_hash_tablegp_hash_table
性能方面,gp 优于 cc,所以此处仅讨论 gp。
使用方法如下:
__gnu_pbds::gp_hash_table<ui64, ui64> t;
需要头文件 #include <bits/extc++.h>(C++ 拓展库的万能头)。
同样,在刻意制造哈希冲突的情况下,依旧会被卡。
由于本题中有专门针对 gp_hash_table 的数据,因此直接使用 gp_hash_table 同样不能通过本题。
#include <bits/stdc++.h>
#include <bits/extc++.h>
using namespace std;
using ui64 = unsigned long long;
char buf[1 << 23], *p1 = buf, *p2 = buf;
#define gc() (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 1 << 21, stdin), p1 == p2) ? EOF : *p1++)
inline unsigned long long rd()
{ // 读入一个 64 位无符号整数
unsigned long long x = 0;
char ch = gc();
while (!isdigit(ch))
ch = gc();
while (isdigit(ch))
x = x * 10 + (ch ^ 48), ch = gc();
return x;
}
__gnu_pbds::gp_hash_table<ui64, ui64> t;
void _()
{
int n = rd();
ui64 ans = 0;
for (int i = 1; i <= n; ++i)
{
ui64 x = rd(), y = rd();
ans += t[x] * i;
t[x] = y;
}
cout << ans << '\n';
}
int main()
{
int T = 1;
// cin >> T;
while (T--)
_();
return 0;
}
自定义哈希函数
不过对于上述提到的 splitmix64,在 gp_hash_table 这里就肥肠好使了。
#include <bits/stdc++.h>
#include <bits/extc++.h>
using namespace std;
using ui64 = unsigned long long;
char buf[1 << 23], *p1 = buf, *p2 = buf;
#define gc() (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 1 << 21, stdin), p1 == p2) ? EOF : *p1++)
inline unsigned long long rd()
{ // 读入一个 64 位无符号整数
unsigned long long x = 0;
char ch = gc();
while (!isdigit(ch))
ch = gc();
while (isdigit(ch))
x = x * 10 + (ch ^ 48), ch = gc();
return x;
}
struct custom_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);
}
};
__gnu_pbds::gp_hash_table<ui64, ui64, custom_hash> t;
void _()
{
int n = rd();
ui64 ans = 0;
for (int i = 1; i <= n; ++i)
{
ui64 x = rd(), y = rd();
ans += t[x] * i;
t[x] = y;
}
cout << ans << '\n';
}
int main()
{
int T = 1;
// cin >> T;
while (T--)
_();
return 0;
}
显然,就算有板子,抄下来这些奇奇怪怪的十六进制数也只一种折磨。
不过,比较友好的是,可以用三个随机数取代这三个奇怪的数,哈希表的性能也没有太大的影响。
#include <bits/stdc++.h>
#include <bits/extc++.h>
using namespace std;
using ui64 = unsigned long long;
char buf[1 << 23], *p1 = buf, *p2 = buf;
#define gc() (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 1 << 21, stdin), p1 == p2) ? EOF : *p1++)
inline unsigned long long rd()
{ // 读入一个 64 位无符号整数
unsigned long long x = 0;
char ch = gc();
while (!isdigit(ch))
ch = gc();
while (isdigit(ch))
x = x * 10 + (ch ^ 48), ch = gc();
return x;
}
mt19937_64 rnd(chrono::steady_clock::now().time_since_epoch().count());
struct rnd_hash
{
ui64 operator()(ui64 x) const
{
static const ui64 s1 = rnd(), s2 = rnd(), s3 = rnd();
x += s1;
x = (x ^ (x >> 33)) * s2;
x = (x ^ (x >> 30)) * s3;
return x;
}
};
__gnu_pbds::gp_hash_table<ui64, ui64, rnd_hash> t;
void _()
{
int n = rd();
ui64 ans = 0;
for (int i = 1; i <= n; ++i)
{
ui64 x = rd(), y = rd();
ans += t[x] * i;
t[x] = y;
}
cout << ans << '\n';
}
int main()
{
int T = 1;
// cin >> T;
while (T--)
_();
return 0;
}








浙公网安备 33010602011771号