目录

题目来源

题目描述

输入描述

输出描述

用例

题目解析

算法源码


题目来源

数位DP_哔哩哔哩_bilibili

题目描述

求区间[L,R]范围内有多少带3的数。

所谓带3的数就是这个数的十进制表示中存在至少一位为3。

比如3, 123, 3333,都是带3的数,如12, 456, 1000都是不带3的数

输入描述

输入一行包含两个整数 L,R(1 ≤ L ≤ R ≤ 10^12)。

输出描述

输出一个整数,表示区间[L,R]范围内带3的数的个数。

用例

输入 100 200
输出 19
说明

103

113

123

130、131、132、133、134、135、136、137、138、139

143

153

163

173

183

193

共19个

题目解析

本题最容易想到的思路就是暴力枚举,从L枚举到R,统计其中含有3的枚举数。但是这种方式是O(n)时间复杂度,对于数量级1 ≤ L ≤ R ≤ 10^12来说,肯定会超时。

本题可以使用数位DP解决。数位 DP - OI Wiki (oi-wiki.org)

再学习数位DP之前,我们需要学习数位搜索,或者数位枚举,这是一种不同于数学枚举的枚举方式。

比如,我们现在需要枚举出234内所有的数,如果是数学枚举,则代码如下:

JS:

for(let i=0; i<=234; i++) {console.log(i)
}

Python:

for i in range(235):print(i)

Java:

public class Main {public static void main(String[] args) {for (int i = 0; i <= 234; i++) {System.out.println(i);}}
}

如果是数位搜索的话,则要复杂得多,首先我们需要将234数字转为字符串,那么得到数位关系如下:

  • 第0位(百位)可选数为:0,1,2
  • 第1位(十位)可选数取决于第0位
  1. 如果第0位的数选择0,1,则第1位可选数可以是0~9
  2. 如果第0位的数选择2,则第1位可选数只能是0~3

如果第0位选择2,第1位选择4,那么形成的最小数为24000,那么就超过了23456;

如果第0为选择1,第1为选择9,那么形成的最大数为19999,那么也不会超过23456;

同理可得:

  • 如果第0位选取2,第1位选取3,则第2位可选数只能是0~4

图示如下:

 如上图红色范围,我们称为limit,即受到上界约束的范围。

比如第0位,我们只能在0~2中选择,因为原始值234的百位最大就是2,因此被上界被约束了。

如果第0位选取了0或1,则第1位的数选择就不受约束了,可以选取0~9任意数。

如果第0位选取了2,则第1位的数选择又受到约束了,只能在0~3中选取,因为原始值百位取2时,十位最高取3。

从上图树形结构来看,我们可以借助dfs来枚举出所有情况

JS:

function dfs(level, limit, str, path) {if (level === str.length) return console.log(path.join(""));// 如果是约束范围,即limit为true,则当前位只能取0~str[level],如果不是约束范围,则当前可以取0~9const max = limit ? str[level] : 9;for (let i = 0; i <= max; i++) {path.push(i);// 判断下一位取值是否为约束范围:当前位取值为约束范围,即limit为true,且当前位取值为maxdfs(level + 1, limit && i === max, str, path);path.pop();}
}function digitSearch(num) {const str = String(num).split("").map(Number);dfs(0, true, str, []);
}digitSearch(234);

Python:

def dfs(level, limit, s, path):if level == len(s):print("".join(map(str, path)))return# 如果是约束范围,即limit为true,则当前位只能取0~str[level],如果不是约束范围,则当前可以取0~9maxV = s[level] if limit else 9for i in range(maxV + 1):path.append(i)# 判断下一位取值是否为约束范围:当前位取值为约束范围,即limit为true,且当前位取值为maxdfs(level + 1, limit and i == maxV, s, path)path.pop()def digitSearch(num):s = list(map(int, list(str(num))))dfs(0, True, s, [])digitSearch(234)

Java:

import java.util.Arrays;
import java.util.LinkedList;public class Main {public static void main(String[] args) {digitSearch(234);}public static void digitSearch(int num) {Integer[] arr =Arrays.stream((num + "").split("")).map(Integer::parseInt).toArray(Integer[]::new);dfs(0, true, arr, new LinkedList<>());}public static void dfs(int level, boolean limit, Integer[] arr, LinkedList<Integer> path) {if (level == arr.length) {StringBuilder sb = new StringBuilder();for (Integer val : path) sb.append(val);System.out.println(sb);return;}// 如果是约束范围,即limit为true,则当前位只能取0~str[level],如果不是约束范围,则当前可以取0~9int max = limit ? arr[level] : 9;for (int i = 0; i <= max; i++) {path.add(i);// 判断下一位取值是否为约束范围:当前位取值为约束范围,即limit为true,且当前位取值为maxdfs(level + 1, limit && i == max, arr, path);path.removeLast();}}
}

以上就是数位枚举的实现。可以看出数位枚举也属于暴力法。

但是,数位枚举是基于数位,因此我们可以更好地基于数位的特征去做一些事。

比如,比如找135内所有带3的数,此时基于数学枚举,就需要枚举0~135个数,才能找到所有带3的数:

  • 3
  • 13
  • 23
  • 30,31,32,33,34,35,36,37,38,39
  • 43
  • 53
  • 63
  • 73
  • 83
  • 93
  • 103
  • 113
  • 123
  • 130,131,132,133,134,135

但是基于数位枚举,我们可以做到一些优化:

如果当前位枚举数为3,则后面位就不需要枚举了,因为肯定含3了,相当于给dfs做了剪枝优化。

但是,我们需要注意两种情况,如上图画圈的3:

如果枚举到的数位3(绿色3),不是上界值,则对于本题的十进制数查找来说,它可以组合出Math.pow(10, str.length - level - 1)个含3数,比如:str.length=3,当前level=1(从0开始),则可以组合出10^1个。

如果枚举到的数位3(红色3),是上界值,则可以组合str.slice(level+1),比如str.slice(2),即'135'.slice(2) = '5',但是需要注意,这里要+1,因为第0位取1,第1位取3时,可以组合130,131,132,133,134,135,有6个数,因为包含了0。

因此最终统计含3数个数的代码如下:

JS:

function dfs(level, limit, str) {if (level === str.length) {return 0;}const max = limit ? str[level] : 9;let count = 0;for (let i = 0; i <= max; i++) {// 枚举到3,则后面位就不需要枚举了,必然含3if (i === 3) {// 统计含3数的个数count +=limit && i === max // 如果当前枚举3是上界值? str.slice(level + 1) - 0 + 1 // 从0计数,因此要加1: Math.pow(10, str.length - level - 1);} else {count += dfs(level + 1, limit && i === max, str);}}return count;
}function digitSearch(num) {const str = String(num).split("").map(Number);const count = dfs(0, true, str);console.log(count);
}digitSearch(135);

Python:

import mathdef dfs(level, limit, s):if level == len(s):return 0maxV = int(s[level]) if limit else 9count = 0for i in range(maxV + 1):# 枚举到3,则后面位就不需要枚举了,必然含3if i == 3:# 统计含3数的个数if limit and i == maxV:  # 如果当前枚举3是上界值count += int("".join(s[level + 1:])) + 1  # 从0计数,因此要加1else:count += int(math.pow(10, len(s) - level - 1))else:count += dfs(level + 1, limit and i == maxV, s)return countdef digitSearch(num):s = str(num)count = dfs(0, True, s)print(count)digitSearch(135)

Java:

import java.util.Arrays;public class Main {public static void main(String[] args) {digitSearch(135);}public static void digitSearch(int num) {Integer[] arr =Arrays.stream((num + "").split("")).map(Integer::parseInt).toArray(Integer[]::new);int count = dfs(0, true, arr);System.out.println(count);}public static int dfs(int level, boolean limit, Integer[] arr) {if (level == arr.length) {return 0;}// 如果是约束范围,即limit为true,则当前位只能取0~str[level],如果不是约束范围,则当前可以取0~9int max = limit ? arr[level] : 9;int count = 0;for (int i = 0; i <= max; i++) {// 枚举到3,则后面位就不需要枚举了,必然含3if (i == 3) {// 统计含3数的个数if (limit && i == max) { // 如果当前枚举3是上界值Integer[] tmp = Arrays.copyOfRange(arr, level + 1, arr.length);StringBuilder sb = new StringBuilder();for (Integer val : tmp) sb.append(val);count += Integer.parseInt(sb.toString()) + 1; // 从0计数,因此要加1} else {count += Math.pow(10, arr.length - level - 1);}} else {count += dfs(level + 1, limit && i == max, arr);}}return count;}
}

我们可以发现统计带3的数的个数的逻辑有些复杂,因此我们可以转换思维,统计不带3的数的个数,之后用总数 - 不带3的数的个数 = 带3的数的个数

JS:

function dfs(level, limit, str) {if (level === str.length) {return 1; // 能走到最后的肯定是不带3的数,因此算统计到1个带3的数,返回1个}const max = limit ? str[level] : 9;let count = 0;for (let i = 0; i <= max; i++) {if (i !== 3) { // 只统计不带3的数的个数count += dfs(level + 1, limit && i === max, str);}}return count;
}function digitSearch(num) {const str = String(num).split("").map(Number);const count = num + 1 - dfs(0, true, str); // 总数 - 不带3数的个数 = 带3数的个数console.log(count);
}digitSearch(135);

Python:

import mathdef dfs(level, limit, s):if level == len(s):return 1  # 能走到最后的肯定是不带3的数,因此算统计到1个带3的数,返回1个maxV = int(s[level]) if limit else 9count = 0for i in range(maxV + 1):#  只统计不带3的数的个数if i != 3:count += dfs(level + 1, limit and i == maxV, s)return countdef digitSearch(num):s = str(num)count = num + 1 - dfs(0, True, s)  # 总数 - 不带3数的个数 = 带3数的个数print(count)digitSearch(135)

Java:

import java.util.Arrays;public class Main {public static void main(String[] args) {digitSearch(135);}public static void digitSearch(int num) {Integer[] arr =Arrays.stream((num + "").split("")).map(Integer::parseInt).toArray(Integer[]::new);int count = num + 1 - dfs(0, true, arr); // 总数 - 不带3数的个数 = 带3数的个数System.out.println(count);}public static int dfs(int level, boolean limit, Integer[] arr) {if (level == arr.length) {return 1; // 能走到最后的肯定是不带3的数,因此算统计到1个带3的数,返回1个}// 如果是约束范围,即limit为true,则当前位只能取0~str[level],如果不是约束范围,则当前可以取0~9int max = limit ? arr[level] : 9;int count = 0;for (int i = 0; i <= max; i++) {// 只统计不带3的数的个数if (i != 3) {count += dfs(level + 1, limit && i == max, arr);}}return count;}
}

但是上面这种剪枝优化,对于数量级1 ≤ L ≤ R ≤ 10^12来说,只能算杯水车薪。

数位DP问题,一个最最最重要的优化点就是:

如下图中,画圈的两个十位,由他们组成的数中含有3的数的个数是相同的。

比如:

000、001、002、003、004、005、006、007、008、009

090、091、092、093、094、095、096、097、098、099

可以发现,它们的共同特点是,百位、十位都不含3,唯一可能含3的就是个位,但是它们的个位取值范围都是相同的,即0~9。因此,最终它们的含3数个数相同。

因此,我们在统计出00x的含3数个数后,可以将结果缓存起来,当统计09x时,直接取缓存值即可。这样就避免了大量的深度递归操作。

我们可以定义一个数组f,f[i]表示第i位含3数的个数。比如f[1]表示第1位,即十位,含3数的个数,比如00x含3数个数,09x含3数个数。

那么可以复用f[i]的条件是什么呢?

如上图画圈的两个十位,它们是否可以复用f[1]的统计结果吗?

答案是不可以。

030、031、032、033、034、035、036、037、038、039

130、131、132、133、134、135

可以发现03x和13x的含3数个数,都不同于00x和09x。

那么到底该如何排除掉这些情况呢?

首先,如果某级取值受到上界约束,那么此时f[i],i代表当前级,就无法复用,比如f[0]

明显的0**和1**下的含3数的个数是不同的。因为1**的十位和个位的取值都会受到约束。而0**的十位和个位取值不会受到约束。

因此,首先要排除掉对处于约束范围的级的f[i]的统计,即如果当前level对应的limit为true,则不需要缓存结果。

另外,如果某级取值不受上界约束,比如03*,但是由于含有3,个位无论取何值都会含3,因此03*和其他如01*、02*、04*、....、09*的含3数个数不同。

由于,我们采用的是深度递归,因此,必然是先统计0**,然后遍历所有01*,记忆到f[1]中,然后遍历02*,从缓存结果f[1]中直接获取结果,之后遍历03*,发现含3,因此跳过,之后遍历04*,从f[1]中直接获取结果,后面同理。

因此实现代码如下,新增了三行关于f数组记忆化搜索的代码

JS:

function dfs(level, limit, str, f) {if (level === str.length) {return 1;}if(!limit && f[level]) return f[level]; // 记忆化搜索const max = limit ? str[level] : 9;let count = 0;for (let i = 0; i <= max; i++) {if (i !== 3) {count += dfs(level + 1, limit && i === max, str, f);}}if(!limit) f[level] = count; // 记忆化return count;
}function digitSearch(num) {const str = String(num).split("").map(Number);const f = new Array(str.length); // f[i]表示第i位不含3的数的个数return num + 1 - dfs(0, true, str, f);
}console.log(digitSearch(135));

Python:

def dfs(level, limit, s, f):if level == len(s):return 1if not limit and f[level] is not None:  # 记忆化搜索return f[level]maxV = int(s[level]) if limit else 9count = 0for i in range(maxV + 1):if i != 3:count += dfs(level + 1, limit and i == maxV, s, f)if not limit:  # 记忆化f[level] = countreturn countdef digitSearch(num):s = str(num)f = [None]*len(s)  # f[i]表示第i位不含3的数的个数count = num + 1 - dfs(0, True, s, f)print(count)digitSearch(135)

Java:

import java.util.Arrays;public class Main {public static void main(String[] args) {digitSearch(135);}public static void digitSearch(int num) {Integer[] arr =Arrays.stream((num + "").split("")).map(Integer::parseInt).toArray(Integer[]::new);int[] f = new int[arr.length]; // f[i]表示第i位不含3的数的个数int count = num + 1 - dfs(0, true, arr, f); // 总数 - 不带3数的个数 = 带3数的个数System.out.println(count);}public static int dfs(int level, boolean limit, Integer[] arr, int[] f) {if (level == arr.length) {return 1; // 能走到最后的肯定是不带3的数,因此算统计到1个带3的数,返回1个}if (!limit && f[level] > 0) return f[level]; //  记忆化搜索// 如果是约束范围,即limit为true,则当前位只能取0~str[level],如果不是约束范围,则当前可以取0~9int max = limit ? arr[level] : 9;int count = 0;for (int i = 0; i <= max; i++) {// 只统计不带3的数的个数if (i != 3) {count += dfs(level + 1, limit && i == max, arr, f);}}if (!limit) f[level] = count; // 记忆化return count;}
}

最后,找L,R范围内的含3数的个数,即先找0~L-1范围内的含3数个数count1,再找0~R范围内的含3数的个数count2,然后用count2 - count1作为题解。

JavaScript算法源码

/* JavaScript Node ACM模式 控制台输入获取 */
const readline = require("readline");const rl = readline.createInterface({input: process.stdin,output: process.stdout,
});rl.on("line", (line) => {const [L, R] = line.split(" ").map(Number);console.log(digitSearch(R) - digitSearch(L - 1));
});function dfs(level, limit, str, f) {if (level === str.length) {return 1;}if (!limit && f[level]) return f[level]; // 记忆化搜索const max = limit ? str[level] : 9;let count = 0;for (let i = 0; i <= max; i++) {if (i !== 3) {count += dfs(level + 1, limit && i === max, str, f);}}if (!limit) f[level] = count; // 记忆化return count;
}function digitSearch(num) {const str = String(num).split("").map(Number);const f = new Array(str.length); // f[i]表示第i位不含3的数的个数return num + 1 - dfs(0, true, str, f);
}

Python算法源码

# 数位搜索 + 记忆化存储
def dfs(level, limit, s, f):if level == len(s):return 1if not limit and f[level] is not None:  # 记忆化搜索return f[level]maxV = int(s[level]) if limit else 9count = 0for i in range(maxV + 1):if i != 3:count += dfs(level + 1, limit and i == maxV, s, f)if not limit:  # 记忆化f[level] = countreturn count# 算法入口
def digitSearch(num):s = str(num)f = [None]*len(s)  # f[i]表示第i位不含3的数的个数return num + 1 - dfs(0, True, s, f)# 输入获取
L, R = map(int, input().split())
print(digitSearch(R) - digitSearch(L-1))

Java算法源码

import java.util.Arrays;
import java.util.Scanner;public class Main {public static void main(String[] args) {Scanner sc = new Scanner(System.in);int l = sc.nextInt();int r = sc.nextInt();System.out.println(digitSearch(r) - digitSearch(l - 1));}public static int digitSearch(int num) {Integer[] arr =Arrays.stream((num + "").split("")).map(Integer::parseInt).toArray(Integer[]::new);int[] f = new int[arr.length]; // f[i]表示第i位不含3的数的个数return num + 1 - dfs(0, true, arr, f);}public static int dfs(int level, boolean limit, Integer[] arr, int[] f) {if (level == arr.length) {return 1; // 能走到最后的肯定是不带3的数,因此算统计到1个带3的数,返回1个}if (!limit && f[level] > 0) return f[level]; //  记忆化搜索// 如果是约束范围,即limit为true,则当前位只能取0~str[level],如果不是约束范围,则当前可以取0~9int max = limit ? arr[level] : 9;int count = 0;for (int i = 0; i <= max; i++) {// 只统计不带3的数的个数if (i != 3) {count += dfs(level + 1, limit && i == max, arr, f);}}if (!limit) f[level] = count; // 记忆化return count;}
}

数位DP - 带3的数相关推荐

  1. 数位DP - 带49的数

    目录 题目来源 题目描述 输入描述 输出描述 用例 题目解析 算法源码 题目来源 20200817-数位DP-带49的数_哔哩哔哩_bilibili 题目描述 求区间 [1,n] 范围内包含多少带 4 ...

  2. Palindromic Numbers LightOJ - 1205 数位dp 求回文数

    传送门 文章目录 题意: 思路: 题意: 求[l,r][l,r][l,r]中有多少个回文数. 思路: 裸的数位dpdpdp啦,记dp[pos][pre][state]dp[pos][pre][stat ...

  3. 题解HDU6148 Valley Numer(数位DP+深搜DFS)

    题解HDU6148 Valley Numer[数位DP+深搜DFS] 题目 解析 参考源码 题目 Description: 众所周知,度度熊非常喜欢数字. 它最近发明了一种新的数字:Valley Nu ...

  4. bzoj 1026: [SCOI2009]windy数 数位DP算法笔记

    数位DP入门题之一 也是我所做的第一道数位DP题目 (其实很久以前就遇到过 感觉实现太难没写) 数位DP题目貌似多半是问从L到R内有多少个数满足某些限制条件 只要出题人不刻意去卡多一个$log$什么的 ...

  5. uestc 250 windy数(数位dp)

    题意:不含前导零且相邻两个数字之差至少为2的正整数被称为windy数. windy想知道,在A和B之间,包括A和B,总共有多少个windy数? 思路:数位dp #include<iostream ...

  6. 【bzoj1026】[SCOI2009]windy数 数位dp

    题目描述 windy定义了一种windy数.不含前导零且相邻两个数字之差至少为2的正整数被称为windy数. windy想知道,在A和B之间,包括A和B,总共有多少个windy数? 输入 包含两个整数 ...

  7. BZOJ1026 [SCOI2009]windy数 数位dp

    欢迎访问~原文出处--博客园-zhouzhendong 去博客园看该题解 题目传送门 - BZOJ1026 题目概括 求区间[A,B]中有多少数满足下面的条件. 条件:该数相邻两位之差不小于2. 题解 ...

  8. 牛客假日团队赛5 F 随机数 BZOJ 1662: [Usaco2006 Nov]Round Numbers 圆环数 (dfs记忆化搜索的数位DP)...

    链接:https://ac.nowcoder.com/acm/contest/984/F 来源:牛客网 随机数 时间限制:C/C++ 1秒,其他语言2秒 空间限制:C/C++ 32768K,其他语言6 ...

  9. 理解至上:数位dp(ybtoj-B数计数)

    文章目录 简要 题目描述 解析 dp定义: 试填法 代码 thanks for reading! 简要 数位dp,天下第一 最重要的应该有两个: 1.状态转移式的确定 2.试填法不断往后模拟 (至今是 ...

最新文章

  1. 找java培训机构如何挑选
  2. PDO绑定含IN的SQL语句的参数注意事项
  3. 搭建分布式架构4--ZooKeeper注册中心安装
  4. 元器件大一点好,还是小一点好?
  5. 批量提取文件创建时间_不要眨眼!批量提取文件名,只需30秒
  6. linux cookie 地址,SYN Cookie原理及其在Linux内核中的实现
  7. C语言判断两字符串同构,c语言实现判断两颗树是否同构
  8. noi 2009 二叉查找树 动态规划
  9. Struts2一个诡异问题的解决
  10. 参观云栖小镇体会_40个绝美小镇
  11. RBF神经网络和拟合实例
  12. [80386]80x86汇编指令
  13. iOS SpriteKit 小游戏开发实例 - Flappy Bird
  14. C++中函数后面加const
  15. 关闭455端口相关服务
  16. 商场触摸互动广告机有哪些功能
  17. 亲情的矛盾都是因为爱而化解 写给17 岁的你
  18. Navicat连接mysql时出现 Access denied for user ‘root‘@‘xxx.xxx.xxx.xxx‘ (using password: YES) 的原因及解决办法。
  19. Oracle Cursor
  20. Matlab 2018a安装教程和破解方法(附Crack文件)

热门文章

  1. java read出错_java读取txt文件时,错误的把txt内容读取了两遍
  2. 6.9 画板的创建与删除 [Illustrator CC教程]
  3. 微信小程序开发学习--7.27日
  4. 2021年9月16日
  5. 论:网上的赚钱路子是真是假?
  6. 在 Apple TV 上模拟夜间模式的方法
  7. Python—爬取图库(一)
  8. 企业微信上传素材,java httpPost Multipart FormData
  9. 转:计算机、游戏和风花雪月—一个IT老兵的成长之路(1)
  10. 基于卡尔曼滤波算法的轨迹跟踪