题目描述
难度分:2000
输入n(1≤n≤105)和长为n的数组a(1≤a[i]≤70)。
输出有多少个非空子序列,其元素乘积是完全平方数。模109+7。
注:子序列不一定连续。
注:只要有元素下标不一样,就算做不同的子序列。
输入样例1
4
1 1 1 1
输出样例1
15
输入样例2
4
2 2 2 2
输出样例2
7
输入样例3
5
1 2 4 5 8
输出样例3
7
算法
状压DP
将一个数分解质因数,如果它是平方数则要求每个质因子的个数都是偶数。而题目中a[i]的取值范围很小,经过枚举,发现[1,70]中的质数只有19个,因此可以用一个19位二进制数mask来表征某个数分解质因数后的状态,如果第i位(从0开始)为1,说明这个数分解质因数后,第i个质数有奇数个,否则就是偶数个。看到奇偶性,就能比较容易地想到要用异或的性质。
先遍历a数组,预处理出每个a[i]的频数,存入到数组cnt中,cnt[i]表示数组a中i的个数。
状态定义
dp[i][mask]表示考虑从数值[1,i]取数,且这些数的乘积状态为mask的情况下,一共有多少种非空子序列。在这个定义下,最终答案就应该是dp[70][0]−1(减去的1是空序列)。
状态转移
当考虑到数值i时,分为以下两种情况:
- 如果cnt[i]=0,直接状态转移dp[i][mask]=dp[i−1][mask],mask∈[0,219)。
- 否则当前数值i可以从a中取奇数个和偶数个,假设i分解质因数后的状态为cur,选奇数个i,由于奇数个i的乘积状态和一个i的乘积状态相同,所以对于mask∈[0,219),状态转移方程为dp[i][mask⊕cur]=dp[i][mask⊕cur]+dp[i−1][mask]×2cnt[i]−1。选偶数个i相当于没有选i,所以状态转移方程为dp[i][mask]=dp[i][mask]+dp[i−1][mask]×2cnt[i]−1。
这里面用到的是二项式定理,C1k+C3k+C5k+…=C2k+C4k+C6k+…=2k−1。
最后需要注意的是空间可能会爆,可以开2×219的DP
数组进行滚动。
复杂度分析
时间复杂度
这个题的时间复杂度跟n没有什么关系,主要在于a[i]的范围。外层循环遍历i∈[1,70],对每个i循环19次求质因数分解的状态,然后遍历状态mask∈[0,219)进行转移,单次转移是O(1)的。因此,算法整体的常数操作不到4×107,可以极限通过。
空间复杂度
我在实现的时候开辟了一个O(n)的空间存p[i]=2i,事实上这个空间可以省去,只留下2×219=220规模的DP
数组。而cnt数组的空间可以忽略不计,因此本算法的额外空间为220。
C++ 代码
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 100010, MOD = 1e9 + 7;
const int primes[19] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67};
int n, a, cnt[71], p[N];
LL dp[2][1<<19];
int main() {
scanf("%d", &n);
memset(cnt, 0, sizeof(cnt));
p[0] = 1;
for(int i = 1; i <= n; i++) {
p[i] = (p[i - 1] << 1) % MOD;
}
for(int i = 1; i <= n; i++) {
scanf("%d", &a);
cnt[a]++;
}
dp[0][0] = 1;
int index = 0;
for(int i = 1; i <= 70; i++) {
if(cnt[i] == 0) continue;
int k = cnt[i], x = i, cur = 0;
index ^= 1;
for(int j = 0; j < 19 && primes[j] <= x; j++) {
while(x % primes[j] == 0) {
cur ^= 1<<j;
x /= primes[j];
}
}
memset(dp[index], 0, sizeof(dp[index]));
for(int mask = 0; mask < (1<<19); mask++) {
dp[index][mask^cur] = (dp[index][mask^cur] + dp[index^1][mask] * p[k - 1] % MOD) % MOD;
dp[index][mask] = (dp[index][mask] + dp[index^1][mask] * p[k - 1] % MOD) % MOD;
}
}
printf("%d\n", (dp[index][0] - 1 + MOD) % MOD); // 注意还要减去一个空序列
return 0;
}