1042 数字0-9的数量(数位DP)

题目链接:https://www.51nod.com/onlineJudge/questionCode.html#!problemId=1042

题目:

给出一段区间a-b,统计这个区间内0-9出现的次数。
比如 10-19,1出现11次(10,11,12,13,14,15,16,17,18,19,其中11包括2个1),其余数字各出现1次。
Input
两个数a,b(1 <= a <= b <= 10^18)
Output
输出共10行,分别是0-9出现的次数

分析:题意比较明确,这种输入典型的数位DP题,没什么好说的。下面稍微谈一下数位DP。

数位DP:顾名思义,就是对数字按照位置进行动态规划求解的一种算法。一般是一个整数,给定一个区间求出区间内,所有的数所有位置上,出现某个数的次数,当然这是最基本的,但是基本这类题都是围绕这一点,可能稍作变化,加一加限制条件之类的。如果我们直接去暴力枚举的一般都会超时的,所以既然本来就求的是0-9的数字出现的总次数,我们一位一位的看,分别统计每一位出现所求数字的次数最后加和就是该数内一共出现的所求数字次数了。对于这种从枚举每一个数转化到每一位的思想,好像大体是有两种实现方式,一种是采用记忆化搜索的方式,也是就是数位DP了,将搜索过的结果记录,减少递归的次数;另一种就是进行数位分析,得出每一位出现数字i的次数和前几位的关系及后几位的关系。这里我只写了数位DP,之前写过一次分析,好像是一级算法题的时候,这次顺便学习一下数位DP。

动态规划由记忆化搜索衍生而来,但是动态规划在有些时候可能直接写出状态转移方程比较困难,或者说难以理解,由于笔者比较菜,所以选择的是以搜索的形式实现的DP。
dp[20][10],如果以i,j表示2维的话,dp[i][j]的意义为i位数,且最高位为j的数,符合题意的枚举个数有几个。本题中需要加一维,来区别前导0的问题,因为0在前导的位置和不是前导的情况结果并不一样,需要两个dp[i][0]来存。具体还是看代码吧,注释比较多(写的时候wa了好几发,确实是很麻烦。。。)
顺便贴一位大佬的数位DP详解:
http://blog.csdn.net/wust_zzwh/article/details/52100392

数位DP:(其中可能需要注意的有一点是一定要搞清楚自己的dp是怎么定义,不该存的状态结果一定不要存,查的时候也是同样)
#include<iostream>
#include<cstring>
#include<math.h>
#include<stdlib.h>
#include<cstring>
#include<cstdio>
#include<utility>
#include<algorithm>
#include<map>
#include<stack>
#include<queue>
using namespace std;
typedef long long ll;
const int maxn = 1e5+5;
const int mod = 1e9+7;
const int Hash = 10000;
const int INF = 1<<30;
const ll llINF = 1e18;

int in;
int shu[20];
ll dp[20][10][2];//储存首位为j的i位数的无限制枚举结果,3维表示前缀(0表示不是前缀1表示是前缀)
//将数字分位
int seq(ll n)
{
    int cnt = 0;
    while(n)
    {
        shu[cnt++] = n%10;
        n /= 10;
    }
    return cnt-1;
}
//记忆化搜索
//pos表示当前位置,limit表示当前位枚举是否被限制,key表示需要计数的数字,highest表示是不是当前位是不是最高位
//number是求解的数
ll dfs(int pos, bool limit, bool highest, int key, ll number)
{
    if(pos == -1)//递归终点
        return 0;
    ll ans = 0, n;
    limit ? n=shu[pos] : n=9;//当前位枚举的上界

    //遍历当前位,统计每一位的枚举结果dp[pos][i],如果没计算过,则去dfs
    for(int i=0; i<=n; i++)
    {
        if(dp[pos][i][1]!=-1 && i==0&&highest)//前缀零的搜索结果
        {
            ans += dp[pos][i][1];
            continue;
        }
        if(dp[pos][i][0]!=-1 && !limit)//之前已经搜索过了,只有无限制的结果才能查,dp数组中没有保存有限制的结果
        {
            ans += dp[pos][i][0];
            continue;
        }
        if(limit && i==shu[pos])//下一位有限制,但是一定不是前导零
            ans += dfs(pos-1, true, false, key, number);
        else if(i==0 && highest)//对下一位无限制,但是是前导零
            ans += dp[pos][i][1] = dfs(pos-1, false, true, key, number);
        else//既不是前导零,也对下一位无限制
            ans += (dp[pos][i][0] = dfs(pos-1, false, false, key, number));
        if(i==key && !(i==0&&highest))//如果该位数与所枚举的数相同,并且不是前导零,进行计数
        {
            ll m = round(pow(10, pos));
            //枚举本位对后面的有限制情况和无限制情况分别计数
            if(!limit || i<n)
            {
                dp[pos][i][0] += m;
                ans += m;
            }
            else
                ans += number%m+1;
        }
    }
    return ans;
}
int main( )
{
    //freopen("input.txt", "r", stdin);
    ll a,b;
    while(cin>>a>>b)
    {
        for(int i=0; i<10; i++)
        {
            memset(dp, -1, sizeof(dp));
            ll ans1 = dfs(seq(a-1), true, true, i, a-1);
            memset(dp, -1, sizeof(dp));
            ll ans2 = dfs(seq(b), true, true, i, b);
            cout<<ans2-ans1<<endl;
        }
    }
    return 0;
}

分析每一位情况:(这里就直接贴代码了,我觉得还是蛮好的一种做法,都可以学习一下)
原文:http://www.cnblogs.com/joeylee97/p/6273496.html
i!=0:
从最小位开始向上考虑,前面和后面对当前位置的影响,对于出现数字i的次数:
当前数字小于i i的出现次数等于高位*temp
当前数字大于i i的出现次数等于高位*temp(此时高位次数应当加一)
当前数字等于i 高位*temp + after + 1(本身)
i==0要单独考虑,每次多算了一个temp,减去即可

 

复制代码
#include<iostream>
#include<stdio.h>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#define LL long long
using namespace std;
//循环两遍,sumright-sumleft
//10132 
//==0:pre*after     !=0:cur*pre*对应位数+after
//0 + 132 + 10*cur*100+after + 101*cur*100*10+2 +1013*1
//19 1*
LL ans[10];
void solve(LL n,bool f)
{
    LL cur,pre,after,temp=1,sign=f?1:-1;
    int cnt = 0;
    while((n/temp)!=0)
    {
        cur = (n/temp)%10;
        pre = (n/(temp*10));
        after = n - (n/temp)*temp;
        //cout<<pre<<' '<<cur<<' '<<after<<endl;
        int tmp = after,mul=1;
        while(tmp){mul*=10;tmp/=10;}
        //cout<<" m is "<<mul<<endl;
        for(int i=0;i<10;i++)
        {
            if(cur>i) ans[i]+=(pre+1)*temp*sign;
            if(cur<i) ans[i]+=pre*temp*sign;
            if(cur==i) ans[i]+=(pre*temp+after+1)*sign;
        }
        ans[0]-=temp*sign;
        temp*=10;
    }
}
//10+1+1
int main()
{
    LL l,r;
    bool f;
    cin>>l>>r;
    f = false;
    solve(l-1,f);
    f = true;
    solve(r,f);
    for(int i=0;i<10;i++)
    {
        cout<<ans[i]<<endl;
    }
    return 0;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值