数位DP总结+例题

数位DP详解与实战

学了数位DP很久了,今天就简单的总结一下吧。

一、什么是数位DP

  数位DP简单来说是一种统计数的DP,该类题一般可以用暴力写出来,但是由于数据太大,一般会超时。数位DP一般是统计[l,r]中满足题中要求的数的个数,至于为什么称为数位DP,因为要对数的每一位进行讨论(个位,十位,百位……)。数位DP一般有两种写法,一种是循环,另一种是递归。通常用循环写,比较的麻烦,所以我们一般都是用递归来写。

二、数位DP的常规写法

  数位DP一般用来统计一段区间中满足题意数的个数,例如区间[l,r],而我们一般计算两个区间的值[0,l],[0,r],然后再相减。所以主函数我们一般的写法为:

    int l,r;
    while(scanf("%d%d",&l,&r)&&l+n)
    {
        printf("%d\n",solve(r)-solve(l-1));
    }
    ///统计[l,r]区间中满足题意数的个数

然后就是要对数的每一位进行分解,分解后用一个数组保存,方便我们后期的使用:

int solve(int n)
{
    int len=0;
    while(n)
    {
        d[len++]=n%10;
        n/=10;
    }
    return dfs(len-1,1,1);
}

最后就是数位DP的核心代码了,一般都可以把这个当做模板来使用,只需要根据题中的意思来改一些地方就可以了。

int dfs(int pos,/*当前讨论位数*/,bool lead/*前导零*/,bool limit/*数位上界变量*/)//不是每个题都要判断前导零
{
    
    if(pos==-1)///递归边界,既然是按位枚举,最低位是0,那么pos==-1说明这个数我枚举完了
        return 1;/*这里一般返回1,表示你枚举的这个数是合法的,那么这里就需要你在枚举时必须每一位都要满足题目条件,也就是说当前枚举到pos位,
        前面的数都是合法的,不过返回值还要看题目要求,后面例题里会体现出来*/
    if(!limit && !lead && dp[pos]!=-1)///第二个就是记忆化(在此前可能不同题目还能有一些剪枝)
        return dp[pos];
    int End=limit?a[pos]:9;//根据limit判断枚举的上界End;
    int ans=0;
    //开始计数
    for(int i=0; i<=End; i++) ///枚举,然后把不同情况的个数加到ans就可以了
    {
        if()///里面的选择判断要根据题中要求来判断
            ...
        else if()
            ...
        ans+=dfs(pos-1,/*状态转移*/,lead && i==0,limit && i==End)//最后两个变量传参都是这样写的
    }
    if(!limit && !lead)///这里对应上面的记忆化,在一定条件下时记录,保证一致性.
        dp[pos]=ans;
    return ans;
}

总体代码:

int a[20];
int dp[20];//不同题目状态不同
int dfs(int pos,/*当前讨论位数*/,bool lead/*前导零*/,bool limit/*数位上界变量*/)//不是每个题都要判断前导零
{
    
    if(pos==-1)///递归边界,既然是按位枚举,最低位是0,那么pos==-1说明这个数我枚举完了
        return 1;/*这里一般返回1,表示你枚举的这个数是合法的,那么这里就需要你在枚举时必须每一位都要满足题目条件,也就是说当前枚举到pos位,
        前面的数都是合法的,不过返回值还要看题目要求,后面例题里会体现出来*/
    if(!limit && !lead && dp[pos]!=-1)///第二个就是记忆化(在此前可能不同题目还能有一些剪枝)
        return dp[pos];
    int End=limit?a[pos]:9;//根据limit判断枚举的上界End;
    int ans=0;
    for(int i=0; i<=End; i++) ///枚举,然后把不同情况的个数加到ans就可以了
    {
        if()///里面的选择判断要根据题中要求来判断
            ...
        else if()
            ...
        ans+=dfs(pos-1,/*状态转移*/,lead && i==0,limit && i==End)//最后两个变量传参都是这样写的
    }
    if(!limit && !lead)///这里对应上面的记忆化,在一定条件下时记录,保证一致性.
        dp[pos]=ans;
    return ans;
}
int solve(int x)
{
    int pos=0;
    while(x)//把数位都分解出来
    {
        a[pos++]=x%10;//个人老是喜欢编号为[0,pos),看不惯的就按自己习惯来,反正注意数位边界就行
        x/=10;
    }
    return dfs(pos-1/*从最高位开始枚举*/,1,1);//刚开始最高位都是有限制并且有前导零的,显然比最高位还要高的一位视为0嘛
}
int main()
{
    int l,r;
	memset(dp,-1,sizeof(dp));
    while(~scanf("%d%d",&l,&r))
    {
        printf("%d\n",solve(r)-solve(l-1));
    }
    return 0;
}

 

三、代码里的一些注意点

1,为什么要记忆化:

    if(!limit && !lead && dp[pos]!=-1)///第二个就是记忆化(在此前可能不同题目还能有一些剪枝)
        return dp[pos];
    if(!limit && !lead)///这里对应上面的记忆化,在一定条件下时记录,保证一致性.
        dp[pos]=ans;

我们就以HDU2089 不要62来讨论,题意是数字中不能出现数字4和62(6和2没有连在一起满足条件)。现在dp[pos],表示的是pos位数的数字里面有多少满足条件,我们讨论[0,256].首先我们百位为0,递归下去讨论十位,limit=0。当十位讨论到5时。递归下去讨论个位limit=0,肯定除了54以为都满足条件,所以在讨论十位为5时,dp[1]加了9,但是当百位讨论到2,十位讨论到5时,dp[1]已经有值了,现在limit是等于1的,要是现在我们不判断(!limit)这个条件,就会直接返回dp[1],但是我们只讨论到256,所以跟题意不符,所以在返回判断那里要判断limit。(可以说的不是太清楚,自己模拟一下就比较好懂)。

2.枚举上限End的确定和limit的传参:

    int End=limit?a[pos]:9;//根据limit判断枚举的上界End;
    ans+=dfs(pos-1,/*状态转移*/,lead && i==0,limit && i==a[pos])//最后两个变量传参都是这样写的

还是讨论[0,256]。中满足的数,我们首先第一次递归传参limit为1(可以看solve函数),然后End等于2,后面递归limit传参都为(limit && i==End),当百位上讨论0-1时,limit传参下去为0,十位都能讨论到0-9,因为0-99,100-199都小于256。当百位讨论到2是,limit传参下去为1,十位只能讨论到5,个位的道理是一样的。

四、例题和题解:


HDU2089不要62

HDU4734 F(x)

POJ 3252 Round Numbers

HDU3709 Balanced Number

HDU4507 吉哥系列故事——恨7不成妻(比较难的数位DP)

         2019长安大学ACM校赛-XOR



HDU2089不要62

题意:就是计算一段区间里,数字里没有出现4和62的数字的个数。

题解:这是数位DP的一个入门题吧,改一下模板就能过,细节看代码:

#include <iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<map>
#include<queue>
#include<set>
#include<cmath>
#include<stack>
#include<string>
const int maxn=5e2+10;
const int mod=1e9+7;
const int inf=1e8;
#define me(a,b) memset(a,b,sizeof(a))
#define lowbit(x) x&(-x)
#define mid (l+r)/2
#define lson l,mid,rt<<1
#define rson mid+1,r,rt<<1|1
#define PI 3.14159265358979323846
int dir[4][2]= {0,-1,-1,0,0,1,1,0};
typedef long long ll;
using namespace std;
int d[10],dp[30];
int dfs(int pos,int x,int lit)
{
    if(pos==-1)///所有数枚举完了,当前数数字肯定满足条件,直接返回1
        return 1;
    if(!lit&&dp[pos]==-1)
        return dp[pos];
    int End=lit?d[pos]:9;
    int ans=0;
    for(int i=0;i<=End;i++)
    {
        if(i==4)///出现4跳过
            continue ;
        else if(x==6&&i==2)///x为该数字的上一位,要是出现62,直接跳过
            continue ;
        else
            ans+=dfs(pos-1,i,lit&&i==End);
    }
    if(!lit)
        dp[pos]=ans;
    return ans;
}
int solve(int n)
{
    int len=0;
    while(n)
    {
        d[len++]=n%10;
        n/=10;
    }
    return dfs(len-1,0,1);
}
int main()
{
    int m,n;
    while(scanf("%d%d",&m,&n)&&m+n)
    {
        printf("%d\n",solve(n)-solve(m-1));
    }
    return 0;
}

 

HDU4734 F(x)

题意:给出两个数a,b F(x) = An * 2n-1 + An-1 * 2n-2 + ... + A2 * 2 + A1 * 1,将a的通过这样算出来算出来,然后计算0-b里面有多少数通过这个公式算出来的值小于等于a算出来的值。

题解:首先通过这个公式将a对应的值算出来,然后每次减去在枚举0-b中的数的每一位转化出来的值,然后看最后的值,要是最后的值大于等于0,说明当前数满足条件,更多细节看代码。

#include <iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<map>
#include<queue>
#include<set>
#include<cmath>
#include<stack>
#include<string>
const int maxn=1e5+10;
const int mod=100000000;
const int inf=1e8;
#define me(a,b) memset(a,b,sizeof(a))
#define lowbit(x) x&(-x)
typedef long long ll;
using namespace std;
int d[30],dp[20][maxn],er[30];
void init()
{
    er[0]=1;///er[i]=2^i
    for(int i=1; i<30; i++)
        er[i]=er[i-1]*2;
}
int dfs(int pos,int sum,int lit)
{
    if(sum<0)///要是sum小于0,说明该数不满足条件,直接返回0
        return 0;
    if(pos==-1)///要是枚举到最后一位,判断当前sum是否大于0,大于等于0,返回1,反之返回0
        return sum>=0;
    if(!lit&&dp[pos][sum]!=-1)
        return dp[pos][sum];
    int ans=0;
    int End=lit?d[pos]:9;
    for(int i=0; i<=End; i++)
        ans+=dfs(pos-1,sum-i*er[pos],lit&&i==End);///sum-i*er[pos],每次减去当前数转化的值
    if(!lit)
        dp[pos][sum]=ans;
    return ans;
}
int inct(int m)///算出a通过式子转化出来的值。
{

    int td[30],len=0,opt=0;
    while(m)
    {
        td[len++]=m%10;
        m/=10;
    }
    for(int i=0; i<len; i++)
        opt+=td[i]*er[i];
    return opt;
}
int solve(int a,int b)
{
    int len=0;
    while(b)
    {
        d[len++]=b%10;
        b/=10;
    }
    return dfs(len-1,inct(a),1);
}
int main()
{
    int t,Case=1;
    scanf("%d",&t);
    init();
    me(dp,-1);
    while(t--)
    {
        int a,b;
        scanf("%d%d",&a,&b);
        printf("Case #%d: %d\n",Case++,solve(a,b));
    }
    return 0;
}

 

POJ 3252 Round Numbers

题意:一个十进制数化成2进制,要是二进制数里0的个数大于1的个数称为圆数,现在给出一个区间,问里面有多少圆数。

题解:与其他题不同,这个题是要化成二进制,我们不需要计算该数中0和1的个数到底有多少个,而是只需要计算两个的差值是否大于等于0,声明一个二维数组dp[pos][sum],表示pos位数,0和1个数差值为sum的数有多少个。因为这两个数的差值是dp数组的其中一个参数,所以sum要一直大于等于0。所以提前将sum提前赋一个初值,避免sum小于0,最后直接判断sum是否大于等于赋的初值,需要注意这个题得判断前置0,相信大家应该能理解为啥要判断前置0,更多细节看代码。

#include <iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<map>
#include<queue>
#include<set>
#include<cmath>
#include<stack>
#include<string>
const int maxn=1e5+10;
const int mod=998244353;
const int inf=1e8;
#define me(a,b) memset(a,b,sizeof(a))
#define lowbit(x) x&(-x)
typedef long long ll;
using namespace std;
int d[50],dp[50][100];
int dfs(int pos,int sum,int lead,int lit)
{
    if(pos==0)
        return sum>=50;///判断0的个数是否大于1的个数
    if(!lead&&!lit&&dp[pos][sum]!=-1)
        return dp[pos][sum];
    int ans=0;
    int End=lit?d[pos]:1;///注意端点
    for(int i=0;i<=End;i++)
    {
        if(i==0)
        {
            if(lead)///要是前一个数字为0,而当前数也为0时,sum值不变,例如00101,前两个0都不能算
                ans+=dfs(pos-1,sum,1,lit&&i==End);
            else
                ans+=dfs(pos-1,sum+1,0,lit&&i==End);///没有前置0,当前为0,sum+1
        }
        else
            ans+=dfs(pos-1,sum-1,0,lit&&i==End);///当前数为1,sum-1
    }
    if(!lit&&!lead)
        dp[pos][sum]=ans;
    return ans;
}
int solve(int n)
{
    int len=0;
    while(n)
    {
        d[++len]=n%2;
        n/=2;
    }
    return dfs(len,50,1,1);///sum初值为50,避免sum小于0
}
int main()
{
    int n,m;
    me(dp,-1);
    while(scanf("%d%d",&m,&n)!=EOF)
    {
        printf("%d\n",solve(n)-solve(m-1));
    }
    return 0;
}

 

HDU3709 Balanced Number

题意:一个十进制数,以其中一位为支点,其两边的位数上的数乘以该位数离支点的距离的和是否相等,要是两边这样算出的值相等则为一个平衡数,现在给出一个区间,问里面有多少平衡数。

题解:一个数,分解过后,枚举每一位当支点,然后把每一个当支点能有的多少个数,再将所有情况加起来。声明一个三维dp数组dp[pos][x][sum],表示pos位数以第x位位支点,两边差值为sum的数有多少个,然后就是常规的模板了,细节看代码。

#include <iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<map>
#include<queue>
#include<set>
#include<cmath>
#include<stack>
#include<string>
const int maxn=1e4+10;
const int mod=998244353;
const int inf=1e8;
#define me(a,b) memset(a,b,sizeof(a))
#define lowbit(x) x&(-x)
typedef long long ll;
using namespace std;
int d[20];
ll dp[20][20][maxn];
ll dfs(int pos,int x,int sum,int lit)
{
    if(pos==0)
        return sum==0;///sum为两边的差值,要是sum等于0,该数为平衡数
    if(sum<0)///因为是左边的数减右边数,要是sum小于0了,当前数肯定不是平衡数
        return 0;
    if(!lit&&dp[pos][x][sum]!=-1)
        return dp[pos][x][sum];
    ll ans=0;
    int End=lit?d[pos]:9;
    for(int i=0;i<=End;i++)
        ans+=dfs(pos-1,x,sum+(pos-x)*i,lit&&i==End);///(pos-x)*i,因为从高位开始,高于x的位数,都为正,小于都为负。这里的写法比较巧妙,大家可以思考下。
    if(!lit)
        dp[pos][x][sum]=ans;
    return ans;
}
ll solve(ll n)
{
    ll ans=0,len=0;
    while(n)
    {
        d[++len]=n%10;
        n/=10;
    }
    for(int i=1;i<=len;i++)
        ans+=dfs(len,i,0,1);
    return ans-len+1;///这里要减去0,00,000……的情况
}
int main()
{

    int t;
    scanf("%d",&t);
    me(dp,-1);
    while(t--)
    {
        ll n,m;
        scanf("%lld%lld",&m,&n);
        printf("%lld\n",solve(n)-solve(m-1));
    }
    return 0;
}

 

HDU4507 吉哥系列故事——恨7不成妻(比较难的数位DP)

题意+题解:传送门

 

2019长安大学ACM校赛-XOR

题意:给出区间[l,r],让你找出有多少x,满足

题解:

通过这个式子得到,我们只需要找出在二进制下,某位数与左边两位数不同数的个数,更多看代码。

#pragma comment(linker, "/STACK:102400000,102400000")
#include <iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<map>
#include<queue>
#include<set>
#include<cmath>
#include<stack>
#include<string>

const int mod = 998244353;
const int maxn = 1e5 + 5;
const int inf = 1e9;
const long long onf = 1e18;
#define me(a, b) memset(a,b,sizeof(a))
#define lowbit(x) x&(-x)
#define lson l,mid,rt<<1
#define rson mid+1,r,rt<<1|1
#define PI 3.14159265358979323846
typedef long long ll;
typedef unsigned long long ull;
using namespace std;
int a[100];
ll dp[80][2][2];
ll dfs(int pos,int sta_2,int sta_1,bool limit){///sta_2保存该位左边2位的数字,sta_1保存该位左边1位的数字
    if(pos==-1)
        return 1;
    if(!limit&&dp[pos][sta_2][sta_1]!=-1)
        return dp[pos][sta_2][sta_1];
    int End=limit?a[pos]:1;
    ll sum=0;
    for(int i=0;i<=End;i++){
        if(i&sta_2)
            continue ;
        sum+=dfs(pos-1,sta_1,i,i==End&&limit);
    }
    if(!limit)
        dp[pos][sta_2][sta_1]=sum;
    return sum;
}
ll solve(ll x){
    int len=0;
    while(x){
        a[len++]=x%2;
        x/=2;
    }
    return dfs(len-1,0,0,1);
}
int main() {
    int t;
    me(dp,-1);
    scanf("%d",&t);
    while(t--){
        ll l,r;
        scanf("%lld%lld",&l,&r);
        printf("%lld\n",solve(r)-solve(l-1));
    }
    return 0;
}

以上都是本人对于数位DP的理解,要是有大佬有更好的做法,希望不吝赐教。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值