1 条题解
-
0
一、题意理解
我们有一个长度为 的数组 。
定义“好区间”:区间内每个数字出现的次数都是奇数。
要求统计所有好区间的个数。
二、样例分析
样例:
n=4 A = [2, 2, 2, 3]好区间:
- 长度1: [2], [2], [2], [3] → 4个
- 长度2: [2,2](两个2出现2次,偶数,不行),[2,2]不行,[2,3](2出现1次,3出现1次,都是奇数)→ 1个
- 长度3: [2,2,2](2出现3次,奇数)→ 1个
- 长度4: [2,2,2,3](2出现3次,3出现1次,都是奇数)→ 1个
总数 = 4 + 1 + 1 + 1 = 7 ✅
三、关键观察
设 表示区间 是否是好区间。
条件:对于每个数值 ,它在 中出现次数为奇数。
1. 奇偶性的表示
我们可以用一个二进制状态(bitmask)表示每个数值出现次数的奇偶性:
- 0 表示出现偶数次
- 1 表示出现奇数次
那么区间 是好区间 ⇔ 这个 bitmask 中所有 bit 都是 0?不对,是所有出现过的数值对应的 bit 都是 1?也不对,应该是:所有在区间中出现过的数值对应的 bit 都是 1,没出现过的数值对应的 bit 是 0。
更准确地说:好区间要求每个出现的数值出现奇数次,没出现的数值出现 0 次(偶数次)。所以整个 bitmask 必须等于“出现过的数值集合”对应的 mask。
2. 异或前缀和
设 表示前 个元素的异或和(每个数值映射到一个随机 bit 或特定 bit)。
但这里“异或”只能记录奇偶性变化,不能直接判断“所有出现过的数值都是奇数次”。
实际上,如果区间 是好区间,那么对于任意数值 :
- 如果 在区间中出现过,那么出现次数是奇数 ⇒ 在 中, 对应的 bit 是 1
- 如果 没出现过,那么出现次数是 0(偶数)⇒ 在 中, 对应的 bit 是 0
所以条件等价于: 中所有为 1 的 bit 对应的数值都在区间中出现过,并且所有在区间中出现的数值对应的 bit 都是 1。
这其实意味着: 必须等于“区间中出现的数值集合”对应的 mask。
3. 更简单的等价条件
设 是 的异或前缀和数组:,。
那么区间 的异或和 。
这个异或和表示的是:在区间中出现奇数次的数值的集合(因为出现偶数次的会抵消)。
好区间要求:每个在区间中出现的数值都出现奇数次 ⇒ 区间中出现的数值集合正好等于 中为 1 的 bit 对应的数值集合。
换句话说:设 ,那么区间中出现的数值集合必须等于 对应的数值集合。
四、问题转化
我们需要统计 满足:
- 区间 中出现的数值集合正好是 对应的数值集合
1. 判定条件
如何判断区间 中出现的数值集合正好是 ?
等价于:
- 区间中出现的所有数值都在 中(即没有不在 中的数值出现)
- 中的所有数值都在区间中出现过
2. 扫描线方法
我们可以枚举右端点 ,维护左端点 的可行性。
对于固定的 , 随 变化。
一个关键性质:好区间的所有前缀也是好区间?不一定。
五、已知高效解法
这是一个经典问题:统计“每个数值出现次数都是奇数”的区间数。
解法:
- 给每个数值分配一个随机 64 位权值
- 定义前缀异或和 $P[i] = h(A[1]) \oplus h(A[2]) \oplus \dots \oplus h(A[i])$,
- 区间 是好区间 ⇔ ,其中 是区间中出现的数值集合,
- 但是 未知,不过注意:如果区间是好区间,那么 其实就是 本身(因为每个出现过的数值出现奇数次,所以异或和就是这些数值权值的异或和)
- 所以条件变为: 必须等于“区间中出现的数值集合”的异或和,而区间中出现的数值集合正好是 中为 1 的 bit 对应的数值集合 ⇒ 这个总是成立?
这里有点循环论证。实际上,这个方法的正确性在于:如果给每个数值随机分配足够长的二进制权值,那么碰撞概率极小,我们可以认为 唯一确定了区间中出现的数值集合。
六、正确解法思路
我们想要:区间中每个数值出现奇数次 ⇒ 对于任意数值 ,要么出现 0 次,要么出现奇数次。
用异或前缀和 (每个数值映射到随机权值):
- 区间 的异或和
- 是区间中出现奇数次的数值的权值异或和
好区间要求:所有在区间中出现的数值都出现奇数次 ⇒ 区间中出现的数值集合正好是 对应的数值集合。
我们可以用另一个哈希函数 表示集合 的哈希值(比如用 Zobrist hashing,,其中 是另一个随机映射)。
那么条件变为: 由于 是权值异或和, 对应的数值集合就是 ,但 是随机权值,我们无法直接得到 对应的数值集合。
七、实用方法
实际上,已知的简单且正确的做法是:
- 给每个数值分配一个随机 64 位权值
- 计算前缀异或和
- 区间 是好区间 ⇔ $pre[r] \oplus pre[l-1] = \bigoplus_{v \in \text{出现的数值集合}} r(v)$
- 但是 正好等于 ,但是不一定,因为 是区间中所有数值的权值异或和,出现偶数次的会抵消,出现奇数次的会保留。好区间要求所有出现过的数值都出现奇数次,所以 正好是“出现过的数值集合”的异或和。
- 因此,条件简化为: 必须等于“区间中出现的数值集合”的异或和,而这是自动满足的,因为好区间中所有出现过的数值都出现奇数次。
八、最终可行解法
实际上,这个问题在 CF 等题库中有类似题,标准解法是:
- 给每个数值 分配随机 64 位权值
- 设
- 设 $total[i] = \bigoplus_{v \in \text{前i个元素中出现的数值集合}} h(v)$
- 那么区间 是好区间 ⇔ $pre[r] \oplus pre[l-1] = total[r] \oplus total[l-1]$
因为:
- 左边 = 区间中所有数值的权值异或和(出现偶数次的抵消)
- 右边 = 区间中出现的数值集合的异或和
- 相等意味着区间中每个出现的数值都出现奇数次(因为如果某个数值出现偶数次,左边会缺少它的权值,而右边有,就不相等)
九、算法步骤
- 随机生成 映射
- 计算 和
- 令
- 区间 是好区间 ⇔
- 问题转化为:统计 且 的对数
- 用哈希表对每个 值计数即可
复杂度 。
十、代码实现(C++)
//#define Plus_Cat "" #include <bits/stdc++.h> #define INF 0x3f3f3f3f #define ll long long #define ull unsigned ll #define RCL(a,b,c,d) memset(a,b,sizeof(c)*(d)) #define FOR(i,a,b) for(int i(a);i<=(int)(b);++i) #define DOR(i,a,b) for(int i(a);i>=(int)(b);--i) #define tomax(a,...) ((a)=max({(a),__VA_ARGS__})) #define tomin(a,...) ((a)=min({(a),__VA_ARGS__})) #define EDGE(g,i,x,y) for(int i=(g).h[(x)],y=(g)[(i)].v;~i;y=(g)[(i=(g)[i].nxt)>0?i:0].v) #define main Main();signed main(){ios::sync_with_stdio(0);cin.tie(0),cout.tie(0);return Main();}signed Main using namespace std; constexpr double C(0.9); constexpr int N(2e5 + 10), BL(448 * C + 10), BN(N / (BL - 10) + 10); mt19937_64 rng(random_device {}()); namespace IOEcat { #define isD(ch) ('0'<=(ch)&&(ch)<='9') #define DE(...) E(#__VA_ARGS__,__VA_ARGS__) struct Icat { inline char getc() { return getchar(); } template<class T>void operator()(T &x) { static bool sign(0); static char ch(0); x = 0, sign = 0; while (ch = getc(), !isD(ch)) if (ch == '-') sign = 1; do x = (x << 1) + (x << 3) + (ch ^ 48); while (ch = getc(), isD(ch)); if (sign) x = -x; } template<class T, class...Types>void operator()(T &a, Types &...args) { return (*this)(a), (*this)(args...); } } I; struct Ocat { inline void putc(const char c) { putchar(c); } template<class T>void operator()(T x, const char lst = '\n') { static int top(0); static char st[50]; if (x < 0) putc('-'), x = -x; do st[++top] = x % 10 ^ 48, x /= 10; while (x); while (top) putc(st[top--]); putc(lst); } template<class T, class...Types>void operator()(const T a, const char lst = '\n', const Types ...args) { return (*this)(a, lst), (*this)(args...); } } O; struct Ecat { template<class T>void operator()(const char *fmt, const T a) { cerr << fmt << ":" << a << ".\n"; } template<class T, class...Types>void operator()(const char *fmt, const T a, const Types ...args) { while (*fmt^',') cerr << *fmt++; return cerr << ':' << a << ", ", (*this)(++fmt, args...); } } E; } using namespace IOEcat; int n, m, it(1); int a[N], b[N], lst[N]; ll ans; ull hs_val[N]; namespace Sub4 { constexpr int M(64 + 10); list<int> lis; unordered_map<ull, int> cnt[M]; bool Check() { return m <= 64; } int Cmain() { ull pre(0); FOR(i, 1, n) { if (lst[a[i]]) { for (auto it(--lis.end()); ; --it) { if (*it == a[i]) { if (next(it) != lis.end()) { const int u(*it), v(*next(it)); if (cnt[u].size() > cnt[v].size()) swap(cnt[u], cnt[v]); for (auto x : cnt[u]) cnt[v][x.first] += x.second; cnt[u].clear(); } lis.erase(it++); break; } if (it == lis.begin()) break; } } lst[a[i]] = i, lis.push_back(a[i]), ++cnt[a[i]][pre]; ull cur(pre ^= hs_val[a[i]]); for (auto it(--lis.end()); ; --it) { cur ^= hs_val[*it]; if (cnt[*it].count(cur)) ans += cnt[*it][cur]; if (it == lis.begin()) break; } } O(ans, '\n'); return 0; } } namespace Divided_Block { short Bl, Bn; short bel[N]; int st[BN], en[BN]; ull val[N], tag[BN]; struct Hash_Table { #define Mod 2047 short tot, h[Mod + 1]; struct node { short nxt, sum; ull cur; node(short nxt = 0, short sum = 0, ull cur = 0): nxt(nxt), sum(sum), cur(cur) {} } tr[BL]; void Clear() { FOR(i, 1, tot)h[tr[i].cur & Mod] = 0; tot = 0; } void Plus(ull x) { for (short i(h[x & Mod]); i; i = tr[i].nxt) if (x == tr[i].cur) return ++tr[i].sum, void(); return tr[++tot] = node(h[x & Mod], 1, x), h[x & Mod] = tot, void(); } short Count(ull x) { for (short i(h[x & Mod]); i; i = tr[i].nxt) if (x == tr[i].cur) return tr[i].sum; return 0; } #undef Mod } hh[BN]; void Build() { Bl = ceil(sqrt(n) * C), Bn = (n - 1) / Bl + 1; FOR(i, 1, Bn) { st[i] = en[i - 1] + 1, en[i] = min(en[i - 1] + Bl, n); FOR(j, st[i], en[i])bel[j] = i; } } void Xor(int x, ull d) { if (!x) return; const short &bx(bel[x]); FOR(i, 1, bx - 1)tag[i] ^= d; FOR(i, st[bx], x)val[i] ^= d; hh[bx].Clear(); FOR(i, st[bx], min(it, en[bx]))hh[bx].Plus(val[i]); } int Sum() { int sum(0); FOR(i, 1, bel[it])sum += hh[i].Count(tag[i]); return sum; } } using namespace Divided_Block; namespace Sub { int Cmain() { for (it = 1; it <= n; ++it) hh[bel[it]].Plus(tag[bel[it]]), Xor(lst[a[it]], hs_val[a[it]]), ans += Sum(), lst[a[it]] = it; O(ans, '\n'); return 0; } } signed main() { #ifdef Plus_Cat freopen(Plus_Cat ".in", "r", stdin), freopen(Plus_Cat ".out", "w", stdout); #endif I(n), Build(); FOR(i, 1, n)I(a[i]), b[i] = a[i]; sort(b + 1, b + n + 1), m = unique(b + 1, b + n + 1) - b - 1; FOR(i, 1, m)hs_val[i] = rng(); FOR(i, 1, n)a[i] = lower_bound(b + 1, b + m + 1, a[i]) - b; if (Sub4::Check()) return Sub4::Cmain(); return Sub::Cmain(); }
十一、总结
本题的关键在于:
- 将“每个数值出现奇数次”的条件转化为异或等式。
- 使用随机权值哈希避免冲突。
- 利用前缀异或和和集合异或和的关系。
- 通过哈希表计数快速统计满足条件的区间数。
- 1
信息
- ID
- 4987
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者