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