数位 DP 模板 V1.0
902. 最大为 N 的数字组合
class Solution {
String strN;
String[] digits;
int[] memo;
public int atMostNGivenDigitSet(String[] digits, int n) {
this.strN = n + "";
this.digits = digits;
memo = new int[strN.length()];
Arrays.fill(memo, -1);
return dfs(0, true, false);
}
private int dfs(int i, boolean isLimit, boolean isNum){
if (i == strN.length()){
return isNum ? 1 : 0;
}
// 不受如何约束的条件下
if (!isLimit && isNum && memo[i] > 0){
return memo[i];
}
int res = 0;
if (!isNum){
res = dfs(i + 1, false, false);
}
char up = isLimit ? strN.charAt(i) : '9';
for (String d : digits){
if (d.charAt(0) > up){
break;
}
res += dfs(i + 1, isLimit && d.charAt(0) == up, true);
}
// 不受如何约束的条件下
if (!isLimit && isNum){
memo[i] = res;
}
return res;
}
}
为什么只能在不受任何条件的情况下进行记忆化?
答:当在收到限制的情况下进行记忆化,计算的结果只有在满足相同限制下才是可用的,其他条件则并不通用。只有在不受限制的情况下,才是一个真正可复用的子问题。
数位 DP 模板 V2.0
模板 V1.0 只能处理 [0, n] 范围内的情况,对于同时有上下界的情况就需要计算两次,V2.0 模板可以一次处理具有上下界限制的情况。核心参数如下:
i:表示当前处理的数位索引;
isLimitLow:表示是否还受到起始数字 low 的约束;
isLimitHigh:表示是否受到终止数字 high 的约束。
其余条件根据题目意思进行约束即可。
2999. 统计强大整数的数目
class Solution {
char[] low;
char[] high;
long[] memo;
int limit;
char[] s;
public long numberOfPowerfulInt(long start, long finish, int limit, String s) {
this.limit = limit;
this.s = s.toCharArray();
// 上界数字
this.high = Long.toString(finish).toCharArray();
// 下届数字补齐前导零
String lowStr = Long.toString(start);
lowStr = "0".repeat(high.length - lowStr.length()) + lowStr;
this.low = lowStr.toCharArray();
// 记忆化搜索
this.memo = new long[high.length];
Arrays.fill(memo, -1);
return dfs(0, true, true);
}
public long dfs(int i, boolean isLimitLow, boolean isLimitHigh) {
if (i == high.length) {
return 1;
}
if (!isLimitLow && !isLimitHigh && memo[i] != -1) {
return memo[i];
}
// 第 i 个数位可以枚举的范围 [lo, hi]
// 如果有其他限制,应当在下面的 for 循环中做限制,不能修改 lo 或 hi
int lo = isLimitLow ? low[i] - '0' : 0;
int hi = isLimitHigh ? high[i] - '0' : 9;
long res = 0;
if (i < high.length - s.length) {
// 枚举当前数位填什么,此时受到 limit 的限制
for (int d = lo; d <= Math.min(hi, limit); d++) {
res += dfs(i + 1, isLimitLow && d == lo, isLimitHigh && d == hi);
}
} else { // 此时需要满足后缀为 s,只能填 s 中对应的字符
int d = s[i - (high.length - s.length)] - '0';
if (lo <= d && d <= Math.min(hi, limit)) {
res += dfs(i + 1, isLimitLow && d == lo, isLimitHigh && d == hi);
}
}
// 不受如何约束的条件下,才进行记忆化
if (!isLimitLow && !isLimitHigh) {
memo[i] = res;
}
return res;
}
}
2719. 统计整数数目
class Solution {
int MOD = 1_000_000_007;
int minSum;
int maxSum;
char[] num1;
char[] num2;
int[][] memo;
public int count(String num1, String num2, int min_sum, int max_sum) {
this.minSum = min_sum;
this.maxSum = max_sum;
int n = num2.length();
// 上界数字
this.num2 = num2.toCharArray();
// 下界数字,补齐前导零
this.num1 = ("0".repeat(n - num1.length()) + num1).toCharArray();
memo = new int[n][Math.min(9 * n, max_sum) + 1];
for (int[] row : memo) {
Arrays.fill(row, -1);
}
// 递归入口(数字的第一位)同时受到上下界的影响
return dfs(0, 0, true, true) % MOD;
}
private int dfs(int i, int sum, boolean limitLow, boolean limitHigh) {
if (sum > maxSum) {
return 0;
}
if (i == num2.length) {
return sum >= minSum ? 1 : 0;
}
if (!limitLow && !limitHigh && memo[i][sum] != -1) {
return memo[i][sum];
}
int lo = limitLow ? num1[i] - '0' : 0; // 下界
int hi = limitHigh ? num2[i] - '0' : 9; // 上界
int res = 0;
for (int d = lo; d <= hi; d++) {
res = (res + dfs(i + 1, sum + d, limitLow && d == lo, limitHigh && d == hi)) % MOD;
}
// 不受如何约束的条件下,才进行记忆化
if (!limitLow && !limitHigh) {
memo[i][sum] = res;
}
return res;
}
}
2843. 统计对称整数的数目
给你两个正整数 low 和 high 。
对于一个由 2 * n 位数字组成的整数 x ,如果其前 n 位数字之和与后 n 位数字之和相等,则认为这个数字是一个对称整数。
返回在 [low, high] 范围内的 对称整数的数目 。
状态定义:dfs(i,start,diff,isLimitLow,isLimitHigh) 表示构造第 i 位及其之后数位的合法方案数。
class Solution {
char[] low, high;
int n, diffLH;
int[][][] memo;
public int countSymmetricIntegers(int low, int high) {
this.low = String.valueOf(low).toCharArray();
this.high = String.valueOf(high).toCharArray();
this.n = this.high.length;
this.diffLH = this.n - this.low.length;
// dfs 中的 start <= diffLh,左半元素和 <= floor(n/2) * 9
this.memo = new int[n][diffLH + 1][this.n / 2 * 9 + 1];
for (int[][] mat : memo) {
for (int[] row : mat) {
Arrays.fill(row, -1);
}
}
return dfs(0, -1, 0, true, true);
}
public int dfs(int i, int start, int diff, boolean isLimitLow, boolean isLimitHigh) {
if (diff < 0) {
return 0;
}
if (i == n) {
return diff == 0 ? 1 : 0;
}
if (start != -1 && !isLimitHigh && !isLimitLow && memo[i][start][diff] != -1) {
return memo[i][start][diff];
}
int lo = isLimitLow && i >= diffLH ? low[i - diffLH] - '0' : 0;
int hi = isLimitHigh ? high[i] - '0' : 9;
// 如果前面没有填数字,剩余数位个数是奇数,则当前数位不能填数字
if (start < 0 && (n - i) % 2 == 1) {
// 如果必须填数字(lo > 0),不合法,直接返回 0
return lo > 0 ? 0 : dfs(i + 1, start, diff, true, false);
}
int res = 0;
// 判断当前数位是否在左半
boolean isLeft = start < 0 || i < (n + start) / 2;
for (int d = lo; d <= hi; d++) {
res += dfs(
i + 1,
start < 0 && d > 0 ? i : start, // 记录第一个填数字的位置
diff + (isLeft ? d : -d), // 左半 +,右半 -
isLimitLow && d == lo,
isLimitHigh && d == hi);
}
// 记忆化
if (start != -1 && !isLimitHigh && !isLimitLow) {
memo[i][start][diff] = res;
}
return res;
}
}