【P9216】写大作业
【HASH】【P9216】写大作业
磕头:
出题时不知道新科技,被随机 hash 老哥直接O(q) 爆标了,成了典题。这篇题解是 std 的启发式合并做法。
Analysis
以下设
首先思考『相似』的定义,容易发现数列
于是考虑维护每个数字在每个数列里的出现次数,对每个数列开一个 map 存一下即可。如果两个数列对应的 map 相同,那么它们相似。
考虑合并两个数列,就是合并两个 map。注意到题意是合并完一个数列以后被合并的数列就被扔掉了,自然考虑启发式合并。合并两个 map 时,枚举较小的 map 里的元素,然后暴力做单点插入到另一个 map 中。注意到一个数字每被枚举到一次,它所属的集合大小至少翻倍,所以至多被枚举
接下来处理判定两个 map 相同,考虑 hash。为了迎合启发式合并的单点插入,我们的 hash 函数必须能直接计算单个数字的贡献。std 的做法是:对一个 map 定义 hash 函数
在合并两个集合时,假设把 map
数据保证了随机,没有卡自然溢出,直接用 unsigned long long 自然溢出 hash 值即可。
这样,我们可以在做启发式合并的同时
Code
#include "assert.h"
#include <map>
#include <iostream>
const int maxn = 100005;
const int maxJ = 5;
typedef unsigned long long int ull;
ull mpow(ull x, int y) {
ull ret = 1;
while (y) {
if (y & 1) ret *= x;
x *= x;
y >>= 1;
}
return ret;
}
ull hash[maxJ][maxn];
std::map<int, int> rec[maxn];
bool oc[maxn];
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, q;
std::cin >> n >> q;
for (int i = 1, m, x; i <= n; ++i) {
for (std::cin >> m; m; --m) {
std::cin >> x;
++rec[i][x];
}
for (int j = 0; j < maxJ; ++j) {
for (auto &[x, y] : rec[i]) {
hash[j][i] += mpow(x, j * y);
}
}
}
int ans = 0;
for (int i = 1, o, x, y; i <= q; ++i) {
std::cin >> o >> x >> y;
if (o == 2) {
bool ret = true;
for (int j = 0; j < maxJ; ++j) if (hash[j][x] != hash[j][y]) {
ret = false; break;
}
if (ret) ans ^= i;
} else {
assert(oc[x] == false);
oc[x] = true;
if (rec[x].size() > rec[y].size()) {
rec[x].swap(rec[y]);
for (int j = 0; j < maxJ; ++j) std::swap(hash[j][x], hash[j][y]);
}
for (auto &[u, v] : rec[x]) {
if (rec[y][u]) for (int j = 0; j < maxJ; ++j) hash[j][y] -= mpow(u, j * rec[y][u]);
for (int j = 0; j < maxJ; ++j) hash[j][y] += mpow(u, v * j);
}
}
}
std::cout << ans << '\n';
}