【数位DP】模板+入门题HDU2089 FZU2109

数位dp 

所谓数位dp,就是用来解决对一串数字有某些限制的这类题型。比如说,给定一个车牌号,要求不能有4出现或者是连续的62出现。这道题出现在HDU2089,可以说是数位dp的经典入门题型,解题思路我们可以用来作为模板。


思路:一般适用dp[cur][state]来表示第cur层,状态为state的方案数,比如这道题,state就可以是0和1,来表示前一位是否为6,为什么需要这个状态呢?因为题目要求不能有连续的62出现,那如果6出现在前一位,当前位肯定就不能有2,需要记录这个状态方便我们去转移。我们先看看作为主体部分的dfs的分解。


int dfs(int pos,int pre,int state,bool limit)
首先这个函数包含了4个状态
pos表示当前枚举到第pos层,一般从高往低深搜,因此pos是递减的,再从底层往回推. 
pre表示前一位的值,方便我们判断连续的62
state表示前一位的值是否为6,方便我们表示dp数组的第二维。状态一般都是与前面已经枚举的数位有关并且通常是根据约束条件当前枚举的这一位能使得状态完整(比如一个状态涉及到连续k位,那么就保存前k-1的状态,当前枚举的第k个是个恰好凑成成一个完整的状态
limit表示当前位是否有限制,有限制的话只能枚举到了当前位所能到达的最大值,没有限制的话就能够枚举0到9所有数 

if(pos==-1) return 1; 

pos==-1是递归函数的边界,递归到了-1层,说明前面的n-1到0层都符合条件,即该方案成立,则返回1。 


if(!limit && dp[pos][state]!=-1) return dp[pos][state];  
这句代码是一个剪枝优化,也叫做记忆化搜索,如果dp[pos][state]已经有值并且没有限制,为什么需要!limit呢,如果没有!limit的话,我们看看会是什么样一种情况.

假设我们求1到261这个区间的个数,第一次枚举百位0,此时根据 limit && i==a[pos],后面的limit都为0,当十位枚举了6的时候,此时我们求dp[0][1],即枚举到个位时,前一位是6个个数,显然dp[0][1]=9(除了2之外的所有数都符合条件).继续dfs,枚举到百位等于2,十位等于6的时候,据 limit && i==a[pos],limit为1,此时求的也是dp[0][1],显然这个值是存在的,那我们直接返回9,这时候问题就出现了,我们的最大上限是261,以26为开头的数,个位数很明显只能枚举0和1,哪有9种情况?这个时候limit就体现出其作用了,只有在!limit,即对枚举位数没有限制的情况下我们才返回dp[pos][state],否则,我们就老老实实去算。 


int up=limit ? a[pos] : 9;

如果上一位是有限制的,那么当前位的枚举也就有了上限,不能一口气枚举到9. 


int tmp=0;  

tmp记录方案数 


for(int i=0;i<=up;i++)  
{  
        if(pre==6 && i==2)continue;  
        if(i==4) continue;//都是保证枚举合法性  
        tmp+=dfs(pos-1,i,i==6,limit && i==a[pos]);  
 }  

开始枚举,在枚举的过程中对不符合条件的答案排除,之后递归自身,传入(pos-1,i,i==6,limit && i==a[pos]),层数为更低一层,下一层的pre为当前枚举到的i,状态是当前i是否等于6,最后的limit是提供给下一层的一个判断,判断下一层能否枚举到9。 


if(!limit)  
{  
dp[pos][sta]=tmp;  
}  

如果当前没有位数限制,则记录当前状态到dp数组中。DP里保存完整的、取到尽头的数据


return tmp;  

最后返回结果


接下来看一下完整代码

#include<iostream>  
#include<cstdio>  
#include<cstring>  
#include<string>  
using namespace std;  
typedef long long ll;  
int a[20];  
int dp[20][2];  
int dfs(int pos,int pre,int sta,bool limit)  
{  
    if(pos==-1) return 1;  
    if(!limit && dp[pos][sta]!=-1) return dp[pos][sta];  
    int up=limit ? a[pos] : 9;  
    int tmp=0;  
    for(int i=0;i<=up;i++)  
    {  
        if(pre==6 && i==2)continue;  
        if(i==4) continue;//都是保证枚举合法性  
        tmp+=dfs(pos-1,i,i==6,limit && i==a[pos]);  
    }  
    if(!limit)  
    {  
        dp[pos][sta]=tmp;  
    }  
    return tmp;  
}  
int solve(int x)  
{  
    int pos=0;  
    while(x)  
    {  
        a[pos++]=x%10;  
        x/=10;  
    }  
    return dfs(pos-1,-1,0,true);  
}  
int main()  
{  
    int le,ri;  
    memset(dp,-1,sizeof dp); 
    while(~scanf("%d%d",&le,&ri) && le+ri)  
    {  
        printf("%d\n",solve(ri)-solve(le-1));  
    }  
}



接下来再看另外一道题FZU2109

题意:问范围内有几个数,满足奇数位上的数,都大于或者等于左右两边 

思路:

dp[i][j][k] 表示在第i个位置时,前面是j,现在这位是奇数位还是偶数位的数目。除此之外,这道题需要考虑前导0的问题, 比如一个5位数,我们枚举的时候可能会出现00132,132是符合条件的,但我们表示出来是00132,直到1这一位开始,才算是第0位。我们要有个标志位,标记前面是不是全是前导0, 如果前面全是,并且我们要算的这一位还是0,那么接下去就还是按全是前导0来算,比如00132这种情况。 只要出现一位不是0了,那就永远不是前导0,就算填的是0,它也不是前导0,比如10002,这种情况。 

代码: 

#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;


int t;
int bit[20],len,l,r;
int dp[20][10][2];
int dfs(int pos,int pre,int odd,int zero,int limit)//pos-当前位置,pre前一个数字,odd,现在位置的奇偶性,zero是否还是前导0,limit是否枚举到了边界
{
    if(pos==-1) return 1;
    if(dp[pos][pre][odd]!=-1 && !limit) return dp[pos][pre][odd];
    int end = limit?bit[pos]:9;
    int ans = 0;
    for(int i = 0; i<=end; i++)
    {
        if(!(i||zero))
            ans+=dfs(pos-1,9,0,zero||i,limit&&i==end);
        else if(odd && pre<=i)
            ans+=dfs(pos-1,i,!odd,zero||i,limit&&i==end);
        else if(!odd && pre>=i)
            ans+=dfs(pos-1,i,!odd,zero||i,limit&&i==end);
    }
    if(!limit)
        dp[pos][pre][odd] = ans;
    return ans;
}


int cal(int x)
{
    len = 0;
    while(x)
    {
        bit[len++] = x%10;
        x/=10;
    }
    return dfs(len-1,9,0,0,1);//刚刚开始在前面加个9,方便后面判断
}


int main()
{
    scanf("%d",&t);
    while(t--)
    {
        memset(dp,-1,sizeof(dp));
        scanf("%d%d",&l,&r);
        printf("%d\n",cal(r)-cal(l-1));
    }


    return 0;
}


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值