数位 d p dp dp 往往都是这样的题型,给定一个闭区间 [ l , r ] [ l , r ] [l,r] ,求这个区间中满足某种条件的数的总数。
考虑人类计数的方式,最朴素的计数就是从小到大开始依次加一。但我们发现对于位数比较多的数,这样的过程中有许多重复的部分。例如,从 7000 7000 7000 数到 7999 7999 7999 、从 8000 8000 8000 数到 89994 89994 89994、和从 9000 9000 9000 数到 9999 9999 9999 的过程非常相似,它们都是后三位从 000 000 000 变到 999 999 999,不一样的地方只有千位这一位,所以我们可以把这些过程归并起来,将这些过程中产生的计数答案也都存在一个通用的数组里。此数组根据题目具体要求设置状态,用递推或 d p dp dp 的方式进行状态转移。
例题:小明数:
思路:
1.预处理:观察给出的数据范围,指定的数最大为 10 10 10 的 9 9 9 次方,也就是最多有 9 9 9 位数,先通过预处理,处理出数字个数为 1 − 9 1-9 1−9 的所有符合要求的数字。用数组 f [ i ] [ j ] f[ i ][ j ] f[i][j] 存储结果, i i i 表示数字的位数, j j j 表示有 i i i 位数字的数的最高位的数字为 j j j(注意每一位都可以取 0 0 0 )。
2.从指定数的最高位开始遍历,且该数要小于指定数,假设求 1 − 7654321 1-7654321 1−7654321 满足相邻位数之差小于 5 5 5 的数的个数,那么就从第 7 7 7 位开始遍历,第 7 7 7 位的数字要小于 7 7 7 ,可以填 1 − 6 1-6 1−6 ,假设填 6 6 6 ,他们的差为 1 1 1 ,符合要求,那么现在第 7 7 7 位后面的数字无论填什么都是小于指定数的,那么当前符合要求的数的个数实际有 f [ 7 ] [ 6 ] f[ 7 ][ 6 ] f[7][6] 个,就是求 7 7 7 位数且固定了第 7 7 7 位为 6 6 6 的满足要求的数的个数,也就是 f [ 7 ] [ 6 ] f[ 7 ][ 6 ] f[7][6] 。这个结果之前已经预处理出来了。其它同理。
3.在第二步求的仅仅是和指定数位数相同的满足要求的数的个数,为什么要先求这个呢?因为数要小于指定数,只要位数小于指定数那么它的值一定小于指定数,所以不用判断,但是位数相同时,需要逐位比较,所以需要特判。在这一步,只需要加上所有的位数小于指定数的满足要求的数就行,而它已经在预处理的时候求出来了。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StreamTokenizer;
import java.util.Arrays;
import java.util.Scanner;
public class Main {
static int[][] f = new int[11][10];
static int k;
public static void main(String[] args) throws IOException {
StreamTokenizer sc=new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
sc.nextToken();
int t = (int)sc.nval;
while(t> 0) {
t--;
sc.nextToken();
k = (int)sc.nval;
sc.nextToken();
int l = (int)sc.nval;
sc.nextToken();
int r = (int)sc.nval;
for (int i = 0; i < 11; i++) {
Arrays.fill(f[i], 0);
}
init();
System.out.println(dp(r) - dp(l - 1));
}
return;
}
private static int dp(int num) {
if(num == 0) {
return 0;
}
int res = 0;
int last = 0;//上一个位数的数字
int[] nu = new int[12];
int n = 1;
while (num > 0 ) {
nu[n++] = num%10;
num = num / 10;
}
n--;
for (int i = n; i > 0; i--) {//遍历位数
int x = nu[i];
int jj;
if(i == n) {
jj = 1;
}else {
jj = 0;
}
for (; jj < x; jj++) {//遍历该位数上可以填的数字
if(Math.abs(jj - last) <= k || i == n) {
//System.out.println("mm" + i);
res += f[i][jj];
}
}
if(Math.abs(x-last) <= k || i == n) {
last = x;
}else {
break;
}
if(i==1) {
res++;
}
}
//加包含前导0的,其实就是加上不是和num同位数的数字,
for (int i = 1; i < n; i++) {
for (int j = 1; j < 10; j++) {//从1开始
res += f[i][j];
}
}
return res;
}
private static void init() {
for (int i = 0; i < 10; i++) {//初始化只有一位数字的时候,一定符合要求
f[1][i] = 1;//注意i一定从0开始
}
for (int i = 2; i < 10; i++) {//初始化其它位数的数字
for (int j = 0; j < 10; j++) {//注意,这里可以包含0
for (int m = 0; m < 10; m++) {
if(Math.abs(m-j) <= k) {
f[i][j] += f[i-1][m];
}
}
}
}
}
}