哈希表 - 练习

哈希表 - 练习

什么是哈希表

哈希表的原理

当我们想要存储一个数是否出现过的时候,我们可以把这个数当作下标,存进数组里面。但是这种方法适用性非常局限,因为一旦下标非常大,我们的数组就开的很大,而且万一下标并不是一个整数呢?因此,伟大的哈希表就诞生了。

哈希表的原理就是将一个很复杂的下标通过哈希函数映射成一个较小范围的整数,然后对映射之后的位置进行操作。这种方法在下标很大,但是存储的数量非常少的时候,复杂度非常有秀。整数的哈希函数就是直接摸上一个数,而序列 / 字符串的操作就需要进行幂的分解,进行计算后进行取模。

冲突的处理

如果你的哈希函数设定的不好,那么会有很大一部分概率会使得两个不同的下标得到同样的哈希值,这时候冲突就出现了。冲突一般用两种方法进行处理:处理和不处理。以下是两种处理方法的总结:

  • 处理:一般都是使用线性探查法,将同样哈希的值存在一个动态数组或是链表当中,然后当哈希值重复的使用就遍历数组 / 链表,如果遇到了相同的键值就说明这个键值存在于哈希表里面,反之则没有;
  • 不处理:相信自己!(也可以用双哈希,但是容易 MLE)

序列 / 字符串哈希

假如有一个长度为 \(n\) 的序列 \(a\),那么我们设定一个进制值 \(b\),哈希值就可以这样计算:\(\sum\limits_{i=1}^{n}a_i\times b^i\)。这里在处理的时候可以使用无符号整数的自然溢出,其实就是相当于摸了一个很大的质数,不需要自己进行取模了。只需要最后取模即可。

模板

字符串哈希模板,其他的只需要改一下哈希函数就行了。

template <class Key, class Ty>
class HhashMap {
private:
  vector<pair<Key, Ty>> f[kMod];

  int hash(Key x) {
    unsigned v = 0;
    for (char c : x) {
      v = v * 128 + c;
    }
    return v % kMod;
  }

public:
  Ty &operator[](const Key &p) {
    int v = hash(p);
    for (auto &i : f[v]) {
      if (i.first == p) {
        return i.second;
      }
    }
    f[v].emplace_back(p, Ty());
    return f[v].back().second;
  }

  bool count(const Key &p) {
    int v = hash(p);
    for (auto &i : f[v]) {
      if (i.first == p) {
        return 1;
      }
    }
    return 0;
  }
};

习题

模板题

\(N\) 个字符串,问你有多少个字符串是不相等的。

这题看着非常简单,熟悉 STL 的基本上都会用 map 或者是 unordered_map 解决,但是其实 map 是红黑树,而 unordered_map 就是哈希表,所以还是老老实实手写哈希表吧!我们使用类把哈希表封装起来,然后面对冲突,我们就使用一种非常古老的方式:直接存进 vector 里面,然后暴力查找。

#include <iostream>
#include <vector>
#include <string>

using namespace std;

const int kMod = 114514 + 1919;

class HhashMap {
private:
  vector<pair<string, int>> f[kMod];

  int hash(string x) {
    unsigned v = 0;
    for (char c : x) {
      v = v * 128 + c;
    }
    return v % kMod;
  }

public:
  int &operator[](const string &p) {
    int v = hash(p);
    for (auto &i : f[v]) {
      if (i.first == p) {
        return i.second;
      }
    }
    f[v].emplace_back(p, int());
    return f[v].back().second;
  }
};

int n, ans;
string s;
HhashMap f;

int main() {
  for (cin >> n; n; n--) {
    cin >> s;
    if (!f[s]) {
      f[s] = 1, ans++;
    }
  }
  cout << ans << '\n';
  return 0;
}

点名

班上有 \(n\) 个人,现在教练报了 \(m\) 个名字。对于每一个名字,如果这个名字正确并且第一次出现,那么输出 OK;如果没有这个人,就输出 WRONG;如果名字正确但是不是第一次报了那么就输出 REPEAT

这题也是一道模板题,我们把没有出现过的人标记成 \(0\),出现过的标记成 \(1\),多次出现的标记成 \(2\),那么这题就像切菜一样十分简单了。

#include <iostream>
#include <vector>
#include <string>

using namespace std;

const int kMod = 114514 + 1919;

class HhashMap {
private:
  vector<pair<string, int>> f[kMod];

  int hash(string x) {
    unsigned v = 0;
    for (char c : x) {
      v = v * 128 + c;
    }
    return v % kMod;
  }

public:
  int &operator[](const string &p) {
    int v = hash(p);
    for (auto &i : f[v]) {
      if (i.first == p) {
        return i.second;
      }
    }
    f[v].emplace_back(p, int());
    return f[v].back().second;
  }
};

int n, m;
string s;
HhashMap f;

int main() {
  for (cin >> n; n; n--) {
    cin >> s;
    f[s] = 1;
  }
  for (cin >> m; m; m--) {
    cin >> s;
    if (f[s] == 1) {
      cout << "OK\n";
      f[s] = 2;
    } else if (f[s] == 2) {
      cout << "REPEAT\n";
    } else {
      cout << "WRONG\n";
    }
  }
  return 0;
}

阅读理解

\(N\) 篇短文,每一篇短文都有长度 \(L\)。接下来有 \(M\) 个统计的生词,你需要输出在哪几篇短文中出现过。

这题我们可以使用双哈希的方式解决。对于一个字符串,我们可以做一遍字符串哈希,但是这题是有可能遇到冲突的时候的。我们就用两个模数来计算字符串哈希,使用两个 set 来记录哈希出来的值。那么判断一个值是否存在我们就可以首先求出这个值的两个哈希函数,只有当两个哈希值都在 set 里面出现过,那么这个字符串才是有可能在这篇短文里面出现过的。

#include <iostream>
#include <set>
using namespace std;
using ll = unsigned long long;

const ll kMaxN = 1e4 + 5, kMaxL = 5e3 + 5, kMod1 = 99999989, kMod2 = 9748417;

ll n, l, m, t1, t2;
string s;
set<ll> s1[kMaxN], s2[kMaxN];

inline ll calc(string &s, const ll p) {
  ll ans = 0;
  for (char c : s) {
    ans *= 129;
    ans += ll(c);
  }
  return ans % p;
}

int main() {
  cin >> n;
  for (int i = 1; i <= n; ++i) {
    cin >> l;
    for (int j = 1; j <= l; ++j) {
      cin >> s;
      s1[i].insert(calc(s, kMod1));
      s2[i].insert(calc(s, kMod2));
    }
  }
  cin >> m;
  for (int i = 1; i <= m; ++i) {
    cin >> s;
    t1 = calc(s, kMod1), t2 = calc(s, kMod2);
    for (int j = 1; j <= n; ++j) {
      if (s1[j].count(t1) && s2[j].count(t2)) {
        cout << j << ' ';
      }
    }
    cout << '\n';
  }
  return 0;
}

登录密码

\(N\) 个用户的密码,当一个用户的密码在另一个用户的密码中出现过的时候,那么这个用户就可以登陆上另一个用户的账户。你需要输出有多少对这种用户。注意自己登录自己不算。

还是一样,这题可以使用字符串哈希解决。首先,题目告诉了我们每一个字符串长度不超过 \(10\),那么我们就可以直接暴力枚举每一个字符串的子字符串。然后,我们将子字符串存到哈希表里面,当这个位置在以前已经有其他的存了的话,就说明他们两个是可以匹配的,我们就累加答案。最后我们要把答案减去 \(n\),因为要出去自己登录自己的情况。

#include <iostream>
#include <utility>
#include <vector>

using namespace std;

const int kMod = 114514 + 1919, kMaxN = 2e5 + 5;

template <class Key, class Ty>
class HhashMap {
private:
  vector<pair<Key, Ty>> f[kMod];

  int hash(Key x) {
    unsigned v = 0;
    for (char c : x) {
      v = v * 128 + c;
    }
    return v % kMod;
  }

public:
  Ty &operator[](const Key &p) {
    int v = hash(p);
    for (auto &i : f[v]) {
      if (i.first == p) {
        return i.second;
      }
    }
    f[v].emplace_back(p, Ty());
    return f[v].back().second;
  }

  bool count(const Key &p) {
    int v = hash(p);
    for (auto &i : f[v]) {
      if (i.first == p) {
        return 1;
      }
    }
    return 0;
  }
};

int n, ans;
string s[kMaxN];
HhashMap<string, pair<int, int>> f;

int main() {
  cin >> n;
  for (int i = 1; i <= n; i++) {
    cin >> s[i];
    f[s[i]].second++;
  }
  for (int i = 1; i <= n; i++) {
    for (int j = 0; j < s[i].size(); j++) {
      for (int k = 1; j + k <= s[i].size(); k++) {
        string t = s[i].substr(j, k);
        if (f.count(t)) {
          ans += f[t].second * (f[t].first != i);
          f[t].first = i;
        }
      }
    }
  }
  cout << ans - n << '\n';
  return 0;
}

项链

有长度为 \(n\) 的数列 \(a\)。你需要找到一个整数 \(k\),然后将 \(a\) 分成 \(\lfloor n\div k\rfloor\) 块,并求出不同的块的数量,然后求最大值。

这题可以使用序列哈希解决。首先我们需要知道,这题枚举可用的序列的时间复杂度是 \(\mathcal O(n\log_2 n)\) 的,但是做一遍序列哈希函数是需要 \(\mathcal O(n)\) 的时长的。因此我们的瓶颈就变成了如何 \(\mathcal O(1)\) 求哈希函数。

我们可以做一个前缀和,那么是 \(p_i=\sum\limits_{j=1}^{i}a_i\times b^j\),那么其实我们可以用一个奇妙的差分:那么就是如果要求 \([l,r]\) 内元素的哈希函数的话,我们可以计算 \(p_r-p_{(l-1)}\times b^{(r-l+1)}\)。那我们直接预处理出 \(b\) 的任意次方,那么我们就可以在 \(\mathcal O(n\log_2 n)\) 的时间之内求出答案了!

posted @ 2023-12-04 18:00  haokee  阅读(34)  评论(0)    收藏  举报