先统计每个 t 的倍数里有多少齿轮,再用 C(cnt[t],k) 算 gcd 是 t 的倍数的方案数,最后按倍数从大到小容斥还原精确 gcd。
OJ: luogu
题目 ID: P6298
难度:提高+/省选-
标签:数论容斥组合计数最大公约数思维
日期: 2026-06-20 07:04
题意
给出
要从中选出恰好
一个齿轮组的损耗因子定义为这
要求对每个
- gcd 恰好等于
的 元组合个数
答案对
思路
先看一个可以直接验证的小数据暴力:
cpp
#include <bits/stdc++.h>
using namespace std;
int n, m, k;
int a[25];
int choose_idx[25];
int ans[1005];
void dfs(int pos, int last) {
if (pos == k + 1) {
int g = 0;
for (int i = 1; i <= k; i++) {
g = std::gcd(g, a[choose_idx[i]]);
}
ans[g]++;
return;
}
for (int i = last + 1; i <= n; i++) {
choose_idx[pos] = i;
dfs(pos + 1, i);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m >> k;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
for (int i = 0; i <= m; i++) {
ans[i] = 0;
}
dfs(1, 0);
for (int i = 1; i <= m; i++) {
cout << ans[i];
if (i == m) {
cout << '\n';
}
else {
cout << ' ';
}
}
return 0;
}暴力版直接枚举所有大小为
这当然只能跑很小的数据,但它把题意表达得很直接。
第一步:先数“gcd 是 的倍数”的方案数
固定一个
如果一个组合的 gcd 是
设:
cnt[t] = $ 数组里有多少个数能被 $t 整除
那么从这些数里任选
所以:
这里
- gcd 属于
这些倍数的所有组合
第二步:从大到小做容斥
设:
ans[t] = $ gcd 恰好等于 $t 的组合数
那么:
于是只要按
这就是这题的核心容斥。
组合数怎么求
因为需要大量计算
然后用:
在
代码
cpp
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
const int MAXM = 1000000 + 5;
const i64 MOD = 1000000007LL;
int n, m, k;
int a[MAXM];
int freq[MAXM];
int divisible_cnt[MAXM];
i64 fact[MAXM], inv_fact[MAXM];
i64 exact_ans[MAXM];
i64 quick_pow(i64 base, i64 exp) {
i64 ans = 1 % MOD;
base %= MOD;
while (exp > 0) {
if (exp & 1) {
ans = ans * base % MOD;
}
base = base * base % MOD;
exp >>= 1;
}
return ans;
}
void init_comb(int up) {
fact[0] = 1;
for (int i = 1; i <= up; i++) {
fact[i] = fact[i - 1] * i % MOD;
}
inv_fact[up] = quick_pow(fact[up], MOD - 2);
for (int i = up; i >= 1; i--) {
inv_fact[i - 1] = inv_fact[i] * i % MOD;
}
}
i64 C(int n, int k) {
if (k < 0 || k > n) {
return 0;
}
return fact[n] * inv_fact[k] % MOD * inv_fact[n - k] % MOD;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m >> k;
for (int i = 1; i <= n; i++) {
cin >> a[i];
freq[a[i]]++;
}
init_comb(n);
for (int d = 1; d <= m; d++) {
for (int x = d; x <= m; x += d) {
divisible_cnt[d] += freq[x];
}
}
// ways[d] = gcd 是 d 的倍数的方案数
// exact[d] = gcd 恰好等于 d 的方案数
for (int d = m; d >= 1; d--) {
i64 ways = C(divisible_cnt[d], k);
i64 sub = 0;
for (int x = d + d; x <= m; x += d) {
sub += exact_ans[x];
if (sub >= MOD) {
sub -= MOD;
}
}
exact_ans[d] = (ways - sub) % MOD;
if (exact_ans[d] < 0) {
exact_ans[d] += MOD;
}
}
for (int d = 1; d <= m; d++) {
cout << exact_ans[d];
if (d == m) {
cout << '\n';
}
else {
cout << ' ';
}
}
return 0;
}复杂度
设值域上界为
- 统计
: - 从大到小做倍数容斥:
- 预处理阶乘:
总时间复杂度:
空间复杂度:
总结
这题的标准套路是:
- 先统计“所有数都能被
整除”的组合数 - 再按倍数关系把“gcd 恰好等于
”还原出来
所以本质上是一题:
- 倍数统计
- 组合数
- 容斥还原 exact gcd
的组合题。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
