2024牛客暑期多校训练营7 D.Interval Selection(异或哈希+双指针)

原题链接:D.Interval Selection


题目大意:


给你一个长度为 n n n 的数组 a a a,定义一个区间 [ l , r ] [l,r] [l,r] 内的连续子数组为好的,当且仅当这个子数组内的所有元素 a l , a l + 1 , . . . , a r a_{l},a_{l+1},...,a_{r} al,al+1,...,ar 在当前区间中恰好出现了 k k k 次。

例如数组 [ 1 , 1 , 2 , 3 , 2 , 3 , 1 ] [1,1,2,3,2,3,1] [1,1,2,3,2,3,1] k = 2 k=2 k=2 时,区间 [ 1 , 2 ] , [ 3 , 6 ] , [ 1 , 6 ] [1,2],[3,6],[1,6] [1,2],[3,6],[1,6] 这几个区间是好的,因为每个元素 a i a_{i} ai 都只出现了两次。而区间 [ 1 , 3 ] [1,3] [1,3] 不是好的,因为值 2 2 2 只出现了一次,同理区间 [ 1 , 7 ] [1,7] [1,7] 不是好的因为值 1 1 1 出现了三次。

问你这个数组有多少个区间是好的。

解题思路:


题解给出了线段树做法,没怎么看懂,这里给出一个更方便的异或哈希做法。

我们知道一个数字异或两次得到的值为 0 0 0,比如 3 ⊕ 3 = 0 3 \oplus3=0 33=0 1 ⊕ 7 ⊕ 7 = 1 1 \oplus 7 \oplus 7=1 177=1,但其实我们可以扩展这个性质到任意 k k k 位数异或为 0 0 0

比如当 k = 5 k=5 k=5 时,我们想要当且仅当 a i a_{i} ai 数量为 5 5 5 的倍数时异或起来为 0 0 0,且其他任意情况个 a a a 异或起来一定不为 0 0 0,我们可以做类似这样的操作。

我们知道一个数字异或两次可以归零,那么我们按照如下方法将每个 a i a_{i} ai 重新赋值。

原数组: [ a , a , a , a , a , a , a ] [a,a,a,a,a,a,a] [a,a,a,a,a,a,a] 7 7 7 a i a_{i} ai

我们随意构造一个长度为 k k k 的随机数列 B B B [ 1 , 9 , 3 , 6 , 8 ] [1,9,3,6,8] [1,9,3,6,8]

之后,按照 a 1 = 1 ⊕ 9 , a 2 = 9 ⊕ 3 , . . . , a 5 = 8 ⊕ 1 , a 6 = 1 ⊕ 9 , . . . a_{1}=1 \oplus 9,a_{2}=9 \oplus 3,...,a_{5}=8 \oplus 1,a_{6}=1 \oplus 9,... a1=19,a2=93,...,a5=81,a6=19,... 的循环顺序给对应的 a i a_{i} ai 赋值,于是我们发现了如下的情况:

修改后数组: [ 1 ⊕ 9 , 9 ⊕ 3 , 3 ⊕ 6 , 6 ⊕ 8 , 8 ⊕ 1 , 1 ⊕ 9 , 9 ⊕ 3 ] [1 \oplus 9,9 \oplus 3,3 \oplus 6,6 \oplus 8,8 \oplus 1,1 \oplus 9,9 \oplus 3] [19,93,36,68,81,19,93]

我们任选一个恰好包含了 k k k 的倍数个 a i a_{i} ai 的区间,例如区间 [ 2 , 6 ] , [ 3 , 7 ] [2,6],[3,7] [2,6],[3,7],那么我们异或的答案会为 0 0 0,而数量不为 k k k 的倍数的区间,比如 [ 1 , 2 ] , [ 4 , 4 ] [1,2],[4,4] [1,2],[4,4] 异或起来一定不为 0 0 0

可以发现,这样选取 k k k a i a_{i} ai 出来时,我们构造的数列 B B B 中的每个数字都参与了异或两次,不过这个数列要足够散乱以保证错误率达到一个极低的概率便可认为无错。

解决了 k k k 个异或为 0 0 0 的情况,假前缀异或和为 S S S,我们考虑答案如何取得。

显然我们可以枚举 r r r 的同时找到 l l l 使得 S [ r ] ⊕ S [ l − 1 ] = 0 S[r] \oplus S[l-1]=0 S[r]S[l1]=0 [ l , r ] [l,r] [l,r] 内不存在某个数字 a i a_{i} ai 的出现次数 c n t [ a i ] > k cnt[a_{i}]>k cnt[ai]>k

保证 [ l , r ] [l,r] [l,r] 内不存在某个数字 a i a_{i} ai 的出现次数 c n t [ a i ] > k cnt[a_{i}]>k cnt[ai]>k,其实只要保证我们双指针移动的同时保证 c n t [ a r ] ≤ k cnt[a_{r}] \leq k cnt[ar]k 即可保证区间内满足情况。

值得注意的是:
[ l , r ] [l,r] [l,r] 内存在 c n t [ a i ] < k cnt[a_{i}]<k cnt[ai]<k 的情况也可能存在答案,例如当 [ l , r ] [l,r] [l,r] 内形成的子数组为 k = 3 , [ 1 , 1 , 3 , 2 , 2 , 3 , 3 , 2 ] k=3,[1,1,3,2,2,3,3,2] k=3,[1,1,3,2,2,3,3,2] 时,即使 c n t 1 = 2 cnt_{1}=2 cnt1=2 也存在 [ 3 , 2 , 2 , 3 , 3 , 2 ] [3,2,2,3,3,2] [3,2,2,3,3,2] 这个子数组为答案,因此不会影响结果计算,只需要保证正确性即可。(显然)

比如数组: k = 2 , [ 2 , 1 , 1 , 2 , 1 , 2 ] k=2,[2,1,1,2,1,2] k=2,[2,1,1,2,1,2] k = 3 , [ 1 , 3 , 3 , 1 , 1 , 3 ] k=3,[1,3,3,1,1,3] k=3,[1,3,3,1,1,3] 是合法的,因为其修改后的数组的值异或为 0 0 0

但是值得注意的是当合法区间 [ l , r ] [l,r] [l,r] 形如: k = 2 , [ 1 , 1 , 2 , 2 , 3 , 3 ] k=2,[1,1,2,2,3,3] k=2,[1,1,2,2,3,3] 时,我们不能单纯让答案加 1 1 1,因为对于当前的 r r r 而言存在 3 3 3 l l l 使得 [ 1 , 1 , 2 , 2 , 3 , 3 ] , [ 2 , 2 , 3 , 3 ] , [ 3 , 3 ] [1,1,2,2,3,3],[2,2,3,3],[3,3] [1,1,2,2,3,3],[2,2,3,3],[3,3] 三个 S [ r ] ⊕ S [ l − 1 ] = 0 S[r] \oplus S[l-1]=0 S[r]S[l1]=0

即我们要进行找到区间 [ l − 1 , r − 1 ] [l-1,r-1] [l1,r1] 内有多少个下标等于 S [ r ] S[r] S[r] 的计数操作。

我们可以对 S [ r ] S[r] S[r] 开一个 v e c t o r vector vector 存下标,然后二分有多少个下标 j j j 满足 l − 1 ≤ j ≤ r − 1 l-1\leq j \leq r-1 l1jr1 即可。

构造和双指针二分用 m a p map map 辅助查询的时间复杂度均为单次操作 O ( log ⁡ n ) O(\log n) O(logn),注意一些细节即可通过。

时间复杂度: O ( n log ⁡ n ) O(n \log n) O(nlogn)

#include <bits/stdc++.h>
#include <chrono>
#include <random>
#include <ctime>

using i64 = long long;
using u64 = unsigned long long;

void solve() {

    //随机数生成器
    std::mt19937_64 rng(std::chrono::steady_clock::now().time_since_epoch().count());

    int n, k;
    std::cin >> n >> k;

    std::vector<u64> A(n + 1);

    for (int i = 1; i <= n; ++i) {
        std::cin >> A[i];
    }

    i64 ans = 0;

    //特判 k = 1
    if (k == 1) {
        std::map<int, int> cnt;
        for (int l = 1, r = 1; r <= n; ++r) {
            ++cnt[A[r]];
            while (cnt[A[r]] > 1) {
                --cnt[A[l++]];
            }
            ans += r - l + 1;
        }
        std::cout << ans << "\n";
        return;
    }

    //前缀异或和
    std::vector<u64> S(n + 1);

    //统计数字 a 的出现次数(获得循环位置)
    std::map<int, int> cnt;
    //数字 a 的哈希值
    std::map<int, std::vector<u64>> hash;

    for (int i = 1; i <= n; ++i) {
        auto& V = hash[A[i]];
        if (V.size() == 0) {
            V.emplace_back(rng());
        }
        u64 value = V.back();
        if (V.size() < k) {
            V.emplace_back(rng());
            value ^= V.back();
        } else {
            int p = cnt[A[i]] % k;
            value = V[p] ^ V[(p + 1) % k];
        }
        ++cnt[A[i]];
        S[i] = S[i - 1] ^ value;//构建循环后的数组
    }

    //pos 记录 l - 1 <= 的 S[r]
    std::map<u64, std::vector<int>> pos;

    cnt.clear();
    pos[S[0]].emplace_back(0);

    for (int l = 1, r = 1; r <= n; ++r) {
        //保证 [l, r] 内所有数字 cnt[A[i]] <= k
        ++cnt[A[r]];
        while (cnt[A[r]] > k) {
            --cnt[A[l++]];
        }
        //边移动 r 边加入 S[r] 下标则二分大于等于 l - 1 的即可
        pos[S[r]].emplace_back(r);
        auto& P = pos[S[r]];
        ans += P.end() - lower_bound(P.begin(), P.end(), l - 1) - 1;
    }

    std::cout << ans << "\n";
}

signed main() {

    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);

    int t = 1;
    std::cin >> t;
    while (t--) {
        solve();
    }

    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柠檬味的橙汁

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值