1 条题解
-
0
题目理解
我们需要统计所有好数组的数量,其中:
- 数组元素取值范围:
- 数组长度任意(非空)
- 定义 为所有长度为 的子数组的最大值的 GCD
- 要求:对于所有 ,有
关键观察
观察 : 的单调性
设 为长度为 的子数组的最大值序列,。
注意到:
- 实际上是 的相邻最大值序列
- 中的每个元素都能整除 ,因此 整除
因此序列 是严格递增的,且 整除 。
观察 :长度限制
由于 ,且每次至少乘以 (因为 且 整除 ),所以:
又因为 ,所以:
$$2^{k-1} \le m \Rightarrow k \le \lfloor \log_2 m \rfloor + 1 $$因此数组长度最多为 。
观察 :非递减序列的特殊情况
考虑一个非递减序列 。
对于这个序列,长度为 的子数组的最大值就是 中的最大值?实际上更精确地说:长度为 的子数组的最大值序列是 (因为滑动窗口在非递减序列中,每个窗口的最大值就是窗口的最后一个元素)。
因此:
为了使所有 不同,必须有 (严格递增),否则会出现相等的后缀 。
观察 :排列计数
对于一个严格递增的序列 ,考虑它的所有排列。
从 开始:(只有最后一个元素)。
要得到 ,我们需要在序列中插入 ,使得相邻最大值序列变为 。经过分析,有 种方式:
- 放在 前面:
- 放在 后面:
类似地,对于 ,也有 种插入方式。因此,一个严格递增序列共有 个好排列。
状态设计
设 表示:以 作为第一个元素的严格递增序列中,整个数组的 GCD 恰好为 的序列数量(乘以对应的 系数)。
但这样状态太多,我们需要优化。
优化 :合并长度维度
由于我们在转移时会乘以 ,可以直接在 中处理这个系数。
设 表示:以 作为第一个元素,且整个数组 GCD 恰好为 的序列的权重和,其中权重为 。
对于长度为 的序列:,权重为 ,GCD 为 。
对于长度 的序列:假设我们已知以某个 开头、 为 的序列权重和为 ,那么在这些序列前加上 后:
- 新序列的 变为
- 长度增加 ,权重乘以
因此转移公式:
$$f_i(\gcd(i, h)) \mathrel{+}= 2 \times (\text{所有以 } j>i \text{ 开头、GCD 为 } h \text{ 的序列权重和}) $$优化 :利用约数关系
令 ,即所有大于 的开头元素、GCD 恰好为 的序列权重和。
对于固定的 ,我们需要对每个 ,将 加到 上。
直接枚举所有 会超时,但注意到 一定是 的约数,所以只需要关心 的约数。
对于 的一个约数 ,我们需要知道:对于所有满足 的 , 的总和。
优化 :容斥原理
对于 的约数 ,定义:
即所有 是 的倍数的 之和。
那么,所有满足 的 对应的 之和就是 。
而满足 的条件等价于: 且对于 的任何大于 的约数 ,。
通过容斥原理:
$$\sum_{\gcd(i, h) = g} sum_h = U_g - \sum_{\substack{g' \mid i \\ g' > g \\ g \mid g'}} \sum_{\gcd(i, h) = g'} sum_h $$更具体地,设 为 的所有约数(从小到大排序),对于 中的 ,从大到小计算:
$$cnt_g = U_g - \sum_{\substack{g' \in divs \\ g' > g \\ g \mid g'}} cnt_{g'} $$这样, 就是所有满足 的 对应的 之和。
然后:
另外,单独的长度为 的序列也要考虑:。
算法实现
. 预处理所有数的约数 . 从 向下遍历到 :
- 对 的每个约数 ,计算 (容斥)
- 设置
- (长度为 的序列)
- 将 累加到答案中
- 更新 数组:对于 的每个约数 的每个约数 ,
时间复杂度
其中 是 的约数个数。对于 ,这个复杂度是可接受的。
完整代码
#include<bits/stdc++.h> using namespace std; const int N = 2e5 + 9, mod = 998244353; using ll = long long; int add(int a, int b) { a += b; if (a >= mod) a -= mod; if (a < 0) a += mod; return a; } // dp[i] = 以 i 开头的序列,GCD 恰好为某个值的权重和 // 但这里我们用滚动的方式:cur 存储当前 i 的 f_i(g) // sum[g] 存储所有 j > i 中,GCD 为 g 的倍数的序列权重和 int dp[N], cur[N], uni[N]; int sum[N]; vector<int> d[N]; void solve() { int m; cin >> m; // 初始化 for (int i = 1; i <= m; i++) { dp[i] = cur[i] = 0; uni[i] = 0; sum[i] = 0; } int ans = 0; // 从大到小枚举第一个元素 for (int i = m; i >= 1; i--) { // 清空当前 i 的 cur 值 for (int j : d[i]) { cur[j] = 0; } int sz = d[i].size(); // 容斥计算 cnt_g for (int idj = sz - 1; idj >= 0; idj--) { int j = d[i][idj]; // uni[j] 初始为所有 j 的倍数的 sum 之和的两倍 uni[j] = add(sum[j], sum[j]); // 减去更大的约数贡献 for (int idk = idj + 1; idk < sz; idk++) { int k = d[i][idk]; if (k % j == 0) { uni[j] = add(uni[j], -uni[k]); } } // cur[j] = 2 * cnt_j - 已有的 dp[j] 贡献 cur[j] = add(uni[j], -add(dp[j], dp[j])); } // 长度为 1 的序列 [i] cur[i] = add(cur[i], 1); // 更新答案和 sum 数组 for (int j : d[i]) { dp[j] = add(dp[j], cur[j]); for (auto k : d[j]) { sum[k] = add(sum[k], cur[j]); } ans = add(ans, cur[j]); } } cout << ans << '\n'; } int32_t main() { ios_base::sync_with_stdio(0); cin.tie(0); // 预处理所有数的约数 for (int i = 1; i < N; i++) { for (int j = i; j < N; j += i) { d[j].push_back(i); } } int t = 1; cin >> t; while (t--) { solve(); } return 0; }代码解释
. 预处理:
d[i]存储 的所有约数. 核心循环:从 到 枚举第一个元素
- 对于 的每个约数 ,通过容斥计算
- (减去之前已经统计过的)
- 加上长度为 的序列:
cur[i] += 1 - 更新
dp[j]和sum[k]( 是 的约数)
.
sum[g]的含义:所有 中,GCD 恰好为 的倍数的序列权重和. 答案累加:每次计算完
cur[j]后加到答案中示例验证
对于 :
- :长度 ,权重
- :长度 ,权重
- :长度 ,权重
- :长度 ,权重
总和 ?但样例输出是 。
等等,这里需要重新检查。实际上题目要求的是数组的个数,不是权重和。我们的权重 正是每个严格递增序列对应的好排列数。所以:
- 严格递增序列 :对应 个数组
- 严格递增序列 :对应 个数组
- 严格递增序列 :对应 个排列 和
总数 ,与样例一致。
- 1
信息
- ID
- 6614
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 1
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者