异或哈希可以用来快速查询序列中的数是否出现了次。
异或哈希入门:区间集合判同
问题:如何判断一个序列是否是另一个序列排列组合,也就是排序后是否相同,或者说元素的集合是否相同(不关心顺序)。
由异或的性质(交换律),可以知道,一个序列如果排序后和另一个序列相等,这两个序列的异或结果相同,相当于一个哈希了。但实际上还有一个问题:排序后相同的序列,异或结果一定相同;但是异或结果一定相同的序列,不一定排序后相同。
这个冲突的概率如何呢?假设序列长度为,每种元素都由到的一个数编码,异或的结果也是。序列长度为,排序后不同的类型序列数的规模最大项是的,映射到的哈希空间的最大项是的,肯定冲突。
如果想要用类似的方式的方式来快速判断,需要想办法扩大哈希空间。因此选用到的数编码,这样哈希空间的最大项就到了,可以很大程度上减少碰撞概率。例如,的时候,碰撞概率是
碰撞的概率还是很小的,因此这种哈希方式是可行的。出于保险还可以选择双哈希。
代码(这题没有找到能交的oj测试过,本地测了一下应该对):
typedef unsigned long long ull;
mt19937_64 rnd(time(0));unordered_map<int, ull> XORHash_map[2];
struct XORHashing { int len; vector<int> s; vector<ull> hash[2]; // 哈希值
void init(const vector<int>& str){ s=str; len=s.size(); ull xort; for (int p=0;p<2;p++) { hash[p].resize(len+1); hash[p][0]=0; for (int i=1;i<=len;i++) { if (XORHash_map[p].find(s[i-1])==XORHash_map[p].end()) XORHash_map[p].insert(make_pair(s[i-1],rnd())); xort=XORHash_map[p][s[i-1]]; hash[p][i]=hash[p][i-1]^xort; } } } // 获取子字符串[s, e]的异或哈希值,序号从0开始 pair<ull, ull> get_hash(int s, int t) { ull hash1=(hash[0][t+1]^hash[0][s]); ull hash2=(hash[1][t+1]^hash[1][s]); return {hash1, hash2}; }};其中,mt19937_64是基于 Mersenne Twister 算法的伪随机数生成器,专门生成 64 位宽的随机数,具有较长周期()。
区间元素出现次数
给定一个序列,判断指定区间每个元素出现次数是否都是偶数次(2的倍数),可以用异或前缀和实现,判断异或前缀和是否是0即可。那如果要判断每个元素出现次数是否都是3或3的倍数呢?或者出现次数都是或的倍数呢?
考虑实现进制异或,异或其实就是不带进位的加法,因此模拟一遍即可,只不过比起二进制要多一个的常数。
更多问题中,要求的不是或的倍数,而是要求判断区间元素是否精准是次。精准是次,我们只需要解决一个问题:维护区间元素出现次数不多于的区间即可。可以从左到右以1的步长移动右端点,同时记录以当前右端点,区间元素出现次数不多于的区间左端点极限,同时用个map维护一下相同异或前缀和即可。
简而言之,也就是双指针加异或前缀和,利用进制异或的性质来维护双指针区间内有多少个满足条件的端点。
codeforces - 1418 - G(出现3次)
给定一个由 个正整数构造的数组 . 我们说子数组 也就是 () 是好的,当且仅当子数组中的每个元素都恰好出现3次。例如 有3个好的子数组:
- ;
- ;
- .
计算给定的数组 的好的子数组的数目。
数据规模:
#include <bits/stdc++.h>using namespace std;
//#undef LOCAL_DEBUG#ifdef LOCAL_DEBUG#include "debug.h"#else#define debug(...) (void)0#define debug_array(arr, len) (void)0#endif
typedef unsigned long long ull;typedef long long ll;typedef pair<int,int> P;const int maxn=5e5;
int n;int a[maxn+5];
#define KNUM (3) // K=3进制#define KLEN (64) // 用K进制64位数表示typedef array<int, KLEN> Num;Num operator ^(const Num &a, const Num &b){ Num c; for (int i=0;i<KLEN;i++){ c[i]=a[i]+b[i]; if (c[i]>=KNUM) c[i]-=KNUM; } return c;}mt19937 rnd(time(0));Num gen(void){ // 生成一个随机的K进制数 Num ret; for (int i=0;i<KLEN;i++) ret[i]=rnd()%KNUM; return ret;}
struct XORHashing { vector<Num> hash; // 初始化哈希[0,n] XORHashing(int n){ hash.resize(n+1); for (int i=0;i<=n;i++) hash[i]=gen(); } Num get_hash(int x){ return hash[x]; }};
void solve(void){ cin>>n; for (int i=1;i<=n;i++) cin>>a[i]; XORHashing xh(n);
vector<vector<int> > pos(n+1); // 存储每个数字出现的位置 vector<Num> pre(n+1); // 前缀和,初始只有0 map<Num,int> cnt; // 存储每个前缀和出现的次数 int l=0; ll ans=0; cnt[pre[0]]=1; for (int i=1;i<=n;i++){ pre[i]=pre[i-1]^xh.get_hash(a[i]); pos[a[i]].push_back(i); if (pos[a[i]].size()>=(KNUM+1)){ int target=pos[a[i]][pos[a[i]].size()-(KNUM+1)]; while (l<target) --cnt[pre[l++]]; } ans+=cnt[pre[i]]; ++cnt[pre[i]]; } cout<<ans<<endl;}
int main(){ std::ios::sync_with_stdio(false); std::cin.tie(nullptr); std::cout.tie(nullptr); int t=1;// cin>>t; while (t--) solve(); return 0;}2024牛客暑期多校7 - D(出现次)
题目和前面类似,但是由次改成了任意次,同时数据规模也从变成了。
对于改成任意次,只需要将KNUM进制改成变量即可:
// #define KNUM (3) // K=3进制#define KLEN (64) // 用K进制64位数表示int KNUM=3;对于的数据规模扩大,只需要离散化即可,这里可以预处理,把每个数都映射回,因为我们不关心具体的值,只关心是否相等。
/* 对x数组做离散化重映射 *//**例如,输入 1 1 4 5 1 4修改为 1 1 2 3 1 2*/void prepare(int x[], int len){ int b[len]; memcpy(b,x,sizeof(b)); sort(b,b+len); unordered_map<int,int> tag; int cnt=1; tag.insert(make_pair(b[0],1)); for (int i=1;i<len;i++) if (b[i]!=b[i-1]) tag.insert(make_pair(b[i],++cnt)); for (int i=0;i<len;i++) x[i]=tag[x[i]];}完整代码:
#include <bits/stdc++.h>using namespace std;
//#undef LOCAL_DEBUG#ifdef LOCAL_DEBUG#include "debug.h"#else#define debug(...) (void)0#define debug_array(arr, len) (void)0#endif
typedef unsigned long long ull;typedef long long ll;typedef pair<int,int> P;const int maxn=5e5;
int n;int a[maxn+5];
// #define KNUM (3) // K=3进制#define KLEN (64) // 用K进制64位数表示int KNUM=3;typedef array<int, KLEN> Num;Num operator ^(const Num &a, const Num &b){ Num c; for (int i=0;i<KLEN;i++){ c[i]=a[i]+b[i]; if (c[i]>=KNUM) c[i]-=KNUM; } return c;}mt19937 rnd(time(0));Num gen(void){ // 生成一个随机的K进制数 Num ret; for (int i=0;i<KLEN;i++) ret[i]=rnd()%KNUM; return ret;}
struct XORHashing { vector<Num> hash; // 初始化哈希[0,n] XORHashing(int n){ hash.resize(n+1); for (int i=0;i<=n;i++) hash[i]=gen(); } Num get_hash(int x){ return hash[x]; }};
void prepare(int x[], int len){ int b[len]; memcpy(b,x,sizeof(b)); sort(b,b+len); unordered_map<int,int> tag; int cnt=1; tag.insert(make_pair(b[0],1)); for (int i=1;i<len;i++) if (b[i]!=b[i-1]) tag.insert(make_pair(b[i],++cnt)); for (int i=0;i<len;i++) x[i]=tag[x[i]];}
void solve(void){ cin>>n>>KNUM; for (int i=1;i<=n;i++) cin>>a[i]; prepare(a+1,n); XORHashing xh(n);
vector<vector<int> > pos(n+1); // 存储每个数字出现的位置 vector<Num> pre(n+1); // 前缀和,初始只有0 map<Num,int> cnt; // 存储每个前缀和出现的次数 int l=0; ll ans=0; cnt[pre[0]]=1; for (int i=1;i<=n;i++){ pre[i]=pre[i-1]^xh.get_hash(a[i]); pos[a[i]].push_back(i); if (pos[a[i]].size()>=(KNUM+1)){ int target=pos[a[i]][pos[a[i]].size()-(KNUM+1)]; while (l<target) --cnt[pre[l++]]; } ans+=cnt[pre[i]]; ++cnt[pre[i]]; } cout<<ans<<endl;}
int main(){ std::ios::sync_with_stdio(false); std::cin.tie(nullptr); std::cout.tie(nullptr); int t=1; cin>>t; while (t--) solve(); return 0;}其他题(TODO):
- https://codeforces.com/problemset/problem/1175/F:查询是否只出现一次
- https://codeforces.com/problemset/problem/869/E
- https://codeforces.com/problemset/problem/1418/G
- https://codeforces.com/contest/895/problem/C
参考: