神仙哈希法-三题小结

前言:

利用哈希的思想解决一类统计序列上合法子数组个数的题目.这里给出三题供参考。


一.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 1ain3e5

题目思路:

问题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} b1b2...bS==balbal+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 1234567=1234545

其实上述等式成立实质上就是等式两边每一个二进制位上的1的个数都相等。

扫描二维码关注公众号,回复: 12901994 查看本文章

那么当二进制位数变多了之后,发生错误的概率也会下降(但不会没有,只是可以近似看作没有)。

问题二:如何找到所有合法子数组?

考虑以下这几个 合法子数组 一定满足的条件:
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,kjk

对于某一个前缀 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=1nCifx,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;
}

猜你喜欢

转载自blog.csdn.net/qq_35577488/article/details/114013592