【NOIP模拟赛六】状态压缩DP day1 second 酷子集

 

    由于这道题我惊人地做出了正解(虽然考试时有小错误只有90分),所以我打算先抛掉模拟赛三day2 third,来写这道题了。。。

很容易发现n的范围很大,O(n)都是无法承受的,所以我们需要换个想法。很容易发现,所有满足条件的集合,每个数字最多出现一次,而总共有10个数字,我们可以考虑枚举这10个数字的状态。

f[s]表示所用的数字集合是s且集合中所有数均不超过n的方案总数,考虑s的一个子集v,用v组成一个数,s-v组成其余的数。记np位,如果|v|<p,那么v的任何一个排列都是满足条件的(当然0不能放在首位,需要特判)。

如果|v|=p,那么我们一位一位考虑。对于某一位k,如果这一位放了一个小于n这一位的数(当然第一位不能放0),之后的数可以任意排列,方案数为(p-k)!,否则我们在这里放置和原数这一位一样的数(前提是这些数中有),继续处理k+1位。

但是很容易发现这样会有重复计算的,例如s={1,2,3,4},会把12,3434,12算重,所以我们在枚举子集时加上一个限制,v必须包含s的一个特定元素(为了方便用lowbit(s)),这样计算时所有的v都不完全相同,并且包含了所有的情况。

这样做,最后把所有的f[1]f[2^10-1]加起来就是答案了,时间复杂度O(2^20*log10(n)+1)

你们爱的代码如下:

#include<queue>
#include<cmath>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define M 1250
#define mod 1000000007
int n,p,all[15];
int bit[M],fac[15];
int ksm[15]={1,2,4,8,16,32,64,128,256,512,1024};
int num[M][15];
int ok[M][15][2];
//每一个状态对于n的每一位是否有对应数字
//以及严格小于这个数字的有多少个
int f[M];//合法方案数
int main()
{
	scanf("%d",&n);
	p=int(log10(n))+1;//计算n的位数
	int e=n;
	for(int i=1;i<=p;i++)//处理n每一位上的数
	{
		all[p-i+1]=e%10;
		e/=10;
	}
	if(n<=9)//特殊情况
	{
		printf("%d",ksm[n]-1);
		return 0;
	}
	fac[0]=1;
	for(int i=1;i<=10;i++)//算阶乘
		fac[i]=fac[i-1]*i;
	for(int i=1;i<ksm[10];i++)
	//预处理状态中包含的元素和包含元素个数
	{
		for(int j=0;j<10;j++)
		{
			if(i&(1<<j))
			{
				num[i][++num[i][0]]=j;
				bit[i]++;
			}
		}
	}
	for(int i=1;i<ksm[10];i++)
	//预处理状态中小于n第k位的元素有几个,是否有恰等于这一位的元素
	{
		for(int j=1;j<=p;j++)
		{
			for(int k=1;k<=num[i][0];k++)
			{
				if(num[i][k]<all[j])
					ok[i][j][1]++;
				if(num[i][k]==all[j])
					ok[i][j][0]=1;
			}
		}
	}
	f[0]=1;//初始状态
	for(int i=1;i<ksm[10];i++)
	{
		for(int j=i;j>0;j=(j-1)&i)
		{
			if(j&(i&-i))
			//避免重复计算,固定使用某一个数
			{
				int m=bit[j];
				if(m<p)//位数小于p直接排列
				{
					if(j&1)//首位不放0
						f[i]=(f[i]+1ll
						*f[i-j]*(fac[m]-fac[m-1])%mod)%mod;
					else
						f[i]=(f[i]+1ll
						*f[i-j]*fac[m]%mod)%mod;
				}
				if(m==p)
				{
					int o=j;
					for(int k=1;k<=p;k++)//依次比较每一位
					{
						if(k==1&&(o&1))//首位不放0
						{
							f[i]=(f[i]+
							1ll*f[i-j]
							*(ok[o][k][1]-1)%mod
							*fac[p-k]%mod)%mod;
						}
						else
						{
							f[i]=(f[i]+
							1ll*f[i-j]
							*ok[o][k][1]%mod
							*fac[p-k]%mod)%mod;
						}
						if(!ok[o][k][0])
							break;
						if(k==p)//如果可以完全构成n,方案加1
							f[i]=(f[i]+f[i-j])%mod;
						o-=ksm[all[k]];
					}
				}
			}
		}
	}
	int ans=0;
	for(int i=1;i<ksm[10];i++)
		ans=(ans+f[i])%mod;
	printf("%d",ans);
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值