class084 数位dp-上【算法】

class084 数位dp-上【算法】

算法讲解084【必备】数位dp-上

在这里插入图片描述

code1 357. 统计各位数字都不同的数字个数

// 统计各位数字都不同的数字个数
// 给你一个整数n,代表十进制数字最多有n位
// 如果某个数字,每一位都不同,那么这个数字叫做有效数字
// 返回有效数字的个数,不统计负数范围
// 测试链接 : https://leetcode.cn/problems/count-numbers-with-unique-digits/

package class084;

// 统计各位数字都不同的数字个数
// 给你一个整数n,代表十进制数字最多有n位
// 如果某个数字,每一位都不同,那么这个数字叫做有效数字
// 返回有效数字的个数,不统计负数范围
// 测试链接 : https://leetcode.cn/problems/count-numbers-with-unique-digits/
public class Code01_CountNumbersWithUniqueDigits {

	public static int countNumbersWithUniqueDigits(int n) {
		if (n == 0) {
			return 1;
		}
		int ans = 10;
		// 1 : 10
		// 2 : 9 * 9
		// 3 : 9 * 9 * 8
		// 4 : 9 * 9 * 8 * 7
		// ...都累加起来...
		for (int s = 9, i = 9, k = 2; k <= n; i--, k++) {
			s *= i;
			ans += s;
		}
		return ans;
	}

}

code2 902. 最大为 N 的数字组合

// 最大为N的数字组合
// 给定一个按 非递减顺序 排列的数字数组 digits
// 已知digits一定不包含’0’,可能包含’1’ ~ ‘9’,且无重复字符
// 你可以用任意次数 digits[i] 来写的数字
// 例如,如果 digits = [‘1’,‘3’,‘5’]
// 我们可以写数字,如 ‘13’, ‘551’, 和 ‘1351315’
// 返回 可以生成的小于或等于给定整数 n 的正整数的个数
// 测试链接 : https://leetcode.cn/problems/numbers-at-most-n-given-digit-set/

package class084;

// 最大为N的数字组合
// 给定一个按 非递减顺序 排列的数字数组 digits
// 已知digits一定不包含'0',可能包含'1' ~ '9',且无重复字符
// 你可以用任意次数 digits[i] 来写的数字
// 例如,如果 digits = ['1','3','5']
// 我们可以写数字,如 '13', '551', 和 '1351315'
// 返回 可以生成的小于或等于给定整数 n 的正整数的个数
// 测试链接 : https://leetcode.cn/problems/numbers-at-most-n-given-digit-set/
public class Code02_NumbersAtMostGivenDigitSet {

	public static int atMostNGivenDigitSet1(String[] strs, int num) {
		int tmp = num / 10;
		int len = 1;
		int offset = 1;
		while (tmp > 0) {
			tmp /= 10;
			len++;
			offset *= 10;
		}
		int m = strs.length;
		int[] digits = new int[m];
		for (int i = 0; i < m; i++) {
			digits[i] = Integer.valueOf(strs[i]);
		}
		return f1(digits, num, offset, len, 0, 0);
	}

	// offset是辅助变量,完全由len决定,只是为了方便提取num中某一位数字,不是关键变量
	// 还剩下len位没有决定
	// 如果之前的位已经确定比num小,那么free == 1,表示接下的数字可以自由选择
	// 如果之前的位和num一样,那么free == 0,表示接下的数字不能大于num当前位的数字
	// 如果之前的位没有使用过数字,fix == 0
	// 如果之前的位已经使用过数字,fix == 1
	// 返回最终<=num的可能性有多少种
	public static int f1(int[] digits, int num, int offset, int len, int free, int fix) {
		if (len == 0) {
			return fix == 1 ? 1 : 0;
		}
		int ans = 0;
		// num在当前位的数字
		int cur = (num / offset) % 10;
		if (fix == 0) {
			// 之前从来没有选择过数字
			// 当前依然可以不要任何数字,累加后续的可能性
			ans += f1(digits, num, offset / 10, len - 1, 1, 0);
		}
		if (free == 0) {
			// 不能自由选择的情况
			for (int i : digits) {
				if (i < cur) {
					ans += f1(digits, num, offset / 10, len - 1, 1, 1);
				} else if (i == cur) {
					ans += f1(digits, num, offset / 10, len - 1, 0, 1);
				} else {
					// i > cur
					break;
				}
			}
		} else {
			// 可以自由选择的情况
			ans += digits.length * f1(digits, num, offset / 10, len - 1, 1, 1);
		}
		return ans;
	}

	public static int atMostNGivenDigitSet2(String[] strs, int num) {
		int m = strs.length;
		int[] digits = new int[m];
		for (int i = 0; i < m; i++) {
			digits[i] = Integer.valueOf(strs[i]);
		}
		int len = 1;
		int offset = 1;
		int tmp = num / 10;
		while (tmp > 0) {
			tmp /= 10;
			len++;
			offset *= 10;
		}
		// cnt[i] : 已知前缀比num小,剩下i位没有确定,请问前缀确定的情况下,一共有多少种数字排列
		// cnt[0] = 1,表示后续已经没有了,前缀的状况都已确定,那么就是1种
		// cnt[1] = m
		// cnt[2] = m * m
		// cnt[3] = m * m * m
		// ...
		int[] cnt = new int[len];
		cnt[0] = 1;
		int ans = 0;
		for (int i = m, k = 1; k < len; k++, i *= m) {
			cnt[k] = i;
			ans += i;
		}
		return ans + f2(digits, cnt, num, offset, len);
	}

	// offset是辅助变量,由len确定,方便提取num中某一位数字
	// 还剩下len位没有决定,之前的位和num一样
	// 返回最终<=num的可能性有多少种
	public static int f2(int[] digits, int[] cnt, int num, int offset, int len) {
		if (len == 0) {
			// num自己
			return 1;
		}
		// cur是num当前位的数字
		int cur = (num / offset) % 10;
		int ans = 0;
		for (int i : digits) {
			if (i < cur) {
				ans += cnt[len - 1];
			} else if (i == cur) {
				ans += f2(digits, cnt, num, offset / 10, len - 1);
			} else {
				break;
			}
		}
		return ans;
	}

}

code3 2719. 统计整数数目

// 统计整数数目
// 给你两个数字字符串 num1 和 num2 ,以及两个整数max_sum和min_sum
// 如果一个整数 x 满足以下条件,我们称它是一个好整数
// num1 <= x <= num2
// min_sum <= digit_sum(x) <= max_sum
// 请你返回好整数的数目
// 答案可能很大请返回答案对10^9 + 7 取余后的结果
// 注意,digit_sum(x)表示x各位数字之和
// 测试链接 : https://leetcode.cn/problems/count-of-integers/

[0,num2]-[0,num1-1]
=[0,num2]-[0,num1]+[num1],因为字符串的减法不好做,所以单独判断num1,是否符合

package class084;

// 统计整数数目
// 给你两个数字字符串 num1 和 num2 ,以及两个整数max_sum和min_sum
// 如果一个整数 x 满足以下条件,我们称它是一个好整数
// num1 <= x <= num2
// min_sum <= digit_sum(x) <= max_sum
// 请你返回好整数的数目
// 答案可能很大请返回答案对10^9 + 7 取余后的结果
// 注意,digit_sum(x)表示x各位数字之和
// 测试链接 : https://leetcode.cn/problems/count-of-integers/
public class Code03_CountOfIntegers {

	public static int MOD = 1000000007;

	public static int MAXN = 23;

	public static int MAXM = 401;

	public static int[][][] dp = new int[MAXN][MAXM][2];

	public static void build() {
		for (int i = 0; i < len; i++) {
			for (int j = 0; j <= max; j++) {
				dp[i][j][0] = -1;
				dp[i][j][1] = -1;
			}
		}
	}

	public static char[] num;

	public static int min, max, len;

	public static int count(String num1, String num2, int min_sum, int max_sum) {
		min = min_sum;
		max = max_sum;
		num = num2.toCharArray();
		len = num2.length();
		build();
		int ans = f(0, 0, 0);
		num = num1.toCharArray();
		len = num1.length();
		build();
		ans = (ans - f(0, 0, 0) + MOD) % MOD;
		if (check()) {
			ans = (ans + 1) % MOD;
		}
		return ans;
	}

	// 注意:
	// 数字,char[] num
	// 数字长度,int len
	// 累加和最小要求,int min
	// 累加和最大要求,int max
	// 这四个变量都是全局静态变量,所以不用带参数,直接访问即可
	// 递归含义:
	// 从num的高位出发,当前来到i位上
	// 之前决定的数字累加和是sum
	// 之前的决定已经比num小,后续可以自由选择数字,那么free == 1
	// 之前的决定和num一样,后续不可以自由选择数字,那么free == 0
	// 返回有多少种可能性
	public static int f(int i, int sum, int free) {
		if (sum > max) {
			return 0;
		}
		if (sum + (len - i) * 9 < min) {
			return 0;
		}
		if (i == len) {
			return 1;
		}
		if (dp[i][sum][free] != -1) {
			return dp[i][sum][free];
		}
		// cur : num当前位的数字
		int cur = num[i] - '0';
		int ans = 0;
		if (free == 0) {
			// 还不能自由选择
			for (int pick = 0; pick < cur; pick++) {
				ans = (ans + f(i + 1, sum + pick, 1)) % MOD;
			}
			ans = (ans + f(i + 1, sum + cur, 0)) % MOD;
		} else {
			// 可以自由选择
			for (int pick = 0; pick <= 9; pick++) {
				ans = (ans + f(i + 1, sum + pick, 1)) % MOD;
			}
		}
		dp[i][sum][free] = ans;
		return ans;
	}

	public static boolean check() {
		int sum = 0;
		for (char cha : num) {
			sum += cha - '0';
		}
		return sum >= min && sum <= max;
	}

}

code4 2376. 统计特殊整数

// 完全没有重复的数字个数
// 给定正整数n,返回在[1, n]范围内每一位都互不相同的正整数个数
// 测试链接 : https://leetcode.cn/problems/count-special-integers/

package class084;

// 完全没有重复的数字个数
// 给定正整数n,返回在[1, n]范围内每一位都互不相同的正整数个数
// 测试链接 : https://leetcode.cn/problems/count-special-integers/
public class Code04_CountSpecialIntegers {

	public static int countSpecialNumbers(int n) {
		int len = 1;
		int offset = 1;
		int tmp = n / 10;
		while (tmp > 0) {
			len++;
			offset *= 10;
			tmp /= 10;
		}
		// cnt[i] :
		// 一共长度为len,还剩i位没有确定,确定的前缀为len-i位,且确定的前缀不为空
		// 0~9一共10个数字,没有选择的数字剩下10-(len-i)个
		// 那么在后续的i位上,有多少种排列
		// 比如:len = 4
		// cnt[4]不计算
		// cnt[3] = 9 * 8 * 7
		// cnt[2] = 8 * 7
		// cnt[1] = 7
		// cnt[0] = 1,表示前缀已确定,后续也没有了,那么就是1种排列,就是前缀的状况
		// 再比如:len = 6
		// cnt[6]不计算
		// cnt[5] = 9 * 8 * 7 * 6 * 5
		// cnt[4] = 8 * 7 * 6 * 5
		// cnt[3] = 7 * 6 * 5
		// cnt[2] = 6 * 5
		// cnt[1] = 5
		// cnt[0] = 1,表示前缀已确定,后续也没有了,那么就是1种排列,就是前缀的状况
		// 下面for循环就是求解cnt的代码
		int[] cnt = new int[len];
		cnt[0] = 1;
		for (int i = 1, k = 10 - len + 1; i < len; i++, k++) {
			cnt[i] = cnt[i - 1] * k;
		}
		int ans = 0;
		if (len >= 2) {
			// 如果n的位数是len位,先计算位数少于len的数中,每一位都互不相同的正整数个数,并累加
			// 所有1位数中,每一位都互不相同的正整数个数 = 9
			// 所有2位数中,每一位都互不相同的正整数个数 = 9 * 9
			// 所有3位数中,每一位都互不相同的正整数个数 = 9 * 9 * 8
			// 所有4位数中,每一位都互不相同的正整数个数 = 9 * 9 * 8 * 7
			// ...比len少的位数都累加...
			ans = 9;
			for (int i = 2, a = 9, b = 9; i < len; i++, b--) {
				a *= b;
				ans += a;
			}
		}
		// 如果n的位数是len位,已经计算了位数少于len个的情况
		// 下面计算一定有len位的数字中,<=n且每一位都互不相同的正整数个数
		int first = n / offset;
		// 小于num最高位数字的情况
		ans += (first - 1) * cnt[len - 1];
		// 后续累加上,等于num最高位数字的情况
		ans += f(cnt, n, len - 1, offset / 10, 1 << first);
		return ans;
	}

	// 之前已经确定了和num一样的前缀,且确定的部分一定不为空
	// 还有len位没有确定
	// 哪些数字已经选了,哪些数字没有选,用status表示
	// 返回<=num且每一位数字都不一样的正整数有多少个
	public static int f(int[] cnt, int num, int len, int offset, int status) {
		if (len == 0) {
			// num自己
			return 1;
		}
		int ans = 0;
		// first是num当前位的数字
		int first = (num / offset) % 10;
		for (int cur = 0; cur < first; cur++) {
			if ((status & (1 << cur)) == 0) {
				ans += cnt[len - 1];
			}
		}
		if ((status & (1 << first)) == 0) {
			ans += f(cnt, num, len - 1, offset / 10, status | (1 << first));
		}
		return ans;
	}

}

code4 1012. 至少有 1 位重复的数字

// 至少有1位重复的数字个数
// 给定正整数n,返回在[1, n]范围内至少有1位重复数字的正整数个数
// 测试链接 : https://leetcode.cn/problems/numbers-with-repeated-digits/

package class084;

// 至少有1位重复的数字个数
// 给定正整数n,返回在[1, n]范围内至少有1位重复数字的正整数个数
// 测试链接 : https://leetcode.cn/problems/numbers-with-repeated-digits/
public class Code04_NumbersWithRepeatedDigits {

	public static int numDupDigitsAtMostN(int n) {
		return n - countSpecialNumbers(n);
	}

	public static int countSpecialNumbers(int n) {
		int len = 1;
		int offset = 1;
		int tmp = n / 10;
		while (tmp > 0) {
			len++;
			offset *= 10;
			tmp /= 10;
		}
		int[] cnt = new int[len];
		cnt[0] = 1;
		for (int i = 1, k = 10 - len + 1; i < len; i++, k++) {
			cnt[i] = cnt[i - 1] * k;
		}
		int ans = 0;
		if (len >= 2) {
			ans = 9;
			for (int i = 2, a = 9, b = 9; i < len; i++, b--) {
				a *= b;
				ans += a;
			}
		}
		int first = n / offset;
		ans += (first - 1) * cnt[len - 1];
		ans += f(cnt, n, len - 1, offset / 10, 1 << first);
		return ans;
	}

	public static int f(int[] cnt, int num, int len, int offset, int status) {
		if (len == 0) {
			return 1;
		}
		int ans = 0;
		int first = (num / offset) % 10;
		for (int cur = 0; cur < first; cur++) {
			if ((status & (1 << cur)) == 0) {
				ans += cnt[len - 1];
			}
		}
		if ((status & (1 << first)) == 0) {
			ans += f(cnt, num, len - 1, offset / 10, status | (1 << first));
		}
		return ans;
	}

}

2023-12-15 15:12:01

### 数位递增的概念 数位递增是指一个正整数中的每一位数字都不大于其右侧相邻的数字。换句话说,对于任意位置 \(i\) 的数字 \(d_i\) 和其右侧相邻的位置 \(j\) 的数字 \(d_j\) (其中 \(i < j\)),满足条件 \(d_i \leq d_j\)。例如,1135 是一个数位递增的数,因为它的每一位都小于等于下一位;而 1024 不是数位递增的数,因为在第二位上出现了较大的数字。 这种定义可以通过逐位比较来验证一个数是否属于数位递增类别[^3]。 --- ### 实现方法与算法 #### 方法一:字符串处理方式 通过将数字转换为字符串形式逐一比较字符大小,可以判断该数是否为数位递增的数。以下是 Python 中的一种实现: ```python def count_increasing_numbers(n): ans = 0 for number in range(1, n + 1): # 遍历从 1 到 n 的所有数字 s = str(number) # 将当前数字转为字符串 flag = True # 假设当前数字符合条件 for j in range(1, len(s)): if s[j - 1] > s[j]: # 如果前一位大于后一位,则不符合条件 flag = False break # 提前退出循环 if flag: # 若标志仍为真,则计数加一 ans += 1 return ans # 返回最终的结果 if __name__ == "__main__": n = int(input()) result = count_increasing_numbers(n) print(result) ``` 这种方法的时间复杂度主要取决于输入范围内的遍历次数以及每次内部的逐位对比操作,总体时间复杂度大约为 O(k * log₁₀n),其中 k 表示总共有多少个数需要被检测,log₁₀n 表示平均每个数所含有的位数。 --- #### 方法二:动态规划优化 另一种更高效的解决方案涉及动态规划的思想。我们可以预先计算出不同长度下的可能组合数目,并利用这些预计算结果快速得出答案。具体来说,假设 dp[l][k] 表示长度为 l 并且最高位不超过 k 的有效数位递增数量,则状态转移方程如下所示: \[ dp[l][k] = \sum_{m=0}^{k}{dp[l-1][m]} \] 初始条件设置为当长度为 1 时,任何单个数字都是有效的数位递增数。因此有: \[ dp[1][k] = k+1,\quad 对于所有的 k \in [0..9] \] 最后统计范围内所有合法数值即可得到总数目。此方法能够显著减少重复运算量,在大规模数据集上的表现优于简单枚举法[^1]。 --- #### Java 实现最长连续递增子序列 虽然题目询问的是关于数位递增的具体概念及其应用,但这里也提供一段基于数组寻找最长严格单调上升子串的代码作为补充说明,这有助于理解如何高效地判定一系列元素之间的相对顺序关系: ```java class Solution { public int findLengthOfLCIS(int[] nums) { if (nums.length == 0) return 0; int max = 0; int num = 1; for (int i = 0; i < nums.length - 1; i++) { if (nums[i] < nums[i + 1]) { num++; } else { if (max < num) max = num; num = 1; } } return Math.max(num, max); } } ``` 上述代码片段展示了如何在一个整型数组中找到最长连续递增子序列的长度。尽管它并不直接解决原问题,但它体现了类似的逻辑思维过程——即通过对前后项之间差值或者大小关系进行评估从而完成特定目标的任务[^2]。 --- ### 总结 综上所述,无论是采用简单的暴力枚举还是复杂的动态规划策略都可以有效地解决问题。选择哪种技术路线应视具体情况而定,比如性能需求、开发周期等因素都会影响决策方向。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

日星月云

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值