hdu 6125 状压dp+分组

一道玄学题...

其实一开始想的是对的,优化一下就好了

首先我们会发现,乘积不能被完全平方数整除等价于所有因子的每个质因子个数和都至多为1

可是500以内的质数很多,全找出来会爆炸的

可我们会发现,如果一个数的平方会在500以内,那么这个数一定<=22!

所以,1~500中会存在的完全平方数的质因子一定在22以内

这些质数只有八个,所以我们可以找出来

至于剩下的部分,显然23和46是不嫩同时出现的,所以我们把含有23这个因子的所有数分到一个背包里,对每个背包只允许使用其中的一个数,这样就能满足23等只出现一次,对其他更大的质数也是同理

什么?你说会不会重?

23*23>500,那么剩下所有的乘积都大于500,也就是说如果我们只用23及以上的质数分组,每组中的元素是互不重叠的。

什么?你说前面几个小的质数怎么办?

状压啊!

求出每个数对于前面八个质数(是的,小于23的质数有8个,分别为2,3,5,7,11,13,17,19),而言质因子的状态,用二进制表示(如15的质因子状态可以表示为110,即0个2,1个3,一个5)

至于那些用大质数表示不了的数,我们把他们每个单独分包即可。

在转移的时候,我们就可以第一层:枚举所有包,第二层:枚举包中每个元素,第三层:枚举前八个质数的所有状态,第四层:枚举前面用过的数的个数

这样就可以实现转移了,因为每次转移都是上一个包向下一个包去转移,所以包中元素是没有重复的。

当然,我本人在写代码的时候,选择把所有不能分到其他包里的元素全分到第一个包里,然后单独处理第一个包。

#include <cstdio>
#include <cmath>
#include <cstring>
#include <cstdlib>
#include <iostream>
#include <algorithm>
#include <queue>
#include <stack>
#define ll long long
#define mode 1000000007
using namespace std;
int cnt=0;
int pri[505];
bool vis[505];
ll dp[2][(1<<8)+5][505];
int sit[505];
bool used[505];
int bag[105][505];
int cct[105];
int posi[505];
int n,k;
void init()
{
	for(int i=2;i<=500;i++)
	{
		if(!vis[i])
		{
			pri[cnt++]=i;
		}
		for(int j=0;j<cnt&&i*pri[j]<=500;j++)
		{
			vis[i*pri[j]]=1;
			if(!i%pri[j])
			{
				break;
			}
		}
	}
}
void get_bag()
{
	bag[1][++cct[1]]=1;
	for(int i=2;i<=500;i++)
	{
		bool flag=0;
		for(int t=8;t<cnt&&i>=pri[t];t++)
		{
			if(i%pri[t]==0)
			{
				cct[t-6]++;
				flag=1;
				bag[t-6][cct[t-6]]=i;
				break;
			}
		}
		if(!flag)
		{
			cct[1]++;
			bag[1][cct[1]]=i;
		}
	}
}
void get_ori()
{
	sit[1]=0;
	for(int i=2;i<=500;i++)
	{
		int t=i;
		for(int j=0;j<8;j++)
		{
			int cot=0;
			while(t%pri[j]==0)
			{
				t/=pri[j];
				cot++;
			}
			if(cot>=2)
			{
				used[i]=1;
				break;
			}else if(cot==1)
			{
				sit[i]|=(1<<j);
			}
		}
	}
}
int main()
{
	int T;
	scanf("%d",&T);
	init();
	get_ori();
	get_bag();
	while(T--)
	{
		scanf("%d%d",&n,&k);
		memset(dp,0,sizeof(dp));
		dp[0][0][0]=1;
		int now=1,past=0;
		for(int i=1;i<=cct[1];i++)
		{
			memset(dp[now],0,sizeof(dp[now]));
			for(int j=0;j<(1<<8);j++)
			{
				for(int t=0;t<=k;t++)
				{
					dp[now][j][t]=dp[past][j][t];
				}
			}
			for(int j=0;j<(1<<8);j++)
			{
				for(int t=0;t<k;t++)
				{	
					if(!(sit[bag[1][i]]&j)&&!used[bag[1][i]]&&bag[1][i]<=n)
					{
						dp[now][j|sit[bag[1][i]]][t+1]+=dp[past][j][t];
						dp[now][j|sit[bag[1][i]]][t+1]%=mode;
					}
				}
			}
			swap(now,past);
		}
		for(int i=2;i<=cnt-7;i++)
		{
			memset(dp[now],0,sizeof(dp[now]));
			for(int j=0;j<(1<<8);j++)
			{
				for(int t=0;t<=k;t++)
				{
					dp[now][j][t]=dp[past][j][t];
				}
			}
			for(int f=1;f<=cct[i];f++)		
			{
				for(int j=0;j<(1<<8);j++)
				{
					for(int t=0;t<k;t++)
					{	
						if(!(sit[bag[i][f]]&j)&&!used[bag[i][f]]&&bag[i][f]<=n)
						{
							dp[now][j|sit[bag[i][f]]][t+1]+=dp[past][j][t];
							dp[now][j|sit[bag[i][f]]][t+1]%=mode;
						}
					}
				}
			}
			swap(now,past);
		}
		ll ans=0;
		for(int i=0;i<(1<<8);i++)
		{
			for(int j=1;j<=k;j++)
			{
				ans+=dp[past][i][j];
				ans%=mode;
			}
		}
		printf("%lld\n",ans%mode);
	}
	return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值