HDU 2089 不要62 初探数位dp

第一次写传说中的数位dp,久仰大名终于得见真容,无比激动!

数位dp就是求在给定的区间[l, r]内满足条件C的数字的个数,这类区间统计问题往往可以用数学上的递推来描述,也就是dp了。

关于这道题,定义状态dp[i][j]表示以数字j开头的i位数字中不含62和4的个数。什么意思呢?dp[2][6]就表示以数字6开头的两位数字中不包含62和4的个数。也就是61,63,65,66,67,68,69,即dp[2][6] = 7。

状态dp[i][j]的转移方程如下:


所以我们可以先把dp数组预处理出来。

现在读者可能会有疑问了,知道了dp数组怎么求[l, r]区间内不含62和4的数字个数呢?其实只要能求出[0, r + 1)和[0, l)内的数字就行了,答案就是前者减去后者。

那么如何求[0, n)区间内不含62和4的数字个数呢?

假如现在要求[0, 323)的数字个数,给出结论:[0, 323)的数字个数等于dp[3][2] + dp[3][1] + dp[3][0] + dp[2][1] + dp[2][0] + dp[1][2] + dp[1][1] + dp[1][0]。

结论分成3部分。

第一部分:dp[3][2] + dp[3][1] + dp[3][0]表示形式为2xx, 1xx, 0xx的满足条件的数字个数(可能会有前导0)。

既然小于300的部分已经计算完成了,接下来我们只需要计算[300, 323)的部分即可,也就是求出[0, 23)的数字个数就行了。

所以第二部分:dp[2][1] + dp[2][0]表示形式为31x, 30x的满足条件的数字个数(可能会有前导0)。

既然小于320的部分已经计算完成了,接下来我们只需要计算[320, 323)的部分即可,也就是求出[0, 3)的数字个数就行了。

所以第三部分:dp[1][2] + dp[1][1] + dp[1][0]表示322, 321,320。

此外在计算的时候要注意两种特殊情况。

第一种:假如是345这个数字,我们在计算了dp[3][2] + dp[3][1] + dp[3][0] + dp[2][3] + dp[2][2] + dp[2][1] + dp[2][0]之后,也就是计算了2xx,1xx,0xx,33x,32x,31x,30x之后,按照之前的想法即将要计算344,343,342,341,340这些数字,但是这些数字已经包含4了,所以不能计算。也就是说如果计算过程中碰到了4就要停止。

第二种:假如是2623这个数字,我们在计算了dp[4][1] + dp[4][0] + dp[3][5] + dp[3][4] + dp[3][3] + dp[3][2] + dp[3][1] + dp[3][0] + dp[2][1] + dp[2][0],也就是计算了1xxx,0xxx,25xx,24xx,23xx,22xx,21xx,20xx,261x,260x之后,按照之前的想法即将要计算2622,2621,2620这些数字,但是这些数字已经包含62了,所以不能计算。也就是说如果计算过程中碰到了62就要停止。

代码如下:

#include <iostream>
#include <cstring>
#include <cstdlib>
#include <cstdio>

using namespace std;

typedef long long int ll;
int dp[10][10];
int digit[10];

void init()
{
    memset(dp, 0, sizeof(dp));
    dp[0][0] = 1;
    for (int i = 1; i <= 9; i++)
        for (int j = 0; j <= 9; j++)
            for (int k = 0; k <= 9; k++)
                if (j != 4 && !(j == 6 && k == 2))
                    dp[i][j] += dp[i - 1][k];
}

// 计算[0, n)中有多少个不含62,4的数字
int Count(int n)
{
    memset(digit, 0, sizeof(digit));
    int len = 0;
    while (n)
    {
        digit[++len] = n % 10;
        n /= 10;
    }
    int ans = 0;
    for (int i = len; i >= 1; i--)
    {
        for (int j = 0; j < digit[i]; j++)
            if (j != 2 || digit[i + 1] != 6)
                ans += dp[i][j];
        if (digit[i] == 4 || (digit[i + 1] == 6 && digit[i] == 2))
            break;
    }
    return ans;
}

int main()
{
    //freopen("test.txt", "r", stdin);

    int n, m;
    init();
    while (~scanf("%d%d", &n, &m) && n && m)
    {
        printf("%d\n", Count(m + 1) - Count(n));
    }
    return 0;
}

补充:

距离第一次写数位dp已经过去两个星期了,这段时间发现网上博客基本都是dfs的写法,笔者仔细想了想,dfs更加容易理解,更加好写,并且速度也不慢。于是乎,今天开始尝试一下用dfs解决这个题目。

dfs理解的方式和上面的讲解略微有所不同,这里有一篇写的很好的博客分享给大家:数位dp入门详解

代码如下(必要的注释都在代码里):

#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <cstdio>

using namespace std;

// dp[i][0]表示在前缀未到上界的情况下枚举第i位,且前一位不为6的个数
// dp[i][1]表示在前缀未到上界的情况下枚举第i位,且前一位为6的个数
int dp[10][2];

int digit[10]; // 保存数字的每一位

// 当前枚举第pos位,flag = 0/1表示前一位不为6/为6, limit = 0/1表示前缀未到上界/到上界
int dfs(int pos, int flag, int limit)
{
    if (!pos)  // 如果能枚举到这个地步,说明当前枚举的数字是合法的,于是返回1
        return 1;
    if (!limit && dp[pos][flag] != -1)  // 理解难点,参见推荐博客
        return dp[pos][flag];
    int up = limit ? digit[pos] : 9;
    int ans = 0;
    for (int i = 0; i <= up; i++)
    {
        if (i == 4 || (flag && i == 2))  // 当前位为4或者当前位和前一位构成了62,就跳过
            continue;
        ans += dfs(pos - 1, i == 6, limit && i == up);
    }
    return limit ? ans : dp[pos][flag] = ans;   // 理解难点,参见推荐博客
}

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

int main()
{
	//freopen("test.txt", "r", stdin);

    int n, m;
    memset(dp, -1, sizeof(dp));
    while (~scanf("%d%d", &n, &m))
    {
        if (!n && !m)
            break;
        printf("%d\n", cal(m) - cal(n - 1));
    }
	return 0;
}




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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值