数位DP
如字面意思,是关于数字位数的一种DP。我们给定dp[i][j]定义为数字长度为i,最高位为j的所有数字集合所能产生的特定结果。例如i = 2,j = 1,最高位为十位数且最高位为1的所有数字中,1出现的次数dp[2][0] = 1,dp[2][1] = 11次,dp[2][2] = 1;
那么dp的转移方程式为:
for (int i = 1; i <= length; i++) {
for (int j = 0; j <= 9; j++) {
if (j == x) {
dp[i][j] = (int) Math.pow(10, i - 1);
}
for (int k = 0; k <= 9; k++) {
dp[i][j] += dp[i - 1][k];
}
}
}
对于一个小于等于n范围的所有数字的集合,它可以用三个部分来构成:
- 所有位数小于数字n的位数的数字
- 所有位数等于数字n位数,且最高位小于n最高位的数字
- 所有位数等于数字n位数,最高位等于n最高位的数字
对于情况一而言,我们可以将所有长度小于当前n的长度len的数字集合用一个循环表示:
for (int i = length - 1; i >= 1; i--) {
for (int j = 1; j <= 9; j++) {
ans += dp[i][j];
}
}
情况二同理:
for (int j = 1; j < bit; j++) {
ans += dp[length][j];
}
bit代表数字n的当前最高位值。
情况三等同于去除最高位后,长度等于len - 1的数字又由情况二和情况三来构成。所以我们可以使用递归或者迭代的思想完成对于数字n的数位dp。
代码如下:
public int countDigitX(int n, int x) {
// dp[i][j] 表示第 i 位作为最高位时值为 j 时 x数字 的出现次数
int[][] dp = new int[11][11];
int length = 0, t = n;
while (t > 0) {
length++;
t /= 10;
}
length = Math.max(1, length);
for (int i = 1; i <= length; i++) {
for (int j = 0; j <= 9; j++) {
if (j == x) {
dp[i][j] = (int) Math.pow(10, i - 1);
}
for (int k = 0; k <= 9; k++) {
dp[i][j] += dp[i - 1][k];
}
}
}
if(n <= 9) {
int ans = 0;
for (int i = 0; i <= n; i++) {
ans += dp[1][i];
}
return ans;
}
int ans = 0;
int bit = n / (int)Math.pow(10, length - 1) % 10;
for (int j = 1; j < bit; j++) {
ans += dp[length][j];
}
for (int i = length - 1; i >= 1; i--) {
for (int j = 1; j <= 9; j++) {
ans += dp[i][j];
}
if (bit == x) {
var sum = n % (int)Math.pow(10, i) + 1;
ans += sum;
}
bit = n / (int)Math.pow(10, i - 1) % 10;
if (i == 1) {
bit++;
}
for (int j = 0; j < bit; j++) {
ans += dp[i][j];
}
}
return ans;
}
值得注意的是,从整个代码可以看出来,只有现在迭代的长度等于1的时候,我们需要获取j小于等于当前bit的值,其余时候j都应该是小于当前bit的值。