前言:
利用哈希的思想解决一类统计序列上合法子数组个数的题目.这里给出三题供参考。
一.Codeforces-ER-66-F. The Number of Subpermutations
题目大意:
给你一个长度为 n n n的序列.问你有多少个子数组 S S S满足其是一个从 1 1 1到 ∣ S ∣ |S| ∣S∣的排列.
1 ≤ a i ≤ n ≤ 3 e 5 1 \leq a_i \leq n \leq 3e5 1≤ai≤n≤3e5
题目思路:
问题1:如何 O ( 1 ) O(1) O(1)判断某一个子数组 ∣ S ∣ |S| ∣S∣是一个排列?
将 1 1 1到 n n n随机的一一映射到 l o n g l o n g long \ long long long值域内,形成新的映射数组 b b b。然后根据异或的特点,我们只需要判断
b 1 ⊕ b 2 ⊕ . . . ⊕ b ∣ S ∣ = = b a l ⊕ b a l + 1 ⊕ . . . ⊕ b a r b_1 \oplus b_2 \oplus ...\oplus b_{|S|} ==b_{a_l}\oplus b_{a_{l+1}} \oplus ...\oplus b_{a_r} b1⊕b2⊕...⊕b∣S∣==bal⊕bal+1⊕...⊕bar即可。
关键点:随机化+异或
1.为何要异或:忽略顺序
2.为何要随机,因为若不随机,二进制位数有限,会存在一些情况使得异或并不正确.
例如: 1 ⊕ 2 ⊕ 3 ⊕ 4 ⊕ 5 ⊕ 6 ⊕ 7 = 1 ⊕ 2 ⊕ 3 ⊕ 4 ⊕ 5 ⊕ 4 ⊕ 5 1\oplus 2\oplus 3\oplus 4\oplus 5\oplus 6\oplus 7 =1\oplus 2\oplus 3\oplus 4 \oplus 5\oplus 4\oplus 5 1⊕2⊕3⊕4⊕5⊕6⊕7=1⊕2⊕3⊕4⊕5⊕4⊕5
其实上述等式成立实质上就是等式两边每一个二进制位上的1的个数都相等。

那么当二进制位数变多了之后,发生错误的概率也会下降(但不会没有,只是可以近似看作没有)。
问题二:如何找到所有合法子数组?
考虑以下这几个 合法子数组 一定满足的条件:
1.序列含有且仅含有一个1.
2.合法子数组的最大值就是它的长度
3.最大值要么出现在1的左边,要么出现在1的右边.
那么我们遍历序列每一个1的位置,先假设最大值出现在1的右边,那么循环遍历它右边的位置,同时统计区间最大值。然后对于每一个位置,我们假设它就是某个合法子数组的右端点。然后确定左端点。再 O ( 1 ) O(1) O(1)hash判排列。然后再revese一下再跑一遍该算法即可。
AC代码:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define pii pair<int,int>
#define pb push_back
#define mp make_pair
#define vi vector<int>
#define vl vector<ll>
const int maxn = 3e5 + 5;
const int mod = 1e9 + 7;
ll bk[maxn] , a[maxn] , b[maxn] , p[maxn];
int main()
{
ios::sync_with_stdio(false);
int n; cin >> n;
srand(9904);
// 将n个数映射到一个ll范围下的桶中
for (int i = 1 ; i <= n ; i++){
bk[i] = ((ll)rand()<<30) + 1ll * (rand()<<16) + 1ll * rand();
p[i] = p[i - 1] ^ bk[i];
}
for (int i = 1 ; i <= n ; i++){
cin >> a[i];
b[i] = bk[a[i]] ^ b[i - 1];
}
ll ans = 0;
for (int t = 0 ; t < 2 ; t++){
for (int i = 1 ; i <= n ; i++)
b[i] = bk[a[i]] ^ b[i - 1];
ll now = -1 , mx = 0;
for (int i = 1 ; i <= n ; i++){
if (a[i] == 1){
ans += (t == 0);
now = i;
mx = 1;
continue;
}
if (now == -1) continue;
mx = max(mx , a[i]);
if (mx < (i - now + 1)) continue;
int x = now - (mx - (i - now + 1));
if (x <= 0) continue;
ans += ((b[i] ^ b[x - 1]) == p[mx]);
}
reverse(a + 1 , a + 1 + n);
}
cout << ans << endl;
return 0;
}
二.牛客练习赛69 E.子串
题目大意:
给你一个长度为 n n n的序列.问你有多少个子数组 [ L , R ] [L,R] [L,R]满足其是一个从 L L L到 R R R的排列.
题目思路:
还是考虑随机化+哈希的方法。思考存在什么等式。
我们发现可以与下标建立关系,即令 a [ i ] : = a [ i ] ⊕ i a[i]:=a[i]\oplus i a[i]:=a[i]⊕i
那么上述条件就转化为了:求一个子数组使得其异或值为0。然后根据上一题讲过的理由。我们还是随机化+哈希,求前缀异或值。丢到map里扫一遍统计一下即可。
三.Codeforces-ER-95-G. Three Occurrences
题目大意:
给你一个长度为 n n n的序列。问你有多少个子数组它里面每个数恰好出现三次.
题目思路:
这个题比较精妙。从暴力的差分角度考虑。
对于某一个前缀 j j j.统计每个数出现的次数模三得到数组 o c c [ j ] = { f j , 1 , . . . , f j , n } , f j , k 代 表 前 缀 j 中 值 k 出 现 的 次 数 occ[j]=\{f_{j,1},...,f_{j,n}\},f_{j,k}代表前缀j中值k出现的次数 occ[j]={ fj,1,...,fj,n},fj,k代表前缀j中值k出现的次数
对于某一个前缀 i i i.统计每个数出现的次数模三得到数组 o c c [ i ] occ[i] occ[i]
若 o c c [ i ] = = o c c [ j ] occ[i]==occ[j] occ[i]==occ[j]那么找到一个合法子数组 [ i , j ] [i,j] [i,j]
但是这么做时间空间复杂度为: O ( n 2 ) O(n^2) O(n2).
那么我们考虑用哈希压缩数组至一个整数,空间复杂度减小,且这样能够使得 O ( 1 ) O(1) O(1)判断两个数组是否全等。(类似字符串hash的思想).
如何压缩,原理和上述两题是一样的,也类似字符串hash.我们令 o c c [ x ] = ∑ i = 1 n C i ∗ f x , i occ[x]=\sum_{i=1}^{n}C_i*f_{x,i} occ[x]=∑i=1nCi∗fx,i.其中 C i C_i Ci是一个长整型范围内的数.
直接这么求还是不太行,时间复杂度过不去,但是我们可以 O ( 1 ) O(1) O(1)的从 o c c [ x ] occ[x] occ[x]计算转移到 o c c [ x + 1 ] occ[x+1] occ[x+1].具体看代码。
所以大致做法就是,枚举合法子数组的右端点,将之前所有前缀丢入 m a p map map中当作潜在左端点。查表计算即可。
这里还得解决一个问题:
我们现在求出来的只是,其里面里每个数出现次数是 3 3 3的倍数的那些子数组。还不完全对。
所以我们还得利用类似尺取法的思想动态维护一个左指针 l l l(其左边所有的前缀都不在map里,因为不合法,右边一直到当前指针 i i i都在map里).若区间 [ l , i ] [l,i] [l,i]内某个数出现了超过三次,那么左端点以左都不可能是合法子数组了。所以我们就得将左端点右移,同时从map中删除这个前缀.
AC代码:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define pii pair<int,int>
#define pb push_back
const int maxn = 5e5 + 5;
ll a[maxn] , ha[maxn] , val[maxn] ,occ[maxn];
unordered_map<ll,int> mp;
queue<int> Q[maxn];
int main()
{
ios::sync_with_stdio(false);
int n; cin >> n;
for (int i = 1 ; i <= n ; i++){
cin >> a[i];
val[i] = ((ll)rand()<<30) + 1ll * (rand()<<16) + 1ll * rand();
}
ll ans = 0;
int st = 0;
ha[0] = 0;
mp[0ll]++;
for (int i = 1 ; i <= n ; i++){
if (Q[a[i]].size() == 3){
int g = Q[a[i]].front();Q[a[i]].pop();
while (st < g) mp[ha[st++]]--;
}
Q[a[i]].push(i);
ha[i] = ha[i - 1] - occ[a[i]] * val[a[i]];
occ[a[i]]++; occ[a[i]] %= 3;
ha[i] += occ[a[i]] * val[a[i]];
ans += mp[ha[i]];
mp[ha[i]]++;
}
cout << ans << endl;
return 0;
}