前言
数位dp适用题型:求某个区间[L, R]满足某种性质的数的个数
用从左到右填数的方法去遍历小于n的所有数字
技巧1:将[L,R]区间转化为[0, R] - [0, L - 1]再求解,如果求[0, R]区间则无需转换
技巧2:从高位到低位依次填数,分类讨论
将整数R用每一位数字存入字符数组中,则
每次填数分成两类:0~ai-1、ai
如果第i位填0-ai-1,则后面每一位可以填0~9
如果第i位填ai,则继续讨论下一位
例如:求小于56789的非负整数满足某种要求有多少个数字
当第一位数字填5时,第二位只能从0-6当中选择
当第一位数字填0-4时,第二位可以从0-9当中选择,此时第二位是不受限制的
参数含义
char s[] / String s
用来存储n的每一位数字,方便填数的时候比较n的对应数位
dp[i][sum]
dp[i][sum]表示从第i位开始填数,且第i位之前填的数满足题目限制为sum,有多少个合法数字
例如前三位填了456,从第四位开始填有dp[3][456]个合法数字
当之后遍历填数,前三位填了654,与之前情形一样,可以直接复用dp[3][456]
isLimit
isLimit 表示当前是否受到了 n的约束。若为真,当前位的上限就是s[i];若为假,当前位的上限是9。参照前言部分填数例子
isNum
isNum 表示当前位前面的位置是否填了数字。若为假,则当前位可以跳过;若为真,则要填入的数字可以从 0开始填
参数可以根据题意增加或减少
模板
class Solution {
// 存储n的每一位数字
char s[];
// 存储子问题的合法数字个数
int dp[][];
public int solve(int n) {
s = Integer.toString(n).toCharArray();
int length = s.length;
dp = new int[length][length];
for (int i = 0; i < length; i++) {
// 将dp数组初始化为-1,代表没遇到过子问题
Arrays.fill(dp[i], -1);
}
// 从第一位开始填,第一位是受到限制的,且之前没填过数字
return f(0, true, false);
}
public int f(int i, boolean isLimit, booklean isNum) {
//如果填到了最后一位,代表找到了一个合法数字
if (i == s.length) {
return 1;
}
// 遇到了相同的子问题,则直接复用
if (!isLimit && isNum dp[i][] != -1) {
return dp[i][];
}
//开始记录答案
int res = 0;
//之前的位置都没有填数,那么当前位置也可以选择跳过不填
if (!isNum) {
res = f(i + 1, false, false);
}
// up是当前位置能填的最大数字
int up = isLimit ? s[i] - '0' : 9;
for (int d = 0; d <= up; d++) {
// 开始填下一个位置
res += f(i + 1, isLimit && d == up, true);
}
// 如果当前位置可以填0 - 9, 且之前的位置填了数,那么这种情况就可以复用,将其记录下来
if (!isLimit && isNum) {
dp[i][] = res;
}
return res;
}
}
例题
1012. 至少有 1 位重复的数字 - 力扣(LeetCode)
class Solution {
char[] s; // 用来存储n的每一位数字
// dp[i][mask]表示从第i位开始填,且之前填过的数为mask有多少种填法
int[][] dp;
public int numDupDigitsAtMostN(int n) {
s = Integer.toString(n).toCharArray(); //用字符存储每一位n的数字
int length = s.length;
dp = new int[length][1 << 10];
for (int i = 0; i < length; i++){
Arrays.fill(dp[i], -1); //-1表示没有计算过
}
return n - f(0, 0, true, false);
}
/*
从第i位开始填数字,i前面位置填过的数字集合是mask
mask是二进制数,第i位为1代表填过的数字包含i
例如10011代表填过了数字{4,1,0}
isLimit表示前面位置填的数字是否都是n对应位置上的
isNum表示前面位置是否填过数字
*/
public int f(int i, int mask, boolean isLimit, boolean isNum) {
if (i == s.length) {
//最后一位数字填完了,如果前面填过数字则增加一个合法数字
return isNum ? 1 : 0;
}
//前面位置没有填n对应位置上的数且填了数,那么后面填的所有数都不受限制
if (!isLimit && isNum && dp[i][mask] != -1) {
return dp[i][mask];
}
int res = 0;
//选择跳过当前位,从下一位开始填数字
if (!isNum) {
res = f(i + 1, mask, false, false);
}
//当前位置能填的数字的上界,如果isLimit则只能填0~s[n],否则可以填0~9
int up = isLimit ? s[i] - '0' : 9;
//开始遍历可以填入的数字d
for (int d = isNum ? 0 : 1; d <= up; d++) {
//mask右移d位,如果是1,则表示d已经在mask里了,当前位置不能填d,如果是0则可以填
if ((mask >> d & 1) == 0) {
//mask|(1 << d)表示把1左移d位存入mask中,代表d这个数已经填过了
//如果之前的位置是isLimit且当前位置填了上限,那么之后的要填的数字还是isLimit
res += f(i + 1, mask|(1 << d), isLimit && d == up, true);
}
}
if (!isLimit && isNum) {
dp[i][mask] = res;
}
return res;
}
}