这个写法不唯一。可以2^30枚举之后打表。也可以dp。
定义状态 d p [ i ] [ c h s 1 ] [ c h s 2 ] dp[i][chs1][chs2] dp[i][chs1][chs2]表示已经决策了2~i
这些数,第一个人选的数的质因数中有chs1(二进制数表示状态),第二个人选的数的质因数有chs2的方案数。
这里i可以用滚动数组,也可以用01背包的套路(倒序枚举)优化。
发现对于大于50的质因数只会有一个数有。那么可以把这些数放在最后决策,它们对于其他点的决策时不影响的,它们决策的方案数就是3(给第一个人,给第二个人,都不给)。就是先进行一遍测试1~3
的算法,然后再计算出2~n
有多少个大于50的质因数。对答案乘上 3 c n t 3^{cnt} 3cnt即可。
如果写法不同有乘法的话要注意P是最大1e10,相乘会爆longlong。要手写一个高精乘法(压两位即可)。这里附上本人丑陋的代码。
long long Mul(long long a,long long b){//Base=100000
long long num1=a/Base,num2=a%Base,num3=b/Base,num4=b%Base;
num[1]=num[2]=num[3]=num[4]=0;
num[4]=1ll*num2*num4;
if(num[4]>=Base)num[3]+=num[4]/Base,num[4]%=Base;
num[3]+=1ll*num1*num4+num2*num3;
if(num[3]>=Base)num[2]+=num[3]/Base,num[3]%=Base;
num[2]+=1ll*num1*num3;
if(num[2]>=Base)num[1]+=num[2]/Base,num[2]%=Base;
long long ans=0;
for(int i=1;i<=4;i++){
ans=1ll*ans*Base+num[i];
ans%=P;
}
return ans;
}
因为500以内的质数很多,故我们如果再用上面的算法就不行了。但是可以发现500以内的每个数只有一个或者没有大于 500 \sqrt{500} 500的质因子。对于一个大于 500 \sqrt{500} 500的质因子K,我们把包含这个K的倍数的集合看做 S K S_K SK,对于这个集合里的数,只能有一个人取这个集合里的数(可以取任意个)。也可以两个人都不取。当然还要满足小于 500 \sqrt{500} 500的质因子没被取重。
于是我们这里定义状态 F [ K ] [ o p ] [ c h s 1 ] [ c h s 2 ] F[K][op][chs1][chs2] F[K][op][chs1][chs2]已经决策了K个这个集合里的数,表示op这个人取了这个集合,第一个人取的数的质因数中有chs1,第二个人取的数的质因数有chs2的方案数。F数组内部的转移和dp数组转移类似。这里的K可以用倒序枚举消掉。
F数组的初值就是dp数组。处理完之后再传回dp数组。也就是 d p [ c h s 1 ] [ c h s 2 ] = F [ 0 ] [ c h s 1 ] [ c h s 2 ] + F [ 1 ] [ c h s 1 ] [ c h s 2 ] − d p [ c h s 1 ] [ c h s 2 ] dp[chs1][chs2]=F[0][chs1][chs2]+F[1][chs1][chs2]-dp[chs1][chs2] dp[chs1][chs2]=F[0][chs1][chs2]+F[1][chs1][chs2]−dp[chs1][chs2]。减去 d p [ c h s 1 ] [ c h s 2 ] dp[chs1][chs2] dp[chs1][chs2]是因为对于两人都不选这个集合的数的方案算了两次,要减掉。
然后继续用上述方法处理剩下的集合。
对于那些没有大于 500 \sqrt{500} 500的质因子的数,就是按照测试点1~3
的方法直接dp即可。这样得到的dp数组就相当于后面处理时的初始数组。
这里转移有个优化,因为(chs1&chs2)==0
故可以通过枚举I^chs1
(I就是 2 c n t − 1 2^{cnt}-1 2cnt−1,cnt是小于 500 \sqrt{500} 500的质数的个数)的子集来枚举chs2。复杂度就只有 O ( n ∗ 3 8 ) O(n*3^{8}) O(n∗38),但是常数会使复杂度变大。
#include
#define M 505
using namespace std;
bool cur1;
int n;
long long P;
void Add(long long &x,long long y){
x=x+y;
if(x>=P)x-=P;
if(x<0)x+=P;
}
struct node{
int x,chs;
bool operator <(const node &_)const{
return x<_.x;
}
}A[M];
int Prime[M],n_p;
void Init(){//预处理每个数的质因子
n_p=0;
for(int i=2;i*i<=500;i++){
bool flag=true;
for(int j=2;j*j<=i;j++)if(i%j==0){flag=false;break;}
if(flag)Prime[++n_p]=i;
}
for(int i=2;i<=n;i++){
int chs=0,x=i;
for(int j=1;j<=n_p;j++){
if(x%Prime[j]==0){
while(x%Prime[j]==0)x/=Prime[j];
chs|=1<<(j-1);
}
}
A[i]=(node){x,chs};//x表示大于根号500的质因子,x==1时说明没有,chs是小于根号500的质因子的集合。
}
}
long long dp[1<<8][1<<8],F[2][1<<8][1<<8];
void Solve(){
memset(dp,0,sizeof(dp));
Init();
int I=(1<<8)-1;
dp[0][0]=1;
sort(A+2,A+n+1);//按照大于根号500的质因子的大小排序。
for(int i=2;i<=n;i++){
if(A[i].x==1){//这个数没有大于根号500的质因子
for(int chs1=I;chs1>=0;chs1--){//枚举第一个人取的数的质因子集合
int II=I^chs1;
for(int chs2=II;;chs2=(chs2-1)&II){//枚举子集
if((chs2&A[i].chs)==0)Add(dp[chs1|A[i].chs][chs2],dp[chs1][chs2]);//这个数给第1个人
if((chs1&A[i].chs)==0)Add(dp[chs1][chs2|A[i].chs],dp[chs1][chs2]);//这个数给第2个人
if(!chs2)break;
}
}
continue;
}
if(i==2||A[i].x!=A[i-1].x){//开一个新的块,处理这一个集合的数
memcpy(F[0],dp,sizeof(dp));
memcpy(F[1],dp,sizeof(dp));
}
for(int chs1=I;chs1>=0;chs1--){
int II=I^chs1;
for(int chs2=II;;chs2=(chs2-1)&II){//这里的转移和上面相同
if((chs2&A[i].chs)==0)Add(F[0][chs1|A[i].chs][chs2],F[0][chs1][chs2]);
if((chs1&A[i].chs)==0)Add(F[1][chs1][chs2|A[i].chs],F[1][chs1][chs2]);
if(!chs2)break;
}
}
if(i==n||A[i].x!=A[i+1].x){
for(int chs1=I;chs1>=0;chs1--){
int II=I^chs1;
for(int chs2=II;;chs2=(chs2-1)&II){
dp[chs1][chs2]=(F[0][chs1][chs2]+F[1][chs1][chs2]-dp[chs1][chs2])%P;
dp[chs1][chs2]=(dp[chs1][chs2]+P)%P;
if(!chs2)break;
}
}
}
}
long long ans=0;
for(int chs1=I;chs1>=0;chs1--){
int II=I^chs1;
for(int chs2=II;;chs2=(chs2-1)&II){
Add(ans,dp[chs1][chs2]);//统计答案
if(!chs2)break;
}
}
printf("%lld\n",ans);
}
bool cur2;
int main(){
//printf("%lf\n",(&cur2-&cur1)/1024.0);
scanf("%d%lld",&n,&P);
Solve();
return 0;
}