# Coprime Subsequences(容斥原理 + DP)
Coprime Subsequences(容斥原理 + DP)
一、题目描述
给定一个长度为 n 的数组 a,求它的非空互质子序列的个数。
一个序列是互质的,当且仅当它所有元素的最大公约数等于 1。
两个子序列只要选取的下标不同,就算不同。答案对 10^9 + 7 取模。
二、思路分析
正面做很难
直接统计「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 的。
三、算法流程
- 统计每个数出现的次数
arr[x] - 预处理
2^0 ~ 2^n的值 - 从大到小枚举
i(从m到2):- 统计所有
i的倍数的元素个数t - 累加所有
i的真倍数(2i, 3i, ...)的dp值 dp[i] = (2^t - 1 - 累加值) % MOD
- 统计所有
- 计算
dp[1] = 2^n - 1 - Σ dp[i] (i ≥ 2) - 输出
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
- 原因
- 值域很小,数组完全够用,而且 map 是 O(log n) 访问,还带常数,访问其实没有数组快。
- 后面要做「倍数法」枚举,如果选用map,要先遍历 map 的 key,判断是不是 d 的倍数 → 其实无法做到n logn 的复杂度优化,甚至更高
2.额外小思考 :如何实现求gcd为k 的子序列个数(比较简单,深入程度较小)
- 我就留个
小疑问给你们了哟

浙公网安备 33010602011771号