《GMOJ-Senior-5464 乘积》题解

题目大意

定义无平方因子数为不能被任意一个质数的平方整除的数。给出 T T T个询问,每个询问给出一个 N N N K K K,问从 [ 1 , N ] [1,N] [1,N]中不重复地选出不超过 K K K个整数,且选出的数的乘积是无平方因子数的情况数。
对于 10 % 10\% 10%的数据, N ≤ 8 N \leq 8 N8
对于 40 % 40\% 40%的数据, N ≤ 16 N \leq 16 N16
对于 70 % 70\% 70%的数据, N ≤ 30 N \leq 30 N30
对于 100 % 100\% 100%的数据, 1 ≤ T ≤ 5 1 \leq T \leq 5 1T5 1 ≤ K ≤ N ≤ 500 1 \leq K \leq N \leq 500 1KN500

分析

这是一道 D P DP DP题。我们考虑分别处理每个询问。由“无平方因子数为不能被任意一个质数的平方整除的数”可知:选出的数的乘积分解质因数后每个质数的指数最高为 1 1 1。于是我们可以用二进制状态表示每个质数的出现情况。但是当我们尝试设 f i , j , s f_{i,j,s} fi,j,s表示在 [ 1 , i ] [1,i] [1,i]中不重复地选出 j j j个整数,且每个质数的出现情况为二进制状态集合 s s s时,选出的数的乘积是无平方因子数的情况数的时候,我们发现由于 [ 1 , 500 ] [1,500] [1,500]中有 95 95 95个质数, s s s的范围( 0 0 0 2 95 − 1 2^{95}-1 2951)会使数组大小超出空间限制。
于是我们考虑对这 95 95 95个质数分类。我们把它们分成两类:小于 23 23 23的(有 2 , 3 , 5 , 7 , 11 , 13 , 17 , 19 2,3,5,7,11,13,17,19 2,3,5,7,11,13,17,19,记为 p 1 p_1 p1),不小于 23 23 23的(有 23 , 29 , 31 , ⋯   , 499 23,29,31,\cdots,499 23,29,31,,499,共有 87 87 87个,记为 p 2 p_2 p2)。因为 2 3 2 = 529 > 500 23^2=529>500 232=529>500,所以 [ 1 , n ] [1,n] [1,n]中每个整数的质因子中最多只有一个 p 2 p_2 p2中的质数。然后我们对 [ 1 , n ] [1,n] [1,n]中的每个整数分类:我们把是 p 2 p_2 p2中某个数的正整数倍的数划分到一类,再把不是 p 2 p_2 p2中任何数的正整数倍的数划分到另外一类中。
我们先处理只选最后一类数时的结果,此时这些数只会是 p 1 p_1 p1中的数的正整数倍。因为 p 1 p_1 p1中的质数只有 8 8 8个,所以我们可以直接设 f i , j , s f_{i,j,s} fi,j,s表示在最后一类数与 [ 1 , i ] [1,i] [1,i]的交集中不重复地选出 j j j个整数,且 p 1 p_1 p1中每个质数的出现情况为二进制状态集合 s s s时,选出的数的乘积是无平方因子数的情况数。转移很容易得到: f i , j , s = ∑ a = 1 i − 1 ( f a , j − 1 , s ⊗ t u r n ( a ) × [ t u r n ( a ) ∈ s ] ) f_{i,j,s}= \sum_{a=1}^{i-1}(f_{a,j-1,s \otimes turn(a)} \times[turn(a) \in s]) fi,j,s=a=1i1(fa,j1,sturn(a)×[turn(a)s]),其中 t u r n ( a ) turn(a) turn(a)表示 p 1 p_1 p1中每个质数在 a a a中的出现情况的状态集合(下同)。初始时 f 1 , 0 , 0 = f 1 , 1 , 0 = 1 f_{1,0,0}=f_{1,1,0}=1 f1,0,0=f1,1,0=1,分别表示选和不选 1 1 1的情况。
其实在计算 f f f时,有一个小优化。因为由无平方因子数的定义可知:只有无平方因子数相乘才能得到无平方因子数,所以我们可以先算出中最后一类数中的无平方因子数(最多有 73 73 73个,记为 a a a)和 p 1 p_1 p1中每个质数在 a a a中每一个数的出现情况 m a s k mask mask,然后直接枚举 a a a中的每一个数转移即可。同时,还可以把 f f f的定义改为“在 a 1 , 2 , ⋯   , i a_{1,2, \cdots ,i} a1,2,,i中不重复地选出 j j j个整数,且 p 1 p_1 p1中每个质数的出现情况为二进制状态集合 s s s时,选出的数的乘积是无平方因子数的情况数”以压缩空间。
处理完只选不是 p 2 p_2 p2中任何数的正整数倍的数,我们要处理剩下的部分了。与 f f f类似,设 g i , j , s g_{i,j,s} gi,j,s表示在最后一类数与前 i i i类数的并集中不重复地选出 j j j个整数,且 p 1 p_1 p1中每个质数的出现情况为二进制状态集合 s s s时,选出的数的乘积是无平方因子数的情况数。转移也很容易得到: g i , j , s = ∑ a ∈ S i , t u r n ( a ) ∈ s g i − 1 , j − 1 , s ⊗ t u r n ( a ) g_{i,j,s}= \sum_{a \in S_i,turn(a) \in s} g_{i-1,j-1,s \otimes turn(a)} gi,j,s=aSi,turn(a)sgi1,j1,sturn(a),其中 S i S_i Si表示第 i i i类数(下同)。初始时 g 1 , i , j = ∑ k ∈ S 1 , t u r n ( k ) ∈ j f l a s t , i − 1 , j ⊗ t u r n ( k ) g_{1,i,j}= \sum_{k \in S_1,turn(k) \in j} f_{last,i-1,j \otimes turn(k)} g1,i,j=kS1,turn(k)jflast,i1,jturn(k),其中 a l a s t ≤ n a_{last} \leq n alastn,同时 l a s t = 73 last=73 last=73 a l a s t + 1 > n a_{last+1}>n alast+1>n。类似于优化计算 f f f,因为只有无平方因子数相乘才能得到无平方因子数,所以我们枚举 k k k时可以改为枚举 a a a中的每一个不大于 n 当 前 集 合 对 应 的 质 数 \frac{n}{当前集合对应的质数} n的数,并利用它对应的 m a s k mask mask值转移。
易得答案为 ∑ i = 1 K ∑ j = 0 2 8 − 1 g   87 , i , j \sum_{i=1}^{K} \sum_{j=0}^{2^8-1} g_{\space 87,i,j} i=1Kj=0281g 87,i,j。至此,这道题目被以 O ( T × ( 73 × m × 2 8 + 87 × 73 × m × 2 8 ) ) O(T \times (73 \times m \times 2^8 + 87 \times 73 \times m \times 2^8)) O(T×(73×m×28+87×73×m×28))的时间复杂度解决。

代码

根据思路,可以写出如下代码:

#include<cstdio>
constexpr int mod = 1000000007/*定义模数*/,
a[] = { 1,2,3,5,6,7,10,11,13,14,15,17,19,21,22,26,30,33,34,35,38,39,42,51,55,57,65,66,70,77,78,85,91,95,102,105,110,114,119,130,133,143,154,165,170,182,187,190,195,209,210,221,231,238,247,255,266,273,285,286,323,330,357,374,385,390,399,418,429,442,455,462,494 },
mask[] = { 0,1,2,4,3,8,5,16,32,9,6,64,128,10,17,33,7,18,65,12,129,34,11,66,20,130,36,19,13,24,35,68,40,132,67,14,21,131,72,37,136,48,25,22,69,41,80,133,38,144,15,96,26,73,160,70,137,42,134,49,192,23,74,81,28,39,138,145,50,97,44,27,161 },
p2[] = { 23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,101,103,107,109,113,127,131,137,139,149,151,157,163,167,173,179,181,191,193,197,199,211,223,227,229,233,239,241,251,257,263,269,271,277,281,283,293,307,311,313,317,331,337,347,349,353,359,367,373,379,383,389,397,401,409,419,421,431,433,439,443,449,457,461,463,467,479,487,491,499 };
int f[505][256]/*DP的数组,第一维被压缩掉了,对应分析中的f和g*/, temp[505][256]/*辅助数组,计算g时用*/;
int main()
{
	static_cast<void>(freopen("mul.in", "r", stdin)); //定义文件输入输出
	static_cast<void>(freopen("mul.out", "w", stdout));
	int T;
	static_cast<void>(scanf("%d", &T)); //读入T
	while (T--)
	{
		int n, K;
		static_cast<void>(scanf("%d%d", &n, &K)); //读入n和K
		f[0][0] = f[1][0] = 1; //初始化f,此时程序中的f对应分析中的f
		int cnt = 2; //减少DP循环范围用
		for (int i = 1; i < 73 && a[i] <= n; ++i) //枚举a中的每一个数
		{
			const int& s = mask[i];
			for (int j = cnt; j >= 1; --j) //计算f
			{
				const int jM1 = j - 1;
				for (int k = 0; k < 256; ++k)
				{
					if (f[jM1][k] != 0 && (k & s) == 0)
					{
						int& upd = f[j][k | s];
						upd += f[jM1][k];
						if (upd >= mod)
						{
							upd -= mod;
						}
					}
				}
			}
			if (cnt < K) //更新cnt
			{
				++cnt;
			}
		}
		for (const auto& i : p2) //通过枚举P2中的质数枚举前78类数,此时程序中的f对应分析中的g
		{
			const int t = n / i;
			for (int j = 0; j < 73 && a[j] <= t; ++j) //枚举当前类中的每一个数
			{
				const int& s = mask[j];
				for (int k = 1; k <= cnt; ++k) //计算g
				{
					const int kM1 = k - 1;
					for (int u = 0; u < 256; ++u)
					{
						if (f[kM1][u] != 0 && (u & s) == 0)
						{
							int& upd = temp[k][u | s];
							upd += f[kM1][u];
							if (upd >= mod)
							{
								upd -= mod;
							}
						}
					}
				}
			}
			for (int j = 1; j <= cnt; ++j) //更新g并清零temp
			{
				for (int k = 0; k < 256; ++k)
				{
					f[j][k] += temp[j][k];
					if (f[j][k] >= mod)
					{
						f[j][k] -= mod;
					}
					temp[j][k] = 0;
				}
			}
			if (cnt < K) //更新cnt
			{
				++cnt;
			}
		}
		int ans = 0;
		for (int i = 1; i <= cnt; ++i) //统计答案并清零数组
		{
			for (int j = 0; j < 256; ++j)
			{
				ans += f[i][j];
				if (ans >= mod)
				{
					ans -= mod;
				}
				f[i][j] = 0;
			}
		}
		printf("%d\n", ans); //输出答案
	}
	return 0;
}

总结

这道题在考察 D P DP DP和状态压缩的同时,还考察了分块、分组的思想,需要灵活的思维才能做对。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值