本文主要是介绍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 3⊕3=0, 1 ⊕ 7 ⊕ 7 = 0 1 \oplus 7 \oplus 7=0 1⊕7⊕7=0,但其实我们可以扩展这个性质到任意 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=1⊕9,a2=9⊕3,...,a5=8⊕1,a6=1⊕9,... 的循环顺序给对应的 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] [1⊕9,9⊕3,3⊕6,6⊕8,8⊕1,1⊕9,9⊕3]
我们任选一个恰好包含了 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[l−1]=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[l−1]=0。
即我们要进行找到区间 [ l − 1 , r − 1 ] [l-1,r-1] [l−1,r−1] 内有多少个下标等于 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 l−1≤j≤r−1 即可。
构造和双指针二分用 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 = 1if (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;
}
这篇关于2024牛客暑期多校训练营7 D.Interval Selection(异或哈希+双指针)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!