「笔记」哈希

写在前面

大部分内容来自高中时期讲课 PPT,在这里整理成了适合自学的形式。

引入

要求维护一个数据结构,支持动态地插入一个数,每次插入之后查询不同的数的个数。
值域 \(0\le x \le 2^{63}−1\)
操作次数 \(\le 10^6\)

若值域 \(0\le x\le 10^6\),直接开个数组,以每个数的值为下标记录每种数是否出现过即可。

值域这么大,但是操作次数很少,出现的不同的数的个数不会多于操作次数,并且只需要判断某个数是否出现过,即在插入之前是否在之前有相同的数,并不关心它们具体的值。

能否把较大值域映射到较小值域上来方便判重?

哈希

  • 一种用于统计复杂信息的的不完美算法。
  • 构造哈希函数将复杂信息映射到便于统计的信息上,通过对映射后的信息进行处理来实现复杂信息的维护。
  • 两元素映射后相同,是两元素相同的必要条件
  • 可能会丢失部分信息。

引入的哈希解决方案

单哈希:

  • 把插入的数对一个不是很大的数 \(p\) 取模,令余数代替原数。
  • 如果两个数的余数相等,就看做相等。
  • 开一个大小为模数 \(p\)bool 数组用于记录某种余数是否出现过用于判重,单次判断复杂度 \(O(1)\)

样例代码:

#include <bits/stdc++.h>
#define LL long long
using namespace std;
const int kN = 1e6 + 10;
const int p = 1e6;
//=============================================================
bool exist[kN];
//=============================================================
int main() {
  int n, num = 0; cin >> n;
  while (n --) {
    LL x; cin >> x;
    if (!exist[x % p]) ++ num;
    exist[x % p] = 1;
    cout << num << "\n";
  }
  return 0;
}

或者用 map 实现,可以支持更大的模数:

#include <bits/stdc++.h>
#define LL long long
using namespace std;
const LL p = 1e9 + 7;
//=============================================================
map <LL, bool> exist;
//=============================================================
int main() {
  int n, num = 0; cin >> n;
  while (n --) {
    LL x; cin >> x;
    if (!exist.count(x % p)) ++ num;
    exist[x % p] = 1;
    cout << num << "\n";
  }
  return 0;
}

上述算法可能存在漏洞:两个不同的数若余数相等则认为相等,可能会出现误判的情况。

类似上述漏洞的,将不同的元素误判为相同的情况称为哈希冲突

完全正确还叫什么不完美算法?/cy

可以发现余数相同是原数相同的必要条件,哈希实质上就是在用必要条件近似充要条件,但是可以通过各种构造使得必要条件与充要条件尽可能相近,比如下列改进:

引入的哈希改进方案

多哈希:

  • 同时对多个模数取模,并记录多个余数来代替原数。
  • 当两个数所有余数均相等,才看做相等。
  • 实现时可以定义一个结构体或是用 pair 类型用来存所有余数,用 set/map 维护,或写哈希表。
#include <bits/stdc++.h>
#define LL long long
using namespace std;
const int kN = 1e6 + 10;
const LL p1 = 998244353;
const LL p2 = 1e9 + 7;
//=============================================================
map <pair <LL, LL>, bool> exist; 
//=============================================================
int main() {
  int n, num = 0; cin >> n;
  while (n --) {
    LL x; cin >> x;
    if (!exist.count(make_pair(x % p1, x % p2))) ++ num;
    exist[make_pair(x % p1, x % p2)] = 1;
    cout << num << "\n";
  }
  return 0;
}

正确性大幅增加但仍有哈希冲突的概率。

但是一般写双哈希(对两个数取模)就够了,可以证明哈希冲突的概率是很小的,很难卡掉。

哈希表

单哈希:

  • 不把余数相等的一些数直接看做相等,而是开个链表把它们链起来。
  • 判重时找到查询的数的余数对应的链表,遍历所有元素判重。
  • 可以用邻接表或 vector 实现。
  • 随机数据下链表最大长度(即每次判重复杂度上界)期望 \(O(\frac{n}{r})\),牺牲了时间复杂度,但是保证了正确性。
//vector 偷懒写法
#include <bits/stdc++.h>
#define LL long long
using namespace std;
const int kN = 1e6 + 10;
const int p = 1e6;
//=============================================================
vector <int> exist[kN];
//=============================================================
int main() {
  int n, num = 0; cin >> n;
  while (n --) {
    LL x; cin >> x;

    bool flag = 1;
    for (auto y: exist[x % p]) {
      if (x == y) {
        flag = 0;
        break;
      }
    }
    if (flag) ++ num, exist[x % p].push_back(x);
    cout << num << "\n";
  }
  return 0;
}
//邻接表写法
#include <bits/stdc++.h>
#define LL long long
using namespace std;
const int kN = 1e6 + 10;
const int p = 1e6;
//=============================================================
vector <int> exist[kN];
int edgenum, head[kN], ne[kN];
LL val[kN];
//=============================================================
void Init() {
  edgenum = 0;
  memset(head, 0, sizeof (head));
}
void Insert(LL x_) {
  int pos = x_ % p;
  for (int i = head[pos]; i; i = ne[i]) {
    if (val[i] == x_) return ;
  }
  val[++ edgenum] = x_;
  ne[edgenum] = head[pos];
  head[pos] = edgenum;
}
bool Count(LL x_) {
  int pos = x_ % p;
  for (int i = head[pos]; i; i = ne[i]) {
    if (val[i] == x_) return true;
  }
  return false;
}
//=============================================================
int main() {
  int n, num = 0; cin >> n;
  Init();
  while (n --) {
    LL x; cin >> x;
    if (!Count(x)) {
      ++ num;
      Insert(x);
    }
    cout << num << "\n";
  }
  return 0;
}

多哈希:

  • 一般是对多个哈希值再进行一次哈希运算来求得应当存到哈希表的哪个位置,再在哈希表对应位置中同时存入多个哈希值。
  • 一般用不着,要是冲突了一般会直接改成双哈希。双哈希还过不了?换个模数多交几遍!!!!相信自己能卡过去!!!!
  • 要是出现了必须用双哈希哈希表才能过的题……那这太变态了,大概是这题肯定有更优的做法,比如下面例题里的求双串最长公共子串。
//vector 偷懒写法
#include <bits/stdc++.h>
#define LL long long
using namespace std;
const int kN = 1e6 + 10;
const LL p1 = 1e9 - 666;
const LL p2 = 1e9 - 233;
const LL p3 = 1e9 - 2077;
const int base = 1145141;
//=============================================================
vector <pair <LL, LL> > exist[base + 10]; 
//=============================================================
void Insert(LL v1_, LL v2_) {
  int pos = (v1_ * p3 + v2_) % base;
  for (auto x: exist[pos]) {
    if (x.first == v1_ && x.second == v2_) return ;
  }
  exist[pos].push_back(make_pair(v1_, v2_));
}
bool Count(LL v1_, LL v2_) {
  int pos = (v1_ * p3 + v2_) % base;
  for (auto x: exist[pos]) {
    if (x.first == v1_ && x.second == v2_) return true;
  }
  return false;
}
//=============================================================
int main() {
  int n, num = 0; cin >> n;
  while (n --) {
    LL x; cin >> x;
    if (!Count(x % p1, x % p2)) {
      ++ num;
      Insert(x % p1, x % p2);
    }
    cout << num << "\n";
  }
  return 0;
}

关于哈希函数

  • 对应哈希函数相等是两元素相等的必要条件。
  • 其实可以随便构造,因为出题人并不知道你用的什么神仙映射方法。
  • 直接取模是一种最简单的哈希。
  • 你甚至可以乘随机数/加随机数/对随机数取模……
  • 模数一般选择大质数,冲突概率低。常用大质数模数:11451419982443531e9 + 71e9 + 9

字符串哈希

用于判重字符串。将字符串映射到一个整数上,通过判断整数是否相等来判断字符串是否相等。

由于字符串是具有前后关系的,一般按下述方法构造:

  • 取一个权值 \(𝑐\),模数 \(𝑝\)。对于长度为 \(n\) 的字符串 \(𝑠\),有:

\[\operatorname{Hash}(𝑠_1s_2\dots𝑠_𝑛)= 𝑐^{𝑛−1}\times 𝑠_1+𝑐^{𝑛−2}\times 𝑠_2+⋯+c^{0}\times 𝑠_𝑛 \pmod p \]

  • 相当于给不同的位置赋上了不同的权值。
  • 构造时 \(𝑂(𝑛)\) 递推即得所有前缀的哈希值:

\[\operatorname{Hash}(𝑠_1s_2\dots𝑠_i) = c\times \operatorname{Hash}(𝑠_1s_2\dots𝑠_{i-1}) + s_i \pmod p \]

由上述公式可知对于长度为 \(n\) 的字符串 \(s\),其子串 \(s_l\sim s_r\) 的哈希值为:

\[\operatorname{Hash}(𝑠_ls_{l+1}\dots𝑠_𝑟)= 𝑐^{𝑟−𝑙}\times 𝑠_𝑙+𝑐^{𝑟−𝑙−1}\times 𝑠_{𝑙+1}+⋯+c^0\times 𝑠_𝑟 \pmod p \]

根据上一步中预处理的前缀哈希值,有:

\[\operatorname{Hash}(𝑠_ls_{l+1}\dots𝑠_𝑟)=\operatorname{Hash}(𝑠_1s_2\dots𝑠_𝑟)−𝑐^{𝑟−𝑙+1}\times \operatorname{Hash}(𝑠_1s_2\dots𝑠_𝑟s_{𝑙−1}) \pmod p \]

预处理 \(𝑐^𝑥\) 后任意子串的哈希值即可 \(O(1)\) 地求得。

例题

字符串判重

给定 \(n\) 个字符串(第 \(i\) 个字符串长度为 \(m_i\),字符串内包含数字、大小写字母,大小写敏感),请求出 \(n\) 个字符串中共有多少个不同的字符串。
\(1\le n\le 10000\)\(m_i\approx 1000\)\(\max{(m_i)}\le 1500\)
1S,128MB。

https://www.luogu.com.cn/problem/P3370

板题。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
using namespace std;
const LL c1 = 114514;
const LL p1 = 998244353;
const LL c2 = 1919810;
const LL p2 = 1e9 + 7;
//=============================================================
int n, ans;
map <pair <LL, LL>, bool> exist;
//=============================================================
int main() {
  //freopen("1.txt", "r", stdin);
  cin >> n;
  while (n --) {
    string s; std::cin >> s;
    LL h1 = 0, h2 = 0;
    for (int i = 0, len = s.length(); i < len; ++ i) {
      h1 = c1 * h1 + s[i];
      h2 = c2 * h2 + s[i];
    }
    if (!exist.count(make_pair(h1, h2))) ++ ans;
    exist[make_pair(h1, h2)] = 1;
  }
  cout << ans << "\n";
  return 0;
}

求最长回文子串

给定字符串 \(𝑠\),求其最长回文子串。
\(1\le |𝑠|≤10^5\)
1S,256MB。

先在所有相邻字符中间都插入一个相同的字符,将长度为偶数的回文串转化为长度为奇数的,则所有回文串均有中心。然后二分答案,检查时 \(O(𝑛)\) 地枚举回文串中心, \(O(1)\) 判断两侧对应长度的子串是否相等即可。

需要预处理正序和倒序的前缀哈希值,总时间复杂度 \(𝑂(𝑛 \log⁡ 𝑛)\) 级别。看起来复杂度还好但是常数比较大,一般使用时空复杂度均为 \(O(n)\) 的还很好写的比哈希高到不知道哪里去了的 Manacher 算法解决,除非忘了 Manacher 怎么写并且场上还没带板子才这么写吸吸。

求双串最长公共子串

给定两字符串 \(S_1, S_2\),求它们的最长公共子串长度。
\(|S_1|,|S_2|\le 2.5\times 10^5\)
294ms,1.46GB。

https://www.cnblogs.com/luckyblock/p/13525501.html

这里只简述哈希的做法,实际上存在线性的做法并且比哈希不知道高到哪里去了。

较长的公共子串中包含了较短的公共子串,显然可匹配的公共子串长度存在单调性。考虑二分答案枚举最长公共子串长度 mid,Check 时将一个串所有长度为 \(mid\) 的子串的哈希值存到哈希表里,枚举另一个串所有长度为 \(mid\) 的子串,检查是否存在即可。

理论复杂度 \(O(n\log n)\),但取模运算过多,所以常数巨大,运行效率较低。

注意写单 hash 或用 set 都会被卡。

//知识点:二分答案,Hash
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 3e5 + 10;
const LL kMod1 = 998244353;
const LL kMod2 = 1e9 + 9;
const LL kBase = 1145141;
//=============================================================
int n1, n2, ans, e_num, head[kBase + 10], ne[kMaxn << 1];
char s1[kMaxn], s2[kMaxn];
LL pow1[kMaxn], pow2[kMaxn];
LL has11[kMaxn], has12[kMaxn], has21[kMaxn], has22[kMaxn];
std::pair <LL, LL> val[kMaxn << 1];
//=============================================================
inline int read() {
  int f = 1, w = 0; char ch = getchar();
  for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
void Insert(LL val_, LL v1_, LL v2_) {
  int pos = val_ % kBase + 1;
  for (int i = head[pos]; i; i = ne[i]) {
    if (val[i].first == v1_ && val[i].second == v2_) return ;
  }
  val[++ e_num] = std::make_pair(v1_, v2_);
  ne[e_num] = head[pos];
  head[pos] = e_num;
}
bool Count(LL val_, LL v1_, LL v2_) {
  int pos = val_ % kBase + 1;
  for (int i = head[pos]; i; i = ne[i]) {
    if (val[i].first == v1_ && val[i].second == v2_) return true; 
  }
  return false;
}
void Prepare() {
  scanf("%s", s1 + 1);
  scanf("%s", s2 + 1);
  n1 = strlen(s1 + 1);
  n2 = strlen(s2 + 1);
  pow1[0] = pow2[0] = 1;
  for (int i = 1; i < std::max(n1, n2); ++ i) {
    pow1[i] = pow1[i - 1] * kBase % kMod1;
    pow2[i] = pow2[i - 1] * kBase % kMod2;
  }
  for (int i = 1; i <= n1; ++ i) {
    has11[i] = (has11[i - 1] * kBase + s1[i]) % kMod1;
    has12[i] = (has12[i - 1] * kBase + s1[i]) % kMod2;
  }
  for (int i = 1; i <= n2; ++ i) {
    has21[i] = (has21[i - 1] * kBase + s2[i]) % kMod1;
    has22[i] = (has22[i - 1] * kBase + s2[i]) % kMod2;
  }
}
bool Check(int lth_) {
  e_num = 0;
  memset(head, 0, sizeof (head));
  for (int l = 1, r = lth_; r <= n1; ++ l, ++ r) {
    LL now_has11 = ((has11[r] - has11[l - 1] * pow1[r - l + 1] % kMod1) + kMod1) % kMod1;
    LL now_has12 = ((has12[r] - has12[l - 1] * pow2[r - l + 1] % kMod2) + kMod2) % kMod2;
    Insert(now_has11 * kMod2 + now_has12, now_has11, now_has12);
  }
  for (int l = 1, r = lth_; r <= n2; ++ l, ++ r) {
    LL now_has21 = ((has21[r] - has21[l - 1] * pow1[r - l + 1] % kMod1) + kMod1) % kMod1;
    LL now_has22 = ((has22[r] - has22[l - 1] * pow2[r - l + 1] % kMod2) + kMod2) % kMod2;
    if (Count(now_has21 * kMod2 + now_has22, now_has21, now_has22)) return true;
  }
  return false;
}
//=============================================================
int main() {
  // freopen("A.txt", "r", stdin);
  Prepare();
  for (int l = 1, r = std::min(n1, n2); l <= r; ) {
    int mid = (l + r) >> 1;
    if (Check(mid)) {
      ans = mid;
      l = mid + 1;
    } else {
      r = mid - 1;
    }
  }
  printf("%d\n", ans);
  return 0;
}
/*
opawmfawklmiosjcas1145141919810asopdfjawmfwaiofhauifhnawf
opawmdawlmioaszhcsan1145141919810bopdjawmdaw
*/

CCPC2023 Shenzhen G

给定 \(n\) 个模式串 \(s_1\sim s_n\)\(q\) 个匹配串 \(t_1\sim t_q\),每个字符串长度均为 \(m\)
给定参数 \(k\),规定两个字符串是 “相似的”,当前仅当两个字符串至多有 \(k\) 处对应位置不同。对于所有匹配串 \(t_1\sim t_q\),求有多少个模式串与它们是“相似的”。
\(1\le n,q\le 300\)\(1\le m\le 6\times 10^4\)\(1\le k\le 10\)
2.5S,512MB。

https://vjudge.net/problem/CSG-1243

场上一看 \(k\le 10\) 笑烂了。

首先预处理所以模式串的串的哈希值,对于每个匹配串都枚举所有串并检查,检查时不断地通过哈希+二分求不同的下一个位置,并判断不同的位置的个数是否超过 \(k\) 即可。

总时间复杂度 \(O(nqk\log m)\) 级别。

场上写的吸吸,无代码。

写在最后

进度有点太赶了!题单太难了!我谢罪!

写不完正常,能写完说明你是超级大神我直接跪下来磕俩头、、、

关注铃木实里喵,关注铃木实里谢谢喵:https://music.163.com/#/artist?id=12090112https://space.bilibili.com/3493090570537482

posted @ 2024-01-18 16:31  Luckyblock  阅读(55)  评论(0编辑  收藏  举报