数位dp

​ 数位 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 的方式进行状态转移。

图片描述 图片描述
例题:小明数:

题目链接:小明数 - 蓝桥云课 (lanqiao.cn)

图片描述

​ 思路:

​ 1.预处理:观察给出的数据范围,指定的数最大为 10 10 10 9 9 9 次方,也就是最多有 9 9 9 位数,先通过预处理,处理出数字个数为 1 − 9 1-9 19 的所有符合要求的数字。用数组 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 17654321 满足相邻位数之差小于 5 5 5 的数的个数,那么就从第 7 7 7 位开始遍历,第 7 7 7 位的数字要小于 7 7 7 ,可以填 1 − 6 1-6 16 ,假设填 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];
                    }
                }
            }
        }
    }
}
  • 10
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值