hdu3555 Bomb (秒懂的数位dp)

The counter-terrorists found a time bomb in the dust. But this time the terrorists improve on the time bomb. The number sequence of the time bomb counts from 1 to N. If the current number sequence includes the sub-sequence “49”, the power of the blast would add one point.
Now the counter-terrorist knows the number N. They want to know the final points of the power. Can you help them?
Input
The first line of input consists of an integer T (1 <= T <= 10000), indicating the number of test cases. For each test case, there will be an integer N (1 <= N <= 2^63-1) as the description.

The input terminates by end of file marker.
Output
For each test case, output an integer indicating the final points of the power.
Sample Input
3
1
50
500
Sample Output
0
1
15

Hint
From 1 to 500, the numbers that include the sub-sequence “49” are “49”,”149”,”249”,”349”,”449”,”490”,”491”,”492”,”493”,”494”,”495”,”496”,”497”,”498”,”499”,
so the answer is 15.

题意:

就是找从0到n的数中含有49的数字个数,4和9必须相邻。

解题思路:

1.先说一下数位dp,数位,就是数字的个,十,百,千位,etc。在数位上dp,在很多人看完数位dp后会说这不是个记忆化搜索吗?其实dp就是由记忆化变形过来的,俩者的复杂度几近相同。这个观点可以看“挑战程序设计”这本书验证。

2.进入正题。
  首先在数位上dp,我们就需要把这个数的每一位分解出来。具体看下面代码块。
    int len = 0;   //x为要分解的数,分解出来储存在a数组中。a是全局数组,大小定位你要分解的最大数的位数
    while(x)
    {
        a[len++] = x % 10;
        x /= 10;
    }
  分解出来,干什么呢?下面就要先看题目要求,拿本题来说,要找含有49的数字个数。但是直接找含有49的数字较为麻烦,那我
  们可以找到不含49的数字个数,然后拿总数相减就可以得到答案。

3.那现在任务变成求,不含49的数字个数。
  先上个最简单的数位dp板子,帮助理解。
ll dfs(int pos,int pre,bool limit)//pos表示现在是数的第几位,pre是前一位数位上的数,limit是核心,一会儿细讲
{
    if(pos == -1) //应为数位数组a是从0开始存储,所以pos变为-1表示这个数每一位都遍历完了,就返回 1
        return 1;       //1表示搜索结束,找到一个合法数字
    int up = limit?a[pos]:9;  //核心:细讲
    ll ans = 0;  //统计合乎要求的数
    for(int i = 0;i <= up;i++)  //遍历当前数位上可能取的每一个数
    {
        if(pre == 4 && i == 9)  //当前一位是4,且当前位是9的我们就不往下遍历,最后含有49的自然就没计数
        {
            continue;
        }else{
            ans += dfs(pos-1,i,limit && i == a[pos]); //不含49,就往下遍历pos是从最大数位开始
        }
    }
    return ans;
}
4.limit这是个好东西,能想出来的人,我现在先Orz(膜拜)一下。
  5752,比如给这个数,我们从最高位去遍历,那么最高位是千位,可以取的数字是0,1,2,3,4,5.
  至于这里的前导零先不管。我们看一下取0~4和取5,有什么分别? 先闭上眼睛想想。
  。
  。
  。
  取0~4,,,那么百位,可以取哪几个数? 答案:0--9
  .
  取5呢,,,那么百位就只能取0--7.。。这里能想通吧。因为一个数是从0一直增大,但是不能超过所给的数字吧
  那么来看代码。
  int up = limit?a[pos]:9;
  如果有限制,limit为真,那么我们遍历这个数位的上限up就是a[pos],a[pos]就是存储的当前最大位。就是千位的7.
  如果没有限制,limit为假,那么up就是9.
  。
  那么我们关心的问题就是什么时候limit为真。对应于刚刚举得例子就是
  千位取5,遍历百位的时候0--7.
  百位取7,遍历十位的时候0--5.
  也就是说遍历当前位的时候,如果前一位是最大的那个,那么limit为真。
  。。
  ans += dfs(pos-1,i,limit && i == a[pos]);
  。
  当往下遍历时,为什么限制limit的值是“limit && i == a[pos]”这个东西。
  。
  我们想i是从0遍历到上限。如果i==a[pos],那么就是说i==当前位最大的那个数。那么下一位是不是受限制了?
  所以当相等且limit为真,下一位受限制。limit就为真。为什么要“limit && i == a[pos]”这个式子
  要且上一个limit,limit表示当前位受前一位限制,
  。。
  我举个例子还是5752
  .。
  如果前位取4.limit为假,那么百位取到a[pos]也就是7的时候受限制吗???写几个数就懂了。

5.下面说循环遍历的注意点。hdu有个不要62的题。
  。
  对于每一个题,我们根据要求改变条件,就可以得到不同的题目要求的答案。例如本题,我们要找不含49,那么
  根据pre和当前位判断,如果含49,就跳过。如果是不要62,那么也是一样。自己思考。

AC代码:

# include <cstdio>
# include <cstring>
# define ll long long

using namespace std;

ll dp[50][10];

int a[50];

ll dfs(int pos,int pre,bool limit)
{
    if(pos == -1)
        return 1;
    if(!limit && dp[pos][pre] != -1)
    {
        return dp[pos][pre];
    }

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

    ll ans = 0;
    for(int i = 0;i <= up;i++)
    {
        if(pre == 4 && i == 9)
        {
            continue;
        }else{
            ans += dfs(pos-1,i,limit && i == a[pos]);
        }
    }
    if(!limit)
    {
        dp[pos][pre] = ans;
    }
    return ans;
}

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

int main()
{
    int T;
    ll ri;
    scanf("%d",&T);
    memset(dp,-1,sizeof(dp));
    while(T--)
    {
        scanf("%lld",&ri);
        printf("%lld\n",ri-solve(ri)+1);
    }
    return 0;
}
贴了代码:才发现没说记忆化的问题。算啦,有时间再补,当然,如果有人需要,请留言。我看到后,马上补齐。
敲了这么多,如果有帮助留言顶一下,点个赞。如果有叙述错误,也请指出,共同进步。
  • 11
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值