0%

[Trick] 随机化的 Hashing

基础思想

异或哈希用到了异或的性质:相同数字异或为 0。

引入随机数的目的是做哈希运算的时候减小哈希碰撞,在使用 mt19937_64 的前提下,随机数强度足以避免哈希碰撞。并且,异或的上述性质可以保证当两侧的哈希相同的时候,运算结果恒定为 0。

异或哈希主要在面对 set 判断等价的时候使用。

还有前缀和哈希,主要是在面对 multiset(也可以说是 vector ) 判断相等的时候使用。

以 CF2014H 为例

思路

经过读题可以发现,警长作为后手压根没有必胜策略,只要罗宾每次打最大的气球,最后的得分一定会大于等于警长的得分。

那么考虑平局的情况,不难发现警长只有和罗宾先后手打同一得分的气球才能打成平手,也就是对于每种在区间 $[l,r]$ 的得分为 $x$ 的气球,必须存在偶数个。

这个问题乍一看很简单,异或前缀和以后求 $[l,r]$ 区间异或是否为 $0$ 就行了,但是实际操作的时候,不难发现由于给出数字比较小,很容易出现碰撞产生错误结果的情况,于是需要对每一个数字进行随机化映射。例如随机化数组为 $ [1930204585,12390125,485989234,1239049102349012]$,则源输入数据的 $[0,1,2,3]$ 分别对应了随机数组中的三个数,他们的 $xor$ 结果就不会为 $0$ 了。

这就是随机化哈希的思想。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using i64 = long long;
using u64 = unsigned long long;
mt19937_64 rng(std::chrono::steady_clock::now().time_since_epoch().count());
constexpr int MAXN = 1e6 + 10;
vector<int> rvec(MAXN);
void init()
{
for (auto &v : rvec)
v = rng();
}
void solve()
{
int n, q;
cin >> n >> q;
vector<i64> vec(n + 1);
vector<u64> pref(n + 1);
for (int i = 1; i <= n; i++)
cin >> vec[i], pref[i] = pref[i - 1] ^ rvec[vec[i]];
while (q--)
{
int l, r;
cin >> l >> r;
cout << (pref[l - 1] ^ pref[r] ? "NO" : "YES") << endl;
}
}

另一个例子:ABC367F

本题来自 @Sun_M ,非常感谢!

思路

这个题目的要求是分别选取数组 $a,b$ 的一段区间,问能否让这两个区间重排后等价。

重排后等价,则两者的数组和应该长度相等并且和相等,考虑用前缀和(注意,此处的数组可能存在多个重复元素,用 xor hash 不可行)。

用随机数将数字打乱避免碰撞即可。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
mt19937_64 rng(std::chrono::steady_clock::now().time_since_epoch().count()); 
using i64 = long long;
constexpr int MAXN = 2e5 + 10;
vector<i64> rnd(MAXN);
void init()
{
for (auto &v : rnd)
v = rng();
}
void solve()
{
int n, q, l1, r1, l2, r2;
cin >> n >> q;
vector<int> a(n), b(n);
vector<i64> pa(n + 10), pb(n + 10);
for (auto &v : a)
cin >> v;
for (auto &v : b)
cin >> v;
for (int i = 1; i <= n; i++)
pa[i] = pa[i - 1] + rnd[a[i - 1]], pb[i] = pb[i - 1] + rnd[b[i - 1]];
while (q--)
{
cin >> l1 >> r1 >> l2 >> r2;
cout << (((pa[r1] - pa[l1 - 1]) == (pb[r2] - pb[l2 - 1]) && r1 - l1 == r2 - l2) ? "Yes" : "No") << endl;
}
}

其他题目

感谢 @Floating-Ocean 提供的练手题目!非常感谢!

CF1996G

tbd.

CF1977D

思路

题目要求反转行后特殊列的最大数量。

如果 $(i,j)$ 是第 $j$ 列唯一一个 $1$ ,则除了第 $i$ 行以外,其他所有行第 $j$ 列为 $1$ 的要变换为 $0$,原本就是 $0$ 的不能变换。

也就是,确定 $grid[i][j]=1$ 后,所有行的变换就固定了。

朴素的做法需要暴力统计,显然是不可能通过的。

但是,对于一种变换方案,他所产生的只有一个列是固定的,而对于列枚举其唯一位置的时候,变换方案也是固定的。他们之间有映射关系,于是可以用双哈希进行映射。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
vector<vector<i64>> randval(2, vector<i64>(3e5 + 5));
void init()
{
for (auto &vec : randval)
for (auto &v : vec)
v = rng();
}
void solve()
{
int n, m;
string s;
cin >> n >> m;
vector<vector<int>> grid(n, vector<int>(m));
for (int i = 0; i < n; i++)
{
cin >> s;
for (int j = 0; j < m; j++)
grid[i][j] = s[j] - '0';
}
map<pair<long long, i64>, int> ans;
int res = 0;
pair<int, int> ind_ans = {0, 0};
for (int j = 0; j < m; ++j)
{
i64 sum1 = 0, sum2 = 0;
for (int i = 0; i < n; ++i)
sum1 ^= grid[i][j] * randval[0][i], sum2 ^= grid[i][j] * randval[1][i];
for (int i = 0; i < n; ++i)
{
sum1 ^= randval[0][i], sum2 ^= randval[1][i];
ans[{sum1, sum2}]++;
if (res < ans[{sum1, sum2}])
res = ans[{sum1, sum2}], ind_ans = {j, i};
sum1 ^= randval[0][i], sum2 ^= randval[1][i];
}
}
cout << res << "\n";
string inds(n, '0');
for (int i = 0; i < n; ++i)
if (grid[i][ind_ans.first])
{
if (i != ind_ans.second)
inds[i] = '1';
}
else if (ind_ans.second == i)
inds[i] = '1';
cout << inds << "\n";
}