HDU 2089 不要62 初探数位dp

来源:互联网 发布:淘宝网店好经营吗 编辑:程序博客网 时间:2024/06/08 19:00

第一次写传说中的数位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;}




原创粉丝点击