1 条题解
-
0
C. Eri 与扩展集合 详细题解
问题重述
给定一个数组 ,定义子数组 是精彩的,如果将其中的元素放入集合后,可以通过有限次操作(选择两个不同元素 ,若 是正整数且不在集合中,则加入它)使集合变成连续的(即排序后相邻元素差为 )。统计精彩子数组的数量。
关键观察
观察 1:最终集合的性质
设最终连续集合为 (已排序)。定义差分数组:
性质 1:每个 必须是奇数。
证明:如果某个 是偶数,设 ,则 。那么 是正整数且介于两者之间。由于最终集合是连续的,这个数必须已经在集合中,矛盾。因此 不能是偶数。
性质 2:相邻的 不能不同。
证明:假设 ,且两者都是奇数。则:
这是偶数。那么 是整数,且不等于 (因为 ),这个数应该存在于连续集合中,矛盾。
结论:所有 相等且为奇数。因此最终集合是一个公差为奇数的等差数列。
观察 2:操作的本质
操作相当于在集合中插入两个数的中点。这类似于在数轴上不断取中点填充空隙。实际上,给定初始集合,能生成的数集是初始数的所有整数线性组合:
$$\{ \sum \lambda_i a_i \mid \lambda_i \in \mathbb{Z}, \sum \lambda_i = 1 \} $$更具体地说,最终能得到的连续集合的公差等于初始集合中所有数差的最大奇因子。
设 ,则:
- 最终公差 是 的奇数部分,即 除以 的幂次后的结果
- 最终集合能成为连续集合当且仅当 是 的幂次
因此:子数组 是精彩的当且仅当其中所有数的差的最大公约数是 的幂次。
观察 3:差分数组的处理
设 ()。对于子数组 ,考虑其内部差分的最大公约数:
则 是精彩的当且仅当 是 的幂次。
注意:当子数组只有一个元素时,差分集合为空,规定 ,此时是精彩的(单个元素集合总是连续的)。
观察 4:单调性与双指针
对于固定的左端点 ,随着 增大, 是单调不增的(因为取更多数的 只会减小或不变)。因此,满足 是 的幂次的最大 可以通过双指针找到。
算法步骤
-
预处理差分:计算 ,长度为
-
处理相同元素:如果 ,意味着相邻元素相等。这些位置需要特殊处理,因为 的 仍为 (被认为是 的幂次)
-
双指针维护:
- 使用两个指针 和 遍历差分数组
- 维护当前区间内所有 的
- 当 的二进制表示中 的个数 (即 的幂次)时, 可以右移
- 否则需要移动左指针
-
计数答案:
- 对于每个左端点,找到满足条件的最右
- 统计所有子数组数量
时间复杂度
- 预处理:
- 双指针:每个元素入队出队一次,
- 总复杂度:( 操作视为 )
完整代码
#include<bits/stdc++.h> using namespace std; #define int long long #define REP(i,b,e) for(int i=(b);i<(int)(e);++i) int n; int a[400005], lsteq[400005]; // 双指针结构体 struct two_pointers { int l, r, mid; int val[400005], rt; void init() { l = r = mid = rt = 0; val[0] = a[0]; } // 右指针右移 inline void addr() { ++r; rt = __gcd(rt, a[r]); } // 左指针右移 inline void addl() { if (l + 1 > r) addr(); ++l; if (l > mid) { rt = 0; mid = r; val[mid] = a[mid]; for (int i = mid - 1; i >= l; --i) { val[i] = __gcd(val[i + 1], a[i]); } } } // 查询当前区间 [l, r] 的 gcd inline int query() { return __gcd(val[l], rt); } }; // 判断 x 是否是 2 的幂次 bool is_power_of_two(int x) { if (x == 0) return true; // 0 被认为是 2^? 的特殊情况 return (x & (x - 1)) == 0; } void Main() { cin >> n; for (int i = 0; i < n; i++) { cin >> a[i]; } // 计算差分数组 for (int i = 1; i < n; i++) { a[n - i] -= a[n - i - 1]; } for (int i = 1; i < n; i++) { a[i - 1] = llabs(a[i]); } --n; // 差分数组长度为 n-1 // 预处理下一个非零位置 lsteq[n] = n; for (int i = n - 1; i >= 0; --i) { lsteq[i] = (a[i] ? i : lsteq[i + 1]); } two_pointers p; p.init(); long long ans = 1; // 单个元素的子数组 int cnt = 1; for (int i = 0; i < n; i++) { if (lsteq[i] == i) { // 当前差分非零 while (p.l < i) p.addl(); // 扩展右指针直到 gcd 不是 2 的幂次 while (p.r < n && is_power_of_two(p.query())) { p.addr(); } ans += (n - p.r) * cnt + 1; cnt = 1; } else { // 当前差分为零,可以合并 ans += lsteq[i] - i + 1; ++cnt; } } cout << ans << "\n"; } void TC() { int tc = 1; cin >> tc; while (tc--) { Main(); cout.flush(); } } signed main() { cin.tie(0); cout.tie(0); ios::sync_with_stdio(0); TC(); return 0; }代码说明
-
差分计算:
-
相同元素处理:当 时,相邻元素相等,这些位置可以"合并"处理
-
双指针维护:
val[l]存储从 到mid的rt存储从mid+1到 的- 整体
-
判断 的幂次:
(x & (x-1)) == 0且 , 特殊处理 -
计数:
- 对于每个差分非零位置,找到能延伸的最大右端点
- 对于连续零的位置,一起处理
示例验证
以第二个测试用例
[1,3,6,10,15,21]为例:- 差分:
- 检查子数组:
- :差 ,是 的幂次 ✓
- :差 ,,是 的幂次 ✓
- 等等
最终答案 与示例一致。
总结
本题的核心技巧:
- 将集合能否变成连续集合转化为差分的 是否为 的幂次
- 利用 的单调性使用双指针
- 处理相同元素时的合并技巧
- 时间解决
- 1
信息
- ID
- 6280
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 1
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者