齿轮

GitHub跳转原题关系图返回列表

先统计每个 t 的倍数里有多少齿轮,再用 C(cnt[t],k) 算 gcd 是 t 的倍数的方案数,最后按倍数从大到小容斥还原精确 gcd。

OJ: luogu

题目 ID: P6298

难度:提高+/省选-

标签:数论容斥组合计数最大公约数思维

日期: 2026-06-20 07:04

题意

给出 nn 个齿轮,第 ii 个齿轮的齿数是 aia_i
要从中选出恰好 kk 个齿轮组成一个齿轮组。

一个齿轮组的损耗因子定义为这 kk 个齿轮齿数的最大公约数。

要求对每个 t[1,m]t \in [1, m],输出:

  • gcd 恰好等于 ttkk 元组合个数

答案对 109+710^9+7 取模。

思路

先看一个可以直接验证的小数据暴力:

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;
}

暴力版直接枚举所有大小为 kk 的组合,算出它们的 gcd,然后计数。
这当然只能跑很小的数据,但它把题意表达得很直接。

第一步:先数“gcd 是 tt 的倍数”的方案数

固定一个 tt
如果一个组合的 gcd 是 tt 的倍数,那么这个组合里的每个数都必须是 tt 的倍数。

设:

  • cnt[t] = $ 数组里有多少个数能被 $t 整除

那么从这些数里任选 kk 个,就得到一个 gcd 为 tt 的倍数的组合。
所以:

  • ways[t]=C(cnt[t],k)ways[t] = C(cnt[t], k)

这里 ways[t]ways[t] 数的不是“gcd 恰好等于 tt”,而是:

  • gcd 属于 t,2t,3t,t, 2t, 3t, \ldots 这些倍数的所有组合

第二步:从大到小做容斥

设:

  • ans[t] = $ gcd 恰好等于 $t 的组合数

那么:

  • ways[t]=ans[t]+ans[2t]+ans[3t]+ways[t] = ans[t] + ans[2t] + ans[3t] + \ldots

于是只要按 tt 从大到小枚举,就可以把更大的倍数答案先减掉:

  • ans[t]=ways[t]ans[2t]ans[3t]ans[t] = ways[t] - ans[2t] - ans[3t] - \ldots

这就是这题的核心容斥。

组合数怎么求

因为需要大量计算 C(x,k)C(x, k),而 n106n \leqslant 10^6,所以直接预处理:

  • fact[i]fact[i]
  • inv_fact[i]inv\_fact[i]

然后用:

  • C(n,k)=fact[n]inv_fact[k]inv_fact[nk]C(n, k) = fact[n] \cdot inv\_fact[k] \cdot inv\_fact[n-k]

O(1)O(1) 时间求每个组合数。

代码

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;
}

复杂度

设值域上界为 mm

  • 统计 cnt[t]cnt[t]O(mlogm)O(m \log m)
  • 从大到小做倍数容斥:O(mlogm)O(m \log m)
  • 预处理阶乘:O(n)O(n)

总时间复杂度:

  • O(n+mlogm)O(n + m \log m)

空间复杂度:

  • O(n+m)O(n + m)

总结

这题的标准套路是:

  1. 先统计“所有数都能被 tt 整除”的组合数
  2. 再按倍数关系把“gcd 恰好等于 tt”还原出来

所以本质上是一题:

  • 倍数统计
  • 组合数
  • 容斥还原 exact gcd

的组合题。

一图流解析

这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

一图流解析