CF55D Beautiful numbers 数位DP

传送门

题目描述

Volodya is an odd boy and his taste is strange as well. It seems to him that a positive integer number is beautiful if and only if it is divisible by each of its nonzero digits. We will not argue with this and just count the quantity of beautiful numbers in given ranges.

输入

The first line of the input contains the number of cases t (1 ≤ t ≤ 10). Each of the next t lines contains two natural numbers li and ri (1 ≤ li ≤ ri ≤ 9 ·1018).

Please, do not use %lld specificator to read or write 64-bit integers in C++. It is preffered to use cin (also you may use %I64d).

输出

Output should contain t numbers — answers to the queries, one number per line — quantities of beautiful numbers in given intervals (from li to ri, inclusively).

样例

样例输入一

1

1 9

样例输出一

9

样例输入二

1

12 15

样例输出二

2

分析

一句话题意:有T组询问,每次询问区间[l, r]中的beautiful number有多少。beautiful number是指这个数可以被组成它的数字整除。例如15是beautiful number,因为15可以被1整除,也可以被5整除。25不是beautiful number, 25不能被2整除。

这道题数据范围达到了9e18,如果暴力枚举的话一定会T掉

所以我们要借助一个神奇的东西-——数位DP

0x00 前置知识:数位DP

引用自StungYep的博客

定义

数位dp(Digit Entry DP)是一种计数用的dp,一般就是要哦统计区间[l,r]内满足一些条件的数的个数。所谓数位dp,字面意思就是在数位上进行dp。数位的含义:一个数有个位、十位、百位、千位......数的每一位就是数位啦!

数位dp的思想

数位dp的实质就是换一种暴力枚举的方式,使得新的枚举方式满足dp的性质,然后记忆化就可以了。

模板

 1 typedef long long ll;
 2 int a[20];
 3 ll dp[20][state];   //不同题目状态不同
 4 ll dfs(int pos,int state,bool lead,bool limit)       //变量,状态,前导0,数位上界;注意不是每题都要判断前导零
 5 {
 6     if(pos==0) return 1;    //递归边界,一般一种递归到结束只能产生一种情况
 7     if(!limit && !lead && dp[pos][state]!=-1) return dp[pos][state];    //记忆化
 8     int up=limit?a[pos]:9;  //枚举上界
 9     ll ans=0;               //计数
10     for(int i=0;i<=up;i++)  //枚举,然后把不同情况的个数加到ans就可以了
11     {
12         if() ...
13         else if()...        //一下条件
14         ans+=dfs(pos-1,/*状态转移*/,lead && i==0,limit && i==a[pos]) //最后两个变量传参都是这样写的
15         //state状态转移要保证i的合法性,比如不能有62,那么当pre==6&&i==2就不合法,这里用state记录pre是否为6即可。
16     }
17     if(!limit && !lead) dp[pos][state]=ans;
18     return ans;
19 }
20 ll solve(ll x)
21 {
22     int tot=0;
23     while(x)
24     {
25         a[++tot]=x%10;
26         x/=10;
27     }
28     return dfs(tot/*从最高位开始枚举*/,/*一系列状态 */,true,true);//刚开始最高位都是有限制并且有前导零的,显然比最高位还要高的一位视为0嘛
29 }
30 int main()
31 {
32     ll le,ri;
33     while(~scanf("%lld%lld",&le,&ri))
34     {
35         //初始化dp数组为-1,这里还有更加优美的优化,后面讲
36         printf("%lld\n",solve(ri)-solve(le-1));
37     }
38 }
View Code

0x01 特征

对于一个正整数,什么情况下它才能被组成它的非零数字整除呢

这个应该比较好想,只有当这个正整数能被它所有数位的最小公倍数整除时,它才能被每一个数位整除

而且,我们还知道1,2,3,4,5,6,7,8,9的最小公倍数是2520(后面要用到)

0x02 数组定义

我们要进行DP的话,肯定要定义一个f 数组存储我们计算过的值

因为这道题和数位有关,所以第一位我们要定义当前遍历到了第几位

而且我们还要判断当前的数是否能被它所有数位的最小公倍数整除

所以我们还要开两维记录当前的数和它所有数位的最小公倍数

所以,最后的f数组就是f[当前枚举到了第几位][上次枚举到的数][之前所有数位的最小公倍数]

题目中给出的最大的数就是9e18,所以最多有19位,那么第一维我们开20就可以了

第二维我们显然不能开到9e18,会超内存

因为我们最终统计的是这个数能否被它所有数位的最小公倍数整除,所以我们不必要记录原数的值

我们只需要记录原数对2520取模的结果就可以,因为2520是1到9的最小公倍数,所以取模之后不会有影响,x与x%2520是等效的

这时我们算一下内存20*2520*2520,还是会M掉

所以我们考虑减省一下第三维,由于第三维记录的是所有数位的最小公倍数,所以有很多数并不会出现

比如11、13、17、19……它们并不是1到9中任意几个数的最小公倍数

所以我们只需要记录2520的因数就可以了,这样的数有48个,所以我们开50就可以了

附48个数:

1 2 3 4 5 6 7 8 9 10 12 14 15 18 20 21 24 28 30 35 36 40 42 45 56 60 63 70 72 84 90 105 120 126 140 168 180 210 252 280 315 360 420 504 630 840 1260 2520 

0x03:主函数

先上代码

 1 int main(){
 2     int t;
 3     scanf("%d",&t);
 4     for(ll i=1,j=0;i<=2520;i++){
 5         if(2520%i==0){
 6             lcm[i]=j++;
 7         }
 8     }
 9     memset(f,-1,sizeof(f));
10     while(t--){
11         ll aa,bb;
12         scanf("%lld%lld",&aa,&bb);
13         ll ans=solve(bb)-solve(aa-1);
14         printf("%lld\n",ans);
15     }
16     return 0;
17 }

前三行没有什么好说的,我们来看一下4到8行,这里就是建立了一种映射关系,把前面的48个数分别看成0到47

这其实和离散化差不多

第九行是初始化,我们这里把它写在询问的外面是因为之后的询问可能会用到前面的值,这样比较快一些

第13行的solve函数我们在下面会提到

这里的solve(bb)-solve(aa-1)实际上是用到了一种差分的思想

0x04:solve函数

1 ll solve(ll x){
2     ll cnt=0;
3     memset(num,0,sizeof(num));
4     while(x!=0){
5         num[cnt++]=x%10;
6         x/=10;
7     }
8     return asd(cnt-1,0,1,1);
9 }

这里的cnt是用来记录当前的数有多少位,num数组是用来记录这个数每一位上的数字的

这个函数的变量只有一个xx,返回值是0到xx之间beautiful number的个数

下面的一个asd函数是最重要的部分

ll asd(ll now,ll mod,ll gbs,ll jud){

它的四个参数分别为:当前处理到第now位; now位之前的数%2520; now位之前的数的每一位数的最小公倍数;jud特判前一位是否为范围内的最大值

前三个参数比较好理解,我们着重来讲一下第四个参数jud

我们来举一个例子

比如说我们要求的数为5456

我们把它的千位设为第3位,百位设为第2位,十位设为第1位,个位设为第0位

 当它的第三位为0、1、2、3、4时,它的第二位可以从0枚举到9

但是当它的第三位枚举到5时,第二位的数就只能枚举到4

所以我们用一个变量jud记录前一位能不能达到最大值,如果上一位达到了最大值,那么这一位就只能枚举到当前位上的数

如果上一位没有达到最大值,那么这一位就可以从0到9随便枚举

0x05:asd函数

 1 ll asd(ll now,ll mod,ll gbs,ll jud){
 2     if(now<0){
 3         if(mod%gbs==0) return 1;
 4         else return 0;
 5     }
 6     if(jud==0 && f[now][mod][lcm[gbs]]!=-1){
 7         return f[now][mod][lcm[gbs]];
 8     }
 9     ll mmax=9,mans=0;
10     if(jud==1) mmax=num[now];
11     for(ll i=0;i<=mmax;i++){
12          mans+=asd(now-1,(mod*10+i)%2520,i!=0?i*gbs/gcd(i,gbs):gbs,jud&&i==mmax);
13     }
14     if(jud==0) f[now][mod][lcm[gbs]]=mans;
15     return mans;
16 }

第一行的参数我们已经在上面讲过了

那么我们来看一下最开始传的参数 cnt-1,0,1,1

cnt是我们要求的那个数的位数,因为我是从0开始编号,所以要传cnt-1,就是该数的最高位

因为最高位前没有数,所以第二个参数我们传0

第三个数是前面所有位数的最小公倍数,那么我们可不可以传0呢

应该是不可以的,因为我们如果传0的话,有可能会出现RE(因为一个数不能对0取模)的情况

所以我们最好还是传一个1

为什么呢?因为1与其他位去求最小公倍数的话,不会影响最终的结果

最后一个参数是判断前一位是否为范围内的最大值,显然是,所以我们传1

2到5行是一个终止条件

if(now<0){
        if(mod%gbs==0) return 1;
        else return 0;
    }

如果当前的位数小于0,说明这个数我已经处理完了,那我们只要判断mod(该数%2520)能否被gbs(最小公倍数)整除就可以了

如果能够整除返回1,否则返回0

6到8行是一个特殊判断,可以提升我们的效率

if(jud==0 && f[now][mod][lcm[gbs]]!=-1){
        return f[now][mod][lcm[gbs]];
    }

如果当前的f值已经求出来过,并且没有数位限制,那我们就可以直接用

如果有数位限制,那我们就不能直接用,因为我们不知道这两次的数位限制是否相等

ll mmax=9,mans=0;
    if(jud==1) mmax=num[now];

第9、10行则是判断该数位可以枚举到几

如果上一个数位没有达到最大值,那我们就把可以枚举到的最大数置为9

否则,可以枚举到的最大数就由当前这一位决定

for(ll i=0;i<=mmax;i++){
         mans+=asd(now-1,(mod*10+i)%2520,i!=0?i*gbs/gcd(i,gbs):gbs,jud&&i==mmax);
    }
    if(jud==0) f[now][mod][lcm[gbs]]=mans;
    return mans;

最后几行则是普通情况,即没有达到边界

这是我们就要一位一位地去枚举下一位的状态

首先我们来看这句话

mans+=asd(now-1,(mod*10+i)%2520,i!=0?i*gbs/gcd(i,gbs):gbs,jud&&i==mmax);

这句话其实就是向下传参数,递归解决问题

第一个参数位数肯定要减去1变成now-1

第二个参数则是之前的数位表示的数乘10再加上当前这一位的数,再对2520取模

为什么要乘10呢,这就要联系数字的实际意义

比如25,它的值是2*10+5

2345,它的值是2*1000+3*100+4*10+5

我们就拿2345来举例子,它一共有4位,要遍历5次

第一次它没有乘10,最后一次遍历时会在第二句话那里触发终止条件,也就是说

2会乘3次10,3会乘2次10,4会乘1次10,5一次也不会乘

把这个过程加起来正好是原数,在中间取一下模并不会影响结果

第三个参数比较长,我们把它展开就是

if(当前位上的数字为0){
    最小公倍数不变
} else {
    和之前数位上的数字取最小公倍数
}

a和b的最小公倍数就是a*b/a和b的最大公因数

下面是求最大公因数的函数

ll gcd(ll a,ll b){
    if(b==0) return a;
    return gcd(b,a%b);
}

最后一个参数是判断最高位

==的优先级要高于位运算,所以其实就是一个判断当前数字是否为该位上的最大数字的操作

如果上一位有限制并且当前枚举到的是该数位上合法的最大的数字,我们就传1,否则传0

比如2345

当我们的千位上枚举到1,百位上枚举到9时,虽然9是最大的数字。但是第一个条件jud==1并不满足,所以这时我们要传0

当我们的千位上枚举到2,百位上枚举到3时,这时两个条件都满足,所以这时我们要传1

最后不要忘了更新f的值

0x06:代码

 1 #include<cstdio>
 2 #include<iostream>
 3 #include<cstring>
 4 #include<algorithm>
 5 using namespace std;
 6 typedef long long ll;
 7 ll lcm[3000],f[20][3000][55],num[55];
 8 ll gcd(ll a,ll b){
 9     if(b==0) return a;
10     return gcd(b,a%b);
11 }
12 ll asd(ll now,ll mod,ll gbs,ll jud){
13     if(now<0){
14         if(mod%gbs==0) return 1;
15         else return 0;
16     }
17     if(jud==0 && f[now][mod][lcm[gbs]]!=-1){
18         return f[now][mod][lcm[gbs]];
19     }
20     ll mmax=9,mans=0;
21     if(jud==1) mmax=num[now];
22     for(ll i=0;i<=mmax;i++){
23          mans+=asd(now-1,(mod*10+i)%2520,i!=0?i*gbs/gcd(i,gbs):gbs,jud&&i==mmax);
24     }
25     if(jud==0) f[now][mod][lcm[gbs]]=mans;
26     return mans;
27 }
28 ll solve(ll x){
29     ll cnt=0;
30     memset(num,0,sizeof(num));
31     while(x!=0){
32         num[cnt++]=x%10;
33         x/=10;
34     }
35     return asd(cnt-1,0,1,1);
36 }
37 int main(){
38     int t;
39     scanf("%d",&t);
40     for(ll i=1,j=0;i<=2520;i++){
41         if(2520%i==0){
42             lcm[i]=j++;
43         }
44     }
45     memset(f,-1,sizeof(f));
46     while(t--){
47         ll aa,bb;
48         scanf("%lld%lld",&aa,&bb);
49         ll ans=solve(bb)-solve(aa-1);
50         printf("%lld\n",ans);
51     }
52     return 0;
53 }
View Code

 

posted @ 2020-04-17 11:18  liuchanglc  阅读(334)  评论(0编辑  收藏  举报