kuangbin带你飞 - 专题十五 - 数位dp

https://vjudge.net/contest/70324


 A - Beautiful numbers

统计区间内的,被数位上各个为零数字整除的数的个数。

下面是暴力的数位dp写法,绝对会TLE的,因为这个要深入到每个数字的最后才能判断是否合法。因为(错误的状态设计导致完全变成暴力dfs搜索)记忆化的意义在询问不多的时候用处不大就去掉了。果然2400分的题不能这么暴力。

#include<bits/stdc++.h>
using namespace std;
#define ll long long

int a[19];
int d[10];
ll dfs(int pos,bool limit,ll sum) {
    if(pos==-1) {
        for(int i=1; i<=9; i++) {
            if(d[i]) {
                if(sum%i)
                    return 0;
            }
        }
        return 1;
    }
    int up=limit?a[pos]:9;
    ll ans=0;
    for(int i=0; i<=up; i++) {
        if(i)
            d[i]++;
        ans+=dfs(pos-1,limit && i==a[pos],sum*10ll+i);
        if(i)
            d[i]--;
    }
    return ans;
}

ll solve(ll x) {
    //特殊处理0
    if(x==0)
        return 1;

    int pos=0;
    while(x) {
        a[pos++]=x%10;
        x/=10;
    }

    memset(d,0,sizeof(d));
    return dfs(pos-1,true,0);
}

int main() {
    int t;
    scanf("%d",&t);
    ll le,ri;
    while(t--) {
        scanf("%lld%lld",&le,&ri);
        printf("%lld\n",solve(ri)-solve(le-1));
    }
}
View Code

没想出来怎么解决,去查了题解,题解暗示说,这样是和最小公倍数有关的。好像的确很有道理,细节只能自己想了。

首先考虑1~9的最小公倍数,也就是 $1*2^3*3^2*5*7=2520$ ,题解提到一个充要条件,就是一个数假如要能被某些数整除,等价于被这些数的最小公倍数整除,这个充要条件的正确性可以由质因数分解得知,就是说这个数的质因数分解必须比他的各个数位的质因数分解“高”,也就比各个数位的质因数分解的“轮廓”也就是最小公倍数“高”。(注: $lcm(a,b)=\frac{a*b}{gcd(a,b)}$ ,且满足结合律)

 然后怎么计数呢?这里受到之前做的数位dp的启发,由下一位的数位dp推出上一位的状态。设计状态的时候借鉴别人的思路, $dp[i][j][k]$ 表示 $i$ 位数中能整除前面数位的最小公倍数 $j$ 的且模2520的余数为 $k$ 的数的个数。

假设某一位的枚举值为 $a$ ,那么 $dp[i][j][k]+=dp[i-1][lcm(j,a)][(k*10+a)%p]$ ,比如现在要求的数位是2836,现在已经枚举过了2,当前在处理8,枚举千位上的值,i=2,j=2,k=2,当千位枚举3时,向下转移一个i=1,j=6,k=23,意义是显然的,因为你多了一个3,必定要能整除最小公倍数6。记忆化的时候要注意,数位受限时不能取用dp值也不能更新dp值

最后还要注意这样做会MLE,改进的方法是给每个可能的1~9的任意组合生成的最小公倍数都生成一个id值或者(假的)hash值,方法是枚举2520的各个质因数统计最后发现是48个。

#include<bits/stdc++.h>
using namespace std;
#define ll long long

int a[19];
ll dp[19][48][2520];

int id[2521];

void gen_id() {
    int top=0;
    for(int i=1; i<=8; i*=2) {
        for(int j=1; j<=9; j*=3) {
            for(int k=1; k<=5; k*=5) {
                for(int l=1; l<=7; l*=7) {
                    id[i*j*k*l]=top++;
                }
            }
        }
    }
}

ll dfs(int pos,bool limit,int lcm,int sum) {
    if(pos==-1){
        return sum%lcm==0;
    }

    if(!limit&&dp[pos][id[lcm]][sum]!=-1)
        return dp[pos][id[lcm]][sum];

    int up=limit?a[pos]:9;
    ll ans=0;
    for(int i=0; i<=up; i++) {
        ans+=dfs(pos-1,limit && i==a[pos],i?(i*lcm)/__gcd(i,lcm):lcm,(sum*10+i)%2520);
    }

    return !limit?dp[pos][id[lcm]][sum]=ans:ans;
}

ll solve(ll x) {
    //特殊处理0
    if(x==0)
        return 1;

    int pos=0;
    while(x) {
        a[pos++]=x%10;
        x/=10;
    }

    return dfs(pos-1,true,1,0);
}

int main() {
    memset(dp,-1,sizeof(dp));
    gen_id();

    int t;
    scanf("%d",&t);
    ll le,ri;
    while(t--) {
        scanf("%lld%lld",&le,&ri);
        printf("%lld\n",solve(ri)-solve(le-1));
    }
}
View Code

最后需要注意是 return !limit?dp[pos][id[lcm]][sum]=ans:ans; ,是当不受限的时候才记录dp,这里WA的一发,不过因为是有两组数据所以很快联想到了。

B - XHXJ's LIS


 

C - 不要62

当然有更简单的数位dp写法。根据自动机的知识我们只需要记录上一位是不是6就可以了。最后还WA了一发是因为dp的第二维开小了。

#include<bits/stdc++.h>
using namespace std;
#define ll long long

int a[6];
ll dp[6][2];

ll dfs(int pos,bool limit,int st) {
    if(pos==-1){
        return 1;
    }

    if(!limit&&dp[pos][st]!=-1)
        return dp[pos][st];

    int up=limit?a[pos]:9;
    ll ans=0;
    for(int i=0; i<=up; i++) {
        if(i==4)
            continue;
        if(st==1&&i==2)
            continue;
        ans+=dfs(pos-1,limit && i==a[pos],i==6);
    }

    return !limit?dp[pos][st]=ans:ans;
}

ll solve(ll x) {
    //特殊处理0
    if(x==0)
        return 1;

    int pos=0;
    while(x) {
        a[pos++]=x%10;
        x/=10;
    }

    return dfs(pos-1,true,0);
}

int main() {
    memset(dp,-1,sizeof(dp));
    ll le,ri;
    while(~scanf("%lld%lld",&le,&ri)) {
        if(le==0&&ri==0)
            break;
        printf("%lld\n",solve(ri)-solve(le-1));
    }
}
View Code

 


 

E - Round Numbers

这里是二进制的数位dp,首先先把基数改成2。然后我们思考怎么设计状态可以使得子问题容易重复,一个很显然的设计方法就是dp[i][j]表示i位数,有j个0的数的个数。那么转移的时候每一步可以选择0或1,每次选择0的时候cnt0-1,最后pos==-1的时候要判断cnt0是否恰为0。需要注意的是虽然在求解7位数是只有3个0的状态是没有用的,但是不代表他不需要被计算,因为在11位数的时候可以先选1个前导1,3个0,转移到7位数的状态,这时候7位数选3个是有用的。

再想想前导0会有什么影响呢?因为前导0中的0是不算的,所以要分开处理一下。

 

但是上面的状态设计方法是有问题的,因为i位数中有j个0的数的个数不容易区分前导0的贡献,导致dp[i][j]的值和实际要求的不一致,在从高位向低位转移时“前导0”是允许存在的,但是单独计算的时候是不可以的。一个解决的办法是再引入cnt1变成dp[i][j][k],表示包括前导0的i位数中,j个非前导0,k个1的数的个数。这样可以顺利地区分前导0带来的影响。

#include<bits/stdc++.h>
using namespace std;
#define ll long long

int a[32];
ll dp[32][32][32];

ll dfs(int pos,bool limit,bool lead,int cnt0,int cnt1) {
    if(pos==-1){
        return cnt0>=cnt1;
    }

    if(!limit&&dp[pos][cnt0][cnt1]!=-1)
        return dp[pos][cnt0][cnt1];

    int up=limit?a[pos]:1;
    ll ans=0;
    for(int i=0; i<=up; i++) {
        ans+=dfs(pos-1,limit&&i==a[pos],lead&&i==0,cnt0+((!lead)&&(i==0)),(cnt1+int(i==1)));
    }

    return (!limit)?dp[pos][cnt0][cnt1]=ans:ans;
}

ll solve(ll x) {
    int pos=0;
    while(x) {
        a[pos++]=x%2;
        x/=2;
    }

    return dfs(pos-1,true,true,0,0);
}

int main() {
    memset(dp,-1,sizeof(dp));
    ll le,ri;
    while(~scanf("%lld%lld",&le,&ri)) {
        printf("%lld\n",solve(ri)-solve(le-1));
    }
}
View Code

 

又被运算符结合坑了,加法运算符比逻辑与运算符的优先级还要高。

上面的代码意思是,dp[i][j][k]表示包括前导0的i位数中,j个非前导0,k个1的数的个数。而lead的真假已经包含在上述的三维(其实只需要两维)中了,所以dp[i][j][k]的存储只受limit影响。


 

G - B-Numbers

数位dp的水题,想清楚状态机怎么运行就可以了。

st0:当前”“,当遇到1时转到st1。

st1:当前”1“,当遇到1时回到本身,当遇到3时转到st2,否则转到st0。

st2:已发现”13“,无论遇到什么都是回到本身。

#include<bits/stdc++.h>
using namespace std;
#define ll long long

const int MAX_LEN=10;
int di[MAX_LEN+1];
ll dp[MAX_LEN+1][3][13];

ll dfs(int len,bool limit,bool lead,int st,int r) {
    if(len==0){
        return (st==2)&&(r%13==0);
    }

    if(!limit&&dp[len][st][r]!=-1)
        return dp[len][st][r];

    int up=limit?di[len]:9;
    ll ans=0;
    for(int i=0; i<=up; i++) {
        if(st==2){
            ans+=dfs(len-1,limit&&i==di[len],lead&&i==0,st,(r*10+i)%13);
        }
        else{
            int nst=0;
            if(i==1)
                nst=1;
            else if(i==3){
                if(st==1)
                    nst=2;
            }
            ans+=dfs(len-1,limit&&i==di[len],lead&&i==0,nst,(r*10+i)%13);
        }
    }

    return (!limit)?dp[len][st][r]=ans:ans;
}

ll solve(ll x) {
    if(x==0)
        return 0;

    int len=0;
    while(x) {
        di[++len]=x%10;
        x/=10;
    }

    return dfs(len,true,true,0,0);
}

int main() {
    memset(dp,-1,sizeof(dp));
    ll ri;
    while(~scanf("%lld",&ri)) {
        printf("%lld\n",solve(ri));
    }
}
View Code

献上越写越短的模板。上面这个个位index为1,也有他的好处。


 

H - F(x)

给定两个数A,B,求不超过B且权不超过A的权的数的数目。一开始错误地估计了权的上界,导致自己把记忆化给去掉了。实际上权不可能超过20000,具体是多少? $9*\sum\limits_{i=1}^{8}2^i\approx9*2^9$ ,5000多一点吧?

#include<bits/stdc++.h>
using namespace std;
#define ll long long

int a[10];
int dp[10][20005];

int weight;

int dfs(int pos,bool limit,int rw) {
    int base=1ll<<pos;

    if(pos==-1){
        return 1;
    }

    if(!limit&&~dp[pos][rw])
        return dp[pos][rw];

    int up=limit?a[pos]:9;
    ll ans=0;
    for(int i=0; i<=up; i++) {
        if(i*base>rw)
            break;
        ans+=dfs(pos-1,limit && i==a[pos],rw-i*base);
    }

    return !limit?dp[pos][rw]=ans:ans;
}

int solve(int x) {
    //特殊处理0
    if(x==0)
        return 1;

    int pos=0;
    while(x) {
        a[pos++]=x%10;
        x/=10;
    }

    return dfs(pos-1,true,weight);
}

void cal_weight(int A){
    weight=0;
    int base=1;
    while(A) {
        weight+=base*(A%10);
        A/=10;
        base<<=1;
    }
}

int main() {
    memset(dp,-1,sizeof(dp));

    int t;
    scanf("%d",&t);
    for(int i=0;i<t;i++){
        int A,B;
        scanf("%d%d",&A,&B);
        cal_weight(A);
        printf("Case #%d: %d\n",i+1,solve(B));
    }
}
View Code

 


 I - BCD Code

这个一看就知道是用自动机就可以了。但是具体怎么建我就陷入了沉思。所以说瘸腿就是瘸腿啊,AC自动机还是要会。多模字符串匹配AC自动机。

 

posted @ 2019-02-28 22:05  韵意  阅读(176)  评论(0编辑  收藏  举报