# Coprime Subsequences(容斥原理 + DP)

Coprime Subsequences(容斥原理 + DP)

一、题目描述

给定一个长度为 n 的数组 a,求它的非空互质子序列的个数。

一个序列是互质的,当且仅当它所有元素的最大公约数等于 1。

两个子序列只要选取的下标不同,就算不同。答案对 10^9 + 7 取模。

对应题目:Codeforces 803F Coprime Subsequences

二、思路分析

正面做很难

直接统计「gcd 恰好为 1」的子序列数量并不好做,因为 gcd 的取值很多,子序列数量又巨大。

反面思考 + 容斥

设 dp[i] 表示 gcd 恰好为 i 的非空子序列数量。

我们考虑 gcd 是 i 的倍数的子序列有多少个,记为 cnt[i]。

  • 如果子序列中所有元素都是 i 的倍数,那么它的 gcd 一定是 i 的倍数
  • 那么对于 i, 2i , 3i , 4i, ... ,
    分别有 arr[i], arr[2i], arr[3i], arr[4i], 累加为t
  • 那么 gcd为i 的倍数的子序列有 2^t - 1 个

容斥转移

dp[i] 表示 gcd 恰好为 i 的数量。

2^t - 1 表示 gcd 是 i 的倍数的数量,这里面包含了 gcd 为 2i、3i、4i... 的情况。

所以:

dp[i] = (2^t - 1) - Σ dp[k]   (k 是 i 的倍数,且 k > i)

这就把「gcd 是 i 的倍数」中,那些 gcd 更大的情况全部减掉了,剩下的就是 gcd 恰好为 i 的数量。

为什么从大到小递推

因为 dp[i] 依赖的是它的倍数 dp[2i]、dp[3i]...,所以要从大到小枚举 i,先算出大的,再算小的。

最终答案

我们要求的是 gcd 恰好为 1 的子序列数量,也就是 dp[1]。

但注意 dp[1] 的定义是 gcd 恰好为 1,而题目要求的是所有非空互质子序列。

实际上,dp[1] 的计算方式是:

dp[1] = (2^n - 1) - Σ dp[i]   (i 从 2 到 m)

即所有非空子序列减去 gcd 大于 1 的。

三、算法流程

  1. 统计每个数出现的次数 arr[x]
  2. 预处理 2^0 ~ 2^n 的值
  3. 从大到小枚举 i(从 m 到 2):
    • 统计所有 i 的倍数的元素个数 t
    • 累加所有 i 的真倍数(2i, 3i, ...)的 dp 值
    • dp[i] = (2^t - 1 - 累加值) % MOD
  4. 计算 dp[1] = 2^n - 1 - Σ dp[i] (i ≥ 2)
  5. 输出 dp[1]

四、AC 代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

const ll MOD = 1e9 + 7;
const int N = 1e5 + 5;

int arr[N];         // arr[x] 表示数值 x 在数组中出现了几次,m 是最大值,作为枚举上界。
ll dp[N];

int main()
{
    int n;
    cin >> n;

    int m = 0;
    for (int i = 0; i < n; i++)
    {
        int a;
        cin >> a;
        arr[a]++;
        m = max(m, a);
    }

    // 预处理 2 的幂
    vector<ll> pow2(n + 1);
    pow2[0] = 1;
    for (int i = 1; i <= n; i++)
        pow2[i] = pow2[i - 1] * 2 % MOD;

    // 从大到小枚举 gcd
    for (int i = m; i > 1; i--)
    {
        ll sum = 0;
        int t = 0;
        for (int k = i; k <= m; k += i)      // 内层循环枚举 i 的所有倍数 k = i, 2i, 3i, ...
        {
            t += arr[k];                   // t 统计所有 i 的倍数的元素总个数
            if (k != i) sum = (sum + dp[k]) % MOD;  // sum 累加所有真倍数(k > i)的 dp 值
        }
        dp[i] = (pow2[t] - 1 - sum + MOD) % MOD;    // dp[i] = gcd 是 i 的倍数数量 - gcd 更大的数量 = gcd 恰好为 i 的数量
    }

    // 计算 dp[1]
    dp[1] = (pow2[n] - 1) % MOD;
    for (int i = 2; i <= m; i++)
        dp[1] = (dp[1] - dp[i] + MOD) % MOD;

    cout << dp[1] << endl;
    return 0;
}

五、思考和反思

1.本题用数组进行统计各个数出现的次数,为何不太建议用map

  • 原因
  1. 值域很小,数组完全够用,而且 map 是 O(log n) 访问,还带常数,访问其实没有数组快。
  2. 后面要做「倍数法」枚举,如果选用map,要先遍历 map 的 key,判断是不是 d 的倍数 → 其实无法做到n logn 的复杂度优化,甚至更高

2.额外小思考 :如何实现求gcd为k 的子序列个数(比较简单,深入程度较小)

  • 我就留个小疑问给你们了哟

六、复杂度 : o(n log(n))

posted @ 2026-10-01 19:18  leaderling  阅读(3)  评论(0)    收藏  举报