beautiful numbers树形dp or 数位dp

题目找链接

题意:

  如果数a能被a中的每一位数整除(0除掉),则称a是一个beautiful number,求一个区间内的beautiful numbers的个数。

分析:

  首先,很显然,l到r的所有beautiful numbers(以下写b n吧,有点长)等于0到r的b n个数减去0到l-1的b n的个数,这个不用证明吧。当然也很好证。然后我们只要能在时间允许的范围内求0到某个数的b n的数量就可以了。

  怎么求能?我们把它转化到树上,先举个例子:如果要求0到23的b n的数量,那么我们假设有一个根为0的树,0这个结点有儿子0 1 2,然后0有儿子0 1 2 3 4 5 6 7 8 9,1有儿子0 1 2 3 4 5 6 7 8 9,2有儿子0 1 2 3,那么我们只要从根开始读,一直读到叶子节点(例如一直向最左走我们就读到000,一直向最右走就读到023,然后中间可以读到这两个数中间所有的数,不重且不漏),到每一个叶子节点记录一下这个数可不可以被经过的所有节点整除就好了,这不还是暴力吗。。。先别急,慢慢来,这样我们先想一下怎么转移,如果他能被x,y整除,一定能被其最小公倍数整除,当然,反过来说也成立,那我们只需要记录途径数字的最小公倍数就可以直接判断这样走的数能不能被所有位数整除。我们定义Dp[i][ha][k]表示所有过第i个节点的b n数(当然已经给节点编号),ha表示我们已经读到的数,于是我们有了转移方程:

  Dp[i][ha][k]=sum{Dp[son[i][ha*10+val[son[i][j]]][j]][divisor(k,val[son[i][j]])]};

  就不写公式了,大家应该可以看懂,divisor表示最小公倍数,val[i]表示第i个节点上写的数字,son[i][j]表示第i个节点的第j个儿子。

  其实到这里,基本的思路就有了,但是解决问题还差一点,首先,这么大的数组开不下,然后就是时间复杂度接受不了。于是我们要进行优化。

  其实大家会发现,很多棵子数是相似的,那是不是有些子树可以只求一边呢,当然可以,首先我们要明确一点,我们所有可能用到的k都有x%2520同余x(mod k),于是我们想一想,如果到某个节点读出的数字是0252025200,其实和读到0000252000(位数要相等)最后找到的状态数没什么不同(前题是两个节点的子树都是满的),那么我们就可以把ha表示为读到的数字%2520,于是此时你会发现,这个点的b n的数量和这个点的i只没有什么关系,所有这一维可以省掉,但是还有问题,就是虽然0000同余00000(mod 2520)但是并不等价,为什么呢,因为子树大小已经不一样了(及还需读的位数不一样),那么我们就可应再加一维表示还要读的数字的数量,然后进行记忆话搜索就可以了。

  现在再重新说一下状态:Dp[i][j][k]表示还剩i个数字没有读,读到的数字数字%2520为j且走过的所有的数的最小公倍数为k读完满的子树之后可以找到b n的数量。注意,刚才我并没有对k进行描述,但现在有了,因为刚才k并不是状态,只是记录一下到这里的最小公倍数,也就是说可以没有k这一维。而这里我们不再区分节点,所有要记录k,k相同才等价。(要想明白是为什么等价)

  说到这里,大家应该都知道该怎么做了但是Dp数组仍然有点大,还能不能缩小一下,可以:离散化,k可以取到的值其实是要小于2520的,我们只要找出可能取到的值就可以了。

  还有就是当然边界特殊处理。

  最后是他的时间复杂度,有人说是O(玄学)。。。其实还是有保证的,首先,Dp中每个值只会算一次,边界最多18层,每层最多有10个儿子,而儿子只有一个是边界,所有还是可以保证复杂度的。

  部分细节见代码注释。

  最后看代码吧:

#include <cstdio>
#include <cstring>
long long Dp[20][3000][55];
int mod=2520;//1-9的最小公倍数
long long Gcd(long long a,long long b){
    if(!b)
        return a;
    return Gcd(b,a%b);
}
long long B(long long a,long long b){//求最小公倍数
    if(!a||!b)//特殊处理0
        return a+b;
    return (a/Gcd(a,b))*b;
}
int w[20];//记录每一位以处理边界
int li[3000];
long long dp(int len,int yu,int BB,bool bi){//开始dp
    if(len==0){//到达叶子节点
        if(yu%BB==0)
            return 1;
        else
            return 0;
    }
    if(!bi&&Dp[len][yu][li[BB]]!=-1)//已经有等价情况处理过了
        return Dp[len][yu][li[BB]];
    int j;
    if(bi)
        j=w[len];
    else
        j=9;
    long long ans=0;
    for(int i=0;i<j;i++)
        ans+=dp(len-1,(yu*10+i)%mod,B(BB,i),0);
    ans+=dp(len-1,(yu*10+j)%mod,B(BB,j),bi);
    if(!bi)
        Dp[len][yu][li[BB]]=ans;
    return ans;
}
long long Cl(long long a){
    int len=0;
    while(a){
        len++;
        w[len]=a%10;
        a/=10;
    }
    return dp(len,0,1,1);//算出位数进行处理
}
int main(){
    memset(Dp,-1,sizeof(Dp));//0可能会tle,因为可能有0的一些节点要算好多遍
    int t;
    scanf("%d",&t);
    int al=0;
    for(int i=1;i<=2050;i++)//离散化
        if(2520%i==0){
            al++;
            li[i]=al;
        }
    for(int i=1;i<=t;i++){
        long long l,r;
        scanf("%I64d%I64d",&l,&r);//题目说不用%lld
        printf("%I64d
",Cl(r)-Cl(l-1));
    }
    return 0;
}
原文地址:https://www.cnblogs.com/wish-all-ac/p/12714997.html