HDU2089 不要62(数位dp,3种写法)

Problem Description

杭州人称那些傻乎乎粘嗒嗒的人为62(音:laoer)。
杭州交通管理局经常会扩充一些的士车牌照,新近出来一个好消息,以后上牌照,不再含有不吉利的数字了,这样一来,就可以消除个别的士司机和乘客的心理障碍,更安全地服务大众。
不吉利的数字为所有含有4或62的号码。例如: 62315 73418 88914
都属于不吉利号码。但是,61152虽然含有6和2,但不是62连号,所以不属于不吉利数字之列。
你的任务是,对于每次给出的一个牌照区间号,推断出交管局今次又要实际上给多少辆新的士车上牌照了。

Input

输入的都是整数对n、m(0<n≤m<1000000),如果遇到都是0的整数对,则输入结束。

Output

对于每个整数对,输出一个不含有不吉利数字的统计个数,该数值占一行位置。

Sample Input

1 100
0 0

Sample Output

80

思路

学习数位dp:

  1. 初探数位dp

这道题是一个数位dp,给了一个区间[l,r]求出这个区间内不包含462的数的个数。

关于数位dp,我们一般是这样算的,首先我们知道一个数a小于另一个数b,那么a的某一位一定小于b的某一位,数位dp正是利用了这种思想来解决问题的:

比如求出从[1,456]中满足某个条件的数的个数,先把这个数拆成每个位置:

位置|4|5|6
—|----
取值范围|4|5|0~6
取值范围|4|04|09
取值范围|03|09|0~9

对于这道题,网上有三种做法.

方法1:

和上面那个讲解的ppt里面的方法一样
我们定义:

dp[i][j] :表示i位数,首位是j的数字有多少符合要求的。

对于一个数,例如335,对应dp[3][3],但是满足要求的数有336,358……这些数超过了335……

所以通过dp[3][0],dp[3][1],dp[3][2]求得首位数字小于3的满足条件的三位数,dp[3][0]求得的数是001,052,093…也就是所有满足条件的一位或两位数……我们求得所有的2xx,1xx,0xx.

接下来要求的是如334,327这类首位是3满足条件的数字,既然首位只能是3,也就是第一位已经选完了,那么我们只要选择满足小于35的数字就可以了。和上面同理,我们只要求出dp[2][0],dp[2][1],dp[2][2],dp[2][3],dp[2][4]就可以求出首位小于3的两位数。这时求得的是32x,31x,30x。

然后我们求所有小于5的数字就ok。小于5也就是dp[0]dp[1]dp[2]dp[3]dp[4]。这时求得的是53x.

到此,所有小于335且满足的数全部求出。

代码:

#include <cstdio>
#include <cstring>
#include <cctype>
#include <stdlib.h>
#include <string>
#include <map>
#include <iostream>
#include <sstream>
#include <set>
#include <stack>
#include <cmath>
#include <queue>
#include <vector>
#include <algorithm>
#include<list>
using namespace std;
#define mem(a,b) memset(a,b,sizeof(a))
#define lson l,m,rt<<1
#define rson m+1,r,rt<<1|1
typedef long long ll;
const int N=40+10;
const int M=N*N;
int dp[10][10];
void init()
{
    mem(dp,0);
    dp[0][0]=1;
    for(int i=1; i<=7; i++) //枚举位数
        for(int j=0; j<=9; j++) //枚举第i位
            for(int k=0; k<=9; k++) //枚举第i-1位
                if(j!=4&&!(j==6&&k==2))
                    dp[i][j]+=dp[i-1][k];
}
int pos[10];
int solve(int n)
{
    int ans=0,len=0;
    while(n)
    {
        pos[++len]=n%10;
        n/=10;
    }
    pos[len+1]=0;
    for(int i=len; i>=1; i--)
    {
        for(int j=0; j<pos[i]; j++)
            if(j!=4&&!(pos[i+1]==6&&j==2))
                ans+=dp[i][j];
        if(pos[i]==4||(pos[i+1]==6&&pos[i]==2))
            break;
    }
    return ans;
}
int main()
{
    int n,m;
    init();
    while(scanf("%d%d",&n,&m)&&(n||m))
        printf("%d\n",solve(m+1)-solve(n));
    return 0;
}

方法2:

这是kuangbin的做法:

定义:

dp[i][0]:长度为i,吉利数字的个数
dp[i][1]:长度为i,最高位为2的吉利数字个数
dp[i][2]:长度为i,不吉利数字的个数

先对于这道题的6位数,预处理出来这些dp[i][0~3]的值,然后再计算的时候先计算出这个区间内不吉利数字的个数再减去吉利数字的个数,就是答案,具体看注释吧:

代码:

#include <bits/stdc++.h>
using namespace std;
#define mem(a,b) memset(a,b,sizeof(a))
int dp[10][3];
/*
dp[i][0]:长度为i,吉利数字的个数
dp[i][1]:长度为i,最高位为2的吉利数字个数
dp[i][2]:长度为i,不吉利数字的个数
*/
void init()
{
    dp[0][0]=1;
    dp[0][1]=0;
    dp[0][2]=0;
    for(int i=1;i<=6;i++)//最高6位数
    {
        dp[i][0]=dp[i-1][0]*9-dp[i-1][1];//在最高位加上除4之外的9个数字,和2之前的6
        dp[i][1]=dp[i-1][0];//在i-1的最高位加上2
        dp[i][2]=dp[i-1][2]*10+dp[i-1][0]+dp[i-1][1];
        //在已有不吉利数字前加任意数字,或者无不吉利数字的最高位加4,或者在2前面加6
    }
}
int bit[10];
int solve(int n)
{
    int len=0,tmp=n;
    while(n)
    {
        bit[++len]=n%10;
        n/=10;
    }
    bit[len+1]=0;
    int ans=0,flag=0;//ans记录不吉利数字个数
    //flag不吉利数字是否出现过
    for(int i=len;i>=1;i--)
    {
        ans+=dp[i-1][2]*bit[i];//每一个不吉利数字加上之后还是不吉利
        if(flag)//高位出现4或62
            ans+=dp[i-1][0]*bit[i];//前一位的吉利数字的个数加上0~bit[i]-1个数字
        else
        {
            if(bit[i]>4) ans+=dp[i-1][0];//当前选择范围是0~bit[i],必然包含4,所以加上4
            if(bit[i]>6) ans+=dp[i-1][1];//当前选择范围是0~bit[i],必然包含6,所以加上6
            if(bit[i+1]==6&&bit[i]>2) ans+=dp[i][1];//当前位置大于2且前一位是6,所以加上以2开头的
            if(bit[i]==4||(bit[i+1]==6&&bit[i]==2)) flag=1;
        }
    }
    if(flag) ans++;
    return tmp-ans;
}
int main()
{
    //freopen("in.txt","r",stdin);
    int n,m;
    init();
    while(scanf("%d%d",&n,&m)&&(n||m))
    {
        printf("%d\n",solve(m)-solve(n-1));
    }
    return 0;
}

方法3:

这种方法是利用了记忆化搜索,代码写的很简洁。
参考博客:

定义:

  • dp[pos][0]表示当前枚举到第pos位前一位不为6的状态数
  • dp[pos][1]表示当前枚举到第pos位前一位为6的状态数

因为在判断是否为4的时候只牵扯到一位很好判断,但是在判断62的状态时要考虑前一位是否为6的状态

#代码

#include <cstdio>
#include <cstring>
#include <cctype>
#include <stdlib.h>
#include <string>
#include <map>
#include <iostream>
#include <sstream>
#include <set>
#include <stack>
#include <cmath>
#include <queue>
#include <vector>
#include <algorithm>
#include<list>
using namespace std;
#define mem(a,b) memset(a,b,sizeof(a))
#define lson l,m,rt<<1
#define rson m+1,r,rt<<1|1
#define inf 0x3f3f3f3f
typedef long long ll;
int dp[10][2],bit[10];
//dfs的参数分别代表当前位数,上一个位置是否是6,当前的位置有没有枚举限制
int dfs(int pos,int six,int flag)
{
    if(pos==0) return 1;
    if(!flag&&dp[pos][six]!=-1)
        return dp[pos][six];
    int len=flag?bit[pos]:9;
    int res=0;
    for(int i=0; i<=len; i++)
    {
        if(i==4||six&&i==2) continue;
        res+=dfs(pos-1,i==6,flag&&i==len);
    }
    if(!flag) dp[pos][six]=res;
    return res;
}
int solve(int n)
{
    int ans=0,len=0;
    while(n)
    {
        bit[++len]=n%10;
        n/=10;
    }
    mem(dp,-1);
    return dfs(len,false,true);
}
int main()
{
    int n,m;
    while(~scanf("%d%d",&n,&m)&&(n||m))
    {
        printf("%d\n",solve(m)-solve(n-1));
    }
    return 0;
}


2018年09月21日19:15:40重制。

关于对模板的理解
关于对模板的理解:

题目求区间[L,R]内不包含462的数的个数。

  • 不包含4用状态num1来表示,1表示当前位包含4,0表示不包含
  • 不包含62用状态num2来表示,num2有三个取值:
    • 0代表:x x(都没有出现过)
    • 1代表:6 x(前一位是6)
    • 2代表:6 2(同时出现6和2)

那么我们的dfs模板是:

limit表示有没有限制,比如要枚举比123小的数,现在要枚举12x,那么x的取值范围只能为0 1 2,如果当前是11x,那么x的取值可以从0到9,limit代表前一位有没有对当前产生限制.

int dp[10][10][10], a[10];
int pd(int num2, int i)//前一个状态是num2,当前位的值是i
{
    if (num2 == 0)//x x前面没有出现过6 2
    {
        if (i == 6)return 1; //变成状态1
        else return 0;
    }
    else if (num2 == 1)//前面出现过6
    {
        if (i == 2) return 2;//变成 6 2也就是状态2
        else if (i == 6) return 1;//还是当前状态
        else return 0;
    }
    else return 2;//已经是6 2了,状态不变
}
//分别代表:当前位,num1的状态,num2的状态,当前位的前一位是否有限制
int dfs(int pos, int num1, int num2, int limit)
{
    if (pos == -1)//当前位枚举完毕 
        return num2 != 2 && num1 != 1;//是否满足条件num2的状态不为2,num1不为1
    if (!limit && ~dp[pos][num1][num2]) return dp[pos][num1][num2];
    int up = limit ? a[pos] : 9, ans = 0;//限制是否存在
    for (int i = 0; i <= up; i++)//枚举当前为可能的数
        ans += dfs(pos - 1, num1 || i == 4, pd(num2, i), limit && (i == up));
    return limit ? ans : dp[pos][num1][num2] = ans;
}
int solve(int x)
{
    int pos = 0;
    while (x)
    {
        a[pos++] = x % 10;
        x /= 10;
    }
    return dfs(pos - 1, 0, 0, 1);
}
int main()
{
    int n, m;
    mem(dp, -1);
    while (scanf("%d%d", &n, &m) && (n || m))
        printf("%d\n", solve(m) - solve(n - 1));
    return 0;
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值