题目描述
输入一个数n,求出 [1, n] 中每个数码出现的次数,即0 - 9每个数出现的次数。
解题思路
首先是无情的暴力法,可以用于判断我们后续的优化代码是否正确。
import java.io.*;
import java.util.*;public class Main1 {static int n;public static void main(String[] args){Scanner sc = new Scanner(System.in);int[] cnt = new int[10];n = sc.nextInt();for (int i = 1; i <= n; i++) {int t = i;while (t > 0) {cnt[t % 10]++;t /= 10;}}System.out.println(Arrays.toString(cnt));}
}
数位dp
用此题来引出数位dp的概念,首先我们寻找规律,比如0-9所有的一位数字正好包含了所有数码各一次;进一步思考,在0-99中,根据十进制递增的简单规律,我们可以发现这10个数字每个数字都出现了相同的次数,有一个例外是0,它由于一位数字的时候十位的0被省略,所以会少一些,但我们只需要改变思路,变成00-99,那么所有数字出现的次数就是完全相等的了。
我们定义 dp[i] 表示 i 位数所有数字出现的次数;dp[1] 的值即1,dp[2]的值我们应当如何思考呢?从00-99考虑,每个数有两位,总共100个数,那么总数字个数就是2*100 = 200个,再平均分配到10个数字,那么dp[2]的值即为20个;相应的,dp[3]的值是3*1000/10 = 300个。
dp[i] = dp[i - 1] * 10 + 10^(i - 1);
虽然我们可以根据规律推出递推式如上,但具体代码中并不需要如此计算,我们知道即可。
举例分析
第二步我们考虑一个数367。
我们可以将367划分为以下几个区间:[000, 099],[100, 199],[200, 299],[300, 367];
我们考虑计算000-367的原因是这样有利于我们根据前面的dp数组进行计算,并且我们会发现一个具有明显规律的bug,这个BUG就是多算了很多0,而其规律就是可以根据n的位数直接得出我们多算了多少个0,最后减掉就可以了。
比如说对于一个个位数8,我们考虑[0, 8]则多算了1个0;对于一个十位数93,我们考虑[00, 93]则多算了11个0;对于一个百位数100,我们考虑[000, 100]则多算了111个0。
如果还不太能理解,则可以从100开始往下并列书写,容易发现在百位上[000, 099]多计算了100个0,在[000,009]的十位上多计算了10个0,在[000,000]的个位上多计算了1个0,构成111个0。
代码设计
我们将[000, 099],[100, 199],[200, 299]看作具有相同的特性,即它们之中都包含了1份[00, 99],他们唯一不同的是,第一个区间除此之外多了100个0,第二个区间还多了100个1,第三个区间还多了100个2,那么这就很有利于我们编写代码。
我们从n的最高位开始考虑,[000, 299]的数码已经计算出,那么跟百位还有关联的则是3字头的数据,很明显,在[300, 367]中包括了68个3和一个区间[00, 67],那么第二轮循环按照相同的逻辑处理[00, 67]即可。
在下面的代码中,我们在init()方法中提前初始化了一些有利于我们计算的数据:
- ten[i] 表示10的 i 次方的数值
- cnt[i] 表示数码 i 出现的次数
- zero 表示最后需要减去的多余的数码0的个数
- num[i] 表示n的第 i 位数的数值
import java.util.*;public class Main {static long n;static int len = 0;static long[] dp, ten, cnt;static long zero;static int[] num;public static void main(String[] args) {Scanner sc = new Scanner(System.in);n = sc.nextLong();init(n);solve(n);}public static void solve(long n) {cnt = new long[10];long num2 = n;for (int l = len; l >= 1; l--) {for (int i = 0; i <= 9; i++) {cnt[i] += num[l] * dp[l - 1];}for (int i = 0; i < num[l]; i++) {cnt[i] += ten[l - 1];}num2 -= num[l] * ten[l - 1];cnt[num[l]] += num2 + 1;}cnt[0] -= zero;for (int i = 0; i <= 9; i++) {System.out.print(cnt[i] + " ");}}public static void init(long n) {num = new int[15];while (n > 0) {num[++len] = (int) (n % 10);n /= 10;}dp = new long[len + 1];ten = new long[len + 1];ten[0] = 1;for (int i = 1; i <= len; i++) {zero = zero * 10 + 1;dp[i] = i * ten[i - 1];ten[i] = 10 * ten[i - 1];}}
}
题后总结
上述算法基于[1, n]的数码数量,对于[n, m]之间的数码数量则可以先计算[1, m]和[1, n-1],再在对应数码位上进行相减即可。