2018.09.29【NOIP训练】美丽数(数位DP)

传送门


解析:

看到这道题很容易就想到状压加数位DP,这是常规做法,但是会T,我们先来看一下这样做的代码。

//O(不能过)
ll l,r;
int mark[20];
ll f[20][512][252][2];
int len;

inline
ll dp(cs int &pos,cs int &cotain,int mod,cs bool &limit){
	if(pos>len){
		int cnt=0;
		for(int re i=1;i<=9;++i){
			if((cotain&(1<<(i-1)))&&mod!=mod/i*i)return 0;
		}
		return 1;
	}
	mod-=mod/252*252;
	if(~f[pos][cotain][mod][limit])return f[pos][cotain][mod][limit];
	ll ret=0;
	int last;
	if(limit)last=mark[pos];
	else last=9;
	for(int re i=0;i<=last;++i){
		ret+=dp(pos+1,i?cotain|(1<<(i-1)):cotain,((mod<<1)+(mod<<3))+i,limit&&(last==i));
	}
	return f[pos][cotain][mod][limit]=ret;
}

inline
ll solve(ll a){
	memset(f,-1,sizeof f);
	for(len=1;a;++len){
		mark[len]=a-a/10*10;
		a/=10;
	}
	--len;
	reverse(mark+1,mark+len+1);
	return dp(1,0,0,true);
}

int main(){
while(~scanf("%lld%lld",&l,&r)){
	outint(solve(r)-solve(l-1));
	pc('\n');
}
	return 0;
}

我们按照数位DP的常规做法,将其拆分,再记忆化搜索一下,记录除以 2520 2520 2520 l c m ( 1 − 9 ) lcm(1-9) lcm(19))的余数( m o d mod mod),由于除8的余数和除5的余数可以用最后一位数搞定,我们可以最后这一维记录252的余数。
以及这个数字包含哪些数( c o t a i n cotain cotain),这个用状态压缩实现。
然后跑一遍简单的数位DP。

然后你就被卡住了。

我们来分析一下复杂度:
最坏情况下是一个以9构成的字符串。我们每一次需要搜索的状态数就是 17 ∗ 512 ∗ 252 ∗ 2 = 4386816 17*512*252*2=4386816 175122522=4386816,这个状态我们每次要搜两个数,还有 100 100 100组数据。这个计算量已经超过 8 e 9 8e9 8e9了,肯定会被卡。(上下界剪枝的不要有意见,这道题一样有数据卡上下界剪枝,我也写过)

事实上,我们把时限调到7000ms都没过。

那我们再想一想,这里还有什么性质没有被我们利用。

要求被几个数整除,那就是要求被它们的最大公约数同时整除。。。

那么我们为什么要记录有哪些数字出现过?我们直接记录出现的非 0 0 0数的最大公约数就行了啊。

然而,这个方法还能继续优化,因为最大公约数最大只有2520,但是这样空间就显得有点吃力。
但是,可能出现的最大公约数有这么多吗?

我们打个表。用下方代码中的 g e t l c m getlcm getlcm函数,可以在 O ( 2 10 ) O(2^{10}) O(210)时间内处理出所有 1 − 9 1-9 19的lcm。输出cnt,只有48种。

我们再考虑一个优化,去掉limit。

我们现在的 f [ n o w ] [ l ] [ r e m a i n ] f[now][l][remain] f[now][l][remain]表示的就是当前位为now,出现数字的 l c m lcm lcm l l l,当前余数为 r e m a i n remain remain,这里的 r e m a i n remain remain就必须是2520的余数了,因为我们不记录上一位。而我们的 f f f只记录 l i m i t limit limit为假的时候的答案,什么意思?就是指我们在小于等于当前位置的所有位可以随便填,不用担心会超出我们搜索的数大小。

而这样一处理,我们发现不需要每次初始化 f f f数组了。

显然,复杂度就这样降下来了。
l i m i t limit limit为假的复杂度是均摊的。
l i m i t limit limit为真的复杂度是数位个数,就是 O ( l o g 10 N ) O(log_{10}N) O(log10N)

这样就不用担心超时了。


代码:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define re register
#define gc getchar
#define pc putchar
#define cs const

inline
ll getint(){
	re ll num;
	re int c;
	while(!isdigit(c=gc()));num=c^48;
	while(isdigit(c=gc()))num=(num<<1)+(num<<3)+(c^48);
	return num;
}

inline
void outint(ll a){
	static char ch[23];
	if(a==0)pc('0');
	while(a)ch[++ch[0]]=a-a/10*10,a/=10;
	while(ch[0])pc(ch[ch[0]--]^48);
}

inline
int gcd(int b,int a){return b?gcd(a-a/b*b,b):a;}
inline
int lcm(int a,int b){return a*b/gcd(a,b);}

int pos[2523];
int cnt;

inline 
void getlcm(int now,int l){
	if(!pos[l])pos[l]=++cnt;
	if(now>=10)return;
	getlcm(now+1,lcm(l,now));
	getlcm(now+1,l);
}

ll f[20][50][2524];
int mark[20],len;

inline
ll dp(int now,int l,int remain,bool limit){
	if(!now)return remain%l==0;
	if(!limit&&~f[now][pos[l]][remain])return f[now][pos[l]][remain];
	ll s=0;
	int r;
	if(!limit)r=9;
	else r=mark[now];
	for(int re i=0;i<=r;++i){
		int nxt;
		if(i)nxt=lcm(i,l);
		else nxt=l;
		s+=dp(now-1,nxt,(remain*10+i)%2520,limit&&i==r);
	}
	if(!limit)f[now][pos[l]][remain]=s;
	return s;
}

ll solve(ll a){
	len=0;
	do{
		mark[++len]=a-a/10*10;
		a/=10;
	}while(a);
	return dp(len,1,0,1);
}

ll l,r;
signed main(){
	getlcm(2,1);
	memset(f,-1,sizeof f);
	while(~scanf("%lld%lld",&l,&r))outint(solve(r)-solve(l-1));pc('\n');
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值